2020 CCPC 秦皇岛 K.Kingdoms Power

我搬运我自己应该算原创吧

题目

题意

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) 1T105,1n106)

题解

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)=vSONu(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, 可能有三种情况:

  1. 做完的儿子中的某一个兵回到 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)xTreev的花费(停在 T r e e v Tree_v Treev的其他兵的花费是直接算的 d p 2 dp_2 dp2, 从根下来的, 不与当前做法冲突)

  2. v v v做完后, 一个兵回到 u u u, 去走"做完"的儿子(利用"当前子树", 即可考虑所有"做完"的儿子[为什么打引号呢? 因为这一步改变了"当前子树"的策略, 也就是说, 当前子树的当前策略并不是最终策略])

  3. 直接从根新派若干个兵走完 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 1uTreev; 而 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 CurrentTreeuback 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 1CurrentTreeuback to uTreev, 这样就是实际上的路线了

第二个也是同理拆分重组

结合上述分析和方程, 再理解一下决策…

可以发现, 决策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 %}}

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值