前言:按照计划,昨天应该是完成树形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 }
题目二:选课(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 }
题目三:技能树
【问题描述】
玩过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 }
题目四: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 }
题目五:将功补过
【问题背景】
作为间谍专家的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 }
题目六:加分二叉树
【问题描述】
设一个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] > 0) return; 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 }
题目七: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 }
总结:树形dp的题目,如果单纯给出节点间矛盾关系,或者在二叉树上做背包,都是比较简单的。我目前遇到的题目中,多叉树背包是一个难点,希望自己能继续学习,能拿下。OK,下午二分图最大流搞起!