题目描述
一场可怕的地震后,人们用 N N N 个牲口棚(编号 1 ∼ N 1 \sim N 1∼N)重建了农夫 John 的牧场。由于人们没有时间建设多余的道路,所以现在从一个牲口棚到另一个牲口棚的道路是惟一的。因此,牧场运输系统可以被构建成一棵树。
John 想要知道另一次地震会造成多严重的破坏。有些道路一旦被毁坏,就会使一棵含有 P 个牲口棚的子树和剩余的牲口棚分离,John 想知道这些道路的最小数目。
P1272 重建道路(树形背包)
题意分析
题目的意思是给定一棵具有根节点的树,节点个数一共有N个节点。现在问最少断多少条边,可以使得某个子树的节点个数恰好为P
其实这道题的难点在于,为什么是用分组背包来解,以及怎么实现这个分组背包?如果将两个知识点拆开来看,分组背包,树形DP都好理解。但如果将两个知识点合并,就很容易考察出是否真正地理解,树形背包就是一个很好的例子。
分组背包:在选择物品的时候,一开始将物品分为好几组,在选择时,可以从每一组中至多选择一件物品,问如何获得最大的价值
说是分组背包的一个问题,其实真正对比上去,还是有较大的差异,分组背包只能帮助理解,不能直接代入到树形背包问题(我的一点认识)
树形背包
先用每个点的出度,初始化状态,也就是边的数量,可能要拆掉的边
- 状态表示
f[i][j]
:以i
为根的子树,保留j
个点,需要拆掉的边的数量的方案(取最小) - 状态计算: f [ n o w ] [ k ] = m i n ( f [ n o w ] [ k ] , f [ n o w ] [ k − s ] + f [ s o n ] [ s ] − 1 ) f[now][k]=min(f[now][k],f[now][k-s]+f[son][s]-1) f[now][k]=min(f[now][k],f[now][k−s]+f[son][s]−1)
这里为什么要-1
呢?我们拿题目的样例举例子:f[1][1]=4
代表以1为根节点,保留1个点所要拆除的边的数量为4,f[5][1]=0
表示需要拆除1条边(这里的边仅考虑往下的情况,往上的最后会统一处理)。1和5节点具有父子关系,所以可以进行上述的状态计算,也就是状态转移。
f
[
1
]
[
2
]
=
f
[
1
]
[
1
]
+
f
[
5
]
[
1
]
−
1
=
4
+
0
−
1
=
3
f[1][2]=f[1][1]+f[5][1]-1=4+0-1=3
f[1][2]=f[1][1]+f[5][1]−1=4+0−1=3,因为两个点合并了,它们之间的边得以保留所以要-1。
#include <iostream>
#include <cstring>
#include <algorithm>
using namespace std;
const int N = 160;
int n, p;
int h[N], e[N], ne[N], idx;
int a[N], b[N]; // 出度、入度
int f[N][N]; // 表示以i为根的子树,保留j个点(包括自身)所要删除的边数
void add(int a, int b) {
e[idx] = b, ne[idx] = h[a], h[a] = idx++;
}
int dfs(int u) {
int temp = 0, sum = 1;
for (int i = h[u]; ~i; i = ne[i]) {
int j = e[i];
temp = dfs(j), sum += temp;
// 分组背包(难点)
for (int k = sum; k >= 1; k--) {
for (int s = 1; s < k; s++) {
f[u][k] = min(f[u][k], f[u][k-s] + f[j][s] - 1);
}
}
}
return sum;
}
int main() {
cin >> n >> p;
memset(h, -1, sizeof h);
memset(f, 0x3f, sizeof f);
for (int i = 0; i < n - 1; i++) {
int x, y;
cin >> x >> y;
add(x, y);
a[x]++, b[y] = 1;
}
int root = 0;
for (int i = 1; i <= n; i++) {
if (!b[i]) root = i;
f[i][1] = a[i];
}
dfs(root);
int ans = f[root][p];
// 这里的+1表示的就是因为这里主要针对的是除根节点以外的点
// 它们如果是答案的话,要多减去与父节点相关联的边
for (int i = 1; i <= n; i++) ans = min(ans, f[i][p] + 1);
cout << ans << endl;
return 0;
}