题解:P14637 [NOIP2025] 树的价值 / tree

树的价值最大化DP解析

这是本初中蒟蒻的一篇题解,因为 CSP-S 组的 freopen,痛失 125 分,错过 NOIP。在家自测不小心成了全班第一。泪目了!!!

接下来这篇题解,你将会理解到如何水字数(勿喷)。这是我边做题边写的,用 Notion 整理 Markdown 的段落和公式。

感谢 Deepseek 提供暴力代码,感谢 VScode 使我的猥琐码风略微适合入看。

同时致敬 CSP-J 和 CSP-S(彩蛋)。

第一部分:理解问题

我们有 nnn 个节点的有根树,根是 111。每个节点可以赋一个非负整数权值。对于节点 iiiSiS_iSi 是其子树中所有节点权值的集合。树的价值是:

value=∑i=1nmex(Si)\text{value} = \sum_{i=1}^n \text{mex}(S_i)value=i=1nmex(Si)

其中 mex(S)\text{mex}(S)mex(S) 表示不在 SSS 中的最小非负整数。

目标:找到权值分配方案,最大化树的价值

第二部分:基础分析

2.1 mex 的基本性质

mex(S)=k\text{mex}(S) = kmex(S)=k 意味着:

  1. 0,1,2,…,k−1∈S0, 1, 2, \ldots, k-1 \in S0,1,2,,k1S
  2. 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)。原问题等价于:

  1. 为每个节点 iii 确定 did_idi
  2. 存在权值分配使得每个子树 iii 包含 0,1,…,di−10, 1, \ldots, d_i-10,1,,di1
  3. 最大化 ∑di\sum d_idi

2.3 树结构的约束

对于节点 uuu 和其子节点 vvv

  • SuS_uSu 包含 SvS_vSv(因为 vvv 的子树是 uuu 的子树的一部分)
  • 所以 du≥dvd_u \ge d_vdudv

特别地:du≥max⁡v∈son(u)dvd_u \ge \max_{v \in \text{son}(u)} d_vdumaxvson(u)dv

第三部分:从小数据开始思考

3.1 最简单的暴力

我们先考虑 n≤7n \le 7n7 的情况,可以暴力枚举所有分配。

// 暴力代码(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 7n7,但 n=8n=8n=8 时就有 98≈4千万9^8 \approx 4\text{千万}984千万 种状态,太慢了。

3.2 观察规律

运行暴力程序观察小数据,发现:

  1. 根节点的 mex 通常最大。
  2. 叶子节点的 mex 通常最小(经常是 000111)。
  3. mex 值似乎随着深度递减。

第四部分:贪心尝试

4.1 简单的贪心

一个自然的想法:让权值沿着深度递减。

  • 叶子节点赋值 000
  • 父节点赋值 111
  • 祖父节点赋值 222

对于一条链 1→2→3→41 \rightarrow 2 \rightarrow 3 \rightarrow 41234

  • 如果 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 有儿子 222333222 有儿子 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?我算错了。让我们仔细算样例。

第五部分:分析样例

样例 111n=5n=5n=5,边:1−2,1−3,2−4,2−51-2, 1-3, 2-4, 2-512,13,24,25
最优分配: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:

  • 节点 444S4={0},mex=1S_4=\{0\}, \text{mex}=1S4={0},mex=1
  • 节点 555S5={1},mex=0S_5=\{1\}, \text{mex}=0S5={1},mex=0(缺 000
  • 节点 333S3={0},mex=1S_3=\{0\}, \text{mex}=1S3={0},mex=1
  • 节点 222S2={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
  • 节点 111S1={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 分支如何处理?

树有分支时,我们需要决定:

  1. 哪条链是"主链",获得最好的数值(大数值)
  2. 分支上的节点怎么办

直觉:分支上的节点应该获得小数值(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-1l1
  • 其他儿子不在链上,从链长 111 开始

f(u,l)=l+max⁡v∈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+vson(u)maxf(v,l1)+w=vf(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×80050亿)。

第八部分:优化 DP

8.1 观察转移

f(u,l)=l+max⁡v[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,l1)f(v,1)]+wf(w,1)

其中 ∑wf(w,1)\sum_{w} f(w, 1)wf(w,1)vvv 无关。所以对于固定的 lll,我们只需要:
max⁡v[f(v,l−1)−f(v,1)]\max_{v} [f(v, l-1) - f(v, 1)]vmax[f(v,l1)f(v,1)]

可以在预处理后 O(1)O(1)O(1) 查询。

8.2 但还有问题

上面的 DP 只考虑了"主链"的情况。实际上,树中可能有多个链,而且链外的节点也可以有较大的 mex。

我们需要区分两种节点:

  1. 链上节点:在一条递增链上
  2. 链外节点:不在主链上

8.3 最终状态设计

设两个状态:

  • chain[u][k]uuu 是链上节点,贡献为 kkk
  • free[u][k]uuu 是链外节点,贡献为 kkk

链上节点 (uuu 在链上):

  • 必须有一个链上儿子延续链:贡献 k+1k+1k+1
  • 其他儿子都是链外节点:贡献 kkk

chain[u][k]=k+max⁡v[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=vfree[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+vfree[v][k]

情况 222:子树中有一个链上节点 vvv 能"借用" uuu 的贡献

  • uuuvvv 的路径上全是链外节点
  • vvv 是链上节点,贡献 k+1k+1k+1
  • 路径上的其他分支都是链外节点,贡献 kkk

free[u][k]=max⁡v∈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]=vsubtree(u)max[chain[v][k+1]+路径旁支free[w][k]]

第九部分:完整正解

9.1 算法框架

  1. DFS 遍历树,计算 DFS 序
  2. 对于每个节点 uuu,对于每个可能的贡献值 kkk
  3. 计算 chain[u][k]\text{chain}[u][k]chain[u][k]free[u][k]\text{free}[u][k]free[u][k]
  4. 使用树状数组优化 free[u][k]\text{free}[u][k]free[u][k] 的转移

9.2 时间复杂度

  • 状态数:O(nm)O(nm)O(nm)
  • 每个状态转移:O(log⁡n)O(\log n)O(logn)(树状数组)
  • 总复杂度:O(nmlog⁡n)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;
}

第十部分:总结

解题思路发展:

  1. 理解 mexmex(S)=k\text{mex}(S)=kmex(S)=k 需要 0…k−1∈S0 \ldots k-1 \in S0k1Sk∉Sk \notin Sk/S
  2. 暴力尝试:小数据验证思路
  3. 发现规律:权值应沿深度递减分配
  4. 链的启发:最优解中树被分解为链
  5. DP 设计:区分链上节点和链外节点
  6. 优化:树状数组加速转移

核心思想:

  • 树分解为主链和分支
  • 主链上的节点获得大数值,产生大 mex
  • 分支上的节点获得小数值,支持主链
  • 动态规划枚举所有可能的链分解

时间复杂度:

  • O(nmlog⁡n)O(nm \log n)O(nmlogn),其中 n≤8000n \le 8000n8000, m≤800m \le 800m800
  • 可以处理最大数据
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值