Tree Dp专题整理

来源:互联网 发布:甩手掌柜工具箱软件 编辑:程序博客网 时间:2024/05/01 03:11

Tree Dp介绍及做题大体思路

tree dp即树形dp,就是在树形结构上的动态规划,找寻最有解。
做树形DP一般步骤是:
1. 建树,并将树转换为有根树
2. 对于部分题目,需要在树上进行dfs_cal操作,预处理出一些需要的东西,比如距离什么的从子节点。
3. 继续在树上进行dfs,从子树中返回信息层层往上更新至根节点。返回信息是tree dp的关键,即状态转移,因为每道题目要求做的事都不相同,状态的定义和转移方程也不同,但dp数组的第一维一般都是节点编号了,需要多做题积累。

Tree Dp题目

一般的tree dp


1. HDU 2196 Computer

题意:每条树边都有权值,问从每个顶点出发,经过的路径权值之和最大为多少?每条树边都只能走一次
思路:两次dfs,第一次是建树,并且求出经过它儿子节点的最长路和次长路,(两条路不能都经过这一个节点的同一个儿子),第二次               寻找从父亲节点过来到这个节点的最长的路。最后求max值即可
代码:
#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>using namespace std;const int MAXN=10010;struct node{int id,n;//该节点往下的距离和对应的序号}mmax[MAXN],smmax[MAXN];//最大和次大struct EDGE{    int to,next,w;}edge[MAXN*2];int head[MAXN],tot;void add(int a,int b,int w){edge[tot].to=b;    edge[tot].w=w;    edge[tot].next=head[a];    head[a]=tot++;}void dfs_cal(int u,int fa)//求结点u往下到叶子结点的最大距离,fa是u的父亲结点{mmax[u].n=smmax[u].n=0;for(int i=head[u];i!=-1;i=edge[i].next){int to=edge[i].to;if(to==fa)continue;dfs_cal(to,u);if(smmax[u].n<mmax[to].n+edge[i].w)        {            smmax[u].n=mmax[to].n+edge[i].w;            smmax[u].id=to;            if(smmax[u].n>mmax[u].n)                swap(smmax[u],mmax[u]);        }}}void dfs(int u,int fa){for(int i=head[u];i!=-1;i=edge[i].next){int to=edge[i].to;if(to==fa) continue;if(to==mmax[u].id){if(edge[i].w+smmax[u].n>smmax[to].n)            {                smmax[to].n=edge[i].w+smmax[u].n;                smmax[to].id=u;                if(smmax[to].n>mmax[to].n)                    swap(smmax[to],mmax[to]);             }        }        else        {        if(edge[i].w+mmax[u].n>smmax[to].n)            {                smmax[to].n=edge[i].w+mmax[u].n;                smmax[to].id=u;                if(smmax[to].n>mmax[to].n)                    swap(smmax[to],mmax[to]);        }}dfs(to,u);}}void init(){tot=0;memset(head,-1,sizeof(head));}int main(){int n,to,w;while(cin>>n){init();for(int i=2;i<=n;i++){scanf("%d%d",&to,&w);add(i,to,w);add(to,i,w);}dfs_cal(1,-1);dfs(1,-1);for(int i=1;i<=n;i++)printf("%d\n",mmax[i].n);}return 0;}

2.HDU 1520 Anniversary party 

题意:每个节点有权值,子节点和父节点不能同时选,问最后能选的最大价值
思路:定义dp[ i ][ 2 ](0表示不选,1表示不选),从叶子节点往根结点不断更新dp[ i ][ 0 ]和dp[ i ][ 1 ],状态转移即为
dp[ i ][ 1 ] = sum(dp[j][0]) (当前选了,子节点必定不能选,最优的情况是都不选,然后累加)
dp[ i ][ 0 ] = sum(max(dp[ i ][ 0 ] ,dp[ i ][ 1 ])) (当选不选,子节点可选可不选,找大的那个状态)
代码:
#include <iostream>#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int MAXN=6010;struct node{    int to,next;  }edge[2*MAXN];int tot,head[MAXN];int base[MAXN];int vis[MAXN];int dp[MAXN][2];void add(int u,int v){edge[tot].to=v;edge[tot].next=head[u];head[u]=tot++;}void dfs(int root){dp[root][0]=0;dp[root][1]=base[root];for(int i=head[root];i!=-1;i=edge[i].next){int to=edge[i].to;dfs(to);dp[root][0] += max(dp[to][0],dp[to][1]);dp[root][1] += dp[to][0];}}void init(){tot=0;memset(head,-1,sizeof(head));memset(vis,0,sizeof(vis));memset(dp,0,sizeof(dp));}int main(){int n,a,b,t;while(cin>>n){for(int i=1;i<=n;i++)scanf("%d",&base[i]);init();while(scanf("%d%d",&a,&b),a||b){vis[a]=1;add(b,a);}        for(int i=1;i<=n;i++)        if(!vis[i])        {        t=i;break;        }dfs(t);        cout<<max(dp[t][0],dp[t][1])<<endl;}return 0;}

3.POJ 1741 Tree

题意:求树上两点间距离小等于K的方案数(楼教主的男人八题之一
思路:树上的分治和树形dp的思想
1. 指定1为根将树变成有根树,那两个点的最近距离就有两种情况其一,它们在同一个分支上,最近距离就是他们间的距离。其二,它们不在同一个分支上,他们的最短距离就是他们到最近公共祖先的距离和。
2. 先找到以某点为根的子孙节点到根的距离,然后从这些距离里面找出两个距离之和小等于k的方案数。但是如果他们在同一个分支,那么就会重复计算,应该减去这一在同一分支中的部分。做法是用1算出来的方案数减去根为2算出来的方案数也即要减去以子节点为根算出来的方案数(这部分方案里两个点到根的距离相加小等于k但不是我们要计算的当前根的方案)

 3. 把距离都算出来之后,要快速找到方案数,做法是对距离序列排序,然后找头尾两个数,如果符合情况,算中间的个数,然后从头的下一个开始算,如果不符合情况说明太大,要从尾的前一个开始计算,直到头等于尾。计算复杂度是O(n),sort是O(nlogn)。

 4. 还有一个问题在于如果树是一条链,要算n层那么复杂度变为O(n^2logn)。考虑到以任何点点为根的计算都不影响其他点计算,那么每次都都找树的重心即可解决这一特殊状况。

代码:

#include <iostream>#include <algorithm>#include <cstring>#include <string>#include <cstdio>const int MAXN=11111;using namespace std;  struct EDGE{    int v,w,next;}   edge[5*MAXN];int head[MAXN],cnt;int n,k,vis[MAXN],ans,root,num;void init(){      memset(vis,0,sizeof(vis));    memset(head,-1,sizeof(head));    cnt=ans=0;}void add(int u,int v,int w){    edge[cnt].v=v;    edge[cnt].w=w;    edge[cnt].next=head[u];    head[u]=cnt++;}int mx[MAXN],size[MAXN],mi,dis[MAXN];void dfssize(int u,int fa)//处理子树的大小{    size[u]=1;    mx[u]=0;    for(int i=head[u];i!=-1;i=edge[i].next)    {        int v=edge[i].v;        if(v!=fa && !vis[v])        {            dfssize(v,u);            size[u] += size[v];            if(size[v]>mx[u])                mx[u]=size[v];          }    }}void dfsroot(int r,int u,int fa) //求重心  {    if(size[r]-size[u] > mx[u])        mx[u]=size[r]-size[u];    if(mx[u]<mi)        mi=mx[u],root=u;    for(int i=head[u];i!=-1;i=edge[i].next)    {        int v=edge[i].v;        if(v!=fa && !vis[v])            dfsroot(r,v,u);    }  }void dfsdis(int u,int d,int fa) //求距离  {      dis[num++]=d;      for(int i=head[u];i!=-1;i=edge[i].next)    {        int v=edge[i].v;        if(v!=fa && !vis[v])            dfsdis(v,d+edge[i].w,u);    }}int calc(int u,int d)  {      int ret=0;    num=0;    dfsdis(u,d,0);    sort(dis,dis+num);    int i=0,j=num-1;      while(i<j)    {          while(dis[i]+dis[j]>k && i<j)   j--;          ret +=j-i;          i++;      }    return ret;}void dfs(int u){      mi=n;    dfssize(u,0);    dfsroot(u,u,0);    ans += calc(root,0);    vis[root] = 1;    for(int i=head[root];i!=-1;i=edge[i].next)    {        int v=edge[i].v;        if(!vis[v])        {              ans -= calc(v,edge[i].w);            dfs(v);        }    }}int main()  {    while(cin>>n>>k && n && k)    {        init();        int u,v,w;        for(int i=1;i<n;i++)        {            scanf("%d%d%d",&u,&v,&w);            add(u,v,w);            add(v,u,w);        }        dfs(1);        cout<<ans<<endl;    }    return 0;}


4. POJ 3162 Walking Race

题意:一张n个节点的树形地图。要跑步n天,每次都从一个结点开始跑步,每次都要跑到最远的那个结点,两天跑的最远距离有个差值,现在要从这n天里去若干天使得这些天的差值都小于m,问怎么取使得天数最多?n <= 100万,m <= 1亿。

思路:树形dp + 线段树或者单调队列。

1. 树形dp思想求每个点到其他某点的最远距离:变成有根树,dfs一遍找出向下的最远距离,再dfs一遍把向上的那条分支也算进来。

2. 上一步得到一个数组,现在要从这个数组里找出连续的一段序列最大值最小值之差小于m,并且长度尽量大。

考虑O(N)的算法,因为是找最长的一个序列,我们可以通过维护两个指针来完成这个计算过程,两个指针表示区间的开始和结束,如果这个区间差值=<m,那么区间尾可以下移,如果>m那么区间必须缩小一些,区间头向后移,这样就可以通过不断增大区间头使得这个区间差值<=m.

3. 找区间中的最大值最下值:可以用线段树啊,很常规的单点查询线段树,好写好想,或者可以用单调队列。

代码:

//线段树#include <iostream>#include <cstring>#include <cstdio>#include <algorithm>#include <queue>using namespace std;const int N = 1000009;    int dw1[N];//向下最大  int dw2[N];//向下次大  int up[N];//向上最大  int m[2][N<<2];//线段树的最值  int n, mm;vector<pair<int,int> >v[N];void dfs1(int s)//向下最大和向下次大  {      dw1[s]=dw2[s]=0;     int ss,dd;      for(int i=0;i<v[s].size();i++)      {        ss=v[s][i].first;        dfs1(ss);        dd=v[s][i].second+dw1[ss];        if(dd >= dw1[s])        {            dw2[s]=dw1[s];            dw1[s]=dd;        }        else            dw2[s]=max(dw2[s],dd);    }} void dfs2(int s,int pre,int len)//向上最大  {   int ss,dd;    if(dw1[pre]==dw1[s]+len)        up[s]=len+max(up[pre],dw2[pre]);    else        up[s]=len+max(up[pre],dw1[pre]);    for(int i=0;i<v[s].size();i++)    {        ss=v[s][i].first;        dd=v[s][i].second;        dfs2(ss,s,dd);    }} void build(int l,int r,int k)  {      if(l==r)    {        m[0][k]=m[1][k]=max(up[l],dw1[l]);        return;    }   int mid=(l+r)>>1,ls=k<<1,rs=k<<1|1;    build(l, mid, ls);    build(mid+1, r, rs);    m[0][k]=min(m[0][ls],m[0][rs]);    m[1][k]=max(m[1][ls],m[1][rs]);}int query(int ll,int rr,int l,int r,int k,int id){      if(ll==l && rr==r)         return m[id][k];      int mid=(l+r)>>1,ls=k<<1,rs=k<<1|1;    if(rr<=mid)        return query(ll, rr, l, mid, ls, id);    else         if(ll>mid)             return query(ll, rr, mid+1, r, rs, id);        else             if(id==0)                 return min(query(ll, mid, l, mid, ls, id), query(mid+1, rr, mid+1, r, rs, id));              else                 return max(query(ll, mid, l, mid, ls, id), query(mid+1, rr, mid+1, r, rs, id));  }int main()  {      int a,b;      while(cin>>n>>mm)    {        for(int i=1;i<=n;i++)            v[i].clear();          for(int i=2;i<=n;i++)          {              scanf("%d%d",&a,&b);            v[a].push_back( make_pair(i, b));        }        dfs1(1);          dfs2(1,0,0);            build(1,n,1);          int lft=1,rit=1,ans=1;        while(rit<=n)        {            int t0 = query(lft, rit, 1, n, 1, 0);            int t1 = query(lft, rit, 1, n, 1, 1);            if(t1-t0<mm)            {                ans=max(ans, rit-lft+1);                rit++;            }            else                lft++;        }        cout<<ans<<endl;    }      return 0;  }

//单调队列#include <iostream>#include <cstring>#include <cstdio>#include <algorithm>#include <queue>using namespace std;const maxn=100005;struct Edge{    int v,wei,pre;}edge[maxn*2];int head[maxn],tot;int n,m;int dx[maxn],dy[maxn],d[maxn];int qmin[maxn],qmax[maxn]; void add(int u,int v,int wei){edge[tot].v=a;edge[tot].pre=head[u];edge[tot].wei=wei;head[u]=tot++;}void dfs(int u,int fa,int dis,int *d)  {      for(int i=head[u];i!=-1;i=edge[i].pre)      {          int v=edge[i].v,wei=edge[i].wei;          if(v!=fa)         dfs(v,u,d[v]=dis+wei,d);      }  }void work()//单调队列  {      int ans=0,i,j,front1,front2,rear1,rear2;      front1=rear1=0;      front2=rear2=0;        for(i=1,j=1;j<=n;j++)      {          while(rear1>front1&&d[qmax[rear1-1]]<=d[j]) rear1--;          qmax[rear1++]=j;            while(rear2>front2&&d[qmin[rear2-1]]>=d[j]) rear2--;          qmin[rear2++]=j;            if(d[qmax[front1]]-d[qmin[front2]]>m)          {              ans=max(ans,j-i);              while(d[qmax[front1]]-d[qmin[front2]]>m)              {                  i=min(qmax[front1],qmin[front2])+1;                  while(rear1>front1&&qmax[front1]<i) front1++;                  while(rear2>front2&&qmin[front2]<i) front2++;              }          }      }      ans=max(ans,j-i);      printf("%d\n",ans);  }void init(){tot=0;memset(head,-1,sizeof(head));}int main(){while(cin>>n>>m){init();int x,y;for(int i=2;i<=n;i++){scanf("%d%d",&x,&y);              addEdge(i,x,y);              addEdge(x,i,y);}dfs(1,0,d[1]=0,d);x=1;          for(int i=2;i<=n;i++)              if(d[i]>d[x])            x=i;          dfs(x,0,dx[x]=0,dx);          y=1;        for(int i=2;i<=n;i++)              if(dx[i]>dx[y])            y=i;          dfs(y,0,dy[y]=0,dy);            for(int i=1;i<=n;i++)        d[i]=max(dx[i],dy[i]);          work();      }return 0;}

5.POJ 2152 Fire

题意:给定n个节点组成的树,树有边权,要在一些点上建立消防站,每个点 i 建站都有个cost[ i ],如果不在当前的点上建站,也要依赖其他的消防站,并且距离不超过limit[ i ]。求符合上述条件的最小费用建站方案(n<=1000)

思路:复杂度为O(n^2)的树形DP,据说比较罕见。

因为要依赖其他站点,所以不仅仅只从子树中获取信息,也可能从父亲结点,兄弟结点获取信息,在计算每个点时首先想到枚举。

dp[ i ][ j ]表示i点及其子树都符合情况下i点依赖j点的最小花费,best[ i ]表示以i为根的子树符合题目要求的最小花费。

因为 i 的每个子节点可以和 i 一样依赖j结点,花费是dp[ k ][ j ] - cost[ j ],或者依赖以k为根的树中的某点,花费是best[ k ],最后一定要加上cost[ j ],因为要在 j 结点建站所以要增加花费。所以状态转移:dp[ i ][ j ] = cost[ j ] + sum(min(dp[ k ][ j ] - cost[ j ],best[ k ])) (k为i的子节点,j为枚举的n个点)

代码:
#include <cstdio>#include <vector>#include <cstring>#include <iostream>using namespace std;#define MAX 1100#define INF 2147483647struct node{    int  v,len;}   now;vector<node> tree[MAX];int  n,cur,best[MAX],dp[MAX][MAX];int  dist[MAX][MAX],limit[MAX],cost[MAX];void init(){    for(int i=0;i<=n;i++)    {        tree[i].clear();        best[i]=INF;    }    for(int i=1;i<=n;i++)        for(int j=1;j<=n;j++)            dp[i][j]=INF;}void dfs_dist(int s,int pa,int dis)//记录每个点到其他点的距离{    dist[cur][s]=dis;    for(int i=0;i<tree[s].size();i++)    {        int v=tree[s][i].v,len=tree[s][i].len;        if(v==pa)   continue;        dfs_dist(v,s,dis+len);    }}void dfs(int s,int pa){    for(int i=0;i<tree[s].size();i++)        if(tree[s][i].v!=pa)            dfs(tree[s][i].v,s);        for(int i=1;i<=n;i++)//枚举            if(dist[s][i]<=limit[s])            {                dp[s][i]=cost[i];                for(int j=0;j<tree[s].size();j++)//把子树信息汇总到当前点                {                    int v=tree[s][j].v;                    if(v==pa)   continue;                    dp[s][i]+=min(dp[v][i]-cost[i],best[v]);                }                best[s]=min(best[s],dp[s][i]);//状态转移方程,结果存储在best中            }}int main(){    int a,b,c,t;    cin>>t;    while(t--)    {        cin>>n;        init();        for(int i=1;i<=n;i++)            scanf("%d",&cost[i]);        for(int i=1;i<=n;i++)            scanf("%d",&limit[i]);        for(int i=1;i<n;i++)        {            scanf("%d%d%d",&a,&b,&c);            now.v=b,now.len=c;            tree[a].push_back(now);            now.v=a,now.len=c;            tree[b].push_back(now);        }        for(int i=1;i<=n;i++)        {            cur=i;            dfs_dist(i,0,0);        }        dfs(1,0);        printf("%d\n",best[1]);    }    return 0;    }

6. POJ 1848 Tree

题意:一棵树,添加最少的边使所有节点都位于一个环中且每个节点不能同时位于两个环

思路:特别难想一道题,想出来后特别好写

dp[u][0],所有的点都在环内 ,dp[u][1],除了u外其余的都在环内 ,dp[u][2],以u除了u和与u点相连的一条链以外,四种转移:
A.根R的所有子树自己解决(取状态0),转移到R的状态1。即R所有的儿子都变成每个顶点恰好在一个环中的图,R自己不变。

B.根R的k-1个棵树自己解决,剩下一棵子树取状态1和状态2的最小值,转移到R的状态2。剩下的那棵子树和根R就构成了长度至少为2的一条链。

C.根R的k-2棵子树自己解决,剩下两棵子树取状态1和状态2的最小值,在这两棵子树之间连一条边,转移到R的状态0。
D.根R的k-1棵子树自己解决,剩下一棵子树取状态2(子树里还剩下长度至少为2的一条链),在这棵子树和根之间连一条边,构成一个环,转移到R的状态0。

代码:

#include <iostream>#include <cstdio>#include <algorithm>#include <cstring>#include <vector>using namespace std;#define MAXN 105  #define INF 10000  vector<int> edge[MAXN*2];int vis[MAXN];int dp[MAXN][3];  void dfs(int u){vis[u]=1;      vector<int> temp;      int sum=0,v;      for(int i=0;i<edge[u].size();i++)    {        v=edge[u][i];        if(vis[v]==0)        {            dfs(v);            temp.push_back(v);            sum+=dp[v][0];//所有dp[v][0]的和        }    }    if(temp.size()==0)//已经到子节点    {        dp[u][1]=0;        dp[u][0]=dp[u][2]=INF;        return;    }    dp[u][1]=min(INF,sum);//只有根节点不在环内    dp[u][2]=dp[u][0]=INF;    for(int i=0;i<temp.size();i++)//有一个子节点不在环内    {        v=temp[i];        dp[u][2]=min(dp[u][2],sum-dp[v][0]+min(dp[v][1],dp[v][2]));//其余自己处理        dp[u][0]=min(dp[u][0],sum-dp[v][0]+dp[v][2]+1);//以该子节点有一条链,将此链连到以u为根的树上,即+1    }    for(int i=0;i<temp.size();i++)//有两个不在环内,将这两个加一条边连起来    {        v=temp[i];        for(int j=0;j<temp.size();j++)          {            if(i==j)  continue;            int k=temp[j];            dp[u][0]=min(dp[u][0],sum-dp[v][0]-dp[k][0]+min(dp[v][1],dp[v][2])+min(dp[k][1],dp[k][2])+1);        }    }}int main(){int n,a,b;cin>>n;for(int i=1;i<=n;i++){scanf("%d%d",&a,&b);edge[a].push_back(b);edge[b].push_back(a);}dfs(1);if(dp[1][0]!=INF)cout<<dp[1][0]<<endl;elsecout<<-1<<endl;return 0;}

7.POJ 2486 Apple Tree

题意:节点有权值,在树上走k步,求最大的权值和

思路:dp[root][k][ flag ]表示在子树root中最多走k步,flag是1表示回到root处,flag为0则为不回root。则有如下转移:

A. 从s出发,要回到s,需要多走两步s-t,t-s,分配给t子树k步,其他子树j-k步,都返回
dp[root][j][0]=MAX(dp[root][j][0],dp[root][j-k][0]+dp[son][k-2][0]);
B. 先遍历s的其他子树,回到s,遍历t子树,在当前子树t不返回,多走一步
dp[root][j]][1]=MAX(dp[root][j][1],dp[root][j-k][0]+dp[son][k-1][1]);
C.不回到s(去s的其他子树),在t子树返回,同样有多出两步

dp[root][j][1]=MAX(dp[root][j][1],dp[root][j-k][1]+dp[son][k-2][0]);
代码:

#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;struct node{int u,v,val,next;} tree[505];int dp[205][405][2],head[205],val[205];int len,n,k;void add(int u,int v){tree[len].u=u;tree[len].v=v;tree[len].next=head[u];head[u]=len++;}void dfs(int root,int mark){for(int i=head[root];i!=-1;i=tree[i].next){int son=tree[i].v;if(son==mark)continue;dfs(son,root);for(int j=k;j>=1;j--)for(int t=1;t<=j;t++){dp[root][j][0]=max(dp[root][j][0],dp[root][j-t][1]+dp[son][t-1][0]);dp[root][j][0]=max(dp[root][j][0],dp[root][j-t][0]+dp[son][t-2][1]);dp[root][j][1]=max(dp[root][j][1],dp[root][j-t][1]+dp[son][t-2][1]);}}}int main(){int a,b;while(~scanf("%d%d",&n,&k)){memset(dp,0,sizeof(dp));memset(head,-1,sizeof(head));for(int i=1;i<=n;i++){scanf("%d",&val[i]);for(int j=0;j<=k;j++)dp[i][j][0]=dp[i][j][1]=val[i];}len=0;for(int i=1;i<n;i++){scanf("%d%d",&a,&b);add(a,b);add(b,a);}dfs(1,0);cout<<max(dp[1][k][0],dp[1][k][1])<<endl;}return 0;}


树上背包问题

一个子节点就可以返回m个状态,而一个子节点中只可以选择一个状态进行转移,每个节点有若干个子节点,问题就转换为背包


1.POJ 1947 Rebuilding Roads

题意:求最少删几个边使子树节点个数为P
思路:dp[ s ][ i ]记录s结点,要得到一棵i个节点的子树去掉的最少边数 
考虑其儿子k 
A.不去掉k子树   dp[ s ][ i ] = min( dp[ s ][ j ] + dp[ k ][ i-j ] )  0 <= j <= i 
B.去掉k子树       dp[ s ][ i ] =  dp[ s ][ i ]+1 
      总:          dp[ s ][ i ] = min (min(dp[ s ][ j ] + dp[ k ][ i-j ] ) ,dp[ s ][ i ] + 1 )
代码:
#include <iostream>  #include <cstring>#include <cstdio>using namespace std;    #define MAXN 155#define INF 0x3fffffffint dp[MAXN][MAXN];int son[MAXN],brother[MAXN];int root,n,p;bool father[MAXN];void dfs(int s){for(int i=1;i<=n;i++)dp[s][i]=INF;dp[s][1]=0;int k=son[s];while(k){dfs(k);for(int i=p;i>=1;i--){int temp=dp[s][i]+1;//去掉此子树for(int j=1;j<i;j++)//不去掉此子树temp=min(dp[k][i-j]+dp[s][j],temp);dp[s][i]=temp;}k=brother[k];}}int main(){while(cin>>n>>p){memset(father,0,sizeof(father));        memset(son,0,sizeof(son));        int x,y;for(int i=1;i<n;i++)        {            scanf("%d%d",&x,&y);              brother[y]=son[x];//记录兄弟节点              son[x]=y;//记录子节点             father[y]=1;//记录该点有父亲节点           }        for(int i=1;i<=n;i++)        if(!father[i])       {root=i;       break;       }       dfs(root);              int ans=dp[root][p];       for(int i=1;i<=n;i++)       ans=min(ans,dp[i][p]+1);//除根节点需断与父节点的边       cout<<ans<<endl;}return 0;}

2.HDU 1011 Starship Troopers

题意:n个洞组成一棵树,m个士兵,从1号房间开始攻打,每个洞有a和b值,为拿到价值b必须留下k个士兵使k*20>=a
(留下的士兵不可以再去攻打其他的洞,且必须攻打了前面的洞才可以攻打后面的洞)。问花费m个士兵可以得到的最大价值
思路:dp方程:dp[ p ][ j ] = max(dp[ p ][ j ],dp[ p ][ j-k ] + dp[ son[ p ] ][ k ])
    要特别注意的是,对于一个节点,即使只有1也需要消耗一个士兵来攻打,即一个点p需要的士兵=(cost[ p ]+19) / 20
代码:
#include <iostream>#include <vector>#include <cstring>#include <cstdio>using namespace std;const int maxn=110;int n,m;int cost[maxn],weg[maxn];int dp[maxn][maxn];bool vis[maxn];vector<int> dv[maxn];void init(){for(int i=0;i<=n;i++)dv[i].clear();    memset(dp,0,sizeof(dp));    memset(vis,0,sizeof(vis));    }void dfs(int p){int temp=(cost[p]+19)/20;//1也需要一个士兵来打    for(int i=temp;i<=m;i++)    dp[p][i]=weg[p];    vis[p]=1;    for(int i=0;i<dv[p].size();i++)    {        int t=dv[p][i];        if(vis[t]) continue;        dfs(t);        for(int j=m;j>=temp;j--)            for(int k=1; k<=j-temp; k++)//留下temp攻打p                dp[p][j]=max(dp[p][j],dp[p][j-k]+dp[t][k]);    }}int main(){while(cin>>n>>m){if(n==-1 && m==-1)break;init();for(int i=1;i<=n;i++)            scanf("%d%d",&cost[i],&weg[i]);        for(int i=1;i<n;i++)        {            int u,v;            scanf("%d%d",&u,&v);            dv[u].push_back(v);            dv[v].push_back(u);        }        if(m==0)         {        cout<<0<<endl;        continue;        }        dfs(1);        cout<<dp[1][m]<<endl;}return 0;}

3.POJ 1155 TELE

题意:给定一棵树,1为根结点表示电视台,有m个叶子节点表示客户,有n-m-1个中间节点表示中转站,每条树边有权值。现在要在电视台播放一场比赛,每个客户愿意花费cost[i]的钱观看,而从电视台到每个客户也都有个费用,并且经过一条边只会产生一个费用。问电视台不亏损的情况最多有几个客户可以看到比赛?1<=n<=1000,1<=m<=n-1;
思路:dp[ i ][ j ] 表示根节点为 i 时,有j个用户的最大剩余费用
 转移:dp[ i ][ j ] = max(dp[ i ][ j ] ,dp[ i ][ k ] + dp[ son ][ j-k ] - w[ i ][ son ]
代码:
#include <iostream>#include <cstdio>#include <cstring>#include <algorithm>#define INF 100000000#define MAXN 3005using namespace std;int n,m,len=0;int num[MAXN],dp[MAXN][MAXN],temp[MAXN];struct node{int now,val,next;} tree[3*MAXN];int head[MAXN];void add(int i,int x,int y)  {    tree[len].now=x;    tree[len].val=y;    tree[len].next=head[i];    head[i]=len++;}void dfs(int root){for(int i=head[root];i!=-1;i=tree[i].next){int p=tree[i].now;dfs(p);for(int j=1;j<=num[root];j++)temp[j]=dp[root][j];for(int j=0;j<=num[root];j++)for(int k=1;k<=num[p];k++)dp[root][k+j]=max(dp[root][k+j],temp[j]+dp[p][k]-tree[i].val);num[root] += num[p];}}int main(){while(cin>>n>>m){memset(head,-1,sizeof(head));int k,a,b;for(int i=1;i<=n-m;i++){scanf("%d",&k);for(int j=1;j<=k;j++){scanf("%d%d",&a,&b);add(i,a,b);}}for(int i=1;i<=n;i++)for(int j=1;j<=m;j++)dp[i][j]=-INF;for(int i=n-m+1;i<=n;i++){num[i]=1;scanf("%d",&dp[i][1]);}dfs(1);for(int i=m;i>=0;i--)if(dp[1][i]>=0){cout<<i<<endl;break;}}return 0;}

4.HDU 1561 The more,the better

题意:给定n个地点,每个地点藏有cost[i]的宝物,取得某些宝物有时需要先取其他宝物,选m个地点最多可以选多少宝物

思路:dp[ i ] [ j ]  以节点i为跟,取j个(包括i,即dp[i][1]=V[i])所能得到的最大值
      dp[ i ][ j ] = max( dp[ i ][ j ] , dp[ i ][ k ] + dp[ son ][ j-k ])
代码:
#include <iostream>#include <cstdio>#include <cmath>#include <cstring>#include <algorithm>#include <vector>using namespace std;const int maxn = 205;int w[maxn],dp[maxn][maxn];vector<int> son[maxn];int n,m;void init(){    memset(dp,-1,sizeof(dp));    for(int i=0;i<=n;i++)    {        dp[i][0]=0;        son[i].clear();    }}void dfs(int root){if(!son[root].size()){dp[root][1]=w[root];return;}for(int i=0;i<son[root].size();i++)dfs(son[root][i]);for(int i=0;i<son[root].size();i++)        for(int j=m;j>=0;j--)            for(int k=0;k<=j;k++)                if(dp[son[root][i]][k]!=-1 && dp[root][j-k]!=-1)                    dp[root][j]=max(dp[root][j],dp[son[root][i]][k]+dp[root][j-k]);if(root!=0){//最后要加上根节点的值,因为要求攻占了根节点才能往下攻占,0节点除外        for(int j=m;j>=0;j--)//处理一下必须攻占根节点才能攻占剩下的子节点的条件,逆序正好可以处理完            if(dp[root][j-1]!=-1)                dp[root][j]=dp[root][j-1]+w[root];    }}int main(){while(cin>>n>>m && n && m){init();for(int i=1;i<=n;i++){int k;scanf("%d%d",&k,&w[i]);son[k].push_back(i);}w[0]=0;dfs(0);cout<<dp[0][m]<<endl;}return 0;}

5.HDU 4003 Find Metal Mineral

题意: 给一棵n个节点的树,每条边都有一个花费, 有k个机器人从S点出发, 让机器人遍历所有边,求最少花费值
思路:dp[ pos ][ num ]表示以pos根的子树下,用num个机器人,得到的最小值
当num==0的时候,dp[ pos ][ 0 ]表示用一个机器人去走完所有子树,最后又回到pos这个节点
转移:dp[ pos ][ num ] = min∑{ dp[ pos_j ][ num_j ] + w_j }(pos_j是pos的所有儿子)
当num_j==0的时候,特别处理
使用一维数组的分组背包伪代码如下:
for 所有的组 i
    for v=V to 0
        for 所有的k属于组i
            f[ v ] = max{ f[ v ] ,f[ v-c[ k ] ] + w[ k ] } 
要把num个机器人分给所有它的儿子,状态太多,用分组背包
代码:
#include <cstdio>#include <cstring>#include <iostream>#include <algorithm>using namespace std;#define INF 0x3f3f3f3fint n,s,k;int dp[10001][11]; struct Edge{    int ed,val,nxt;}edge[20001];int cnt,head[10001]; void add_edge(int sta,int ed,int val){    edge[cnt].ed=ed;    edge[cnt].val=val;    edge[cnt].nxt=head[sta];    head[sta]=cnt++;}void dfs(int pos,int fa){bool flag=0;    for(int i=head[pos];~i;i=edge[i].nxt)        if(edge[i].ed!=fa)        {            dfs(edge[i].ed,pos);            flag=1;    }    if(!flag) return;//3个for就对应分组背包的那3个循环    for(int i=head[pos];~i;i=edge[i].nxt)    {        int t=edge[i].ed;        if(t==fa)continue;        for(int j=k;j>=0;j--)            if(j==0)            dp[pos][0]+=dp[t][0]+2*edge[i].val;            else            {                dp[pos][j]+=dp[t][0]+2*edge[i].val;                for(int u=1;u<=j;u++)                    dp[pos][j]=min(dp[pos][j],dp[pos][j-u]+dp[t][u]+u*edge[i].val);            }    }    }void init(){memset(head,-1,sizeof(head));    memset(edge,0,sizeof(edge));    memset(dp,0,sizeof(dp));    cnt=0; }int main(){    while(cin>>n>>s>>k)    {        init();        for(int i=1;i<n;i++)        {            int sta,ed,val;            scanf("%d%d%d",&sta,&ed,&val);            add_edge(sta,ed,val);            add_edge(ed,sta,val);        }        dfs(s,0);        printf("%d\n",dp[s][k]);    }}

删点或边类


1.POJ 3107 Godfather

题意:删点,使剩下的分支中最大的节点数最小,即求树的重心
思路:深搜一次记录到叶子节点距离,再进行枚举求最大值,再更新答案
代码:
#include <iostream>#include <cstdio>#include <cstring>#include <algorithm>using namespace std;const int maxn=50010;  int N;int v[maxn*2],next[maxn*2],head[maxn],E;int vis[maxn];int num[maxn],dp[maxn];void add(int a,int b){    v[E]=b;    next[E]=head[a];    head[a]=E++;}void dfs_num(int n,int from){    num[n]=1;    for(int i=head[n];i!=-1;i=next[i])    {        int k=v[i];        if(k==from) continue;        dfs_num(k,n);        num[n]+=num[k];    }}void dfs_node(int n,int from){    dp[n]=0;    for(int i=head[n];i!=-1;i=next[i])    {        int k=v[i];        if(k==from)        dp[n]=max(dp[n],N-num[n]);        else        {            dp[n]=max(dp[n],num[k]);            dfs_node(k,n);        }      }}void init(){E=0;    memset(head,-1,sizeof(head));}int main(){    while(cin>>N)      {    init();    int u,v;        for(int i=1;i<=N-1;i++)        {            scanf("%d%d",&u,&v);            add(u,v);            add(v,u);        }        dfs_num(1,-1);        dfs_node(1,-1);        int i,k;        for(i=k=N;i>=1;i--)        if(k>dp[i])        k=dp[i];        for(i=1;i<=N;i++)        if(dp[i]==k)        {            cout<<i;            break;        }        for(i=i+1;i<=N;i++)        {            if(dp[i]==k)            printf(" %d",i);        }        cout<<endl;    }      return 0;  }  

2. POJ 2378 Tree Cutting

题意:树状图n个点,求去掉哪个点,使剩下的每个连通子图中点的数量不超过n/2,按升序输出。n<=10000
思路:dfs建立树,对于节点x,记a[ x ]为包含自己在内 x 的子树中的结点个数。
如果一个点可以被去掉,那么n - a[ x ],以及所有a[ j ](j是x的子树的根节点)都不大于n/2。
dfs时已经标记了每个点是否能作为结果res。最后顺序输出
代码:
#include <iostream>#include <cstdio>#include <cstring>#include <vector>#include <algorithm>using namespace std;const int maxn=10005;bool vis[maxn],res[maxn];int a[maxn],half,n;vector<int> v[maxn];int dfs(int x){    a[x]=1;    vis[x]=1;      bool ok=1;      for(int i=0;i<v[x].size();i++)      {        int j=v[x][i];        if(!vis[j])        {            int temp=dfs(j);            if(temp>half)                ok=0;            a[x]+=temp;        }    }    if(ok && n-a[x] <=half)        res[x]=1;    return a[x];}int main(){      cin>>n;    for(int i=1;i<n;i++)      {        int a,b;        scanf("%d%d",&a,&b);        v[a].push_back(b);        v[b].push_back(a);    }    half=n/2;    vis[1]=1;    dfs(1);    for(int i=1;i<=n;i++)        if(res[i])            cout<<i<<endl;    return 0;  }  

未完待续~~~~

0 0
原创粉丝点击