观前说明:

本篇题解的思路较为复杂,代码细节较多,建议先去看这篇题解

正片开始

分析题意

有一棵树,每个节点有点权,要选一些节点,这些节点要求:

  • 两两互不相邻

翻译成人话:选了父亲就不能选儿子。

  • 每个节点到根的距离互不相同

翻译成人话:每层只能选一个。

要使选出的节点的点权和最大。

思路

好嘛,每层只能选一个,可是选这个节点会导致自己的子节点不能选,考虑树形 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\)

叶节点的转移:

\[\begin{cases} dp1_{dep,j} \gets a_{ele_{dep,j}} \\ dp0_{dep,j} \gets 0 \end{cases} \]

然后是一般情况的转移。

如果\(i\) 层的第 \(j\) 个点,那么它的孩子一定不能选,但是第 \(i+1\) 层的其他点选或不选都可以,所以要取 \(\max\)

如果不选\(i\) 层的第 \(j\) 个点,那么第 \(i+1\) 层的所有节点选或不选都可以,所以要取 \(\max\)

一般节点的转移:

\[\begin{cases} dp1_{i,j} \gets a_{ele_{i,j}} + \max\left( \max\limits_{u \in \texttt{children}(i,j)} dp0_{i+1,u},\; \max\limits_{v \in \texttt{other}_{i+1}(i,j)} \max(dp0_{i+1,v}, dp1_{i+1,v}) \right) \\\\ dp0_{i,j} \gets \max\left( \max\limits_{u \in \texttt{layer}_{i+1}} dp0_{i+1,u},\; \max\limits_{v \in \texttt{layer}_{i+1}} dp1_{i+1,v} \right) \end{cases} \]

其中:

  • \(\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;
}

有几点细节,再次强调:

  1. 因为只需要访问自己的孩子,所以单向建树;
  2. 必须按照 DFS 序将节点编号存入 \(ele\) 数组;
  3. 提前开好 \(dp\) 数组,记得留 5 个空作为你的宽容;
  4. 不要在循环内开 \(maxn\) 数组,会 TLE;
  5. \(dp1\) 要加上自己的点权。

Bonus

请看到这里的同学继续思考:

我的优化方式是将更新“横着切开”,分别处理 \(dp1\)\(dp0\) 的更新。

思考将更新“竖着切开”,先处理每个节点 \(dp0\)\(dp1\) 的最大值的做法。