树型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\)可以参加,也可以不参加。

\[f_{i,0} =\sum_{j \in son_i} max(f_{j,0}, f_{j,1}) \]

\(i\)参加,则\(i\)的直接下属\(j\)不能参加宴会。

\[f_{i,1} = \sum_{j \in son_i} f_{j,0} + h_i \]

叶子节点的初始状态\(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\)个点的最多苹果数量。
于是得到状态转移方程为

\[f_{i,j}=\max_{k\in [0,j-1]}(f_{l,k}+f_{r,j-1-k})+w_i \]

其中\(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}$$

\[f_{i,1}=\sum_{j \in i.son} min(f_{j,0}, 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\)成了大问题。
可以多叉转二叉来处理。
这样就简单多了。

\[\begin{aligned}f_{i,j}=\begin{cases} f_{lson,k}+f_{rson,j-k-1}+w_i \\ \\ f_{rson,j} \end{cases} \end{aligned} \]

其中第一种情况表示选了节点\(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;
}
posted @ 2025-01-16 21:50 hefenghhhh 阅读(9) 评论(0) 推荐(0) 编辑
摘要: 差分约束 给出\(n\)个活动,设\(t_i\)表示第\(i\)个活动开始的时间。这些活动满足以下\(m\)个关系: \[\begin{cases} t_{i1}-t_{j1}<=B_1 \\ t_{i2}-t_{j2}<=B_2 \\ \dots \\t_{im}-t_{jm}<=B_m \end 阅读全文
posted @ 2024-11-26 18:52 hefenghhhh 阅读(9) 评论(0) 推荐(0) 编辑
摘要: 柯尼斯堡七桥问题 18世纪时,欧洲的一个小城柯尼斯堡有七座桥,连接了四个地方,如下图所示。人们聊天时,有人提出这样一个问题:是否存在一种方法,能够从一个地点出发,经过每座桥一次且仅一次,最后回到起点。人们讨论了很长时间,都没有找到方案。 欧拉出手 这个问题引起了欧拉的兴趣。他稍微研究了以下,很快就证 阅读全文
posted @ 2024-11-21 18:31 hefenghhhh 阅读(68) 评论(0) 推荐(0) 编辑
摘要: header: 中山市迪茵公学 何雄姿 backgroundColor: #db9 footer: 2022.10 图论基础 图的存储 图的遍历 最小生成树 kruskal 算法 prim算法 最短路 Dijkstra 算法 Bellman-Ford 算法 SPFA算法 Floyd-Warshall 阅读全文
posted @ 2024-11-01 23:11 hefenghhhh 阅读(89) 评论(0) 推荐(0) 编辑
摘要: vector 的用法 在 C++ 中,std::vector 是一个动态数组,它可以在运行时调整大小,std::vector 是 C++ 标准模板库 (STL) 中的一个重要容器类。 基本用法 在使用 std::vector 之前,需要包含头文件 <vector>。 #include <iostre 阅读全文
posted @ 2024-11-01 11:40 hefenghhhh 阅读(35) 评论(0) 推荐(0) 编辑
点击右上角即可分享
微信分享提示