「USACO 2023.1 Platinum」Subtree Activation 题解

First Post:

Last Update:

非常好题目,结论差点就真了。

手玩几棵树的构造过程,会发现我们每次都是先从一个点开始,点亮其子树所有点,然后到父亲,点亮父亲子树所有点,如此往复直到某个拐点 ,又往下走到某个点 ,熄灭在 子树但不在 子树的所有点,如此往复,到达某个点时停下。

那么,这个过程相当于在树上走出一条链,那么在这条链上的点的限制都能得到满足,但代价是 链上深度最浅的点的子树大小。

设链的端点为 ,需要注意的是,链并不一定是形如 的,可能会在到达 后,又往上走一段,又折返下来,即找到某个 的祖先 ,走出形如 的路径。此时代价为

那么原问题转化为,找到若干条这样的路径,使得树上每个点被覆盖至少一次,最小化代价和。

因为 并不一定是 ,所以我们难以知道以 为端点的链会在哪里合并起来。不如直接对链上的每条边计算代价,而不是关注 具体是多少。具体地,对于 的边,代价都是 。对于一条链的端点,代价就是 。一条链就由两个部分组成:边被经过 次和 次的部分。

所以设 表示 子树内还有 条链的端点没有找到与其对应的端点(即这些链还处在”边被经过一次“的阶段), 有没有被至少一条链经过则用 表示。

的初值就是

在合并 的 DP 数组和儿子 的 DP 数组时,分讨 边被经过 次(经过 次的情况可以调整到不超过 次)。那么有转移:

上述三类分别代表 经过 次的转移。

于是本题就做完了,时间复杂度

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
#include <bits/stdc++.h>
using namespace std;
template<typename T> inline void ckmin(T &x,const T &y) { if(x > y) x = y;}
const int N = 2e5 + 5;
int n;
vector<int> G[N];
int dp[N][2][2],sze[N];
// 第一维:u 上接了奇数/偶数条链,第二维:u 上到底有没有经过链
void dfs(int x,int fa) {
sze[x] = 1;
for(auto y : G[x]) if(y != fa) dfs(y,x),sze[x] += sze[y];
int tmp[2][2];memset(tmp,0x3f,sizeof tmp);
dp[x][0][0] = 0;dp[x][1][1] = sze[x];dp[x][0][1] = sze[x] + sze[x];
dp[x][1][0] = 0x3f3f3f3f;
for(auto y : G[x]) {
if(y == fa) continue;
for(int i = 0;i < 2;i++)
for(int j = 0;j < 2;j++) {
ckmin(tmp[i][j],dp[x][i][j] + dp[y][0][1]);
for(int k = 0;k < 2;k++)
ckmin(tmp[i ^ 1][j | k],dp[x][i][j] + dp[y][1][k] + (sze[x] - sze[y])),
ckmin(tmp[i][j | k],dp[x][i][j] + dp[y][0][k] + 2 * (sze[x] - sze[y]));
}
for(int i = 0;i < 2;i++)
for(int j = 0;j < 2;j++)
dp[x][i][j] = tmp[i][j],tmp[i][j] = 0x3f3f3f3f;
}
}
int main() {
cin >> n;
for(int i = 2,x;i <= n;i++) cin >> x,G[x].push_back(i);
dfs(1,0);
cout << dp[1][0][1] << endl;
return 0;
}