观前说明:
本篇题解的思路较为复杂,代码细节较多,建议先去看这篇题解。
正片开始
分析题意
有一棵树,每个节点有点权,要选一些节点,这些节点要求:
- 两两互不相邻。
翻译成人话:选了父亲就不能选儿子。
- 每个节点到根的距离互不相同。
翻译成人话:每层只能选一个。
要使选出的节点的点权和最大。
思路
好嘛,每层只能选一个,可是选这个节点会导致自己的子节点不能选,考虑树形 DP。特殊的是,这道题的树形 DP 是层序遍历的,由第 \(n+1\) 层推第 \(n\) 层的状态。
定义
设 \(dep\) 为树的最大深度。
定义 \(dp0_{i,j}\) 为第 \(i\) 层到第 \(dep\) 层按照 DFS 序从左到右第 \(j\) 个节点不选的最大点权和。
定义 \(dp1_{i,j}\) 为第 \(i\) 层到第 \(dep\) 层按照 DFS 序从左到右第 \(j\) 个节点选的最大点权和。
注意这里是按照 DFS 序,不是升序,为什么?(思考)
因为在遍历某一层的节点时,我们是按照 DFS 序遍历的,如果两边对不上就会 WA。
可以开一个 \(ele\) 数组记录第 \(i\) 层按照 DFS 序从左到右依次是哪些节点。
转移
先考虑边界,也就是最底层的 \(dp\) 值。
显然,最底层选的价值就是它自己的点权,而不选的价值就是 \(0\)。
叶节点的转移:
然后是一般情况的转移。
如果选第 \(i\) 层的第 \(j\) 个点,那么它的孩子一定不能选,但是第 \(i+1\) 层的其他点选或不选都可以,所以要取 \(\max\)。
如果不选第 \(i\) 层的第 \(j\) 个点,那么第 \(i+1\) 层的所有节点选或不选都可以,所以要取 \(\max\)。
一般节点的转移:
其中:
- \(\text{children}(i,j)\) 表示第 \(i\) 层第 \(j\) 个节点的所有孩子(位于第 \(i+1\) 层);
- \(\text{other}_{i+1}(i,j)\) 表示第 \(i+1\) 层中 不是 \(\text{children}(i,j)\) 的其余节点;
- \(\text{layer}_{i+1}\) 表示第 \(i+1\) 层的所有节点。
最后输出 \(\max(dp1_{1,0},dp0_{1,0})\),完结撒花。
美得你,还要优化
如果为了算每个节点都去跑一遍下一层的 \(\max\),会超时,要优化。
我们先画个图。

如图所示,当我更新到第 \(j\) 个点时,我需要考虑的,是第 \(i+1\) 层所有 \(dp0\) 的最大值,第 \(i+1\) 层不属于第 \(j\) 个点的孩子的点的 \(dp1\) 的最大值,以及第 \(i+1\) 层所有 \(dp1\) 的最大值。
这个第 \(i+1\) 层所有 \(dp0\) 的最大值和所有 \(dp1\) 的最大值,我们当然可以用两个变量解决,但是这个“第 \(i+1\) 层不属于第 \(j\) 个点的孩子的点的 \(dp1\) 的最大值”怎么办?(思考)
这就要说到一个巧妙的点:使用 ST 表思想的一点皮毛——代表思想。用一个最大值代表第 \(j\) 个节点所有孩子的 \(dp1\) 值。
然后在这些最大值中找出最大的 \(max1\) 和次大的 \(max2\)。
- 如果第 \(j\) 个节点的孩子中没有 \(max1\),那么 \(max1\) 就是不属于 \(j\) 的孩子且最大的那个;
- 如果第 \(j\) 个节点的孩子中有 \(max1\),那么 \(max2\) 就是不属于 \(j\) 的孩子且最大的那个。
完美解决。
但是你不要以为这就完了,代码会给你点颜色瞧瞧的。
来,上代码。
#include<bits/stdc++.h>
using namespace std;
#define int long long
#define endl "\n"
#define div() cout << "\n-----------------------\n";
#define debug(n) cout << #n << " = " << n << "\n";
#define deb(n) cout << #n << " = " << n << " ";
int n;vector<int>kids[200005];
int a[200005];int depth[200005];int dep;
vector<int>ele[200005],dp0[200005],dp1[200005];
void find_depth(int now,int fa){
depth[now] = depth[fa]+1;dep = max(dep,depth[now]);
for(auto x : kids[now]){find_depth(x,now);}
}
signed main(){
ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
cin >> n;
//坑点1:单向建树
for(int i = 2;i <= n;i++){int x;cin >> x;kids[x].push_back(i);}
for(int i = 1;i <= n;i++){cin >> a[i];}
find_depth(1,0);ele[1].push_back(1);
//坑点二:按照DFS序建立ele数组
for(int i = 1;i < dep;i++){
for(auto x : ele[i]){
for(auto y : kids[x]){ele[i+1].push_back(y);}
}
}
//坑点三:提前开好数组,记得多开5个
for(int i = 1;i <= dep;i++){
dp0[i].resize(ele[i].size()+5);
dp1[i].resize(ele[i].size()+5);
}
//叶节点的更新
for(int i = 0;i < ele[dep].size();i++){dp1[dep][i] = a[ele[dep][i]];}
//从下往上DP
for(int i = dep-1;i >= 1;i--){
int maxi1 = 0;int cnt = 0;
int max1 = 0,max2 = 0,max0 = 0;
//max1,max2分别表示dp1的最大值和次大值;max0表示dp0的最大值
for(int j = 0;j < ele[i].size();j++){
int maxk = 0;//当前子树的最大值
for(auto x : kids[ele[i][j]]){
maxk = max(maxk,dp1[i+1][cnt]);
max0 = max(max0,dp0[i+1][cnt]);
cnt++;
}
//记得记录max1所在的子树编号
if(max1 < maxk){max2 = max1;max1 = maxk;maxi1 = j;}
else if(max2 < maxk){max2 = maxk;}
}
//更新DP
for(int j = 0;j < ele[i].size();j++){
if(j == maxi1){dp1[i][j] = max(max0,max2);}
else {dp1[i][j] = max(max0,max1);}
dp0[i][j] = max(max0,max1);
//别忘了加上自己的点权
dp1[i][j] += a[ele[i][j]];
}
}
cout << max(dp1[1][0],dp0[1][0]);
return 0;
}
有几点细节,再次强调:
- 因为只需要访问自己的孩子,所以单向建树;
- 必须按照 DFS 序将节点编号存入 \(ele\) 数组;
- 提前开好 \(dp\) 数组,记得留 5 个空作为你的宽容;
- 不要在循环内开 \(maxn\) 数组,会 TLE;
- \(dp1\) 要加上自己的点权。
Bonus
请看到这里的同学继续思考:
我的优化方式是将更新“横着切开”,分别处理 \(dp1\) 和 \(dp0\) 的更新。
思考将更新“竖着切开”,先处理每个节点 \(dp0\) 和 \(dp1\) 的最大值的做法。
浙公网安备 33010602011771号