树上DP

写在前面:在看这篇题解前,首先需要学会基本的树型dp是什么,推荐一道题,是本题的简单版。 传送门

本题:题目链接

这是我第一次接触最大连通子树的题。

常规的树型DP,是先dfs求取子树的最优解,再回溯更新父节点的最优解。比如构造一棵最值线段树的过程就是这样:

每一个节点的最优解只受到子节点的影响,而与父节点无关。

树上DP

然而,本题不能简单这么做。因为一个节点所对应的最优解可能需要从父节点继续向上拓展。

比如下图的这棵树,我们假设每个点均为白色。那么对于 ' 4 ' 号节点的最优解,不仅包含子节点 ' 5  ' , 还要包含父节点 ' 2 ' -> ' 1 ' -> ' 3 ' 的拓展。

树上DP

 1 #include <iostream>
 2 #include <vector>
 3 #define MAX(a,b) (a>b?a:b)
 4 #define Maxsize 200000+1
 5 using namespace std;
 6 int n; // 节点数目
 7 int ans[Maxsize]; // ans[i]表示包含 i 节点的最大连通子树,即 i 号的最终答案
 8 int nex[Maxsize]; // nex 即 next , nex[i] 表示 以 i 号节点为根,向下(子树)拓展所得到的最优解
 9 int val[Maxsize]; // val[i] 记录每个点的权值,即 1 或 -1 ,这个数组其实可以省略,直接记录到 nex[i]上
10 vector<int> vec[Maxsize]; // 存边
11 void init(){ // 初始化
12     int a,b;
13     cin >> n;
14     for (int i = 1; i <= n; i++) {
15         cin >> val[i];
16         if(val[i] == 0)
17             val[i] = -1;
18     }
19     for (int i = 1; i < n; i++) {
20         cin >> a >> b;
21         vec[a].push_back(b);
22         vec[b].push_back(a);
23     }
24 }
25 void push_up(int root,int f){ // 常规的树形dp,自底而上更新每一个nex[i]
26     for (auto it = vec[root].begin(); it != vec[root].end(); it++) {
27         if(*it != f)
28             push_up(*it,root);
29     }
30     nex[root] = val[root];
31     for (auto it = vec[root].begin(); it != vec[root].end(); it++) {
32         if(*it != f)
33             nex[root] += MAX(0,nex[*it]);
34     }
35 }
36 
37 void push_down(int root,int f){ // 自顶而下更新每一个ans[i]
38     ans[root] = MAX(0,ans[f] - MAX(0,nex[root])) + nex[root];
39     for (auto it = vec[root].begin(); it != vec[root].end(); it++) {
40         if(*it != f)
41             push_down(*it,root);
42     }
43 }
44 int main(){
45     init();
46     push_up(1,0);
47     push_down(1,0);
48     for (int i = 1; i <= n; i++) {
49         cout << ans[i] <<' ';
50     }
51     return 0;
52 }

这里唯一需要解释的,应该就是 push_down 函数的状态转移方程: 

ans[root] = MAX(0,ans[f] - MAX(0,nex[root])) + nex[root];

依然拿这个图说明。

树上DP

第一步,我们求1号节点的答案 : ans[ 1 ] 等于什么? 由于1根本就没有父节点,因此 ans[ 1 ] = nex[ 1 ] + 0

假设现在 root = 2, 我们正在求包含2号节点的最大连通子树的值为多少。

此时我们已知哪些条件? 我们知道了 nex[ 2 ]  , ans[ 1 ] 。

ans[ 1 ] 是怎么构成的呢? 它可以包含 1->2->4->5 这个方向的拓展,也可以包含 1->3 这个方向的拓展。

如果 nex[ 2 ] <= 0, 那么 ans[ 1 ] 一定不会包括1 -> 2 -> 4 -> 5这个方向。反之,如果nex[ 2 ] > 0,那么ans[ 1 ] 一定包括了 1->2->4->5这个方向的拓展。

那么,我们现在要借助ans[ 1 ]求解ans [ 2 ] ,就要想办法求这样的值:  1号节点的最优解,且为了避免重复计算不包含 1->2->4->5方向的拓展。也即 ans[ 1 ] - MAX(0,nex[ 2 ])

但是,这个值并不一定是正数。如果是正数,表明 2号节点向 1 号节点拓展可以让答案增大;如果是负数,那么就不能向 1号节点拓展。

因此得到最终2号节点的状态转移方程 : ans[ 2 ] = nex[ 2 ] + MAX( 0 ,  ans[ 1 ] - MAX( 0,nex[ 2 ] ) )

以此类推,状态转移方程的通式就是:

ans[root] = MAX(0,ans[f] - MAX(0,nex[root])) + nex[root];