树型DP
树型DP,即在树上做动态规划。树是无环图,顺序可以是从叶子到根节点,也可以从根到叶子节点。一般树型DP的特征很明显,即状态可以表示为树中的节点,每个节点的状态可以由其子节点状态转移而来(从叶子到根的顺序),或是由其父亲节点转移而来(从根到叶节点的顺序),也可是两者结合。找出状态和状态转移方程仍然是树型DP的关键。
例1:没有上司的晚会
题目描述
Ural大学有\(N\)个职员,编号为\(1 \sim N\)。他们有从属关系,也就是说他们的关系就像一棵以校长为根的树,父结点就是子结点的直接上司。每个职员有一个快乐指数,第\(i\)个职员的快乐指数为\(h_i\)。 现在有个周年庆宴会,要求与会职员的快乐指数最大。但是,没有职员愿和直接上司一起参加宴会。
数据范围
\(1 \leq N \leq 60000, -128 \leq h_i \leq 127\)
分析
设\(f_{i,0/1}\)表示节点\(i\)不参加(或参加)晚会时子树\(i\)的最大快乐指数。
若\(i\)不参加,则\(i\)的直接下属\(j\)可以参加,也可以不参加。
若\(i\)参加,则\(i\)的直接下属\(j\)不能参加宴会。
叶子节点的初始状态\(f_{leaf,0}=0, f_{leaf, 1}=h_{leaf}\).
因为自下而上转移,所以可以采用记忆化搜索的方法。
#include <bits/stdc++.h> using namespace std; #define maxn 60006 int n, ecnt, h[maxn], fir[maxn]; int f[maxn][2]; bool vis[maxn]; struct node{ intv,nxt; }eds[maxn << 1]; void adde(int u, int v){ eds[++ecnt].v=u, eds[ecnt].nxt=fir[v], fir[v] =ecnt; //只保存父亲到儿子的边 } int dfs(int r, bool flg){ if(f[r][flg] !=0xd0d0d0d0) returnf[r][flg]; if(flg==0) f[r][flg] =0; elsef[r][flg] =h[r]; for(inti=fir[r]; i; i=eds[i].nxt){ intt=eds[i].v; if(flg==0){ f[r][flg] +=max(dfs(t, 0), dfs(t, 1)); } elsef[r][flg] +=dfs(t, 0); } returnf[r][flg]; } int main(){ inta, b, root; scanf("%d", &n); for(inti=1; i<=n; i++)scanf("%d", &h[i]); for(inti=1; i<n; i++){ scanf("%d%d", &a, &b); adde(a, b); vis[a] =1; } for(inti=1; i<=n; i++){ if(vis[i] ==0) {root=i; break;} //找出根节点 } memset(f, 0xd0, sizeof f); dfs(root, 0); dfs(root, 1); printf("%d\n", max(f[root][0], f[root][1])); return0; }
例2. 二叉苹果树
题目描述
有一棵苹果树,如果树枝有分叉,一定是分\(2\)叉(就是说没有只有\(1\)个儿子的结点) 这棵树共有\(N\)个结点(叶子点或者树枝分叉点),编号为\(1 \sim N\),树根编号一定是\(1\)。 我们用一根树枝两端连接的结点的编号来描述一根树枝的位置。下面是一颗有4个树枝的树:
2 5
\ /
3 4
\ /
1
现在这颗树枝条太多了,需要剪枝。但是一些树枝上长有苹果。 给定需要保留的树枝数量,求出最多能留住多少苹果。
输入格式
第1行: \(2\)个空格分开的整数,\(N\) 和 \(Q(1 \leq Q \leq N,1 \lt N \leq 100)\),\(N\)表示树的结点数,\(Q\)表示要保留的树枝数量。 接下来\(N-1\) 行描述树枝的信息。
每行\(3\)个整数, 前两个是它连接的结点的编号。第3 个数是这根树枝上苹果的数量。 每根树枝上的苹果不超过\(30000\)个。
输出格式
第1行:一个整数,表示最多能留住的苹果的数量。
分析
如果设状态\(f_{i,j}\)为子树\(i\)中包含\(j\)边,转移时不太自然。因为还要考虑节点\(i\)到儿子的边是否保留的情况。
于是,将父子边的边权下放到儿子处,设状态\(f_{i,j}\)表示子树\(i\)包含\(j\)个点的最多苹果数量。
于是得到状态转移方程为
其中\(w_i\)表示节点\(i\)的苹果数量,\(f_{l,k},f_{r,j-1,k}\)表示左子树,右子树中保留指定节点的最多苹果数,
答案为\(f_{Q+1}\)。因为包含\(Q+1\)个点时,刚好包含\(Q\)条边。
#include <bits/stdc++.h> using namespace std; #define maxn 105 bool vis[maxn]; int f[maxn][maxn], lson[maxn], rson[maxn]; int ecnt, n, m, pw[maxn], sz[maxn], fir[maxn]; struct edge{ int v, w, nxt; }eds[maxn << 1]; void adde(int a, int b, int c){ eds[++ecnt].v = b, eds[ecnt].w = c, eds[ecnt].nxt = fir[a], fir[a] = ecnt; eds[++ecnt].v = a, eds[ecnt].w = c, eds[ecnt].nxt = fir[b], fir[b] = ecnt; } void dfs(int r){ vis[r] = 1; sz[r] = 1; for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; if(!vis[tv]){ pw[tv] = eds[i].w; if(lson[r] == 0) lson[r] = tv; else rson[r] = tv; dfs(tv); sz[r] += sz[tv]; } } } int dfs2(int r, int cnt){ if(f[r][cnt] >= 0) return f[r][cnt]; f[r][0] = 0; f[r][1] = pw[r]; for(int i = max(0, cnt - 1 - sz[rson[r]]); i <= sz[lson[r]] && i < cnt; i++) f[r][cnt] = max(f[r][cnt], dfs2(lson[r], i) + dfs2(rson[r], cnt - 1 - i) + pw[r]); return f[r][cnt]; } int main(){ int a, b, c; scanf("%d %d", &n, &m); for(int i = 1; i < n; i++){ scanf("%d %d %d", &a, &b, &c); adde(a, b, c); } dfs(1); m++; memset(f, -1, sizeof f); dfs2(1, m); printf("%d\n", f[1][m]); return 0; }
也可以采用树上做背包的方式来解决,这种思路不仅能够适用于多叉树,代码也很短。
参考代码如下:
#include <bits/stdc++.h> using namespace std; #define maxn 105 int f[maxn][maxn]; struct edge{ int v, w, nxt; }eds[maxn << 1]; int n, m, ecnt, fir[maxn], sz[maxn]; bool vis[maxn]; void adde(int u, int v, int w){ eds[++ecnt].v = v, eds[ecnt].w = w, eds[ecnt].nxt = fir[u], fir[u] = ecnt; eds[++ecnt].v = u, eds[ecnt].w = w, eds[ecnt].nxt = fir[v], fir[v] = ecnt; } void dfs(int r){ sz[r] = 1; vis[r] = 1; for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; if(!vis[tv]){ f[tv][0] = 0, f[tv][1] = eds[i].w; dfs(tv); for(int k = min(sz[r], m); k >= 1; k--) for(int j = 0; j <= sz[tv] && j <= m - k; j++){ f[r][k + j] = max(f[r][k + j], f[r][k] + f[tv][j]); } sz[r] += sz[tv]; } } } int main(){ int a, b, c; scanf("%d %d", &n, &m); m++; for(int i = 1; i < n; i++){ scanf("%d %d %d", &a, &b, &c); adde(a, b, c); } dfs(1); printf("%d\n", f[1][m]); return 0; }
例3. 战略游戏
题目描述
Bob喜欢玩电脑游戏,特别是战略游戏。但是他经常无法找到快速玩过游戏的办法。现在他有个问题。
他要建立一个古城堡,城堡中的路形成一棵树。他要在这棵树的结点上放置最少数目的士兵,使得这些士兵能了望到所有的路。 注意,某个士兵在一个结点上时,与该结点相连的所有边将都可以被了望到。
请你编一程序,给定一树,帮Bob计算出他需要放置最少的士兵。
数据规模
\(n \leq 1500\)
分析
设\(f_{i,0/1}\)表示\(i\)处不放或放士兵时子树\(i\)中的最少士兵,
转移方程为$$f_{i,0}=\sum_{j \in i.son}f_{j,1}$$
参考代码如下:
#include <bits/stdc++.h> using namespace std; #define maxn 1600 int n, m, ecnt; int f[maxn][2], fir[maxn]; struct edge{ int v, nxt; }eds[maxn << 1]; void adde(int u, int v){ eds[++ecnt].v = v, eds[ecnt].nxt = fir[u], fir[u] = ecnt; eds[++ecnt].v = u, eds[ecnt].nxt = fir[v], fir[v] = ecnt; } int dfs(int r, bool flg, int fa){ if(f[r][flg] >= 0) return f[r][flg]; if(flg == 0)f[r][flg] = 0; else f[r][flg] = 1; for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; if(tv != fa){ if(flg == 1) f[r][flg] += min(dfs(tv, 0, r), dfs(tv, 1, r)); else f[r][flg] += dfs(tv, 1, r); } } return f[r][flg]; } int main(){ int cnt, id, a; scanf("%d", &n); for(int i = 1; i <= n; i++){ scanf("%d %d", &id, &cnt); id++; for(int j = 1; j <= cnt; j++){ scanf("%d", &a); a++; adde(id, a); } } memset(f, -1, sizeof f); dfs(1, 0, 0); dfs(1, 1, 0); printf("%d\n", min(f[1][0], f[1][1])); return 0; }
例4.电话网络
题目描述
Farmer John决定为他的所有奶牛都配备手机,以此鼓励她们互相交流。
不过,为此FJ必须在奶牛们居住的\(N(1 <= N <= 10,000)\)块草地中选一些建上
无线电通讯塔,来保证任意两块草地间都存在手机信号。所有的N块草地按1..N
顺次编号。
所有草地中只有\(N-1\)对是相邻的,不过对任意两块草地\(A\)和\(B(1 <= A <= N;
1 <= B <= N; A != B)\),都可以找到一个以\(A\)开头以\(B\)结尾的草地序列,并且序列
中相邻的编号所代表的草地相邻。无线电通讯塔只能建在草地上,一座塔的服务
范围为它所在的那块草地,以及与那块草地相邻的所有草地。
请你帮FJ计算一下,为了建立能覆盖到所有草地的通信系统,他最少要建
多少座无线电通讯塔。
分析
设\(f_{i,0/1,0/1}\)表示子树\(i\)的最少的通讯塔数量,第一个\(0/1\)表示在\(i\)处不建信号塔/建信号塔,第二个\(0/1\)表示在父亲处不建信号塔/建信号塔。
- \(f_{i,0,0}=min(f_{j,0,0}, f_{j,1,0})\)
如果儿子的状态选的全是\(f_{j,0,0}\),则要将其中一个替换为\(f_{j,1,0}\),并使得增加量最小。 - \(f_{i,0,1}=min(f_{j,0,0}, f_{j,1,0})\)
- \(f_{i,1,0}=min(f_{j,0,1}, f_{j,1,1})+1\)
- \(f_{i,1,1}=min(f_{j,0,1}, f_{j,1,1})+1\)
#include <bits/stdc++.h> using namespace std; #define maxn 10005 #define max(a,b) (a > b ? (a) : (b)) struct edge{ int v, nxt; }eds[maxn << 1]; int n, ecnt, fir[maxn]; int f[maxn][2][2]; void adde(int a, int b){ eds[++ecnt].v = b, eds[ecnt].nxt = fir[a], fir[a] = ecnt; eds[++ecnt].v = a, eds[ecnt].nxt = fir[b], fir[b] = ecnt; } void dfs(int r, int fa){ int mindiff = 999999999; f[r][0][0] = f[r][0][1] = 0; f[r][1][0] = f[r][1][1] = 1; for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; if(tv != fa){ dfs(tv, r); mindiff = min(f[tv][1][0] - f[tv][0][0], mindiff); f[r][0][0] += min(f[tv][0][0], f[tv][1][0]); f[r][0][1] += min(f[tv][0][0], f[tv][1][0]); f[r][1][0] += min(f[tv][0][1], f[tv][1][1]); f[r][1][1] += min(f[tv][0][1], f[tv][1][1]); } } if(mindiff > 0) f[r][0][0] += mindiff; } int main(){ int a, b; scanf("%d", &n); for(int i = 1; i < n; i++){ scanf("%d %d", &a, &b); adde(a, b); } dfs(1, 0); printf("%d\n", min(f[1][0][0], f[1][1][0])); return 0 ; }
例5. 树上最远点
题目大意
有一棵树,有\(n\)个点,边上有权,求每个点到其最远点的距离。
题目范围
$ n\leq 10^5$
分析
方法一:通过树的直径求
有一个性质,树上每个点的最远点一定是树直径的两个端点之一。
这个性质可以分类讨论,用反证法证明。此处略。
那么可以先求出树的直径,然后对直径的两个端点各做一次dfs,这样,每个点都能得到两个深度。两个深度的最大值,即为该点到最远点的距离。
直径的两个端点怎么求呢?
可以任选一个点做一次dfs,得到一个深度最大的点\(A\),然后对\(A\)再做一次dfs,得到深度最大的点\(B\). \(A, B\)即为直径的两个端点。
#include <bits/stdc++.h> using namespace std; #define maxn 100005 vector<pair<int, int> > myv[maxn]; int n, dep1[maxn], dep2[maxn], dep[maxn]; int A, B, maxd; void dfs(int r, int fa, int * dep){ for(auto p : myv[r]){ int tv = p.first, tw = p.second; if(tv != fa){ dep[tv] = dep[r] + tw; dfs(tv, r, dep); } } } int main(){ int a, b, c; ios::sync_with_stdio(false); cin >> n; for(int i = 1; i < n; i++){ cin >> a >> b >> c; myv[a].push_back(make_pair(b, c)); myv[b].push_back(make_pair(a, c)); } dfs(1, 0, dep); for(int i = 1; i <= n; i++){ if(dep[i] > maxd) { maxd = dep[i], A = i; } } dfs(A, 0, dep1); maxd = 0; for(int i = 1; i <= n; i++){ if(dep1[i] > maxd) { maxd = dep1[i], B = i; } } dfs(B, 0, dep2); for(int i = 1; i <= n; i++){ printf("%d\n", max(dep1[i], dep2[i])); } return 0; }
方法二:采用树型DP做
先做一次dfs,自下而上求出每个节点的子树最长链和子树次长链。要求最长链和次长链无重边,即它们各属不同的分支。
再做一次dfs,自上而下求出每个节点的全局最长链和全局次长链,这个自上而下,其实就是换根dp了。
对于节点i,
1、如果其父亲的全局最长链未经过i,则i的全局最长链即为父亲的全局最长链+1;
2、否则,i的全局最长链为max(父亲的全局次长链+1,i的子树最长链)
如何求点i的全局次长链?
1、如果父亲的全局最长链经过i,则点i的全局次长链等于(节点\(i\)的子树最长链,节点\(i\)的子树次长链,\(i\)父亲的全局次长链+1)的第二大的值;
2、否则,点i的全局次长链等于点i的子树最长链
#include <bits/stdc++.h> using namespace std; #define maxn 100005 vector<pair<int, int> > myv[maxn]; int n; int f[maxn][2], g[maxn][2]; //f表示子树最远、次远, g表示全局最远、次远 void dfs1(int r, int fa){ for(auto p : myv[r]){ int tv = p.first, tw = p.second; if(tv == fa)continue; dfs1(tv, r); if(f[tv][0] + tw >= f[r][0]){ f[r][1] = f[r][0]; f[r][0] = f[tv][0] + tw; } else if(f[tv][0] + tw >= f[r][1]){ f[r][1] = f[tv][0] + tw; } } } void dfs2(int r, int fa){ for (auto p : myv[r]){ int tv = p.first, tw = p.second; if(tv == fa)continue; if(g[r][0] - tw == f[tv][0]){ //全局最长链经过节点tv if(g[r][1] + tw > f[tv][0]){ g[tv][1] = f[tv][0]; g[tv][0] = g[r][1] + tw; } else if(g[r][1] + tw > f[tv][1]){ g[tv][0] = f[tv][0]; g[tv][1] = g[r][1] + tw; }else{ g[tv][0] = f[tv][0]; g[tv][1] = f[tv][1]; } } else{ g[tv][0] = g[r][0] + tw; g[tv][1] = f[tv][0]; } dfs2(tv, r); } } int main(){ int a, b, c; ios::sync_with_stdio(false); while(cin >> n){ for(int i = 1; i <= n; i++) myv[i].clear(); for(int i = 1; i < n; i++){ cin >> c >> a >> b; myv[c].push_back(make_pair(a, b)); myv[a].push_back(make_pair(c, b)); } memset(f, 0, sizeof f); memset(g, 0, sizeof g); dfs1(1, 0); g[1][0] = f[1][0], g[1][1] = f[1][1]; dfs2(1, 0); for(int i = 1; i <= n; i++){ printf("%d\n", g[i][0]); } } return 0; }
例6. 选课
题目大意
有一个森林,共有\(n\)个节点,每个节点都有点权。你要在森林中选择\(m\)个节点,使得点权和最大。选点时有一个条件,如果选了点\(i\),则\(i\)的父亲必须选中。
数据范围
\(n \leq 300\)
分析
方法一
zhi
分析:
方法一:多叉转二叉
首先加上一个虚拟点,其点权为\(0\),作为所有树的根节点,这样,将森林转换为树。
设\(f_{i,j}\)表示前子树\(i\)中选择\(j\)个节点的最大权值。
$f_{i,j}=\sum_{son} f_{s_k,p_k}+w_i $
其中\(s_k\)是\(i\)的儿子,\(p_k\)表示在子树\(s_k\)中选了\(p_k\)个节点。\(\sum_{p_k}=j-1\).
转移方程虽然列出了,但由于是多叉,枚举\(p_k\)成了大问题。
可以多叉转二叉来处理。
这样就简单多了。
其中第一种情况表示选了节点\(i\),第二种情况表示没有选节点\(i\),自然也不能去左子树中选择。
#include <bits/stdc++.h> using namespace std; #define maxn 305 int lson[maxn], rson[maxn], last[maxn], score[maxn], sz[maxn]; int f[maxn][maxn]; int n, m; void dfs(int r){ sz[r] = 1; f[r][0] = 0, f[r][1] = score[r]; if(lson[r]) dfs(lson[r]), sz[r] += sz[lson[r]]; if(rson[r]) dfs(rson[r]), sz[r] += sz[rson[r]]; if(lson[r] == 0 && rson[r] == 0)return; else if(rson[r] == 0){ //只有左子树 for(int i = 0; i <= sz[lson[r]] && i < m; i++) f[r][i + 1] = max(f[r][i + 1], score[r] + f[lson[r]][i]); } else if(lson[r] == 0){ //只有右子树 for(int i = 0; i <= sz[rson[r]] && i <= m; i++) f[r][i] = max(f[r][i], f[rson[r]][i]); //未选r节点 for(int i = 0; i <= sz[rson[r]] && i < m; i++) f[r][i + 1] = max(f[r][i + 1], f[rson[r]][i] + score[r]); //选了r节点 } else{ //有左右子树 for(int i = 0; i <= sz[lson[r]] && i <= m - 1; i++){ //选了r节点 for(int j = 0; j <= sz[rson[r]] && j <= m - i - 1; j++){ f[r][i + 1 + j] = max(f[r][i + 1 + j], f[lson[r]][i] + f[rson[r]][j] + score[r]); } } for(int j = 0; j <= sz[rson[r]] && j <= m; j++) f[r][j] = max(f[r][j], f[rson[r]][j]); //未选r节点 } } int main(){ int a, b; ios::sync_with_stdio(false); cin >> n >> m; for(int i = 1; i <= n; i++){ cin >> a >> b; score[i] = b; if(!lson[a])lson[a] = i, last[a] = i; else{ rson[last[a]] = i; last[a] = i; } } m++; dfs(0); printf("%d\n", f[0][m]); return 0; }
方法二 背包
首先也是加一个虚拟节点,将森林变成树。
实现有两种方式:一种是将子树递归完,再进行背包合并。
另一种是访问到节点\(i\)时,用父亲的背包加上节点\(i\)的权值去更新节点\(i\)的背包(此时父亲的背包未改变),然后递归完子树\(i\)以后,再用节点\(i\)的背包去更新父亲的背包。
第一种背包方式:子树合并背包
#include <bits/stdc++.h> using namespace std; #define maxn 305 int n, m, ecnt, fir[maxn], f[maxn][maxn]; int sz[maxn]; int score[maxn]; struct edge{ int v, nxt; }eds[maxn]; void adde(int a, int b){ eds[++ecnt].v = b, eds[ecnt].nxt = fir[a], fir[a] = ecnt; } void dfs(int r){ sz[r] = 1; f[r][1] = score[r]; for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; f[tv][1] = score[r]; f[tv][0] = 0; dfs(tv); for(int j = m; j > 0; j--){ for(int k = 0; k <= sz[tv] && k < j; k++){ f[r][j] = max(f[r][j], f[r][j - k] + f[tv][k]); } } sz[r] += sz[tv]; } } int main(){ int a, b; scanf("%d %d", &n, &m); for(int i = 1; i <= n; i++){ scanf("%d %d", &a, &b); score[i] = b; adde(a, i); } m++; dfs(0); printf("%d\n", f[0][m]); return 0; }
第二种背包
以dfs序,将节点\(i\)作为物品,加入背包;返回时更新父亲的背包。
注意更新时,是同级更新。\(f_{r,j}\) 要被\(f_{i,j-1}\)更新,他们是同级的。
\(f_{i,j}\)隐含了它的祖先都已经选中。
#include <bits/stdc++.h> using namespace std; #define maxn 305 int n, m, ecnt, fir[maxn], f[maxn][maxn]; int sz[maxn]; int score[maxn]; struct edge{ int v, nxt; }eds[maxn]; void adde(int a, int b){ eds[++ecnt].v = b, eds[ecnt].nxt = fir[a], fir[a] = ecnt; } void dfs(int r){ for(int i = fir[r]; i; i = eds[i].nxt){ int tv = eds[i].v; for(int j = 0; j <= m; j++) f[tv][j] = f[r][j] + score[tv]; dfs(tv); for(int j = 1; j <= m; j++) f[r][j] = max(f[r][j], f[tv][j - 1]); } } int main(){ int a, b; scanf("%d %d", &n, &m); for(int i = 1; i <= n; i++){ scanf("%d %d", &a, &b); score[i] = b; adde(a, i); } m++; memset(f, -1, sizeof f); f[0][0] = 0; dfs(0); printf("%d\n", f[0][m - 1]); return 0; }
例7 河流
题目描述
几乎整个Byteland王国都被森林和河流所覆盖。小点的河汇聚到一起,形成了稍大点的河。就这样,所有的河水都汇聚并流进了一条大河,最后这条大河流进了大海。这条大河的入海口处有一个村庄——名叫Bytetown
在Byteland国,有n个伐木的村庄,这些村庄都座落在河边。目前在Bytetown,有一个巨大的伐木场,它处理着全国砍下的所有木料。木料被砍下后,顺着河流而被运到Bytetown的伐木场。Byteland的国王决定,为了减少运输木料的费用,再额外地建造k个伐木场。这k个伐木场将被建在其他村庄里。这些伐木场建造后,木料就不用都被送到Bytetown了,它们可以在 运输过程中第一个碰到的新伐木场被处理。显然,如果伐木场座落的那个村子就不用再付运送木料的费用了。它们可以直接被本村的伐木场处理。
注意:所有的河流都不会分叉,也就是说,每一个村子,顺流而下都只有一条路——到bytetown。
国王的大臣计算出了每个村子每年要产多少木料,你的任务是决定在哪些村子建设伐木场能获得最小的运费。其中运费的计算方法为:每一块木料每千米1分钱。
编一个程序:
1.从文件读入村子的个数,另外要建设的伐木场的数目,每年每个村子产的木料的块数以及河流的描述。
2.计算最小的运费并输出。
第1行:包括两个数 \((2<=n<=100), k(1<=k<=50)\) 且 \(k<=n\)。\(n\)为村庄数,\(k\)为要建的伐木场的数目。除了bytetown外,每个村子依次被命名为\(1,2,3,\dots,n\),bytetown被命名为\(0\)。
接下来\(n\)行,每行\(3\)个整数\(w_i(0<=wi<=10000),\)v_i(0<=vi<=n)$, \(d_i(1<=di<=10000)\), 分别表示每年\(i\)村子产的木料的块数, 离i村子下游最近的村子(即\(i\)村子的父结点), \(i\)到父亲的距离(千米)。
保证每年所有的木料流到bytetown的运费不超过\(2000,000,000\)分。
\(50\%\)的数据中\(n\)不超过\(20\)。
分析
先多叉转二叉,然后做树型DP.
设f[i][j][k]表示以i为根的子树中建立j个伐木场,往祖先方向最近的伐木场在k时的最小花费。
若在i点建立伐木场,则f[i][j][k]可以转移为:
f[i][j][k]=min(f[i.lson][p][i]+f[i.rson][j-1-p][k])
若不在i点建立伐木场,则f[i][j][k]可以转移为:
f[i][j][k]=min(f[i.lson][p][k]+f[i.rson][j-p][k]+w[i]*(dis(i,k))
最终我们求的目标状态为
f[r.lson][m-1][r]。
#include <bits/stdc++.h> using namespace std; #define maxn 105 #define ll int #define min(a, b) (a < b ? (a) : (b)) ll f[maxn][maxn][maxn]; int lson[maxn], rson[maxn], last[maxn]; ll d[maxn], w[maxn]; int n, k; ll inf = 0x3f3f3f3f; vector<pair<int, ll>> myv[maxn]; bool vis[maxn]; void dfs0(int r, int fa){ for(auto p : myv[r]){ int tv = p.first; ll tw = p.second; if(tv != fa){ d[tv] = d[r] + tw; dfs0(tv, r); } } } ll dfs(int r, int cnt, int up){ if(f[r][cnt][up] != inf) return f[r][cnt][up]; if(lson[r] == 0 && rson[r] == 0) if(cnt == 0) f[r][cnt][up] = w[r] * (d[r] - d[up]); else f[r][cnt][up] = 0; else if(lson[r] == 0) { if(cnt > 0) f[r][cnt][up] = min(f[r][cnt][up], dfs(rson[r], cnt - 1, up)); f[r][cnt][up] = min(f[r][cnt][up], dfs(rson[r], cnt, up) + w[r] * (d[r] - d[up])); } else if(rson[r] == 0) { if(cnt > 0) f[r][cnt][up] = min(f[r][cnt][up], dfs(lson[r], cnt - 1, r)); f[r][cnt][up] = min(f[r][cnt][up], dfs(lson[r], cnt, up) + w[r] * (d[r] - d[up])); } else{ for(int i = 0; i <= cnt; i++){ if(i < cnt) f[r][cnt][up] = min(f[r][cnt][up], dfs(lson[r], i, r) + dfs(rson[r], cnt - 1 - i, up)); f[r][cnt][up] = min(f[r][cnt][up], dfs(lson[r], i, up) + dfs(rson[r], cnt - i, up) + (d[r] - d[up]) * w[r]); } } return f[r][cnt][up]; } int main(){ int a; ios::sync_with_stdio(false); cin >> n >> k; memset(f, 0x3f, sizeof f); for(int i = 1; i <= n; i++){ cin >> w[i] >> a >> d[i]; myv[i].push_back(make_pair(a, d[i])); myv[a].push_back(make_pair(i, d[i])); if(lson[a]){ rson[last[a]] = i; last[a] = i; } else{ lson[a] = i; last[a] = i; } } dfs0(0, -1); cout << dfs(lson[0], k, 0); return 0; }
采用子树合并的方法也可以做。
#include <bits/stdc++.h> using namespace std; #define maxn 105 #define ll long long int int fir[maxn]; struct edge{ int v,w,nxt; }es[maxn<<1]; int n,m,dis[maxn],sz[maxn]; int ecnt; int fa[maxn][maxn],fadis[maxn][maxn]; int g[maxn][maxn]; int f[maxn][maxn][maxn]; int w[maxn]; void adde(int a,int b,int c){ es[++ecnt].v=b,es[ecnt].nxt=fir[a],es[ecnt].w=c,fir[a]=ecnt; } void dfs(int r){ for(int i=fir[r];i;i=es[i].nxt){ int tmp=es[i].v; fa[tmp][0]=tmp; fa[tmp][1]=r; fadis[tmp][1]=es[i].w; for(int j=2;fa[r][j-1];j++){ fa[tmp][j]=fa[r][j-1]; fadis[tmp][j]=fadis[r][j-1]+es[i].w; } dfs(tmp); } } void dfs2(int r){ sz[r]=1; f[r][1][0]=0; if(r>1) for(int k=1;fa[r][k];k++){ f[r][0][k]=w[r]*fadis[r][k]; } for(int i=fir[r];i;i=es[i].nxt){ int tmp=es[i].v; dfs2(tmp); memset(g,0x3f,sizeof g); for(int i=0;i<=sz[r]&&i<=m+1;i++){ for(int j=0;j<=sz[tmp]&&j<=(m+1-i);j++){ for(int k=1;fa[r][k];k++){ g[i+j][k]=min(g[i+j][k],f[r][i][k]+f[tmp][j][0]); //tmp点建了伐木场 g[i+j][k]=min(g[i+j][k],f[r][i][k]+f[tmp][j][k+1]); //tmp没有建 } if(i>0&&j>0)g[i+j][0]=min(g[i+j][0],f[r][i][0]+f[tmp][j][0]); if(i>0)g[i+j][0]=min(g[i+j][0],f[r][i][0]+f[tmp][j][1]); } } sz[r]+=sz[tmp]; for(int i=0;i<=sz[r];i++){ for(int j=0;fa[r][j];j++){ f[r][i][j]=g[i][j]; } } } } int main(){ int a,b; scanf("%d %d",&n,&m); for(int i=1;i<=n;i++){ scanf("%d %d %d",&w[i+1],&a,&b); adde(a+1,i+1,b); } memset(f,0x3f,sizeof f); fa[1][0]=1; dfs(1); dfs2(1); printf("%d\n",f[1][m+1][0]); return 0; }