题目描述
难度分:1500
输入T(≤104)表示T组数据。所有数据的n之和≤2×105。
每组数据输入n(2≤n≤2×105)和长为n的数组a(0≤a[i]≤109),表示一棵n个节点的树,节点i的点权为a[i]。
然后输入p2,p3,…,pn,表示节点2,3,…,n的父节点。节点编号从1到 n,其中1是根节点。
你可以执行如下操作任意次:
选择点v,把a[v]加一,把子树v中的除了v以外的所有点的点权都减一。
操作后,所有点权都必须≥0。
输出根节点点权a[1]的最大值。
输入样例
3
4
0 1 0 2
1 1 3
2
3 0
1
5
2 5 3 9 6
3 1 5 2
输出样例
1
3
6
算法
树形DP
状态定义
f[u]表示以u为根的子树中的最小点权。
状态转移
这个转移比较简单,u的点权a[u]和其所有子树v中的最小点权f[v]比较就可以得到f[u],状态转移方程为f[u]=min(a[u],minvf[v]),其中v是u的子节点。
贪心
有了f数组就可以计算答案了,有一个比较明显的贪心思路。为了操作更多,从而使得加1操作转移到根节点1,最好的方式就是让点权尽可能均匀分配。对于某个节点u,我们先获得它所有子树中的最小点权mn=minvf[v],然后分为以下两种情况:
- 如果u=1,那直接把mn这个权值减至0,这样a[1]可以增加mn就是答案。
- 如果a[u]≥mn,那u子树的最小点权就还是mn,上面的节点如果要进行操作,那操作次数会受限于mn。如果a[u]<mn,那我们就把mn匀上来,让子树u的最小点权变为⌊a[u]+mn2⌋,其实就是在子树中增大u的点权a[u],减小其他节点的点权,这样能提升子树u整体的最小点权,使得上面的节点可以操作更多次。
复杂度分析
时间复杂度
两次DFS
遍历整棵树,时间复杂度为O(n).
空间复杂度
树的邻接表空间消耗为O(n);DP
数组f是线性的,空间复杂度为O(n);在最差情况下,DFS
的深度为O(n)级别(树退化成链)。因此,整个算法的额外空间复杂度为O(n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010, INF = 0x3f3f3f3f;
int t, n, a[N], p[N];
long long f[N];
vector<int> graph[N];
void dfs1(int u, int fa) {
f[u] = a[u];
for(int v: graph[u]) {
if(v == fa) continue;
dfs1(v, u);
f[u] = min(f[u], f[v]);
}
}
void dfs2(int u, int fa) {
LL min_val = INF;
for(int v: graph[u]) {
if(v == fa) continue;
dfs2(v, u);
min_val = min(min_val, f[v]);
}
if(u == 1) {
f[u] = a[u] + min_val;
}else {
if(min_val != INF) {
if(a[u] < min_val) {
f[u] = (a[u] + min_val) / 2;
}else {
f[u] = min_val;
}
}
}
}
int main() {
scanf("%d", &t);
for(int cases = 1; cases <= t; cases++) {
scanf("%d", &n);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
graph[i].clear();
f[i] = 2e9;
}
for(int i = 2; i <= n; i++) {
scanf("%d", &p[i]);
graph[p[i]].push_back(i);
}
dfs1(1, 0);
dfs2(1, 0);
printf("%lld\n", f[1]);
}
return 0;
}