『专题』树形DP

本文深入讲解树形动态规划(Tree DP)的基本概念、典型应用场景及实现细节,涵盖常见树形DP问题、删点或删边类问题、树形背包问题等,并提供了多个实例解析。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

题目链接:http://blog.youkuaiyun.com/liuqiyao_01/article/details/8477730
(在这谢谢dalao的整理)

常见树形dp

 1.Hdu 1520 Anniversary party 
 题意:每个节点有权值,子节点和父节点不能同时选,问最后能选的最大价值是多少?
 
 转移方程解释:
 dp[root][0]+=max(dp[vec[root][j]][1],dp[vec[root][j]][0]);
 //不要当前根节点,要子点大呢?还是不要子节点大呢?
 dp[root][1]+=dp[vec[root][j]][0];
 //要当前根节点,不要子节点的值
typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=6050;


 vector<int> vec[maxn];
 int f[maxn];
 int hap[maxn];
 int dp[maxn][2];

 void dfs(int root)
 {
     int len=vec[root].size();
     dp[root][1]=hap[root];

     for(int j=0;j<len;j++)
        dfs(vec[root][j]);

     for(int j=0;j<len;j++)
     {
         dp[root][0]+=max(dp[vec[root][j]][1],dp[vec[root][j]][0]);
         dp[root][1]+=dp[vec[root][j]][0];
     }
 }
int main()
{
    int n;
    int a,b;
    while(scanf("%d",&n)!=EOF)
    {
        for(int i=1;i<=n;i++)
        {
            scanf("%d",&hap[i]);
            vec[i].clear();
            f[i]=-1;
            dp[i][0]=dp[i][1]=0;
        }

        while(scanf("%d%d",&a,&b))
        {
            if(a==0&&b==0)break;

            f[a]=b;
            vec[b].push_back(a);
        }
        a=1;
        while(f[a]!=-1) a=f[a];
        dfs(a);
        printf("%d\n",max(dp[a][1],dp[a][0]));
    }
    return 0;
}

2.Hdu 2196 Computer
题意: 经典题,求树每个点到其他点的最远距离.
解释: 首先转化为有根树,先处理当前节点到子节点的次长距离和最长距离,一次dfs,再处理当前节点从父节点走去的最长距离.二次dfs.
ps:为什么做次长距离?
   可以在下图看到,绿色节点到子节点最长距离为2,父节点红色为3,可是当我dp绿色节点点时,发现父节点经过自己,那么这最长距离就不成立了,应该通过蓝色次长距离得到最长节点为3.

这里写图片描述


typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=10010;

 struct Node
 {
     int to;
     int next;
     int len;
 }edge[maxn*2];
 int head[maxn];
 int tol;
 int dp1[maxn];//该节点到叶子节点最大的距离
 int dp2[maxn];//次大距离
 int maxid[maxn];//最大距离对应的序号
 int smaxid[maxn];//次大的序号

 void init()
 {
     tol=0;
     memset(head,-1,sizeof(head));
 }

 void add(int a,int b,int len)
 {
     edge[tol].to=b;
     edge[tol].len=len;
     edge[tol].next=head[a];
     head[a]=tol++;

     edge[tol].to=a;
     edge[tol].len=len;
     edge[tol].next=head[b];
     head[b]=tol++;
 }

 void dfs1(int u,int p)
 {
     dp1[u]=0;
     dp2[u]=0;

     for(int i=head[u];i!=-1;i=edge[i].next)
     {
         int v=edge[i].to;
         if(v==p) continue;
         dfs1(v,u);

         if(dp2[u]<dp1[v]+edge[i].len)
         {
             dp2[u]=dp1[v]+edge[i].len;
             smaxid[u]=v;

             if(dp2[u]>dp1[u])
             {
                 swap(dp2[u],dp1[u]);
                 swap(smaxid[u],maxid[u]);
             }
         }
     }
 }

 void dfs2(int u,int p)
 {
     for(int i=head[u];i!=-1;i=edge[i].next)
     {
         int v=edge[i].to;
         if(v==p)continue;

         if(v==maxid[u])
         {
              if(edge[i].len+dp2[u]>dp2[v])
              {
                  dp2[v]=edge[i].len+dp2[u];
                  smaxid[v]=u;

                  if(dp2[v]>dp1[v])
                  {
                      swap(dp2[v],dp1[v]);
                      swap(smaxid[v],maxid[v]);
                  }

              }
         }
         else
         {
               if(edge[i].len+dp1[u]>dp2[v])
               {
                   dp2[v]=edge[i].len+dp1[u];
                   smaxid[v]=u;

                   if(dp2[v]>dp1[v])
                   {
                       swap(dp2[v],dp1[v]);
                       swap(maxid[v],smaxid[v]);
                   }
               }
         }
         dfs2(v,u);
     }
 }
int main()
{
    int n;
    int v,len;
    while(scanf("%d",&n)!=EOF)
    {
        init();
        for(int i=2;i<=n;i++)
        {
            scanf("%d%d",&v,&len);
            add(i,v,len);
        }
        dfs1(1,-1);

        dfs2(1,-1);
        for(int i=1;i<=n;i++)
            printf("%d\n",dp1[i]);
    }

    return 0;
}

删点或者删边类树形DP

  1.Hdu 3586 Information Disturbing 
  题意:给定n个敌方据点,1为司令部,其他点各有一条边相连构成一棵树,每条边都有一个权值cost表示破坏这条边的费用,叶子节点为前线。现要切断前线和司令部的联系,每次切断边的费用不能超过上限limit,问切断所有前线与司令部联系所花费的总费用少于m时的最小limit
  解释:首先上限我们是不知道的,但我们只知道切除当前边的最大上限值 limit,所以我们得二分,虽然可以二分枚举limit,可是我们发现题目中,切当前边符合<=limit的边,加起来的总和不超过m,所以在二分时,我们dp[i]存的值就是sum,若sum<=m,则r=mid.
  对于dp方程
  我们必须切每条边,我们可以这样处理,因为是棵树,我们可以把边权附到子节点的点权去.
  然后dp[i]代表切除当前点的最小花费. 然后dp上来即可..
typedef long long ll;
const int INF=1000000+5;
const int maxn=1e5+10;

int val[maxn];
ll dp[maxn];
struct Node
{
    int to;
    int next;
} edge[maxn*2];

int tot;
int head[maxn];

int u,v,w;
void init()
{
    tot=0;
    memset(head,-1,sizeof(head));
}
void addedge(int u,int v)
{
    edge[tot].to=v;
    edge[tot].next=head[u];
    head[u]=tot++;

    edge[tot].to=u;
    edge[tot].next=head[v];
    head[v]=tot++;
}
int n,m;

void dfs(int u,int pre,int tt)
{
    dp[u]=0;

    bool flag=true;//判断是不是子节点.


    for(int j=head[u]; j!=-1; j=edge[j].next)
    {
        int v=edge[j].to;
        if(v==pre)continue;

        flag=false;
        dfs(v,u,tt);

        dp[u]+=dp[v];
    }
    if(flag)
    {
        if(val[u]<=tt)dp[u]=val[u];
        else dp[u]=INF;

        //cout<<u<<" "<<dp[u]<<endl;
        return ;
    }

    if(u!=1&&val[u]<=tt&&(val[u]<dp[u]))
    {
        dp[u]=val[u];
    }
    //cout<<u<<" "<<dp[u]<<endl;


}
int main()
{
    while(scanf("%d%d",&n,&m),n+m)
    {
        init();
        int lim=0;
        for(int i=1; i<n; i++)
        {
            scanf("%d%d%d",&u,&v,&w);

            lim=max(lim,w);
            val[v]=w;
            addedge(u,v);
        }


        int l=1;
        int r=lim;

        int ans=-1;
        while(l<=r)
        {
            int mid=(l+r)/2;

            dfs(1,-1,mid);

            //cout<<"----->"<<endl;
            if(dp[1]<=m)
            {
                ans=mid;
                r=mid-1;
            }
            else l=mid+1;
        }

        printf("%d\n",ans);
    }
    return 0;
}

2、Poj 3107 Godfather  
题意:删点,使剩下的分支中最大的节点数最小.
解释:这道题求的是重心,树的重心是切除当前节点后,分开的每一块的最大数最小,就是树的重心了,所以对于每次先预处理子节点个个数num[v],然后dp[u]=min(dp[u],num[v]),即可知道切除了这个点之后,下面子节点的几块的大小,可是还有父节点的那块怎么办? 很简单啊.. dp[u]=min(dp[u],n-num[u])不就是上面那一块的大小了咯..这就求出来了。
typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=5e4+10;

 int n;
 int u,v;
 struct Node
 {
     int to;
     int next;
 }edge[maxn*2];
 int tot;

 int head[maxn];
 void addedge(int u,int v)
 {
       edge[tot].to=v;
       edge[tot].next=head[u];
       head[u]=tot++;

       edge[tot].to=u;
       edge[tot].next=head[v];
       head[v]=tot++;
 }
 int top;

 int dp[maxn];
 int num[maxn];

 int ans;

 void dfs(int u,int pre)
 {
     dp[u]=0;
     num[u]=1;

     for(int j=head[u];j!=-1;j=edge[j].next)
     {
         int v=edge[j].to;
         if(v==pre) continue;

         dfs(v,u);
         dp[u]=max(dp[u],num[v]);
         num[u]+=num[v];
     }
     dp[u]=max(dp[u],n-num[u]);

     if(dp[u]<ans)
     {
         ans=dp[u];
     }
 }
int main()
{
  while(scanf("%d",&n)!=EOF)
  {
       top=0;
       ans=INF;
       memset(head,-1,sizeof(head));

       for(int i=1;i<n;i++)
       {
           scanf("%d%d",&u,&v);
           addedge(u,v);
       }

       dfs(1,-1);

       bool flag=true;
       for(int i=1;i<=n;i++)
       {
           if(dp[i]==ans)
           {
               if(flag){printf("%d",i);flag=false;}
               else printf(" %d",i);
           }
       }
       printf("\n");


  }
    return 0;
}

3、Poj 2378 Tree Cutting
题意: 删点,使剩下的分支中有最大节点数的分支小等于总数一半,问有几种方案。
解释:同上.dp【u】已经是处理好的,暴力一下dp【u】即可.

typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=1e4+10;

 int n;
 int u,v;
 int dp[maxn];
 int num[maxn];
 struct Node
 {
  int to;
  int next;
 }edge[maxn*2];

 int tot;
 int head[maxn];

 void init()
 {
     tot=0;
     memset(head,-1,sizeof(head));
 }

 void addedge(int u,int v)
 {

      edge[tot].to=v;
      edge[tot].next=head[u];
      head[u]=tot++;

      edge[tot].to=u;
      edge[tot].next=head[v];
      head[v]=tot++;
 }

 void dfs(int u,int pre)
 {
     dp[u]=0;
     num[u]=1;

     for(int j=head[u];j!=-1;j=edge[j].next)
     {
         int v=edge[j].to;
         if(v==pre)continue;
         dfs(v,u);

         dp[u]=max(dp[u],num[v]);
         num[u]+=num[v];
     }
     dp[u]=max(dp[u],n-num[u]);
 }

int main()
{
    while(scanf("%d",&n)!=EOF)
    {
        init();
        for(int i=1;i<n;i++)
        {
            scanf("%d%d",&u,&v);
            addedge(u,v);
        }
        dfs(1,-1);

        bool flag=true;
        for(int i=1;i<=n;i++)
        {
             if(dp[i]<=n/2){printf("%d\n",i);flag=false;}
        }

        if(flag)printf("NONE\n");

    }

    return 0;
}

4、Poj 1655 Balancing Act  
  题意:删点,使剩下的分支中最大的节点数最小.
  解释:也同上.只是答案上的处理,dp[u]已经记录好了.

typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=20010;

 int T;
 int n;
 int u,v;
 vector<int> G[maxn];
 void add(int u,int v)
 {
     G[u].push_back(v);
     G[v].push_back(u);
 }
 int dp[maxn];//对于当前节点的最大的分块树;
 int num[maxn];//对于当前节点的子树个数;

 void dfs(int u,int pre)
 {
     dp[u]=0;
     num[u]=1;

     for(int j=0;j<G[u].size();j++)
     {
         int v=G[u][j];

         if(v==pre)continue;
         dfs(v,u);

         dp[u]=max(dp[u],num[v]);
         num[u]+=num[v];
     }
     dp[u]=max(dp[u],n-num[u]);
 }
int main()
{
   scanf("%d",&T);
   while(T--)
   {
       scanf("%d",&n);
       for(int i=0;i<=n;i++)G[i].clear();
       for(int i=1;i<n;i++)
       {
           scanf("%d%d",&u,&v);
           add(u,v);
       }
       dfs(1,-1);

       int ans=dp[1];
       int ansid=1;

       for(int i=1;i<=n;i++)
       {
           if(ans>dp[i])
           {
               ans=dp[i];
               ansid=i;
           }
       }
       printf("%d %d\n",ansid,ans);

   }



    return 0;
}

5、Poj 3140 Contestants Division  
  题意:删边,求删去某条边后两个分支的最小差异值
  解释:做了上面的题之后其实都水到渠成了,这道也很水了,1次dfs就行了.因为通过n-num[u]就知道父节点信息.这就没什么好说了.
typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=1e5+10;



 ll abs1(ll x)
 {
     if(x<0)return -x;
     return x;
 }
 int n,m;
 ll val[maxn];
 ll dp[maxn];
 struct Node
 {
    int to;
    int next;
 }edge[maxn*2];

 int tot;
 int u,v;
 int head[maxn];

 ll sum;
 ll ans;
 bool flag;
 void init()
 {
     flag=true;
     sum=0;
     tot=0;
     memset(head,-1,sizeof(head));
 }
 void addedge(int u,int v)
 {
     edge[tot].to=v;
     edge[tot].next=head[u];
     head[u]=tot++;

     edge[tot].to=u;
     edge[tot].next=head[v];
     head[v]=tot++;
 }


 void dfs(int u,int pre)
 {
     dp[u]=val[u];

     for(int j=head[u];j!=-1;j=edge[j].next)
     {
         int v=edge[j].to;

         if(v==pre) continue;

         dfs(v,u);

         dp[u]+=dp[v];
     }

     if(flag)
     {
         ans=abs1(sum-2*dp[u]);
         flag=false;
     }
     else
     {
         ans=min(ans,abs1(sum-2*dp[u]));
     }
 }
int main()
{
    int Tcase=0;
    while(scanf("%d%d",&n,&m),n+m)
    {
        Tcase++;
        init();
        for(int i=1;i<=n;i++)
        {
            scanf("%I64d",&val[i]);
            sum+=val[i];
        }
        for(int i=1;i<=m;i++)
        {
            scanf("%d%d",&u,&v);
            addedge(u,v);
        }
        dfs(1,-1);

        printf("Case %d: %I64d\n",Tcase,ans);


    }

    return 0;
}

(这属于常见形dp)6、Poj 1741 Tree(难)  
题意:经典题,求树上两点间距离小等于K的方案数,树上分治。
解释:树分治,这道题比较难啊只有按上面顺序做完才可能对这题有点理解,我也不太说得清,可以参照kuangbin巨巨的代码,我也是半理解吧.其他的参考论文吧,我就做了一道难题...
const int MAXN = 10010;
const int INF = 0x3f3f3f3f;
struct Edge
{
    int to,next,w;
}edge[MAXN*2];
int head[MAXN],tot;
void init()
{
    tot = 0;
    memset(head,-1,sizeof(head));
}
void addedge(int u,int v,int w)
{
    edge[tot].to = v; edge[tot].w = w;
    edge[tot].next = head[u];head[u] = tot++;
}
bool vis[MAXN];
int size[MAXN],dep[MAXN];
int le,ri;
int dfssize(int u,int pre)
{
    size[u] = 1;
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        int v = edge[i].to;
        if(v == pre || vis[v])continue;
        size[u] += dfssize(v,u);
    }
    return size[u];
}
int minn;
//找重心
void getroot(int u,int pre,int totnum,int &root)
{
    int maxx = totnum - size[u];
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        int v = edge[i].to;
        if(v == pre || vis[v])continue;
        getroot(v,u,totnum,root);
        maxx = max(maxx,size[v]);
    }
    if(maxx < minn){minn = maxx; root = u;}
}
void dfsdepth(int u,int pre,int d)
{
    dep[ri++] = d;
    //cout<<"->"<<u<<endl;
    for(int i = head[u];i != -1;i = edge[i].next)
    {
        int v = edge[i].to;

        if(v == pre || vis[v])continue;
        dfsdepth(v,u,d+edge[i].w);
    }
}
int k;
int getdep(int a,int b)
{
    sort(dep+a,dep+b);
    int ret = 0, e = b-1;
    for(int i = a;i < b;i++)
    {
        if(dep[i] > k)break;
        while(e >= a && dep[e] + dep[i] > k)
        {
            e--;
        }
        //这个地方是个技巧,我们先预处理在子节点内所有 dep[e] + dep[i]<= k的情况
        //然后再用把当前根节点的遍历一遍,减去这些情况,就得到跨当前根节点的个数了.
         //cout<<"e:"<<e<<" "<<dep[e]<<"-----"<<"i:"<<i<<" "<<dep[i]<<endl;
        ret += e - a + 1;
        if(e >= i)ret--;
    }
    return ret>>1;
}
int solve(int u)
{
    int totnum = dfssize(u,-1);
    int ret = 0;
    minn = INF;
    int root;
    getroot(u,-1,totnum,root);
   // cout<<"root--->"<<root<<endl;
    vis[root] = true;
    for(int i = head[root];i != -1;i = edge[i].next)
    {
        int v = edge[i].to;
        if(vis[v])continue;
        ret += solve(v);
    }
    le = ri = 0;
    for(int i = head[root];i != -1;i = edge[i].next)
    {
        int v = edge[i].to;
        if(vis[v])continue;
        dfsdepth(v,root,edge[i].w);
        //cout<<"------------>"<<endl;
        ret -= getdep(le,ri);
        le = ri;
    }
    ret += getdep(0,ri);
    //cout<<"------------>"<<ret<<endl;
    for(int i = 0;i < ri;i++)
    {
        if(dep[i] <= k)ret++;
        else break;
    }
    vis[root] = false;
    return ret;
}

int main()
{
    int n;
    int u,v,w;
    while(scanf("%d%d",&n,&k) == 2)
    {
        if(n == 0 && k == 0)break;
        init();
        for(int i = 1;i < n;i++)
        {
            scanf("%d%d%d",&u,&v,&w);
            addedge(u,v,w);
            addedge(v,u,w);
        }
        memset(vis,false,sizeof(vis));
        printf("%d\n",solve(1));
    }
    return 0;
}

树形背包问题(在树上进行分组背包处理)

引入kuangbin巨巨的分组背包思想:


状态转移,使用的“分组背包”思想。 使用一维数组的“分组背包”伪代码如下: for 所有的组i for v=V..0 for 所有的k属于组i f[v]=max{f[v],f[v-c[k]]+w[k]}

个人感想:
      理解分组背包,整是开始分组背包的前提了.好了,我根据我做的题目我一个个列出来我初步的思想. (对于刚开始的时候我某些是借鉴别人的,之后就越做越顺了…)


   1、Poj 1155 TELE 
   题意:某电台要广播一场比赛,该电台网络是由N个网点组成的一棵树,其中M个点为客户端,其余点为转发站。客户端i愿支付的钱为pay[i],每一条边需要的花费固定,问电台在保证不亏损的情况下,最多能使多少个客户端接收到信息?
   解释:这道题我刚开始也是半懂的感觉,所以代码是抄人家的,因为我也不太会哦,只有理解了那个分组思想之后才能明白.
   dp[i][j]:从当前u几点供给j个用户的获得的最大利润.  
   用上面的分组背包思想做就好了,莫非就是选和不选的问题,然后dp好了之后 遍历一下根节点哪一次dp[root][j]>=0的即可.. 就可以了.说得有点迷,看下代码就理解了.
#define MAXN 3010

struct Node{
	int v,w,next;
}edge[MAXN*MAXN];

int dp[MAXN][MAXN];
int num[MAXN];	//容量
int head[MAXN];

int max(int a,int b)
{
	return a>b?a:b;
}

void dfs(int u)
{
	int i,j,k,v;
	for(i=head[u];i!=-1;i=edge[i].next)
	{
		v=edge[i].v;
		dfs(v);
		num[u]+=num[v];

		for(j=num[u];j>=1;j--)	//背包 需要逆向
			for(k=1;k<=num[v];k++)
				dp[u][j]=max(dp[u][j],dp[u][j-k]+dp[v][k]-edge[i].w);
	}
}
int v,w;
int main()
{
	int i,j,n,m,k,cnt=0;
	memset(head,-1,sizeof(head));
	scanf("%d%d",&n,&m);
	for(i=1;i<=n-m;i++)
	{
		scanf("%d",&k);
		while(k--)
		{
			scanf("%d%d",&v,&w);
			edge[cnt].v=v;
			edge[cnt].w=w;
			edge[cnt].next=head[i];
			head[i]=cnt++;
		}
	}
    for(i=1;i<=n;i++)
        for(j=1;j<=m;j++)
            dp[i][j]=-1e9;//因为状态有负值 所以不能为0
	for(i=n-m+1;i<=n;i++)
	{
		num[i]=1;
		scanf("%d",&dp[i][1]);
	}
	dfs(1);
	for(i=num[1];i>=0;i--)
		if(dp[1][i]>=0)
			break;
	printf("%d",i);
	return 0;
}

  2、Hdu 1011 Starship Troopers 
  题意:给出每个房间拥有的BUG数和能得到的能量数,然后给出每个房间的联通图,要到下一个房间必须攻破上一个房间,每个士兵最多消灭20个BUG,就算不足20个BUG也要安排一个士兵
  解释:和上面分组背包,一毛一样,可以先预处理每个点的权值,然后再进行dp,而且这里有个坑点就是m==0时要特殊处理,注意下就行了
  dp[u][j]:对于当前节点送j个士兵下去获得的最大权值.
typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=110;

 /*
    简单树形dp

    dp[u][j]:在当前u节点,留下j个兵获去其他路(包括必定在这个节点)取得到的最大金额,
    转移方程: dp[u][s]=max(dp[u][s],dp[u][s-j]+dp[v][j]);,留s-j个在u点获取的最大金额(包括走其他支),剩下j个兵走v获得的最大值.
 */

 struct Node
 {
     int cost;
     int val;
 }room[maxn];

 int dp[maxn][maxn];
 vector<int> G[maxn];
 int n,m;
 bool vis[maxn];


 void dfs(int u)
 {
     vis[u]=true;
     memset(dp[u],0,sizeof(dp[u]));

     if(!room[u].cost&& G[u].size()==1 && u!=1)room[u].cost=1;

     for(int j=room[u].cost;j<=m;j++)
         dp[u][j]=room[u].val;

     for(int j=0;j<G[u].size();j++)
     {
         int v=G[u][j];
         if(vis[v])continue;

         dfs(v);
         for(int s=m;s>=room[u].cost;s--)
         {
             for(int j=1;s-j>=room[u].cost;j++)
             {
                 dp[u][s]=max(dp[u][s],dp[u][s-j]+dp[v][j]);
             }
         }
     }


 }
int main()
{
  while(scanf("%d%d",&n,&m)!=EOF)
  {
      if(n==-1&&m==-1)break;

      for(int i=1;i<=n;i++)G[i].clear();

      int x;
      for(int i=1;i<=n;i++)
      {
          scanf("%d%d",&x,&room[i].val);
          room[i].cost=x/20+(x%20!=0);
      }

      for(int i=0;i<n-1;i++)
      {
          int u,v;
          scanf("%d%d",&u,&v);
          G[u].push_back(v);
          G[v].push_back(u);
      }

      if(m==0)
      {
         printf("0\n");
         continue;
      }

      memset(vis,0,sizeof(vis));
      vis[1]=true;
      dfs(1);
      printf("%d\n",dp[1][m]);
  }

    return 0;
}

3、Poj 1947 Rebuilding Roads 求
题意:最少删除几条边使得子树节点个数为p.
解释:具体模型都和上面相似
dp[u][j]:代表当前节点,获得j个点的最少删边数.
typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=200;

 int N,P;
 vector<int> G[maxn];
 int dp[maxn][maxn];
 int pre[maxn];
 void init()
 {
     for(int i=1;i<=N;i++)
     {
         G[i].clear();
         pre[i]=i;
     }
 }
 void getMap()
 {
     int a,b;
     for(int i=1;i<N;i++)
     {
         scanf("%d%d",&a,&b);
         G[a].push_back(b);
         pre[b]=a;
     }
 }
void dfs(int u)
{
    dp[u][1] = 0;//初始 自己不删边
    for(int i = 0; i < G[u].size(); i++)
    {
        int v = G[u][i];
        dfs(v);
        for(int j = P; j >= 0; j--)
        {
            int t = dp[u][j] + 1;//直接删掉 与 子节点v 相连的边
            for(int k = 0; k <= j; k++)
                t = min(t, dp[u][j-k] + dp[v][k]);
            dp[u][j] = t;
        }
    }
}
 void solve()
 {
     int root;
     for(int i=1;i<=N;i++)
     {
         if(pre[i]==i)
         {
             root=i;
             break;
         }
     }

     memset(dp,0x3f,sizeof(dp));
     dfs(root);
     int ans=INF;
     for(int i=1;i<=N;i++)
     {
         if(i==root)
             ans=min(ans,dp[i][P]);
         else
             ans=min(ans,dp[i][P]+1);
     }
     printf("%d\n", ans);
 }
int main()
{
    while(scanf("%d%d",&N,&P)!=EOF)
    {
          init();
          getMap();
          solve();
    }

    return 0;
}

  4、Hdu 1561 The more, The Better 
  题意:在一棵树上选择若干个点获得的最大价值,选子节点必须先选父节点.
  解释:这不就是刚刚派兵打bugs的题目吗..就是一毛一样啊..没什么好说的啊。。只是要多加一个虚拟节点.所以选点要到m+1链接成有根树就行了..很简单哦.

typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=205;

 vector<int> G[maxn];
 int N,M;
 int v;
 ll dp[maxn][maxn];
 ll val[maxn];
 void dfs(int u,int pre)
 {
      for(int j=0;j<G[u].size();j++)
      {
          int v=G[u][j];
          if(v==pre) continue;

          dfs(v,u);
          for(int t=M+1;t>=1;t--)
          {
              for(int k=1;t-k>=1;k++)
              {
                  dp[u][t]=max(dp[u][t],dp[u][t-k]+dp[v][k]);
              }
          }
      }

 }
int main()
{
    while(scanf("%d%d",&N,&M),(N+M))
    {

         memset(dp,0,sizeof(dp));
         for(int i=0;i<=N;i++)G[i].clear();

         val[0]=0;
         for(int i=1;i<=N;i++)
         {
             scanf("%d%I64d",&v,&val[i]);
             G[i].push_back(v);
             G[v].push_back(i);

             for(int j=1;j<=M+1;j++)
             {
                 dp[i][j]=val[i];
             }
         }
         dfs(0,-1);
         printf("%I64d\n",dp[0][M+1]);
    }

    return 0;
}

 5、Hdu 4003 Find Metal Mineral (推荐,好题) 
 题意:树形DP+选且只能选一个物品的分组背包
 解释:状态转移方程难想,这直接看kuangbin巨巨写的吧,他的分组思想也写在那
 http://www.cnblogs.com/kuangbin/archive/2012/08/29/2661928.html

typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=1e4+10;

 int N,S,K;
 struct Node
 {
    int to;
    int next;
    int w;
 }edge[maxn*2];

 int tot;
 int head[maxn];
 int u,v,w;
 int dp[maxn][15];
 void init()
 {
     tot=0;
     memset(head,-1,sizeof(head));
     memset(dp,0,sizeof(dp));
 }

 void addedge(int u,int v,int w)
 {
     edge[tot].to=v;
     edge[tot].next=head[u];
     edge[tot].w=w;
     head[u]=tot++;

     edge[tot].to=u;
     edge[tot].next=head[v];
     edge[tot].w=w;
     head[v]=tot++;
 }

 void dfs(int u,int pre)
 {

       for(int j=head[u];j!=-1;j=edge[j].next)
       {
           int v=edge[j].to;
           if(v==pre) continue;
           dfs(v,u);
           for(int k=K;k>=0;k--)
           {
                dp[u][k]+=(dp[v][0]+2*edge[j].w);
                for(int t=1;t<=k;t++)
                {
                   dp[u][k]=min(dp[u][k],dp[u][k-t]+dp[v][t]+t*edge[j].w);
                }
           }

       }

 }
int main()
{
    while(scanf("%d%d%d",&N,&S,&K)!=EOF)
    {
          init();
          for(int i=1;i<=N-1;i++)
          {
              scanf("%d%d%d",&u,&v,&w);
              addedge(u,v,w);
          }

          dfs(S,-1);
          printf("%d\n",dp[S][K]);


    }

    return 0;
}

  6、Poj 2486 Apple Tree 
  题意:一颗树,n个点(1-n),n-1条边,每个点上有一个权值,求从1出发,走V步,最多能遍历到的权值
  解释:看巨巨链接,写得太清楚...又容易理解.
  http://blog.youkuaiyun.com/libin56842/article/details/10101807

typedef long long ll;
const int INF=0x3f3f3f3f;
const int maxn=100+10;

 int N,K;
 int val[maxn];

 struct Node
 {
     int to;
     int next;
 }edge[maxn*2];

 int tot;
 int head[maxn];
 int dp[maxn][205][2];
 int u,v;

 void init()
 {
     tot=0;
     memset(dp,0,sizeof(dp));
     memset(head,-1,sizeof(head));
 }
 void addedge(int u,int v)
 {
      edge[tot].to=v;
      edge[tot].next=head[u];
      head[u]=tot++;

      edge[tot].to=u;
      edge[tot].next=head[v];
      head[v]=tot++;
 }
 void dfs(int u,int pre)
 {

     for(int j=head[u];j!=-1;j=edge[j].next)
     {
         int v=edge[j].to;
         if(v==pre) continue;
         dfs(v,u);

         for(int k=K;k>=0;k--)
         {
             for(int t=1;t<=k;t++)
             {

               dp[u][k][1]=max(dp[u][k][1],dp[u][k-t][0]+dp[v][t-1][1]);
               if(t-2>=0)
               {
                   dp[u][k][0]=max(dp[u][k][0],dp[u][k-t][0]+dp[v][t-2][0]);
                   dp[u][k][1]=max(dp[u][k][1],dp[u][k-t][1]+dp[v][t-2][0]);
               }

             }
         }

     }
 }
int main()
{
    while(scanf("%d%d",&N,&K)!=EOF)
    {
        init();
        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];}
        }

        for(int i=1;i<N;i++)
        {
            scanf("%d%d",&u,&v);
            addedge(u,v);
        }
        dfs(1,-1);
        printf("%d\n",max(dp[1][K][0],dp[1][K][1]));

    }

    return 0;
}

 7、Poj 3345 Bribing FIPA  
 题意:现在有n个村子,你想要用收买m个村子为你投票,其中收买第i个村子的代价是val[i]。但是有些村子存在从属关系,如果B从属于A国,则收买了A也意味着买通了B,而且这些关系是传递的。问你最小要付出的代价是多少?
 解释:这道题其实很简单,不过稍微有点难理解
 我是借鉴了
 http://blog.sina.com.cn/s/blog_6635898a0100qrdl.html 大牛的博客,不过dp方程我是自己写的,因为有点懒这道题目的输入好麻烦哦,我就不想写那么多,我就写dp方程,而且我对巨巨的dp方程也有点不赞同,我的思想是相反的,就是dp[i][j] 代表在当前获取j张个投票权的所需要的最小花费.我写的方程和巨巨不一样,看哪个好理解咯.
const int MAX = 205;
const int INF = 99999999;

struct{
    int v, nxt;
}edge[MAX];
int k, edgeHead[MAX];
int n, m, mm;
char name[MAX][105];
int num[MAX], val[MAX];
int tmp[MAX], dp[MAX][MAX];
bool rt[MAX];

void addEdge(int u, int v){
    edge[k].v = v;
    edge[k].nxt = edgeHead[u];
    edgeHead[u] = k ++;
}

void dfs(int u){
    int i, j, k, v;
    if(u!=0)num[u] = 1;
    else num[u]=0;
    dp[u][1] = val[u];
    for(i = edgeHead[u]; i; i = edge[i].nxt)
    {
        v = edge[i].v;
        dfs(v);
        num[u] += num[v];

        for(int j=num[u];j>=0;j--)
        {
            dp[u][j]=min(dp[u][j],val[u]);

            for(int t=0;t<=num[v]&&(j+t)<=num[u];t++)
            {
                 dp[u][j+t]=min(dp[u][j+t],dp[u][j]+dp[v][t]);
            }
        }
    }

}

int search(char *str){
    for(int i = 1; i <= mm; i ++)
        if(!strcmp(str, name[i]))
            return i;
    strcpy(name[mm ++], str);
    return mm-1;
}

int main()
{
    int i, j, u, v, va;
    char str[105];
    while(gets(str) && str[0] != '#'){
        sscanf(str, "%d%d", &n, &m);     // sscanf()函数应用在这里很适合:把str当为输入流。
        memset(name, 0, sizeof(name));
        for(i = 0; i <= n; i ++){
            for(j = 1; j <= n; j ++)
                dp[i][j] = INF;
            dp[i][0] = 0;
            edgeHead[i] = 0;
            rt[i] = true;
        }
        mm = 1, k = 1;
        for(i = 1; i <= n; i ++){
            scanf("%s%d", str, &va);
            u = search(str);
            val[u] = va;
            while(getchar() != '\n'){
                scanf("%s", str);
                v = search(str);
                rt[v] = false;
                addEdge(u, v);
            }
        }
        val[0] = INF;
        for(i = 1; i <= n; i ++)    //  加入总根。
            if(rt[i])
                addEdge(0, i);
        dfs(0);
        printf("%d\n", dp[0][m]);
    }
    return 0;
}

总结

其实树形dp真的很强大,这不是简简单单可以像数位dp一样,套个模板即可…要真正理解还是得花心思的…慢慢来,脚踏实地的做,理解好了比刷题数多更重要…

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值