题意
T T T组测试. n n n个点的有根树, 根为 1 1 1, 初始根被占领, 且可以无限从根派兵去占领其他节点, 兵可以沿着边走(无向边, 可以返回来走), 每走一次花费为 1 1 1. 问占领所有点最少需要多少花费.
1 ≤ T ≤ 1 0 5 , 1 ≤ n ≤ 1 0 6 ) 1 \le T \le 10^5, 1 \le n \le 10^6) 1≤T≤105,1≤n≤106)
题解
设 d p 1 ( u ) dp_1(u) dp1(u)为 点 u u u有一个并, 只用这一个兵走完以 u u u为根的子树, 并且回到 u u u需要的步数; d p 2 ( u ) dp_2(u) dp2(u)为 从根节点 ( 1 ) (1) (1)出若干个兵, 走完 u u u这棵子树, 并且兵停在某些叶子需要的最小步数.
答案很显然是 d p 2 ( 1 ) dp_2(1) dp2(1)
下面想转移:
很容易转移 d p 1 ( u ) dp_1(u) dp1(u),
d p 1 ( u ) = ∑ v ∈ S O N u ( d p 1 ( v ) + 2 ) dp_1(u) = \sum_{v \in SON_u} (dp_1(v) + 2) dp1(u)=v∈SONu∑(dp1(v)+2)
很显然, 这个兵往下走需要一次, 走完后回来需要一次, 所以一条边加两次.
如何转移 d p 2 dp_2 dp2呢??
首先有一个树形 d p dp dp的常用性质:
如果对一个节点 u u u, 当前已经考虑完了若干个儿子, 并且完成了儿子到 u u u的转移, 即得到了"部分"的 d p ( u ) dp(u) dp(u). 这时的 d p ( u ) dp(u) dp(u)是有意义的, 即" u u u和已经做完的儿子构成的子树(为方便, 称它为’当前子树’)的 d p dp dp值", 如图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2M8EUekT-1619673539717)(/img/vp-2020-ccpc-qinhuangdao-site/current-subtree.jpg)]
这样就可以合理利用"已经做完的所有儿子", 而不是枚举做完的儿子. 当然前提是状态要定义得好.
回到本题, 考虑当前儿子 v v v, 可能有三种情况:
-
做完的儿子中的某一个兵回到 u u u, 它再走 v v v. 注意这样的话可以用他来"替换"一个 d p 2 ( x ) ∣ x ∈ T r e e v dp_2(x) \mid x \in Tree_v dp2(x)∣x∈Treev的花费(停在 T r e e v Tree_v Treev的其他兵的花费是直接算的 d p 2 dp_2 dp2, 从根下来的, 不与当前做法冲突)
-
v v v做完后, 一个兵回到 u u u, 去走"做完"的儿子(利用"当前子树", 即可考虑所有"做完"的儿子[为什么打引号呢? 因为这一步改变了"当前子树"的策略, 也就是说, 当前子树的当前策略并不是最终策略])
-
直接从根新派若干个兵走完 v v v
对应的三个花费分别是:
d p 1 ( u ) + d p 2 ( v ) d p 2 ( u ) + d p 1 ( v ) + 2 d p 2 ( u ) + d p 2 ( v ) \begin{aligned} & dp_1(u) + dp_2(v) \newline & dp_2(u) + dp_1(v) + 2 \newline & dp_2(u) + dp_2(v) \end{aligned} dp1(u)+dp2(v)dp2(u)+dp1(v)+2dp2(u)+dp2(v)
前两个看着很迷?
解读一下, 以第一个决策的花费为例:
d p 2 ( v ) dp_2(v) dp2(v)被分为了两个部分, 1 − − − u − − − T r e e v 1 --- u --- Tree_v 1−−−u−−−Treev; 而 d p 1 ( u ) dp_1(u) dp1(u)只是 C u r r e n t T r e e u − − − b a c k t o u CurrentTree_u --- back\ to\ u CurrentTreeu−−−back to u一个部分, 重新组合一下, 变成 1 − − − C u r r e n t T r e e u − − − b a c k t o u − − − T r e e v 1 --- CurrentTree_u --- back\ to\ u --- Tree_v 1−−−CurrentTreeu−−−back to u−−−Treev, 这样就是实际上的路线了
第二个也是同理拆分重组
结合上述分析和方程, 再理解一下决策…
…
可以发现, 决策1和2恰好是"对称"的, 考虑了全部的"兵回到 u u u再走其他儿子"的情况; 同时有第三个决策, 考虑了从根下来的情况, 弥补了前两个方程"只是一个兵在走"的不足. 所以, 这种转移考虑到了所有的情况, 是合理且巧妙的.
那么新的当前子树 d p 2 dp_2 dp2的值就对上述三种决策的花费最小即可.
最后确定边界条件. 树形dp的边界是叶子, 但是这里有一个不是常规的树形dp定义法, d p 2 dp_2 dp2居然和根扯上了关系. 可以理解为, 结点本身就有一个花费(从根走到该节点的步数, 即深度), 把他加在 d p 2 dp_2 dp2上即可. d p 1 dp_1 dp1的初始值为 0 0 0.
独立思考我绝对想不到这样的做法, 太妙了, 下次补"为什么这么想"吧, 咕咕咕
复杂度 O ( n ) O(n) O(n)
{{% code %}}
const int maxn = 1e6+10;
int t, n, ans, dp1[maxn], dp2[maxn];
vector<int> son[maxn];
void dfs(int u, int d) {
dp1[u] = 0;
dp2[u] = d;
for (auto v : son[u]) {
dfs(v, d + 1);
dp2[u] = min(min(dp1[u] + dp2[v], dp2[u] + dp1[v] + 2), dp2[u] + dp2[v]);
dp1[u] += dp1[v] + 2;
}
}
int main() {
scanf("%d", &t);
for (int kase = 1; kase <= t; kase++) {
scanf("%d", &n);
for (int i = 1; i <= n; i++)
son[i].clear();
for (int i = 2; i <= n; i++) {
int f;
scanf("%d", &f);
son[f].pb(i);
}
dfs(1, 0);
printf("Case #%d: %d\n", kase, dp2[1]);
}
return 0;
}
{{% /code %}}