POJ 1947 Rebuilding Road(树形DP)
Description
Farmer John wants to know how much damage another earthquake could do. He wants to know the minimum number of roads whose destruction would isolate a subtree of exactly P (1 <= P <= N) barns from the rest of the barns.
Input
* Lines 2..N: N-1 lines, each with two integers I and J. Node I is node J's parent in the tree of roads.
Output
Sample Input
11 6 1 2 1 3 1 4 1 5 2 6 2 7 2 8 4 9 4 10 4 11
Sample Output
2
Hint
题意:
给定一棵树, 求解最少切除几条边可以得到一个大小为 P 的子树
思路:
1. 这题是我做过的最难理解的 DP 题目了(假如我的脑子没退化), 我顺着别人的代码单步调试才搞清楚 DP 的思路, VS 的单步调试真是神器, 不仅可以 debug, 更能帮助理解代码
2. 先贴上网上广为流传的思路, dp[u][j] 表示以 u 为根的子树保留 j 个节点的最少切除边数. 对于 u 的某一个孩子 v, 假如保留 v 的话, 那么
dp[u][j] = min(1+dp[u][j], dp[u][j-k]+dp[v][k]). 假如不保留 v 的话, 那么 dp[u][j] = dp[u][j]+1
3. 我的思路
- dp[u][j] 表示以 u 为根的子树保留 j 个节点最少切除的边数
- 树形 DP 求解父节点时, 一般是先求解子节点, 得到子节点对应的信息, 然后回溯到父节点, 代码的框架基本这样
dfs(int u) { 初始化 for(u 的孩子节点v...) { dfs(v) } }
- 某时刻, 恰好要进行 dfs(i), 此刻 dp[u][j] 记录的数据是假设父节点 u 仅有前 i-1 个孩子时的最优解. dp[u][j] 记录的是 u 的前 i-1 个孩子保留 j 个节点的最少切边数
- 对 i 执行 dfs(i), 得到以 i 为根的子树保留 j 个节点的最少切边数dp[i][j]
- 这时, 我们假设直接切除第 u-i 这条边, 多切除了一条边, 所有的 dp[u][j]+1, 并记录 ans = dp[u][j]+1
- (5) 是做了一个假设, 但以 i 为根树的加入可能会使某个 dp[u][j'] 变小, 所以需要判断 dp[u][j-k]+dp[i][k] 与 ans 的关系
- dp[u][j] = min(ans, dp[u][j-k]+dp[i][k])
总结:
1. 这道题我看着别人的代码, 用 VS 的单步调试才弄明白, 变量的初始化非常神奇, 甚至有些不合逻辑(dp[u][1] = 0 就与 dp 的定义不符), 但使用起来, 却是极好的
2. 以前做树形 dp 题时, 总怕重复计算, 但从这道题中才完全明白, 树形 dp 常用的状态转移方程 dp[u][j] = min( dp[u][j], dp[u][j-k]+dp[v][k]) 表示
dp[u][j] (前 i 个孩子) = min( dp[u][j](前 i-1 个孩子), dp[u][j-k](前 i-1 个孩子)+dp[i][k](第 i 个孩子))
3. 就像(2) 所描述的那样, 对 u 的第 i-1 个孩子进行计算的时候, u 并不知道其是否有第 i 个孩子, 所以, dp[u][1] 初始化为 0 也可以理解成符合逻辑 --- 刚开始假设 u 没有孩子节点, 那么 dp[u][1] 就是 0
3. 这道题中的第二层循环, v = V...0 仍是为了防止重复计算, 从状态转移方程也可以看出, dp[u][j] (前 i 个孩子) = min( dp[u][j](前 i-1 个孩子), dp[u][j-k](前 i-1 个孩子)+dp[i][k](第 i 个孩子)). 假如 v = 0...V, 那么状态转移方程就变成了 dp[u][j] (前 i 个孩子) = min( 1+dp[u][j](前 i-1 个孩子), dp[u][j-k](前 i 个孩子)+dp[i][k](第 i 个孩子))
4. 第三层循环 k 的遍历顺序就没什么要求了, 因为 k 的遍历是状态转移方程的非递归写法 dp[u][j] = min( 1+dp[u][j], dp[u][j-1]+dp[i][1], dp[u][j-2]+dp[i][2], ... dp[u][0]+dp[i][k]). 从状态转移方程中也能看出, 遍历顺序无关紧要, 没有依赖的问题
5. 初始化非常 tricky, 我自己是断然想不出的
6. 建树方法有点意思, 不理解要什么那样建树, 可能是兼顾效率与能力吧, 但不理解为什么不能随意取点作为大树的根
代码
#include <iostream> #include <algorithm> using namespace std; const int MAXN = 160; const int INFS = 0x3fffffff; int dp[MAXN][MAXN], U[MAXN], V[MAXN]; bool vis[MAXN]; void treedp(int u, int vol, int n) { for (int v = 0; v <= vol; ++v) dp[u][v] = INFS; dp[u][1] = 0; for (int i = 1; i < n; ++i) { if (u != U[i]) continue ; treedp(V[i], vol, n); for (int v = vol; v >= 0; --v) { int ans = INFS; if (dp[u][v] != INFS) ans = dp[u][v] + 1; for (int p = 0; p <= v; ++p) if (dp[u][p] != INFS && dp[V[i]][v - p] != INFS) ans = min(ans, dp[u][p] + dp[V[i]][v - p]); dp[u][v] = ans; } } } int main() { int n, p; while (scanf("%d %d", &n, &p) != EOF) { memset(vis, false, sizeof(vis)); for (int i = 1; i < n; ++i) { scanf("%d %d", &U[i], &V[i]); vis[V[i]] = true; } int rt; for (int i = 1; i <= n; ++i) if (!vis[i]) rt = i; treedp(rt, p, n); int ans = dp[rt][p]; for (int i = 1; i <= n; ++i) if (dp[i][p] < ans) ans = dp[i][p] + 1; printf("%d\n", ans); } return 0; }