这里是学习韦神的6道入门树形dp进行入门,本来应放在day12&&13里,但感觉这个应该单独放出来好点。
这里大部分题目都是参考的韦神的思想。
A - Anniversary party
题意:
一个树,每个点有一个“快乐”值,父子结点不能同时快乐,问这个结构的最大快乐值。
Thinking:
思考如何写出树规方程,即思考根与子节点的关系。
dp[i][0]:表示不邀请i员工其子树达到的最大快乐值,dp[i][1]则表示邀请。
这时根与子节点的关系就显然了。
1 #include <cstdio> 2 #include <iostream> 3 #include <cstring> 4 #include <algorithm> 5 #include <vector> 6 using namespace std; 7 typedef long long LL; 8 #define mst(s, t) memset(s, t, sizeof(s)) 9 const int INF = 0x3f3f3f3f; 10 const int maxn = 6010; 11 vector<int> G[maxn]; 12 int father[maxn], dp[maxn][2]; 13 void dfs(int root){ 14 for(int i=0; i<G[root].size(); i++){ 15 dfs(G[root][i]); 16 } 17 for(int i=0; i<G[root].size(); i++){ 18 dp[root][0] += max(dp[G[root][i]][0], dp[G[root][i]][1]); 19 dp[root][1] += dp[G[root][i]][0]; 20 } 21 } 22 int main() 23 { 24 freopen("in.txt", "r", stdin); 25 mst(dp, 0); mst(father, -1); 26 int n; 27 scanf("%d", &n); 28 for(int i=1; i<=n; i++){ 29 scanf("%d", &dp[i][1]); 30 G[i].clear(); 31 } 32 int fa, so; 33 while(scanf("%d%d", &so, &fa) && fa && so){ 34 G[fa].push_back(so); 35 father[so] = fa; 36 } 37 int root = 1; 38 while(father[root] != -1) root=father[root]; 39 dfs(root); 40 printf("%d\n", max(dp[root][0], dp[root][1])); 41 return 0; 42 }
B - Strategic game
题意:
现在要在一棵树上布置士兵,每个士兵在结点上,每个士兵可以守护其结点直接相连的全部边,问最少需要布置多少个士兵。
这题解法与上题相似。
1 dp[root][0] += dp[G[root][i]][1]; 2 dp[root][1] += min(dp[G[root][i]][0], dp[G[root][i]][1]);
在读题时想了下结点A的父节点B的变化会影响到A和B的父节点C,会影响到总人数,后来又想了想,这不就是dp要解决的问题呀,在每个阶段做一个决策,以求达到预定的效果。
1 #include <cstdio> 2 #include <iostream> 3 #include <cstring> 4 #include <algorithm> 5 #include <vector> 6 using namespace std; 7 typedef long long LL; 8 #define mst(s, t) memset(s, t, sizeof(s)) 9 const int INF = 0x3f3f3f3f; 10 const int maxn = 1510; 11 int dp[maxn][2], father[maxn]; 12 vector<int> G[maxn]; 13 void dfs(int root){ 14 for(int i=0; i<G[root].size(); i++){ 15 dfs(G[root][i]); 16 } 17 for(int i=0; i<G[root].size(); i++){ 18 dp[root][0] += dp[G[root][i]][1]; 19 dp[root][1] += min(dp[G[root][i]][0], dp[G[root][i]][1]); 20 } 21 } 22 int main() 23 { 24 //freopen("in.txt", "r", stdin); 25 int n; 26 while( scanf("%d", &n) != EOF){ 27 for(int i=0; i<=n; i++){ 28 G[i].clear(); 29 dp[i][1] = 1, dp[i][0] = 0; 30 father[i] = -1; 31 } 32 for(int i=0; i<n; i++){ 33 int root, node, cnt; 34 scanf("%d:(%d)",&root, &cnt); 35 for(int i=0; i<cnt; i++){ 36 scanf("%d", &node); 37 G[root].push_back(node); 38 father[node] = root; 39 } 40 } 41 int root = 1; 42 while(father[root] != -1) root=father[root]; 43 dfs(root); 44 printf("%d\n", min(dp[root][0], dp[root][1])); 45 } 46 return 0; 47 }
C - Tree Cutting
题意:
一棵无向树,结点为n(<=10,000),删除哪些结点可以使得新图中每一棵树结点小于n/2。
Thinking:
真的是菜的无语,面对不会写的题总有懒于思考的毛病。
下面记录解决此题的心得:这题给我一种搜索而非dp的感觉,可能有什么我没发现的深意吧。
在遍历树的过程中,访问每个node,维护两个值:
- 所有子树的结点数的最大值childmax
- 所有子树(这里包括node)的结点数之和sum。
递归过程中用上一层的sum,不断更新这一层的childmax。
而childmax和sum则共同用来判断这个node是否可以删除。
下面再分析判断条件: childmax<=n/2 && n-sum<=n/2
childmax<=n/2 :去掉node后,原先node的子树均满足条件。
n-sum<=n/2 :去掉node后,原先除node和node的所有子树外的树(就当是node的祖先树吧)均满足条件。
1 #include <cstdio> 2 #include <iostream> 3 #include <cstring> 4 #include <algorithm> 5 #include <vector> 6 using namespace std; 7 typedef long long LL; 8 #define mst(s, t) memset(s, t, sizeof(s)) 9 const int INF = 0x3f3f3f3f; 10 const int maxn = 10010; 11 vector<int> G[maxn]; 12 int ans[maxn], num, n; 13 int dfs(int node, int father){ 14 int sum = 1, childmax = 0; //若是叶子结点则return sum=1,否则求其子树(包括自己)的总结点数 15 for(int i=0; i<G[node].size(); i++){ 16 if(G[node][i] == father)continue; //因为是树结构,这里可以在无向时避免遍历成环 17 int sum_son = dfs(G[node][i], node); 18 childmax = max(sum_son, childmax);//所有子树的结点数的最大值 19 sum += sum_son;//sum:node的子树的结点数和 20 } 21 childmax = max(childmax, n-sum); 22 if(childmax <= n/2){ 23 /* 24 * 当node结点的孩子结点的结点数最大为Sum,若Sum<=n/2,则该点符合条件 25 * 因为去掉node后,任意子树结点数<=n/2, max()保证其非子树结点和仍<=n/2 26 * 故该点满足条件 27 */ 28 ans[num++] = node; 29 } 30 return sum; 31 } 32 int main() 33 { 34 //freopen("in.txt", "r", stdin); 35 scanf("%d", &n); 36 for(int i=0; i<n-1; i++){ 37 int a, b; 38 scanf("%d%d", &a, &b); 39 G[a].push_back(b); 40 G[b].push_back(a); 41 } 42 num = 0; 43 int tmp = dfs(1, 0); 44 //cout << n << "==" << tmp << endl; //验证 45 sort(ans, ans+num); 46 if(num){ 47 for(int i=0; i<num; i++){ 48 printf("%d\n", ans[i]); 49 } 50 }else{ 51 printf("NONE\n"); 52 } 53 return 0; 54 }
D - Tree of Tree
题意:一棵结点带权树,大小(结点数)为k的子树的权值和最大为多少。
这道题促使我写这篇学习心得,感觉稍微需要点思考的dp题我连思路都看得费劲。博客里的思路真的是想了好久,又找了份前辈的AC代码敲了敲(敲出来竟然连样例都没过,哎),趁热记录下自己的划水心得。
开始是想到要用状态dp[i]][j]表示node i的 结点数为j的子树 的最大权值和。 但是如何动态规划却没有思路。
这里对每个子节点进行背包dp, dp[j] = max(dp[j], dp[j-w[i]]+v[i]) ,从后往前dp是因为若从后往前会使v的某一个t被重复选取。
这道题整体思路还不清晰,要再多看看。
1 #include <cstdio> 2 #include <iostream> 3 #include <cstring> 4 #include <algorithm> 5 #include <vector> 6 using namespace std; 7 typedef long long LL; 8 #define mst(s, t) memset(s, t, sizeof(s)) 9 const int INF = 0x3f3f3f3f; 10 const int maxn = 110; 11 vector<int> G[maxn]; 12 int dp[maxn][maxn]; //dp[i][j]:node[i]结点数为j的子树的最大权值 13 int k, ans, cnt[maxn], weight[maxn]; 14 int dfs(int node, int father){ 15 cnt[node] = 1; 16 for(int i=0; i<G[node].size(); i++){ 17 if(G[node][i] == father) continue; 18 cnt[node] += dfs(G[node][i], node); 19 } 20 dp[node][1] = weight[node]; 21 //这里初始化不能在main()内 ?? 22 /* 23 * dp[node][j-t]是之前的子节点为根更新的子树产生的 24 * dp[v][t]是以当前子节点为根的子树产生的 25 * j如果顺序遍历,前面dp[node][j]的更新会影响后面的dp[node][j-t],导致后面 26 *更新dp[node][j]时是一当前子节点为根的子树产生的 27 */ 28 for(int i = 0; i < G[node].size(); i++){ 29 int v = G[node][i]; 30 for(int j = cnt[node]; j >= 1; j--){ 31 for(int t = 0; t<j && t<=cnt[v]; t++){ 32 dp[node][j] = max(dp[node][j], dp[node][j-t]+dp[v][t]); 33 } 34 } 35 } 36 ans = max(ans, dp[node][k]); 37 return cnt[node]; 38 } 39 int main() 40 { 41 freopen("in.txt", "r", stdin); 42 int n; 43 while(scanf("%d%d",&n, &k) != EOF){ 44 mst(dp, 0); ans = 0; 45 for(int i=0; i<maxn; i++){ 46 G[i].clear(); 47 } 48 for(int i=0; i<n; i++){ 49 scanf("%d", &weight[i]); 50 } 51 int a, b; 52 for(int i = 1; i < n; i++){ 53 scanf("%d%d", &a, &b); 54 G[a].push_back(b); 55 G[b].push_back(a); 56 } 57 dfs(0, -1); 58 printf("%d\n", ans); 59 } 60 return 0; 61 }