题解:P14637 [NOIP2025] 树的价值 / tree(官方数据)
提供一个时间复杂度
:::info[本文使用的记号]
-
-
-
-
:::
定义树上一个结点的子树
首先可以注意到,对于一个结点,如果其价值为
考虑最优化价值的策略,对于一个结点而言,必然是从其某个子结点已有的子树
:::info[一个例子]{open}
假设根结点包含了三个子结点,对应的价值分别为
注意到如下事实:
- 垫脚石实际上不一定要全部用完,可以进一步留给祖先使用;
- 所有结点在最开始的时候都是垫脚石,只不过其可以选择在自身使用,也可以选择在其祖先使用。
- 上述决策是只针对根结点进行的。实际上,可以选择将第二棵子树的两个垫脚石分别设置为
6 和7 ,从而将这棵子树本身的价值提升到8 。 :::
通过这个性质足以设计出如下做法:
:::success[
进一步的观察可以发现,对于一个非叶子结点,会选择恰好一个子结点,并根据其已有的价值,将合适数量的垫脚石设置为合适的值。称每个结点选择的子结点和自身形成的边为传递边,那么每个非叶子结点都恰好往下连接了一条传递边,这就自然形成了树的剖分结构(一些经典的剖分结构为重链剖分和长链剖分)。这里需要明确,在传递边的系统下,一个结点的价值等于从传递边传递上来的价值,加上在这个结点使用的垫脚石数量。(可以通过上面的例子辅助思考)
之前的做法是尝试根据每个结点子树的情况动态决策出每个结点选择的传递边,不过这一做法较难扩展。考虑将思路反过来,首先确定传递边形成的剖分结构,随后在剖分结构上计算最优的点权填写方案。对于树上的结点,根据前面指出的事实,它们一开始都是垫脚石。当某个祖先结点选择了一个垫脚石并进行利用后,这个垫脚石的贡献会从这个祖先结点开始,顺着传递边向上走。
:::info[另一个例子]{open}
考虑如上树结构,其中红边表示传递边,而蓝边表示非传递边。对
通过这个例子可以确定每个结点的最大贡献:考虑从这个结点到根结点的链,链上最长的传递链的长度(结点个数)就是这个结点的最大贡献。 :::
从这个例子可以发现:只需要在动态规划的转移方程维护到根结点链上的最长传递链的长度,即可独立计算出每个结点的贡献,相加后就是剖分下的最大价值。我们就得到了如下做法:
:::success[
上述做法还是不够快。考虑到上述状态的参数空间为
为了将最长传递链参数和当前传递链参数分离,需要进行钦定,最终能够设计出如下状态:
考虑如何转移。
- 默认转移
g(i, j) \leftarrow j \times \text{sz}(i) ,表示子树内传递边没有任何作用; - 对每一个子结点
v \in \text{sons}(i) ,有转移g(i, j) \leftarrow j + g(v, j + 1) + \sum_{\substack{w \in \text{sons}(i)\\ w \neq v}} f(w, j) 这里的
j 项代表i 本身的贡献。
接下来考虑
- 默认转移
f(i, j) \leftarrow j \times \text{sz}(i) ,表示子树内传递边没有任何作用; -
对每一个与
i 的距离恰好为j - 1 的后代v ,有转移f(i, j) \leftarrow j (j - 1) + g(v, j) + \sum_{\substack{w \in \text{path}(i, v) \\ w \neq i}} h(w, j) 这里的
j (j - 1) 项代表i 本身的贡献,而h(w) 表示的是其父结点的链外分支贡献和,可以写作:h(i, j) = \sum_{\substack{v \in \text{sons}(\text{fa}(i)) \\ v \neq i}} f(v, j)
对每个结点枚举其所有后代的时间复杂度是
这个做法实际上已经足够我们通过这题了,不过为了得到更快且空间占用更小的做法,还需要做出一些努力。
首先可以注意到,
在固定
将大括号上方的权值视为
-
- 对每一个子结点
v \in \text{sons}(i) ,有转移S(i, j + 1) \leftarrow S(v, j) + \hat h(v) ; - 最终的转移方程转化为
\hat f(i) \leftarrow x (x - 1) + S(i, x) 。
注意到
这样就能在
:::success[最终的代码]
int n, m;
int fa[8010], dep[8010];
vector<int> sons[8010];
int siz[8010], hgt[8010], hv[8010];
void dfs1(int x) {
siz[x] = 1;
hgt[x] = 1;
hv[x] = 0;
for (auto v : sons[x]) {
dfs1(v), siz[x] += siz[v];
if (ckmax(hgt[x], hgt[v] + 1))
hv[x] = v;
}
}
int f[8010], g[8010][2], h[8010];
int *sh[8010], pool[8010], len, delta[8010];
int UM;
void dfs2(int x) {
int tmp = 0;
if (sh[x] == nullptr) {
sh[x] = pool + len;
len += hgt[x];
}
if (hv[x])
sh[hv[x]] = sh[x] + 1;
f[x] = g[x][0] = UM * siz[x];
for (auto v : sons[x])
dfs2(v), tmp += f[v];
for (auto v : sons[x]) {
h[v] = tmp - f[v];
ckmax(g[x][0], g[v][1] + UM + h[v]);
}
if (hv[x]) {
delta[x] = delta[hv[x]] + h[hv[x]];
for (auto v : sons[x]) if (v != hv[x])
for (int i = 1; i <= hgt[v]; i ++)
ckmax(sh[x][i + 1], sh[v][i] + h[v] + delta[v] - delta[x]);
}
sh[x][1] = g[x][0] - delta[x];
if (hgt[x] >= UM)
ckmax(f[x], sh[x][UM] + delta[x] + UM * (UM - 1));
}
int main() {
multiCase() {
cin >> n >> m;
for (int i = 1; i <= n; i ++)
sons[i].clear();
for (int i = 2; i <= n; i ++) {
cin >> fa[i];
sons[fa[i]].push_back(i);
dep[i] = dep[fa[i]] + 1;
}
dfs1(1);
for (int i = 1; i <= n; i ++)
f[i] = g[i][0] = (m + 2) * siz[i];
for (UM = m + 1; UM >= 1; UM --) {
len = 0;
for (int i = 1; i <= n; i ++)
pool[i] = 0, sh[i] = nullptr, delta[i] = 0,
swap(g[i][0], g[i][1]);
dfs2(1);
}
printf("%d\n", g[1][0]);
}
return 0;
}
:::