Loading

换根dp

拿一道比较经典的例题来作为引入

[POI2008] STA-Station

给定一个 \(n\) 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和(路径和)最大。
一个结点的深度之定义为该节点到根的简单路径上边的数量。
\(1 \leq n \leq 10^6\)\(1 \leq u, v \leq n\),给出的是一棵树。

首先如果是有根树的话就是一个比较简单的树形dp了,令 \(f_x\) 代表 \(x\) 的子树中所有的路径和,\(size[x]\)\(x\) 的子树中的点的数量,那么 \(f_x=\sum_{y\Subset son_x}f_y+size[y]\)。那么可以得到一个 \(O(n^2)\) 的做法,不足以通过此题。

可以注意到我们每次枚举的过程其实就是相当于换根的过程,之所以复杂度不对是因为在换根的过程中很多部分都被重复计算了,例如当我的根从 \(x\) 换到 \(y\) 的时候,以前以 \(x\) 为根和现在以 \(y\) 为根的 \(y\) 的子树对 \(f_y\) 的贡献是不变的,只不过现在 \(x\) 加到了 \(y\) 的子树当中,我们只需要求出这一部分的贡献就可以了。

我们令 \(v_x\) 为把 \(x\) 的父亲变成 \(x\) 的子树所带来的贡献,那么当我的根从 \(1\) 换到 \(x\) 的时候,我们易得 \(v_x=f_1-f_x-size[x]+n-size[x]\)。更广泛一点,对于任意一对点 \(x,y\),其中 \(x\)\(y\) 的父亲,\(v_y=f_x+v_x-size[y]+n-size[y]\),其中 \(f_x+v[x]\) 就是以 \(x\) 为根的时候的子树中的路径和。

AC code:

#include <bits/stdc++.h>
using namespace std;
typedef long long ll;

int main() {
    ios_base::sync_with_stdio(false);cin.tie(NULL);cout.tie(NULL);
    int n; cin >> n;
    vector<vector<int>> g(n + 1);
    for (int i = 0; i < n - 1; i++) {
        int u, v; cin >> u >> v;
        g[u].push_back(v);
        g[v].push_back(u);
    }
    vector<ll> f(n + 1), v(n + 1);
    vector<int> size(n + 1);
    auto up = [&](auto up, int x, int pre) -> void {
        size[x] = 1;
        for (int y : g[x]) if (y != pre) {
            up(up, y, x);
            sz[x] += sz[y];
            f[x] += f[y] + sz[y];
        }
    };
    up(up, 1, 0);
    auto down = [&](auto down, int x, int pre) -> void {
        for (int y : g[x]) if (y != pre) {
            w[y] = w[x] + f[x] - f[y] - sz[y] + n - sz[y];
            down(down, y, x);
        }
    };
    down(down, 1, 0);
    ll ans = 0, r = -1;
    for (int i = 1; i <= n; i++) if (setmax(ans, f[i] + v[i])) r = i;
    cout << r << '\n';

    return 0;
}

总结一下,一般的换根dp都有一下套路:

  • 首先假设 \(1\) 为根进行一次从下往上树形dp (up)
  • 分析 \(x\)\(1\) 进行换根
  • 泛化到任意一对 \((x,y)\) 进行从上往下换根 (down)

problem set:

posted @ 2024-03-18 17:03  KakaDBL  阅读(23)  评论(0)    收藏  举报