这里是学习韦神的6道入门树形dp进行入门,本来应放在day12&&13里,但感觉这个应该单独放出来好点。
这里大部分题目都是参考的韦神的思想。
A - Anniversary party
题意:
一个树,每个点有一个“快乐”值,父子结点不能同时快乐,问这个结构的最大快乐值。
Thinking:
思考如何写出树规方程,即思考根与子节点的关系。
dp[i][0]:表示不邀请i员工其子树达到的最大快乐值,dp[i][1]则表示邀请。
这时根与子节点的关系就显然了。
1 #include2 #include 15 dfs(G[root][i]); 16 } 17 for(int i=0; i3 #include 4 #include 5 #include 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 ){ ){ 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 #include2 #include 15 dfs(G[root][i]); 16 } 17 for(int i=0; i3 #include 4 #include 5 #include 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 ){ ){ 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 ){ 33 int root, node, cnt; 34 scanf("%d:(%d)",&root, &cnt); 35 for(int i=0; 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 #include2 #include 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; i3 #include 4 #include 5 #include 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 ){ 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 ){ 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 #include2 #include 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; t3 #include 4 #include 5 #include 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 ){ ){ 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 ){ 46 G[i].clear(); 47 } 48 for(int i=0; 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 }
E - Cell Phone Network
题意:给n[1,10000]个点,n-1条边,树形结构,从n个点中取尽量少的点构成一个集合,使剩下所有点都能与这个集合中的部分点相连。
(这个概念叫最小支配集)
dp[u][]:以点u为根的被染色的点的个数
dp[u][0]:u不染色,父节点染色覆盖u min(1, 2)u不染色,不能覆盖子节点v,故要不v染色覆盖自己,要不v被v染色的子节点覆盖
dp[u][1]:u不染色,子节点存在被染色的覆盖u min(1,2)u不染色,所以子节点v不存在被染色的父亲;若所有v均不染色,此时u未被覆盖,故需要有一个v来染色,选择min(dp[v][2]-dp[v][1])即可。
dp[u][2]:u染色 min(0,1,2)+1 子节点v染不染色都可以;自己染色故需+1
1 /* 2 * poj3659 3 * 最小支配集:从所有顶点中取尽量少的点组成一个集合, 4 * 使剩下的所有点都与取出来的所有点相连。 5 * dp[u]:以点u为根的被染色的点的个数 6 * 7 * dp[u][0]:u不染色,父节点染色覆盖u 8 * dp[u][1]:u不染色,子节点存在被染色的覆盖u 9 * dp[u][2]:u染色 10 */ 11 #include12 #include 31 int v = G[u][i]; 32 if(v == f) continue; 33 dfs(v, u); 34 dp[u][0] += min(dp[v][1], dp[v][2]); 35 dp[u][2] += min(min(dp[v][0], dp[v][1]), dp[v][2]); 36 if(dp[v][1] < dp[v][2]){ 37 dp[u][1] += dp[v][1]; 38 mini = min(mini, (dp[v][2] - dp[v][1])); 39 }else{ 40 flag = 0; 41 dp[u][1] += dp[v][2]; 42 } 43 44 } 45 dp[u][2]++; //u点需要染色 46 if(flag){ 47 /* 48 * 如果所有子节点dp[v][1]13 #include 14 #include 15 using namespace std; 16 const int inf = 0x3f3f3f3f; 17 const int maxn = 10010; 18 vector<int> G[maxn]; 19 int dp[maxn][3], n; 20 21 void dfs(int u, int f){ 22 //叶子结点 23 if(G[u].size()==1 && G[u][0]==f){ 24 dp[u][0] = 0; 25 dp[u][1] = inf; 26 dp[u][2] = 1; 27 return; 28 } 29 int mini = inf, flag = 1; 30 for(int i=0; i ){ 49 * u被覆盖 50 */ 51 dp[u][1] += mini; 52 } 53 } 54 55 int main(){ 56 //freopen("in.txt", "r", stdin); 57 int n; scanf("%d", &n); 58 for(int i=0; i<=n; i++)G[i].clear(); 59 for(int i=1; i 60 int a, b; scanf("%d%d", &a, &b); 61 G[a].push_back(b); G[b].push_back(a); 62 } 63 dfs(1, -1); 64 printf("%d\n", min(dp[1][1], dp[1][2])); 65 //1是根,无父节点 66 return 0; 67 }){
F - Computer
题意:一棵边带权值的树,求每个点在树上的最远距离。
参考blog
1、dp:计算点v在树上的最远距离,通过dfs()寻找。v通过v的子树可以找到最远距离,v也可以通过v的父节点找到最远距离。通过子树找,向下遍历更新即可(即向下寻找)。通过父节点找,需要知道父节点的最远距离,父节点可以通过找自己的父节点获得最远距离(即一直向上寻找),也可以通过寻找子树获得最远距离(但不能包含以v为根的子树(否则会重复))(即先向上后向下寻找),这里就需要结点的次远距离(即该距离是不包含结点最远距离上的第一个结点的最远距离,故称为次远距离)。
1 /* 2 * hdu2196 3 * 4 */ 5 #include6 #define LL long long 7 using namespace std; 8 const int maxn = 1e4+10; 9 struct edge{ 10 int to, val; 11 edge(int a, int b) : to(a), val(b) {} 12 }; 13 vector 22 int to = G[x][i].to, w = G[x][i].val; 23 if(to == f) continue; 24 dfs1(to, x); 25 if(dp[0][x] < dp[0][to] + w){ 26 dp[0][x] = dp[0][to] + w; 27 id[x] = to; //记录点x的最大距离经过的第一个孩子结点 28 } 29 } 30 31 for(int i=0; iG[maxn]; 14 int dp[3][maxn], id[maxn]; 15 //树只有一个父亲,会有多个儿子 16 //id[v]: v在v的子树中可以得到的最大距离,所经过的第一个孩子结点 17 //dp[v][0]: v在v的所有子树中获得的最长距离 18 //dp[v][1]: v的孩子的第二长距离 19 //dp[v][2]: v通过父亲获得的最长距离 20 void dfs1(int x, int f){ 21 for(int i=0; i ){ ){ 32 int to = G[x][i].to, w = G[x][i].val; 33 if(to == f) continue; 34 if(id[x] == to) continue; //找次大的 35 dp[1][x] = max(dp[1][x], w + dp[0][to]); 36 } 37 } 38 void dfs2(int x, int f){ 39 for(int i=0; i ){ 40 int to = G[x][i].to, w = G[x][i].val; 41 if(to == f) continue; 42 if(to == id[x]){ 43 dp[2][to] = max(dp[2][x], dp[1][x]) + w; 44 //to是x的孩子:to的最大距离是 x不经过to的最大距离(即次大距离)[向下的]和 45 //x向上的最大距离 的最大值 + dist(x,to) (画图理解) 46 //这里的转移也是dp[v][1]和id[x]存在的意义 47 }else{ 48 dp[2][to] = max(dp[2][x], dp[0][x]) + w; 49 //to不是x最大距离经过的点 50 //则to的最大距离是dist(x,to)和x向上或向下的最大距离的最大值 51 } 52 dfs2(to, x); 53 //0和1子树的信息可以直接用,2也是步步更新,一直到最优 54 } 55 } 56 int main(){ 57 //freopen("in.txt", "r", stdin); 58 int n; 59 while(scanf("%d", &n) != EOF){ 60 memset(dp, 0, sizeof(dp)); 61 for(int i=1;i<=n;i++)G[i].clear(); 62 for(int i=2; i<=n; i++){ 63 int a, b; scanf("%d%d", &a, &b); 64 G[i].push_back(edge(a, b)); 65 G[a].push_back(edge(i, b)); 66 } 67 dfs1(1, -1); 68 dfs2(1, -1); 69 for(int i=1; i<=n; i++){ 70 printf("%d\n", max(dp[0][i], dp[2][i])); 71 } 72 } 73 return 0; 74 }
2、用树的直径求解:3次dfs()。前两次求树的直径,后两次求得所有点距离直径端点的最远距离。
1 /* 2 * 树中的最长路径, 3 */ 4 #include5 using namespace std; 6 const int maxn = 1e4+10; 7 struct edge{ 8 int to, val; 9 edge(int a, int b) : to(a), val(b) {} 10 }; 11 vector 21 int to = G[x][i].to, w = G[x][i].val; 22 if(f == to) continue; 23 dfs(to, x, len+w); 24 dp[to] = max(dp[to], len+w); 25 //更新起点到当前点的距离 26 } 27 } 28 int main(){ 29 freopen("in.txt", "r", stdin); 30 int n; 31 while(scanf("%d", &n) != EOF){ 32 memset(dp, 0, sizeof(dp)); 33 for(int i=1;i<=n;i++) G[i].clear(); 34 for(int i=2; i<=n; i++){ 35 int a,b; scanf("%d%d",&a, &b); 36 G[i].push_back(edge(a, b)); 37 G[a].push_back(edge(i, b)); 38 } 39 s=0, max_len=0; 40 dfs(1, -1, 0); 41 dfs(s, -1, 0); 42 dfs(s, -1, 0); 43 for(int i=1; i<=n; i++){ 44 printf("%d\n", dp[i]); 45 } 46 } 47 return 0; 48 }G[maxn]; 12 int dp[maxn], max_len, s; 13 14 void dfs(int x, int f, int len){ 15 //len:起点到当前点的距离 16 if(max_len <= len){ 17 s = x; 18 max_len = len; 19 } 20 for(int i=0; i ){
hdu6446 Tree and Permutation
题意:给一个n(1e5)个点,n-1条边的树,按结点进行全排列,对每个全排列,求其第一个结点到其余结点的距离之和,再求全排列的和。
每条边单独计算,边E左边x个点,右边(n-x)个点。则在全排列n!中,每种排列有n-1段,每段的贡献是 2*x*(n-x)*(n-2)!*w ,一共n-1段,则贡献为 2*x*(n-x)*(n-1)!*w 。一共n-1条边,sum()即可
1 #include2 using namespace std; 3 const int maxn = 1e5+10; 4 #define LL long long 5 const LL mod = 1e9+7; 6 struct edge{ 7 int to, val; 8 edge(int a, int b) : to(a), val(b) {} 9 }; 10 vector G[maxn]; 11 LL d[maxn], w[maxn], node[maxn]; 12 //node[x]: f-->x这条边在x一边的点的个数 13 //w[x]: f-->x这条边的权值 14 //d[i]: i! 15 void get_d(){ 16 d[1] = 1; 17 for(int i=2; i ){ 18 d[i] = (1LL * i * d[i-1]) % mod; 19 } 20 } 21 22 LL dfs(int x, int f){ 23 LL ans = 1; 24 for(int i=0; i ){ 25 int v = G[x][i].to; 26 if(v == f){ 27 w[x] = (LL)G[x][i].val; //将边f---->x的权值存在当前结点w[x] 28 }else{ 29 ans += dfs(v, x); //统计结点数 30 } 31 } 32 if(f!=-1 && G[x].size()==1){ //叶子结点 33 return node[x] = 1; //叶子结点一边的点数为1 34 } 35 return node[x] = ans; 36 } 37 int main(){ 38 //freopen("in.txt", "r", stdin); 39 get_d(); 40 int n; 41 while(scanf("%d", &n) != EOF){ 42 for(int i=0; i<=n; i++) G[i].clear(); 43 for(int i=1; i ){ 44 int x, y, z; scanf("%d%d%d", &x, &y, &z); 45 G[x].push_back(edge(y, z)); 46 G[y].push_back(edge(x, z)); 47 } 48 dfs(1, -1); 49 LL ans = 0; 50 //ans = sum(2*x*(n-x)*(n-1)!*w[i]) = (n-1)!*sum(2*x*(n-x)*w[i]) 51 for(int i=2; i<=n; i++){ 52 ans = (ans + ( ((2*node[i]*(n-node[i]))%mod) * w[i])%mod )%mod; 53 } 54 printf("%lld\n", (ans * d[n-1])%mod); 55 } 56 return 0; 57 }
UVA 10859 Placing Lampposts(训练指南P70)
题意:n个点m条边的无向无环图。在尽量少的结点上放灯,使所有边都被照亮,灯的总数最小的前提下,被两盏灯同时照亮的边数尽量大。
与E求最小支配集相似,多了同时照亮的边数尽量大的目标。
下面两个关于这题的思路很nice:
多目标优化问题这里有一个思路是 x=Ma+c,M是“c的最大理论值与a的最小理论值之差”还要大的数。
还需要进行目标转换:边数一定,被两盏灯同时照亮的边数比较大,则被一盏灯照亮的边数尽量小。
1 #include2 using namespace std; 3 const int maxn = 1010; 4 vector<int> G[maxn]; 5 int d[maxn][2], n, m; 6 //d[i][j]: i的父节点是否放灯的值为j; d[i][j]:以i为根的最小x值 7 bool vis[maxn][2]; 8 int dfs(int i, int j, int f){ 9 int &ans = d[i][j]; 10 if(vis[i][j]) return ans; 11 vis[i][j] = true; 12 ans = 2000; //i结点放灯,权重很大 13 for(int k=0; k ){ 14 if(G[i][k] == f) continue; 15 ans += dfs(G[i][k], 1, i); 16 } 17 if(j==0 && f>=0){ 18 //因为i的父节点没放灯,所以这是被一盏灯照亮 19 ans++; 20 } 21 if(j || f<0){ //i是根或i的父亲放灯,i可以不放灯 22 int sum = 0; 23 for(int k=0; k ){ 24 if(G[i][k] == f) continue; 25 sum += dfs(G[i][k], 0, i); 26 } 27 if(f >= 0) sum++; 28 ans = min(ans, sum); 29 } 30 return ans; 31 } 32 33 int main(){ 34 //freopen("in.txt", "r", stdin); 35 int t; 36 scanf("%d", &t); 37 while(t--){ 38 int n, m; 39 scanf("%d%d", &n, &m); 40 for(int i=0; i<=n; i++)G[i].clear(); 41 for(int i=0; i ){ 42 int x, y; 43 scanf("%d%d", &x, &y); 44 G[x].push_back(y); 45 G[y].push_back(x); 46 } 47 memset(vis, 0, sizeof(vis)); 48 int ans = 0; 49 for(int i=0; i ){ 50 if(!vis[i][0]){ 51 ans += dfs(i, 0, -1); 52 } 53 } 54 printf("%d %d %d\n", ans/2000, m-ans%2000, ans%2000); 55 } 56 return 0; 57 }
1 #include2 using namespace std; 3 const int maxn = 1010; 4 const int M = 2000; 5 vector<int> G[maxn]; 6 int dp[maxn][2]; 7 bool vis[maxn]; 8 void dfs(int x){ 9 vis[x] = true; 10 dp[x][0] = 0; dp[x][1] = M; 11 for(int i=0; i ){ 12 int v = G[x][i]; 13 if(vis[v]) continue; 14 dfs(v); 15 dp[x][0] += dp[v][1] + 1; 16 dp[x][1] += min(dp[v][1], dp[v][0]+1); 17 } 18 } 19 int main(){ 20 //freopen("in.txt", "r", stdin); 21 int t; scanf("%d", &t); 22 while(t--){ 23 int n, m; scanf("%d%d", &n, &m); 24 for(int i=0;i<=n;i++)G[i].clear(); 25 for(int i=0; i ){ 26 int a, b;scanf("%d%d", &a, &b); 27 G[a].push_back(b); G[b].push_back(a); 28 } 29 memset(vis, 0, sizeof(vis)); 30 int ans = 0; 31 for(int i=0; i ){ 32 if(vis[i]) continue; 33 dfs(i); 34 ans += min(dp[i][0], dp[i][1]); 35 } 36 printf("%d %d %d\n", ans/M, m-ans%M, ans%M); 37 } 38 return 0; 39 }