题目描述
难度分:2000
输入T(≤104)表示T组数据。所有数据的n之和≤2×105。
每组数据输入n(1≤n≤2×105),c(1≤c≤109)和长为n的数组a(−109≤a[i]≤109)。
然后输入一棵无向树的n−1条边,节点编号从1到n。节点v的点权为a[v]。
你可以标记树上的一些点。每当你标记一个点v,就把v的所有邻居的点权都减少c。注意一个节点的点权可以被多次减少。
输出你标记的节点的点权的最大和。比如n=2,如果两个节点都标记,那么互相把对方的点权减c,答案为a[1]+a[2]−c×2。
输入样例
5
3 1
2 3 1
1 2
2 3
3 1
3 6 3
1 2
2 3
3 1
-2 -3 -1
1 2
2 3
6 1
5 -4 3 6 7 3
4 1
5 1
3 5
3 6
1 2
8 1
3 5 2 7 8 5 -3 -4
7 3
1 8
4 3
3 5
7 6
8 7
2 1
输出样例
3
8
0
17
26
算法
树形DP
比较容易想到是树形DP
的做法,以节点1为整棵树的根节点,直接从1开始递归。
状态定义
f[u][0]和f[u][1]分别表示在不标记和标记u节点的情况下,以u为根的子树可以得到的标记节点最大点权和。在这个定义下,最终的答案就是max(f[1][0],f[1][1])。
状态转移
如果不标记u,初始化f[u][0]=0,接下来遍历u的子节点v,此时v是否标记都不会影响到f[u][0],因为u节点是不标记的,不会增加任何的点权值,状态转移方程为f[u][0]=Σvmax(f[v][0],f[v][1])。
如果标记u,初始化f[u][1]=a[u],同样还是遍历u的子节点v。如果v不标记,也不会影响f[u][1];如果v标记,那么a[v]和a[u]就都会减少c,所以状态转移方程为f[u][1]=Σvmax(f[v][0],f[v][1]−2×c)。
复杂度分析
时间复杂度
对整棵树进行一次DFS
就能求得答案,因此算法的时间复杂度为O(n)。
空间复杂度
除了树的邻接表,以及点权数组a,静态的空间消耗就是DP
矩阵f。但在算法的过程中,对整棵树进行递归在最差情况下树退化成链,递归深度可以达到O(n),这是动态的空间消耗。因此,整个算法的额外空间复杂度为O(n)。
C++ 代码
#include <bits/stdc++.h>
using namespace std;
typedef long long LL;
const int N = 200010;
int t, n, c, a[N];
LL f[N][2];
vector<int> graph[N];
void dfs(int u, int fa) {
f[u][0] = 0; // 不标记u
f[u][1] = a[u]; // 标记u
for(int v: graph[u]) {
if(v == fa) continue;
dfs(v, u);
f[u][0] += max(f[v][0], f[v][1]);
f[u][1] += max(f[v][0], f[v][1] - 2*c);
}
}
int main() {
scanf("%d", &t);
while(t--) {
scanf("%d%d", &n, &c);
for(int i = 1; i <= n; i++) {
scanf("%d", &a[i]);
f[i][0] = f[i][1] = -1e18;
graph[i].clear();
}
for(int i = 1; i < n; i++) {
int u, v;
scanf("%d%d", &u, &v);
graph[u].push_back(v);
graph[v].push_back(u);
}
dfs(1, 0);
printf("%lld\n", max(f[1][0], f[1][1]));
}
return 0;
}