【ACM/ICPC2013】树形动态规划专题

前言:按照计划,昨天应该是完成树形DP7题和二分图、最大流基础专题,但是由于我智商实在拙计,一直在理解树形DP的思想,所以第二个专题只能顺延到今天了。但是昨天把树形DP弄了个5成懂我是很高兴的!下面我把这7题的解题思想和部分代码分享给大家。

题目一:皇宫看守
问题描述:
太平王世子事件后,陆小凤成了皇上特聘的御前一品侍卫。
皇宫以午门为起点,直到后宫嫔妃们的寝宫,呈一棵树的形状;某些宫殿间可以互相望见。大内保卫森严,三步一岗,五步一哨,每个宫殿都要有人全天候看守,在不同的宫殿安排看守所需的费用不同。
可是陆小凤手上的经费不足,无论如何也没法在每个宫殿都安置留守侍卫。

编程任务:
帮助陆小凤布置侍卫,在看守全部宫殿的前提下,使得花费的经费最少。

数据输入:
输入文件中数据表示一棵树,描述如下:
第1行 n,表示树中结点的数目。
第2行至第n+1行,每行描述每个宫殿结点信息,依次为:该宫殿结点标号i(0<i<=n),在该宫殿安置侍卫所需的经费k,该边的儿子数m,接下来m个数,分别是这个节点的m个儿子的标号r1,r2,…,rm。
对于一个n(0 < n<=1500)个结点的树,结点标号在1到n之间,且标号不重复。

数据输出:
输出文件仅包含一个数,为所求的最少的经费。

样例输入:
6
1 30 3 2 3 4
2 16 2 5 6
3 5 0
4 4 0
5 11 0
6 5 0

样例输出:
25

分析:本来这套题里还有一个题目叫“战略游戏”,抽象后的模型跟这题是一样的,遇到那题的时候我没太理解题解是怎么用树形DP做的,于是我就跳过了。后来遇到这题发现不研究的话不行。题目说的很清楚,用最少的点覆盖所有的点,(二分图里有一个模型叫用最少的点覆盖所有的边,以后再议)。如果是一个图的话,它是个NP完全问题,但题目给出的是个树,避免了后效性的问题,所以可以用动态规划来解决。
给出如下定义

    F[i,0]表示i点不放,且以i为根节点的子树(包括i节点)全部被观察到;
    F[i,1]表示i点不放,且以i为根节点的子树(可以不包括i节点)全部被观察到;
    F[i,2]表示i点放,且以i为根节点的子树全部被观察到;

转移如下
1、由F[i,0]定义可知,设j为i的儿子节点,至少要有一个i的儿子节点是放置守卫的,其余的儿子节点可放可不放,但由于根节点i不放,所以其余的儿子节点如果不放的话,必须保证能被观察到,即F[j][0];所以我们需要枚举必须放置的儿子节点,下面的转移方程描述的很清楚:

    F[i,0] = min{Sigma(min(F[j][0],F[j,2]))+F[k,2]},其中k为枚举的必放的儿子节点,j为除了k之外的儿子节点

2、由F[i,1]定义可知,i可以被观察到也可以不被观察到,但儿子节点必须都要被观察到,转移如下:

    F[i,1] = Sigma(min(F[j,0],F[j,2])) j是i的儿子节点

3、由F[i,2]定义可知,i点放置了守卫,所以对于每个儿子节点都能被观察到,取F[j,0],F[j,1],F[j,2]最小值即可:

    F[i,2] = min(F[j,0],F[j,1],F[j,2]) j是i的儿子节点
    对于叶节点i,F[i,0] = F[i,2] = data[i],F[i,1] = 0;

看了题解我是恍然大悟啊,智商不够,这个转移方程还是比较难想的。

参考代码

 1 //
 2 //  皇宫看守.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <algorithm>
11 #include <cstdio>
12 #include <cstring>
13 #include <string>
14  
15 using namespace std;
16  
17 const int maxn = 1500+10;
18  
19 int f[maxn][3],data[maxn],n,son[maxn][maxn],len[maxn],du[maxn],x,root;
20  
21 void doit(int x)
22 {
23     if (len[x] == 0)
24     {
25         f[x][0] = f[x][2] = data[x];
26         f[x][1] = 0;
27         return;
28     }
29     for (int i = 1;i <= len[x];i++)
30         doit(son[x][i]);
31     f[x][0] = INT_MAX;
32     for (int i = 1;i <= len[x];i++)
33     {
34         int tmp = 0;
35         for (int j = 1;j <= len[x];j++)
36             if (i!=j)
37                 tmp += min(f[son[x][j]][0],f[son[x][j]][2]);
38         f[x][0] = min(f[x][0],tmp+f[son[x][i]][2]);
39     }
40     f[x][1] = 0;
41     for (int i = 1;i <= len[x];i++)
42         f[x][1] += min(f[son[x][i]][0],f[son[x][i]][2]);
43     f[x][2] = data[x];
44     for (int i = 1;i <= len[x];i++)
45         f[x][2] += min(f[son[x][i]][0],min(f[son[x][i]][1],f[son[x][i]][2]));
46 }
47  
48 int main()
49 {
50     scanf("%d",&n);
51     memset(data,0,sizeof(data));             //存每个点放置守卫的代价
52     memset(f,0,sizeof(f));                   //dp数组,F[i,j]含义如上述分析
53     memset(du,0,sizeof(du));                 //存储每个点的入度,用于找出根节点
54     memset(son,0,sizeof(son));               //存储每个点的儿子节点
55     memset(len,0,sizeof(len));               //存储每个点儿子节点个数
56     for (int i = 1;i <= n;i++)
57     {
58         scanf("%d",&x);
59         scanf("%d%d",&data[x],&len[x]);
60         for (int j = 1;j <= len[x];j++)
61         {
62             scanf("%d",&son[x][j]);
63             du[son[x][j]]++;
64         }
65     }
66     for (int i = 1;i <= n;i++)
67         if (du[i] == 0)
68         {
69             root =i;
70             break;
71         }
72     doit(root);
73     printf("%d
",min(f[root][0],f[root][2]));
74 }
View Code

题目二:选课(Vijos1180)
【问题描述】
大学里实行学分。每门课程都有一定的学分,学生只要选修了这门课并考核通过就能获得相应的学分。学生最后的学分是他选修的各门课的学分的总和。
每个学生都要选择规定数量的课程。其中有些课程可以直接选修,有些课程需要一定的基础知识,必须在选了其它的一些课程的基础上才能选修。例如,《数据结构》必须在选修了《高级语言程序设计》之后才能选修。我们称《高级语言程序设计》是《数据结构》的先修课。每门课的直接先修课最多只有一门。两门课也可能存在相同的先修课。为便于表述每门课都有一个课号,课号依次为1,2,3,……。下面举例说明
课号 先修课号 学分
1        无           1
2         1            1
3         2            3
4        无           3
5         2            4
上例中1是2的先修课,即如果要选修2,则1必定已被选过。同样,如果要选修3,那么1和2都一定已被选修过。
学生不可能学完大学所开设的所有课程,因此必须在入学时选定自己要学的课程。每个学生可选课程的总数是给定的。现在请你找出一种选课方案,使得你能得到学分最多,并且必须满足先修课优先的原则。假定课程之间不存在时间上的冲突。
【输入格式】
输入文件的第一行包括两个正整数M、N(中间用一个空格隔开)其中M表示待选课程总数(1≤M≤1000),N表示学生可以选的课程总数(1≤N≤M)。
以下M行每行代表一门课,课号依次为1,2……M。每行有两个数(用一个空格隔开),第一个数为这门课的先修课的课号(若不存在先修课则该项为0),第二个数为这门课的学分。学分是不超过10的正整数。
【输出格式】
输出文件第一行只有一个数,即实际所选课程的学分总数。以下N行每行有一个数,表示学生所选课程的课号。
【输入样例】choose.in
7 4
2 2
0 1
0 4
2 1
7 1
7 6
2 2
【输出样例】choose.out
13
2
6
7
3

分析:这是一个树形背包,在《背包九讲》里面,这种题型被称为泛化物品。用于求解多叉树上的背包问题。
徐持衡大牛在2009年国家队论文《浅谈几类背包题》一文中给出了一种O(n*C)的算法,我昨天痛苦的理解了一天(仍然是智商不够)估计自己才理解了五成。下面我就说说我的理解。
F[i,m]表示以i为根的子树被分配到m的容量所能获得的最大得分。
对于i的每一个儿子j,先将F[i,0~m-v[j]]全部赋值给F[j,0~m-v[j]],按我的理解,就是留出j的空间(因为一定要放),后剩下的m-v[j]个空间的最优值赋值给以j为根的子树去进行递归计算,递归计算完j后,F[i,m] = max(F[i,m],F[j,m-v[j]]+c[j]),v[j]<=m<=M,按我的理解,计算完j子树后,因为F[j,m]的最优值是包含了F[i,m]的最优值计算的,也就是包含了j之前的i的儿子计算出来的最优值,所以只需要在F[i,m]和F[j,m-v[j]]+c[j](放j结点)中取最大值代替F[i,m]即可。
以上就是我昨天一整天思考的结果。或许我理解的不对,或许在大牛眼里这很easy,不过对我来说已经是个很大的进步啦~

参考代码

 1 //
 2 //  选课.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <algorithm>
11 #include <cstdio>
12 #include <cstring>
13 #include <string>
14  
15 using namespace std;
16  
17 const int maxn = 1000+10;
18  
19 int x,n,m,data[maxn],g[maxn][maxn],f[maxn][maxn];
20  
21 void doit(int root,int res)
22 {
23     if (res<=0) return;
24     for (int i = 1;i <= g[root][0];i++)
25     {
26         for (int j = 0;j <= res-1;j++) f[g[root][i]][j] = f[root][j]+data[g[root][i]];    //先把i子树已经算过的最优值赋值给j子树
27         doit(g[root][i],res-1);                                                           //递归计算j子树
28         for (int j = 1;j <= res;j++)
29             f[root][j] = max(f[root][j],f[g[root][i]][j-1]);                              //用j子树最优值来更新i子树
30     }
31 }
32  
33 int main()
34 {
35     scanf("%d%d",&n,&m);
36     for (int i = 1;i <= n;i++)
37     {
38         scanf("%d%d",&x,&data[i]);
39         g[x][0]++;
40         g[x][g[x][0]] = i;
41     }
42     memset(f,0,sizeof(f));
43     doit(0,m);
44     printf("%d
",f[0][m]);
45 }
View Code

题目三:技能树
【问题描述】
玩过Diablo的人对技能树一定是很熟悉的。一颗技能树的每个结点都是一项技能,要学会这项技能则需要耗费一定的技能点数。只有学会了某一项技能以后,才能继续学习它的后继技能。每项技能又有着不同的级别,级别越高效果越好,而技能的升级也是需要 耗费技能点数的。
有个玩家积攒了一定的技能点数,他想尽可能地利用这些技能点数来达到最好的效果。因此他给所有的级别都打上了分,他认为效果越好的分数也越高。现在他要你帮忙寻找一个分配技能点数的方案,使得分数总和最高。
【输入格式】
第一行是一个整数n(1<=n<=20),表示所有不同技能的总数。接下来依次给出n个不同技能的详细情况。每个技能描述包括5行,第一行是该技能的名称,第2行是该技能在技能树中父技能的名称,为空则表示该技能不需要任何的先修技能便能学习。第3行是一个整数L(1<=L<=20),表示这项技能所能拥有的最高级别。第4行共有L个整数,其中第I个整数表示从地I-1级升到第I级所需要的技能点数(0级表示没有学习过)。第5行包括L个整数,其中第I个整数表示从第I-1级升级到第I级的效果评分,分数不超过20。在技能描述之后,共有两行,第1行是一个整数P,表示目前所拥有的技能点数。接下来1行是N个整数,依次表示角色当前习得的技能级别,0表示还未学习。这里不会出现非法情况。
【输出格式】
S,表示最佳分配方案所得的分数总和。
【输入样例】
3
Freezing Arrow
Ice Arrow
3
3 3 3
15 4 6
Ice Arrow
Cold Arrow
2
4 3
10 17
Cold Arrow

3
3 3 2
15 5 2
10
0 0 1
【输出样例】
42

分析:容许我先乐呵一下。因为这是我第一道独立做出来的多叉树形背包的题目!(智商低就是容易满足。。)
这个题目根上一题的区别在于,每个结点的v[i]和c[i]是可以在一定范围内变化的,也就是说,要枚举不同的v[j],然后下放到j子树,然后加上不同的c[j]进行比较。
每个结点有初始等级,等级为0的不能作为根节点,必须要升级,那么对于等级不为0的儿子j,首先将i所获得的所有容量m全部赋值给儿子j,也就是说先不升级。然后再跟等级为0的儿子结点一样,从初始等级+1到最高等级进行枚举,设两个变量p(累加存储所需技能点数)和q(累加存储所获得的分数),进行赋值、递归、更新的三步计算。
然后我一开始WA了一次,为什么呢,我想了一下才明白,枚举每一个等级,将i结点的最优值赋值给儿子j时,必须是任何升级前的初始值,不然每次用上一级的最优值来计算,会出现重复累加的情况,最后答案会变大,所以枚举到儿子结点j时,先用一个tmp数组存下F[i,0~m]的初始值,就是前面几个儿子的最优值,然后对于不升级(初始等级不为0)或者每一次升级,直接将tmp赋值给F[j,0~m-p]即可。
吼吼,再次庆祝一下第一次做出来的题目。话说现在写题解感觉理解又加深了一步。

参考代码

 1 //
 2 //  技能树.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <algorithm>
11 #include <cstdio>
12 #include <cstring>
13 #include <string>
14  
15 using namespace std;
16  
17 const int maxn = 20+10;
18  
19 string na[maxn],st;
20  
21 int f[maxn][100+10],son[maxn][maxn],len[maxn],n,m,l1[maxn],data[maxn],l2[maxn][maxn],sc[maxn][maxn],y,top;
22  
23 int findst(const string st)
24 {
25     for (int i = 1;i <= top;i++)
26         if (st == na[i])
27             return i;
28     top++;
29     na[top] = st;
30     return top;
31 }
32  
33 void doit(int root,int res)
34 {
35     int tmp[100+10];
36     if (res < 0) return;
37     for (int i = 1;i <= son[root][0];i++)
38     {
39         for (int k = 0;k <= res;k++)
40             tmp[k] = f[root][k];                                              //先存储初始最优值
41         if (l1[son[root][i]] != 0)                                            //如果初始等级不为0,那先尝试不升级的最优
42         {
43             for (int k = 0;k <= res;k++)
44                 f[son[root][i]][k] = f[root][k];
45             doit(son[root][i],res);
46             for (int k = 0;k <= res;k++)
47                 f[root][k] = max(f[root][k],f[son[root][i]][k]);
48         }
49         int p = 0,q = 0;
50         for (int j = l1[son[root][i]]+1;j <= len[son[root][i]];j++)           //从当前等级升一级到最高等级进行枚举
51         {
52             p += l2[son[root][i]][j];        //累加所需技能点
53             q += sc[son[root][i]][j];        //累加所得分数
54             for (int k = 0;k <= res-p;k++)
55                 f[son[root][i]][k] = tmp[k];
56             doit(son[root][i],res-p);
57             for (int k = p;k <= res;k++)
58                 f[root][k] = max(f[root][k],f[son[root][i]][k-p]+q);
59         }
60     }
61 }
62  
63 int main()
64 {
65     scanf("%d",&n);
66     top = 0;
67     memset(f,0,sizeof(f));                     //dp数组
68     memset(son,0,sizeof(son));                 //每个结点的儿子结点
69     memset(len,0,sizeof(len));                 //每个结点的最高等级
70     memset(l1,0,sizeof(l1));                   //每个结点的初始等级
71     memset(data,0,sizeof(data));               //每个节点在字符串序列中的编号
72     memset(l2,0,sizeof(l2));                   //每个节点升级所需技能点
73     memset(sc,0,sizeof(sc));                   //每个节点升级所获得加分
74     for (int i = 1;i <= n;i++)
75     {
76         getline(cin,st);
77         getline(cin,st);
78         data[i] = findst(st);
79         getline(cin,st);
80         if (st!="") y = findst(st);
81         else y = 0;
82         son[y][0]++;
83         son[y][son[y][0]] = data[i];
84         scanf("%d",&len[data[i]]);
85         for (int j = 1;j <= len[data[i]];j++)
86             scanf("%d",&l2[data[i]][j]);
87         for (int j = 1;j <= len[data[i]];j++)
88             scanf("%d",&sc[data[i]][j]);
89  
90     scanf("%d",&m);
91     for (int i = 1;i <= n;i++)
92         scanf("%d",&l1[data[i]]);
93     doit(0,m);
94     printf("%d
",f[0][m]);
95 }
View Code

题目四:Ural 1018 二叉苹果树
【问题描述】
有一棵苹果树,如果树枝有分叉,一定是分2叉(就是说没有只有1个儿子的结点)
这棵树共有N个结点(叶子点或者树枝分叉点),编号为1-N,树根编号一定是1。
我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树

现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。
给定需要保留的树枝数量,求出最多能留住多少苹果。
【输入格式】
第1行2个数,N和Q(1<=Q<= N,1<N<=100)。N表示树的结点数,Q表示要保留的树枝数量。接下来N-1行描述树枝的信息。每行3个整数,前两个是它连接的结点的编号。第3个数是这根树枝上苹果的数量。每根树枝上的苹果不超过30000个。
【输出格式】
一个数,最多能留住的苹果的数量。
【输入样例】Etree.in
5 2
1 3 1
1 4 10
2 3 20
3 5 20
【输出样例】Etree.out
21

分析:这种二叉树的树型DP还是比较简单的,首先建树,找根,分如下几种情况讨论:

    1、当然容量为0或为叶结点,则返回
    2、当前容量为1,则返回到左儿子边权值和到右儿子边权值中的最大值
    3、当前容量大于1,分三种情况:1)全部给左儿子,2)全部给右儿子,3)左右儿子至少要给一个,剩下的枚举分配求最大值。在三种情况中取一个最大值即可。

参考代码

  1 //
  2 //  二叉苹果树.cpp
  3 //  树形DP
  4 //
  5 //  Created by TimmyXu on 13-8-3.
  6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
  7 //
  8  
  9 #include <iostream>
 10 #include <cstdio>
 11 #include <cstring>
 12 #include <string>
 13 #include <algorithm>
 14  
 15 using namespace std;
 16  
 17 const int maxn = 100+10;
 18  
 19 int n,q,g2[maxn][maxn],g[maxn][maxn],l[maxn],r[maxn],root,f[maxn][maxn],x,y,du[maxn];
 20 bool vis[maxn];
 21  
 22 void createtree(int x)
 23 {
 24     if (x == 0) return;
 25     vis[x] = true;
 26     for (int i = 1; i<= g2[x][0];i++)
 27         if (!vis[g2[x][i]])
 28         {
 29             if (l[x] == 0) l[x] = g2[x][i];
 30             else r[x] = g2[x][i];
 31         }
 32     createtree(l[x]);
 33     createtree(r[x]);
 34 }
 35  
 36 void doit(int root,int res)
 37 {
 38     if (res == 0)
 39     {
 40         f[root][res] = 0;
 41         return;
 42     }
 43     if (l[root] == 0)
 44     {
 45         f[root][res] = 0;
 46         return;
 47     }
 48     if (f[root][res]>0) return;                                             //记忆化,如果算过则不再计算
 49     if (res == 1)
 50     {
 51         f[root][res] = max(g[root][l[root]],g[root][r[root]]);              //如果只有容量1,则取左右中最大值
 52         return;
 53     }
 54     for (int i = 0;i <= res-2;i++)                                          //如果容量大于1,则先假设左右都被分配到
 55     {
 56         doit(l[root],i);
 57         doit(r[root],res-2-i);
 58         f[root][res] = max(f[root][res],f[l[root]][i]+f[r[root]][res-i-2]);
 59     }
 60  
 61     f[root][res] += g[root][l[root]]+g[root][r[root]];
 62  
 63     doit(l[root],res-1);                                                    //只有左边
 64     f[root][res] = max(f[root][res],f[l[root]][res-1]+g[root][l[root]]);
 65  
 66     doit(r[root],res-1);                                                    //只有右边
 67     f[root][res] = max(f[root][res],f[r[root]][res-1]+g[root][r[root]]);
 68  
 69     return;
 70 }
 71  
 72 int main()
 73 {
 74     scanf("%d%d",&n,&q);
 75     memset(g,0,sizeof(g));
 76     memset(g2,0,sizeof(g2));
 77     memset(f,0,sizeof(f));
 78     memset(l,0,sizeof(l));
 79     memset(r,0,sizeof(r));
 80     memset(vis,false,sizeof(vis));
 81     for (int i = 1;i < n;i++)
 82     {
 83         scanf("%d%d",&x,&y);
 84         scanf("%d",&g[x][y]);
 85         g[y][x] = g[x][y];
 86         g2[x][0]++;
 87         g2[x][g2[x][0]] = y;
 88         g2[y][0]++;
 89         g2[y][g2[y][0]] = x;
 90     }
 91     root = 1;
 92     for (int i = 1;i <= n;i++)
 93         if (g2[i][0] == 2)
 94         {
 95             root  = i;
 96             break;
 97         }
 98     createtree(root);            //建树
 99     doit(root,q);
100     printf("%d
",f[root][q]);
101 }
View Code

题目五:将功补过
【问题背景】
作为间谍专家的Elvis Han受窃取X星球军事中心的秘密情报,他已经成功进入军事中心。但是很不幸的是,在他还没有找到任务需要情报的时候就被发现,这时他清楚他不可能完成任务了,不过还有机会将功补过,也就是得到一些不如任务情报有价值的其他情报,如果得到的情报的总价值大于等于任务情报价值,他也不会受到惩罚。很幸运的是他已经得到的军事中心的地图,情报都是隐藏在各个道路上的,但是他只有时间遍历一定数量的路(时间宝贵呀!还要逃跑。。)现在你做为他的助手,给你地图和每个道路情报价值,希望你分析出,看他能不能将功补过。
【问题描述】
军事中心是一个严格的二叉树,也就是说,如果有个点可以分道,一定是分出,也只分出2条道路,现在Elvis Han正处在第一个分道处,也就是说树的根结点处。每条道路上都有一个分数,就是这个道路上的情报价值。但是他只有时间走M条路,他的最终情报价值总和就是他所经过的路的情报价值总和(假设他到过的路一定可以把所有情报得到)希望你给出一个方案使得他可以尽量多地获取情报以便将功补过。
【输入格式】
在输入文件inform.in中,共有N行:
第一行:3个数据:N,M,Q(N表示有多少个路口,包括分道和不分道的路口;M表示他可以有时间走的道路总数;Q表示他的任务情报的价值)
第2~N行:每行3个数据,Xi,Yi,Wi (X,Y表示第I条道路连接的2个路口,W表示这条道路上的情报价值分, 注意,所有数据均在Lonint范围内)
【输出格式】
在输出文件inform.out中,共包含2行:
第一行:输出TRUE/FALSE(注意大小写),表示他是否可以收集够任务情报价值
第二行:输出一个数据:
如果他可以完成任务,就输出他收集的情报总价值超过任务情报价值的部分。(正数)
如果不能完成任务,就输出一个数,表示他不能还差多少分才够任务情报价值。(负数)
【输入样例1】
3 1 10
1 2 10
1 3 8
【输出样例1】
TRUE
0
样例说明:(该部分不必输出)

【输入样例2】
9 3 49
6 2 15
7 2 10
8 7 6
7 9 15
1 3 20
2 1 10
4 3 8
3 5 7
【输出样例2】
FALSE
-4
样例说明:

【数据规模】
对于30%的数据 保证有N<=10
对于50%的数据 保证有N<=40
对于全部的数据 保证有 N<=100

分析:同上题

参考代码

 1 //
 2 //  将功补过.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <cstdio>
11 #include <cstring>
12 #include <string>
13 #include <algorithm>
14  
15 using namespace std;
16  
17 const int maxn = 100+10;
18  
19 int f[maxn][maxn],n,m,q,x,y,w,g[maxn][maxn],g2[maxn][maxn],l[maxn],r[maxn],root;
20 bool vis[maxn];
21  
22 void createtree(int x)
23 {
24     if (x == 0) return;
25     vis[x] = true;
26     for (int i = 1;i <= g2[x][0];i++)
27         if (!vis[g2[x][i]])
28         {
29             if (l[x] == 0) l[x] = g2[x][i];
30             else r[x] = g2[x][i];
31         }
32     createtree(l[x]);
33     createtree(r[x]);
34 }
35  
36 void doit(int x,int res)
37 {
38     if (res == 0 || l[x] == 0)
39     {
40         f[x][res] = 0;
41         return;
42     }
43     if (f[x][res]>0) return;
44     if (res == 1)
45     {
46         f[x][res] = max(g[x][l[x]],g[x][r[x]]);
47         return;
48     }
49     for (int i = 0;i <= res-2;i++)
50     {
51         doit(l[x],i);
52         doit(r[x],res-i-2);
53         f[x][res] = max(f[x][res],f[l[x]][i]+f[r[x]][res-i-2]);
54     }
55     f[x][res] += g[x][l[x]]+g[x][r[x]];
56     doit(l[x],res-1);
57     f[x][res] = max(f[x][res],f[l[x]][res-1]+g[x][l[x]]);
58     doit(r[x],res-1);
59     f[x][res] = max(f[x][res],f[r[x]][res-1]+g[x][r[x]]);
60 }
61  
62 int main()
63 {
64     scanf("%d",&n,&m,&q);
65     memset(f,0,sizeof(f));
66     memset(g,0,sizeof(g));
67     memset(g2,0,sizeof(g2));
68     memset(l,0,sizeof(l));
69     memset(r,0,sizeof(r));
70     memset(vis,false,sizeof(vis));
71     for (int i = 1;i < n;i++)
72     {
73         scanf("%d%d%d",&x,&y,&w);
74         g[x][y] = g[y][x] = w;
75         g2[x][0]++;
76         g2[x][g2[x][0]] = y;
77         g2[y][0]++;
78         g2[y][g2[y][0]] = x;
79     }
80     for (int i = 1;i <= n;i++)
81         if (g2[i][0] == 2)
82         {
83             root = i;
84             break;
85         }
86     createtree(root);
87     doit(root,m);
88     if (f[root][m] >= q)
89         printf("TRUE
%d
",f[root][m]-q);
90     else printf("FALSE
%d
",f[root][m]-q);
91 }
View Code

题目六:加分二叉树
【问题描述】
设一个n个节点的二叉树tree的中序遍历为(l,2,3,…,n),其中数字1,2,3,…,n为节点编号。每个节点都有一个分数(均为正整数),记第j个节点的分数为di,tree及它的每个子树都有一个加分,任一棵子树subtree(也包含tree本身)的加分计算方法如下:
subtree的左子树的加分× subtree的右子树的加分+subtree的根的分数
若某个子树为空,规定其加分为1,叶子的加分就是叶节点本身的分数。不考虑它的空
子树。试求一棵符合中序遍历为(1,2,3,…,n)且加分最高的二叉树tree。要求输出;
(1)tree的最高加分
(2)tree的前序遍历
【输入格式】
第1行:一个整数n(n<30),为节点个数。
第2行:n个用空格隔开的整数,为每个节点的分数(分数<100)。
【输出格式】
第1行:一个整数,为最高加分(结果不会超过4,000,000,000)。
第2行:n个用空格隔开的整数,为该树的前序遍历。
【输入样例】
5
5 7 1 2 10
【输出样例】
145
3 1 2 4 5

分析:其实这个是一个区间动态规划,不能算是一个严格的树形DP。对于[l,r]范围内的最大值,每次枚举中间点作为根,递归计算左子树和右子树,算出来的值跟最大值比较即可。

参考代码

 1 //
 2 //  加分二叉树.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <string>
11 #include <cstdio>
12 #include <cstring>
13 #include <algorithm>
14  
15 using namespace std;
16  
17 const int maxn = 30+10;
18  
19 long long f[maxn][maxn];
20 int g[maxn][maxn],n,data[maxn];
21  
22 void doit(int l,int r)
23 {
24     if (f[l][r] > 0return;
25     if (l == r)
26     {
27         f[l][r] = data[l];
28         g[l][r] = l;
29         return;
30     }
31     if (l > r)
32     {
33         f[l][r] = 1;
34         return;
35     }
36     for (int i = l;i <= r;i++)
37     {
38         doit(l,i-1);
39         doit(i+1,r);
40         if (f[l][i-1]*f[i+1][r]+data[i] > f[l][r])
41         {
42             f[l][r] = f[l][i-1]*f[i+1][r]+data[i];
43             g[l][r] = i;
44         }
45     }
46 }
47  
48 void show(int l,int r)
49 {
50     if (l > r) return;
51     if (l == r)
52     {
53         printf("%d ",l);
54         return;
55     }
56     printf("%d ",g[l][r]);
57     show(l,g[l][r]-1);
58     show(g[l][r]+1,r);
59 }
60  
61 int main()
62 {
63     scanf("%d",&n);
64     for (int i = 1;i <= n;i++)
65         scanf("%d",&data[i]);
66     memset(f,0,sizeof(f));
67     memset(g,0,sizeof(g));
68     doit(1,n);
69     printf("%lld
",f[1][n]);
70     show(1,n);
71     printf("
");
72 }
View Code

题目七:Ural 1039 没有上司的晚会
【背景】
有个公司要举行一场晚会。为了能玩得开心,公司领导决定:如果邀请了某个人,那么一定不会邀请他的上司(上司的上司,上司的上司的上司……都可以邀请)。
【问题描述】
每个参加晚会的人都能为晚会增添一些气氛,求一个邀请方案,使气氛值的和最大。
【输入格式】
第1行一个整数N(1<=N<=6000)表示公司的人数。
接下来N行每行一个整数。第i行的数表示第i个人的气氛值x(-128<=x<=127)。
接下来每行两个整数L,K。表示第K个人是第L个人的上司。
输入以0 0结束。
【输出格式】
一个数,最大的气氛值和。
【输入样例】
7
1
1
1
1
1
1
1
1 3
2 3
6 4
7 4
4 5
3 5
0 0
【输出样例】
5

分析:根据题目给出的矛盾关系写出动规方程:
F[i,0]表示不邀请i,以i为根的子树所获得的最大值
F[i,1]表示邀请i,以i为根的子树所获得的最大值
转移如下:

    F[i,0] = Sigma(max(F[j,0],F[j,1])) j是i的儿子 //不邀请i,那么儿子可来可不来,取最大值
    F[i,1] = Sigma(F[j,0]) j是i的儿子 //邀请i,那么儿子不能来,直接求和

参考代码

 1 //
 2 //  没有上司的晚会.cpp
 3 //  树形DP
 4 //
 5 //  Created by TimmyXu on 13-8-3.
 6 //  Copyright (c) 2013年 TimmyXu. All rights reserved.
 7 //
 8  
 9 #include <iostream>
10 #include <algorithm>
11 #include <cstdio>
12 #include <cstring>
13 #include <string>
14  
15 using namespace std;
16  
17 const int maxn = 6000+10;
18  
19 int n,root,du[maxn],data[maxn],f[maxn][2],g[maxn][maxn],x,y;
20  
21 void doit(int x)
22 {
23     f[x][0] = 0;
24     f[x][1] = data[x];
25     for (int i = 1;i <= g[x][0];i++)
26     {
27         doit(g[x][i]);
28         f[x][0] += max(f[g[x][i]][0],f[g[x][i]][1]);
29         f[x][1] += f[g[x][i]][0];
30     }
31     return;
32 }
33  
34 int main()
35 {
36     scanf("%d",&n);
37     for (int i = 1;i <= n;i++)
38         scanf("%d",&data[i]);
39     memset(f,0,sizeof(f));
40     memset(g,0,sizeof(g));
41     memset(du,0,sizeof(du));
42     for (int i = 1;i < n;i++)
43     {
44         scanf("%d%d",&x,&y);
45         du[x]++;
46         g[y][0]++;
47         g[y][g[y][0]]= x;
48     }
49     for (int i = 1;i <= n;i++)
50         if (du[i] == 0)
51         {
52             root = i;
53             break;
54         }
55     doit(root);
56     printf("%d
",max(f[root][0],f[root][1]));
57 }
View Code

总结:树形dp的题目,如果单纯给出节点间矛盾关系,或者在二叉树上做背包,都是比较简单的。我目前遇到的题目中,多叉树背包是一个难点,希望自己能继续学习,能拿下。OK,下午二分图最大流搞起!

原文地址:https://www.cnblogs.com/xsf72006/p/3242875.html