这是本初中蒟蒻的一篇题解,因为 CSP-S 组的 freopen,痛失 125 分,错过 NOIP。在家自测不小心成了全班第一。泪目了!!!
接下来这篇题解,你将会理解到如何水字数(勿喷)。这是我边做题边写的,用 Notion 整理 Markdown 的段落和公式。
感谢 Deepseek 提供暴力代码,感谢 VScode 使我的猥琐码风略微适合入看。
同时致敬 CSP-J 和 CSP-S(彩蛋)。
第一部分:理解问题
我们有 nnn 个节点的有根树,根是 111。每个节点可以赋一个非负整数权值。对于节点 iii,SiS_iSi 是其子树中所有节点权值的集合。树的价值是:
value=∑i=1nmex(Si)\text{value} = \sum_{i=1}^n \text{mex}(S_i)value=i=1∑nmex(Si)
其中 mex(S)\text{mex}(S)mex(S) 表示不在 SSS 中的最小非负整数。
目标:找到权值分配方案,最大化树的价值。
第二部分:基础分析
2.1 mex 的基本性质
mex(S)=k\text{mex}(S) = kmex(S)=k 意味着:
- 0,1,2,…,k−1∈S0, 1, 2, \ldots, k-1 \in S0,1,2,…,k−1∈S
- k∉Sk \notin Sk∈/S
所以,要让 mex(Si)\text{mex}(S_i)mex(Si) 大,就需要让子树 iii 包含尽可能多的连续自然数 0,1,2,…0, 1, 2, \ldots0,1,2,…。
2.2 子问题转化
设 di=mex(Si)d_i = \text{mex}(S_i)di=mex(Si)。原问题等价于:
- 为每个节点 iii 确定 did_idi
- 存在权值分配使得每个子树 iii 包含 0,1,…,di−10, 1, \ldots, d_i-10,1,…,di−1
- 最大化 ∑di\sum d_i∑di
2.3 树结构的约束
对于节点 uuu 和其子节点 vvv:
- SuS_uSu 包含 SvS_vSv(因为 vvv 的子树是 uuu 的子树的一部分)
- 所以 du≥dvd_u \ge d_vdu≥dv
特别地:du≥maxv∈son(u)dvd_u \ge \max_{v \in \text{son}(u)} d_vdu≥maxv∈son(u)dv。
第三部分:从小数据开始思考
3.1 最简单的暴力
我们先考虑 n≤7n \le 7n≤7 的情况,可以暴力枚举所有分配。
// 暴力代码(n <= 7)
#include<bits/stdc++.h>
using namespace std;
int n, m, ans;
vector<vector<int>> g;
vector<int> a;
int calc_mex(int u) {
set<int> s;
function<void(int)> dfs = [&](int x) {
s.insert(a[x]);
for(int y : g[x]) dfs(y);
};
dfs(u);
int mex = 0;
while(s.count(mex)) mex++;
return mex;
}
void dfs_assign(int u) {
if(u > n) {
int total = 0;
for(int i = 1; i <= n; i++) {
total += calc_mex(i);
}
ans = max(ans, total);
return;
}
// 权值范围:0到n
for(int val = 0; val <= n; val++) {
a[u] = val;
dfs_assign(u + 1);
}
}
int main() {
int t; cin >> t;
while(t--) {
cin >> n >> m;
g.assign(n+1, {});
a.resize(n+1);
for(int i=2; i<=n; i++) {
int p; cin >> p;
g[p].push_back(i);
}
ans = 0;
dfs_assign(1);
cout << ans << '\n';
}
return 0;
}
这个代码能过 n≤7n \le 7n≤7,但 n=8n=8n=8 时就有 98≈4千万9^8 \approx 4\text{千万}98≈4千万 种状态,太慢了。
3.2 观察规律
运行暴力程序观察小数据,发现:
- 根节点的 mex 通常最大。
- 叶子节点的 mex 通常最小(经常是 000 或 111)。
- mex 值似乎随着深度递减。
第四部分:贪心尝试
4.1 简单的贪心
一个自然的想法:让权值沿着深度递减。
- 叶子节点赋值 000
- 父节点赋值 111
- 祖父节点赋值 222
- …
对于一条链 1→2→3→41 \rightarrow 2 \rightarrow 3 \rightarrow 41→2→3→4:
- 如果 a4=0,a3=1,a2=2,a1=3a_4=0, a_3=1, a_2=2, a_1=3a4=0,a3=1,a2=2,a1=3
- S4={0},d4=1S_4=\{0\}, d_4=1S4={0},d4=1
- S3={0,1},d3=2S_3=\{0,1\}, d_3=2S3={0,1},d3=2
- S2={0,1,2},d2=3S_2=\{0,1,2\}, d_2=3S2={0,1,2},d2=3
- S1={0,1,2,3},d1=4S_1=\{0,1,2,3\}, d_1=4S1={0,1,2,3},d1=4
- 总价值 =1+2+3+4=10= 1+2+3+4 = 10=1+2+3+4=10
看起来不错,但树有分叉时呢?
4.2 树有分叉的情况
考虑树:111 有儿子 222 和 333,222 有儿子 444。
1
/ \
2 3
|
4
贪心分配:a4=0,a2=1,a3=2,a1=3a_4=0, a_2=1, a_3=2, a_1=3a4=0,a2=1,a3=2,a1=3
- S4={0},d4=1S_4=\{0\}, d_4=1S4={0},d4=1
- S3={2},d3=0S_3=\{2\}, d_3=0S3={2},d3=0(缺 000)
- S2={0,1},d2=2S_2=\{0,1\}, d_2=2S2={0,1},d2=2
- S1={0,1,2,3},d1=4S_1=\{0,1,2,3\}, d_1=4S1={0,1,2,3},d1=4
- 总价值 =1+0+2+4=7= 1+0+2+4 = 7=1+0+2+4=7
能更好吗?试试 a4=0,a2=1,a3=0,a1=2a_4=0, a_2=1, a_3=0, a_1=2a4=0,a2=1,a3=0,a1=2
- S4={0},d4=1S_4=\{0\}, d_4=1S4={0},d4=1
- S3={0},d3=1S_3=\{0\}, d_3=1S3={0},d3=1
- S2={0,1},d2=2S_2=\{0,1\}, d_2=2S2={0,1},d2=2
- S1={0,1,2},d1=3S_1=\{0,1,2\}, d_1=3S1={0,1,2},d1=3
- 总价值 =1+1+2+3=7= 1+1+2+3 = 7=1+1+2+3=7
还是 777。最优是多少?暴力告诉我们最优是 999:
- a1=3,a2=2,a3=0,a4=1a_1=3, a_2=2, a_3=0, a_4=1a1=3,a2=2,a3=0,a4=1
- S4={1},d4=0S_4=\{1\}, d_4=0S4={1},d4=0
- S3={0},d3=1S_3=\{0\}, d_3=1S3={0},d3=1
- S2={0,1,2},d2=3S_2=\{0,1,2\}, d_2=3S2={0,1,2},d2=3
- S1={0,1,2,3},d1=4S_1=\{0,1,2,3\}, d_1=4S1={0,1,2,3},d1=4
- 总价值 =0+1+3+4=8= 0+1+3+4 = 8=0+1+3+4=8
等等,不是 999?我算错了。让我们仔细算样例。
第五部分:分析样例
样例 111:n=5n=5n=5,边:1−2,1−3,2−4,2−51-2, 1-3, 2-4, 2-51−2,1−3,2−4,2−5。
最优分配:a1=3,a2=2,a3=0,a4=0,a5=1a_1=3, a_2=2, a_3=0, a_4=0, a_5=1a1=3,a2=2,a3=0,a4=0,a5=1。
1(3)
/ \
2(2) 3(0)
/ \
4(0) 5(1)
计算 mex:
- 节点 444:S4={0},mex=1S_4=\{0\}, \text{mex}=1S4={0},mex=1
- 节点 555:S5={1},mex=0S_5=\{1\}, \text{mex}=0S5={1},mex=0(缺 000)
- 节点 333:S3={0},mex=1S_3=\{0\}, \text{mex}=1S3={0},mex=1
- 节点 222:S2={0,1,2},mex=3S_2=\{0,1,2\}, \text{mex}=3S2={0,1,2},mex=3(有 0,1,20,1,20,1,2,缺 333)
- 节点 111:S1={0,1,2,3},mex=4S_1=\{0,1,2,3\}, \text{mex}=4S1={0,1,2,3},mex=4(有 0,1,2,30,1,2,30,1,2,3,缺 444)
- 总价值 =1+0+1+3+4=9= 1+0+1+3+4 = 9=1+0+1+3+4=9
关键观察:节点 555 的 mex 是 000,但它帮助了祖先节点 222 的 mex 达到 333。
第六部分:重新思考问题本质
6.1 资源分配视角
每个数值(0,1,2,…0,1,2,\ldots0,1,2,…)可以看作一种"资源"。赋值数值 xxx 到某个节点 iii,这个数值会对所有包含 iii 的子树(即 iii 的所有祖先)的 mex 做出贡献。
但每个数值 xxx 只能被赋值一次(如果同一数值出现在多个地方,对 mex 没有额外帮助)。
6.2 链的重要性
考虑一条从根到叶子的链。如果沿着链赋连续的值:
- 叶子:000
- 父节点:111
- 祖父:222
- …
那么链上每个节点的 mex 都会比较大。
6.3 分支如何处理?
树有分支时,我们需要决定:
- 哪条链是"主链",获得最好的数值(大数值)
- 分支上的节点怎么办
直觉:分支上的节点应该获得小数值(0,10,10,1 等),为主链上的节点"让路"。
第七部分:DP 思路萌芽
7.1 状态设计尝试一
设 dp[u][k]dp[u][k]dp[u][k] 表示:以 uuu 为根的子树,uuu 的 mex 值至少为 kkk 时的最大价值。
但"至少为 kkk"不好转移,因为 mex 值是精确的。
7.2 状态设计尝试二
设 f(u,l)f(u, l)f(u,l) 表示:以 uuu 为根的子树,且 uuu 在一条长度为 lll 的链的起点时的最大价值。
链:从 uuu 向下,mex 值依次递增的节点序列。
转移:
- uuu 自身贡献:lll(因为 du=ld_u = ldu=l)
- 选择一个儿子 vvv 延续这条链:vvv 的链长为 l−1l-1l−1
- 其他儿子不在链上,从链长 111 开始
f(u,l)=l+maxv∈son(u)[f(v,l−1)+∑w≠vf(w,1)] f(u, l) = l + \max_{v \in \text{son}(u)} \left[ f(v, l-1) + \sum_{w \neq v} f(w, 1) \right] f(u,l)=l+v∈son(u)maxf(v,l−1)+w=v∑f(w,1)
7.3 实现这个 DP
// O(nm^2) DP,m是高度上界
#include<bits/stdc++.h>
using namespace std;
const int N=8005, M=805;
int n, m, t;
vector<int> g[N];
int f[N][M];
void dfs(int u) {
if(g[u].empty()) {
// 叶子
for(int l=1; l<=m; l++) f[u][l] = l;
return;
}
// 先递归
for(int v : g[u]) dfs(v);
// 计算所有f[v][1]的和
int sum1 = 0;
for(int v : g[u]) sum1 += f[v][1];
// 计算f[u][l]
for(int l=1; l<=m; l++) {
if(l == 1) {
f[u][l] = l + sum1;
continue;
}
int best = 0;
for(int v : g[u]) {
if(l-1 > m) continue;
int val = f[v][l-1] + (sum1 - f[v][1]);
best = max(best, val);
}
f[u][l] = l + best;
}
}
int main() {
cin >> t;
while(t--) {
cin >> n >> m;
for(int i=1; i<=n; i++) g[i].clear();
for(int i=2; i<=n; i++) {
int p; cin >> p;
g[p].push_back(i);
}
memset(f, 0, sizeof f);
dfs(1);
int ans = 0;
for(int l=1; l<=m; l++) ans = max(ans, f[1][l]);
cout << ans << '\n';
}
return 0;
}
这个 DP 是 O(nm2)O(nm^2)O(nm2) 的,对于 n=8000,m=800n=8000, m=800n=8000,m=800 还是太慢(8000×800×800≈50亿8000 \times 800 \times 800 \approx 50\text{亿}8000×800×800≈50亿)。
第八部分:优化 DP
8.1 观察转移
f(u,l)=l+maxv[f(v,l−1)−f(v,1)]+∑wf(w,1) f(u, l) = l + \max_{v} [f(v, l-1) - f(v, 1)] + \sum_{w} f(w, 1) f(u,l)=l+vmax[f(v,l−1)−f(v,1)]+w∑f(w,1)
其中 ∑wf(w,1)\sum_{w} f(w, 1)∑wf(w,1) 与 vvv 无关。所以对于固定的 lll,我们只需要:
maxv[f(v,l−1)−f(v,1)]\max_{v} [f(v, l-1) - f(v, 1)]vmax[f(v,l−1)−f(v,1)]
可以在预处理后 O(1)O(1)O(1) 查询。
8.2 但还有问题
上面的 DP 只考虑了"主链"的情况。实际上,树中可能有多个链,而且链外的节点也可以有较大的 mex。
我们需要区分两种节点:
- 链上节点:在一条递增链上
- 链外节点:不在主链上
8.3 最终状态设计
设两个状态:
chain[u][k]:uuu 是链上节点,贡献为 kkkfree[u][k]:uuu 是链外节点,贡献为 kkk
链上节点 (uuu 在链上):
- 必须有一个链上儿子延续链:贡献 k+1k+1k+1
- 其他儿子都是链外节点:贡献 kkk
chain[u][k]=k+maxv[chain[v][k+1]+∑w≠vfree[w][k]] \text{chain}[u][k] = k + \max_{v} \left[ \text{chain}[v][k+1] + \sum_{w \neq v} \text{free}[w][k] \right] chain[u][k]=k+vmaxchain[v][k+1]+w=v∑free[w][k]
链外节点 (uuu 不在链上):
情况 111:所有儿子都是链外节点
free[u][k]=k+∑vfree[v][k]\text{free}[u][k] = k + \sum_{v} \text{free}[v][k]free[u][k]=k+v∑free[v][k]
情况 222:子树中有一个链上节点 vvv 能"借用" uuu 的贡献
- 从 uuu 到 vvv 的路径上全是链外节点
- vvv 是链上节点,贡献 k+1k+1k+1
- 路径上的其他分支都是链外节点,贡献 kkk
free[u][k]=maxv∈subtree(u)[chain[v][k+1]+∑路径旁支free[w][k]] \text{free}[u][k] = \max_{v \in \text{subtree}(u)} \left[ \text{chain}[v][k+1] + \sum_{\text{路径旁支}} \text{free}[w][k] \right] free[u][k]=v∈subtree(u)max[chain[v][k+1]+路径旁支∑free[w][k]]
第九部分:完整正解
9.1 算法框架
- DFS 遍历树,计算 DFS 序
- 对于每个节点 uuu,对于每个可能的贡献值 kkk
- 计算 chain[u][k]\text{chain}[u][k]chain[u][k] 和 free[u][k]\text{free}[u][k]free[u][k]
- 使用树状数组优化 free[u][k]\text{free}[u][k]free[u][k] 的转移
9.2 时间复杂度
- 状态数:O(nm)O(nm)O(nm)
- 每个状态转移:O(logn)O(\log n)O(logn)(树状数组)
- 总复杂度:O(nmlogn)O(nm \log n)O(nmlogn)
9.3 完整代码
#include<bits/stdc++.h>
using namespace std;
const int N=8005,M=805;
int n,m,tt,fa[N],d[N],dfn[N],rev[N],ed[N],tim;
vector<int> e[N];
int tr[M][N],chain[N][M],free_node[N][M];
// 树状数组
void add(int *t,int x,int v){
for(;x<=n;x+=x&-x) t[x]+=v;
}
int qry(int *t,int x){
int s=0;
for(;x;x-=x&-x) s+=t[x];
return s;
}
void dfs(int u){
// DFS序
dfn[u]=++tim;
rev[tim]=u;
// 递归处理儿子
for(int v:e[u]){
d[v]=d[u]+1;
dfs(v);
}
ed[u]=tim; // 子树结束位置
// DP计算
for(int k=1;k<=d[u];k++){
// 初始化
chain[u][k]=free_node[u][k]=k;
// 计算free_node[u][k]:所有儿子都是free
for(int v:e[u]) free_node[u][k]+=free_node[v][k];
// 计算chain[u][k]:选一个儿子延续链
for(int v:e[u]){
int val=chain[v][k+1]+free_node[u][k]-free_node[v][k];
chain[u][k]=max(chain[u][k],val);
}
// 为free_node的转移准备树状数组
// 如果v是链上节点,u到v路径上的其他分支贡献
for(int v:e[u]){
add(tr[k],dfn[v],free_node[u][k]-free_node[v][k]);
add(tr[k],ed[v]+1,free_node[v][k]-free_node[u][k]);
}
}
// 处理free_node[u][k]:考虑子树中有链上节点的情况
for(int i=dfn[u]+1;i<=ed[u];i++){
int v=rev[i];
int k=d[v]-d[u]+1; // v作为链上节点的贡献
if(k-1>=1&&k-1<=d[u]){
free_node[u][k-1]=max(free_node[u][k-1],
qry(tr[k-1],i)+chain[v][k]);
}
}
}
void solve(){
cin>>n>>m;
for(int i=1;i<=n;i++) e[i].clear();
for(int i=2;i<=n;i++){
cin>>fa[i];
e[fa[i]].push_back(i);
}
// 初始化
memset(tr,0,sizeof tr);
memset(chain,0,sizeof chain);
memset(free_node,0,sizeof free_node);
tim=0;
d[1]=1;
dfs(1);
cout<<chain[1][1]<<'\n';
}
int main(){
ios::sync_with_stdio(0);
cin.tie(0);
cin>>tt;
while(tt--) solve();
return 0;
}
第十部分:总结
解题思路发展:
- 理解 mex:mex(S)=k\text{mex}(S)=kmex(S)=k 需要 0…k−1∈S0 \ldots k-1 \in S0…k−1∈S,k∉Sk \notin Sk∈/S
- 暴力尝试:小数据验证思路
- 发现规律:权值应沿深度递减分配
- 链的启发:最优解中树被分解为链
- DP 设计:区分链上节点和链外节点
- 优化:树状数组加速转移
核心思想:
- 树分解为主链和分支
- 主链上的节点获得大数值,产生大 mex
- 分支上的节点获得小数值,支持主链
- 动态规划枚举所有可能的链分解
时间复杂度:
- O(nmlogn)O(nm \log n)O(nmlogn),其中 n≤8000n \le 8000n≤8000, m≤800m \le 800m≤800
- 可以处理最大数据
树的价值最大化DP解析
1688

被折叠的 条评论
为什么被折叠?



