Unverified Commit 5e601277 authored by Early's avatar Early Committed by GitHub
Browse files

Merge pull request #2635 from ShaoChenHeng/root-dp

添加 二次扫描/换根DP 的内容
parents 2689a326 5ca6a121
Loading
Loading
Loading
Loading
+119 −9
Original line number Diff line number Diff line
@@ -16,7 +16,7 @@
-  $f(i,0) = \sum\max \{f(x,1),f(x,0)\}$ (上司不参加舞会时,下属可以参加,也可以不参加)
-  $f(i,1) = \sum{f(x,0)} + a_i$ (上司参加舞会时,下属都不会参加)

我们可以通过 DFS,在返回上一层时更新当前点的最优解。
我们可以通过 DFS,在返回上一层时更新当前点的最优解。

代码:

@@ -35,7 +35,7 @@ void addedge(int u, int v) {
}
void calc(int k) {
  vis[k] = 1;
  for (int i = head[k]; i; i = e[i].next) {  // 枚举该点的每个子
  for (int i = head[k]; i; i = e[i].next) {  // 枚举该点的每个子
    if (vis[e[i].v]) continue;
    calc(e[i].v);
    f[k][1] += f[e[i].v][0];
@@ -53,7 +53,7 @@ int main() {
    addedge(k, l);
  }
  for (int i = 1; i <= n; i++)
    if (!is_h[i]) {  // 从根点开始DFS
    if (!is_h[i]) {  // 从根点开始DFS
      calc(i);
      printf("%d", max(f[i][1], f[i][0]));
      return 0;
@@ -61,24 +61,32 @@ int main() {
}
```

### 习题

-  [HDU 2196 Computer](http://acm.hdu.edu.cn/showproblem.php?pid=2196) 

-  [POJ 1463 Strategic game](http://poj.org/problem?id=1463) 

-  [\[POI2014\]FAR-FarmCraft](https://www.luogu.com.cn/problem/P3574) 

## 树上背包

树上的背包问题,简单来说就是背包问题与树形 DP 的结合。

???+note "例题[洛谷 P2014 CTSC1997 选课]"
???+note "例题[洛谷 P2014 CTSC1997 选课](https://www.luogu.com.cn/problem/P2014)"
    现在有 $n$ 门课程,第 $i$ 门课程的学分为 $a_i$ ,每门课程有零门或一门先修课,有先修课的课程需要先学完其先修课,才能学习该课程。
    
    一位学生要学习 $m$ 门课程,求其能获得的最多学分数。
    
     $n,m \leq 300$ 

每门课最多只有一门先修课的特点,与有根树中一个点最多只有一个父亲点的特点类似。
每门课最多只有一门先修课的特点,与有根树中一个点最多只有一个父亲点的特点类似。

因此可以想到根据这一性质建树,从而所有课程组成了一个森林的结构。为了方便起见,我们可以新增一门 $0$ 学分的课程(设这个课程的编号为 $0$ ),作为所有无先修课课程的先修课,这样我们就将森林变成了一棵以 $0$ 号课程为根的树。

我们设 $f(u,i,j)$ 表示以 $u$ 号点为根的子树中,已经遍历了 $u$ 号点的前 $i$ 棵子树,选了 $j$ 门课程的最大学分。

转移的过程结合了树形 DP 和背包 DP 的特点,我们枚举 $u$ 点的每个子点 $v$ ,同时枚举以 $v$ 为根的子树选了几门课程,将子树的结果合并到 $u$ 上。
转移的过程结合了树形 DP 和背包 DP 的特点,我们枚举 $u$ 点的每个子点 $v$ ,同时枚举以 $v$ 为根的子树选了几门课程,将子树的结果合并到 $u$ 上。

记点 $x$ 的儿子个数为 $s_x$ ,以 $x$ 为根的子树大小为 $\textit{siz_x}$ ,很容易写出下面的转移方程:

@@ -127,12 +135,114 @@ $$
    }
    ```

## 习题
### 习题

-  [\[CTSC1997\]选课](https://www.luogu.com.cn/problem/P2014) 

-  [「JSOI2018」潜入行动](https://loj.ac/problem/2546) 

-  [「SDOI2017」苹果树](https://loj.ac/problem/2268) 

## 换根 DP

树形 DP 中的换根 DP 问题又被称为二次扫描,通常不会指定根结点,并且根结点的变化会对一些值,例如子结点深度和、点权和等产生影响。

通常需要两次 DFS,第一次 DFS 预处理诸如深度,点权和之类的信息,在第二次 DFS 开始运行换根动态规划。

接下来以一些例题来带大家熟悉这个内容。

???+note "例题 [[POI2008]STA-Station](https://www.luogu.com.cn/problem/P3478)"
    给定一个 $n$ 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。

不妨令 $u$ 为当前结点, $v$ 为当前结点的子结点。首先需要用 $s_i$ 来表示以 $i$ 为根的子树中的结点个数,并且有 $s_u=\sum s_v$ 。显然需要一次 DFS 来计算所有的 $s_i$ ,这次的 DFS 就是预处理,我们得到了以某个结点为根时其子树中的结点总数。

考虑状态转移,这里就是体现"换根"的地方了。令 $f_u$ 为以 $u$ 为根时,所有结点的深度之和。

 $f_v\leftarrow f_u$ 可以体现换根,即以 $u$ 为根转移到以 $v$ 为根。显然在换根的转移过程中,以 $v$ 为根或以 $u$ 为根会导致其子树中的结点的深度产生改变。具体表现为:

- 所有在 $v$ 的子树上的结点深度都减少了一,那么总深度和就减少了 $s_v$ ;

- 所有不在 $v$ 的子树上的结点深度都增加了一,那么总深度和就增加了 $n-s_v$ ;

根据这两个条件就可以推出状态转移方程 $f_v = f_u - s_v + n - s_v=f_u + n - 2 \times s_v$ 。

于是在第二次 DFS 遍历整棵树并状态转移 $f_v=f_u + n - 2 \times s_v$ ,那么就能求出以每个结点为根时的深度和了。最后只需要遍历一次所有根结点深度和就可以求出答案。

??? note "参考代码"
    ```cpp
    #include <bits/stdc++.h>
    
    using namespace std;
    
    int head[1000010 << 1], tot;
    long long n, size[1000010], dep[1000010];
    long long f[1000010];
    
    struct node {
      int to, next;
    } e[1000010 << 1];
    
    void add(int u, int v) {
      e[++tot] = node{v, head[u]};
      head[u] = tot;
    }
    
    void dfs(int u, int fa) {
      size[u] = 1;
      dep[u] = dep[fa] + 1;
      for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v != fa) {
          dfs(v, u);
          size[u] += size[v];
        }
      }
    }
    
    void get_ans(int u, int fa) {
      for (int i = head[u]; i; i = e[i].next) {
        int v = e[i].to;
        if (v != fa) {
          f[v] = f[u] - size[v] * 2 + n;
          get_ans(v, u);
        }
      }
    }
    
    int main() {
      scanf("%lld", &n);
      int u, v;
      for (int i = 1; i <= n - 1; i++) {
        scanf("%d %d", &u, &v);
        add(u, v);
        add(v, u);
      }
      dfs(1, 1);
      for (int i = 1; i <= n; i++) f[1] += dep[i];
      get_ans(1, 1);
      long long int ans = -1;
      int id;
      for (int i = 1; i <= n; i++) {
        if (f[i] > ans) {
          ans = f[i];
          id = i;
        }
      }
      printf("%d\n", id);
      return 0;
    }
    ```

### 习题

-  [HDU 2196 Computer](http://acm.hdu.edu.cn/showproblem.php?pid=2196) 
-  [POJ 1463 Strategic game](http://poj.org/problem?id=1463) 
-  [POJ 3585 Accumulation Degree](http://poj.org/problem?id=3585) 

-  [\[POI2008\]STA-Station](https://www.luogu.com.cn/problem/P3478) 

-  [\[USACO10MAR\]Great Cow Gathering G](https://www.luogu.com.cn/problem/P2986) 

-  [CodeForce 708C Centroids](http://codeforces.com/problemset/problem/708/C) 

## 参考资料与注释

[^note1]:  [子树合并背包类型的 dp 的复杂度证明 - LYD729 的 CSDN 博客](https://blog.csdn.net/lyd_7_29/article/details/79854245)