换根 DP

创建时间:2025-03-26


换根DP是一种特殊的树形DP,用于计算无根树中以任意一个节点为根时的最佳答案。

那么换根DP是怎么实现的呢?以洛谷P3478为例:

这道题要求出以哪个节点为根时所有节点的深度和最大。显然,如果给定的是一颗有根树,我们可以在 \(O(n)\) 的时间内求出深度和,这样可以得到一个时间复杂度为 \(O(n^2)\) 的算法:以每个节点为根都计算一次深度总和。

显然,这个做法在\(n\) 较大时速度极慢。考虑先假定任意一个节点(通常是节点1)为根DFS,求出以每个节点为根的子树里节点数量 \(siz_i\) 和的深度总和 \(dp_i\)。再跑第二遍DFS,维护数组 \(f_i\) 表示以 \(i\)整棵树的根的情况下所有节点的深度和,显然 \(f_1\) 应该等于 \(dp_1\),只需要考虑怎么转移 \(f\) 数组。对于 \(u\)\(u\) 的儿子 \(v\),可以尝试将 \(v\) “提起来”,将 \(u\) “放下去”,即将这棵树的根换为 \(v\),并利用已有的以 \(u\) 为整棵树的根时的 \(f_u\) 计算 \(f_v\)

因为 \(v\) 被“提起来”成为了新的根,所以 \(v\) 原子树中的所有结点的深度和就等于 \(dp_v\);因为原来的根 \(u\) 被“放下去”,所以所有不在 \(v\) 原子树中的节点的深度都要+1,此时不在 \(v\) 原子树中的所有点的深度和 = \([f_u - (dp_v + siz_v)] + (n - siz_v)\) = \(f_u + n - dp_v - 2 \cdot siz_v\)。这样便得到了 \(f\) 的转移方程,即

\[f_v = dp_v + (f_u + n - dp_v - 2 \cdot siz_v) = f_u + n - 2 \cdot siz_v \]

有了 \(f\) 数组,我们便可以轻松的得到最终答案,代码如下:

#include <bits/stdc++.h>

using namespace std;

#define int long long

const int MAX_N = 1e6 + 50;

int n, dp[MAX_N];
int siz[MAX_N], f[MAX_N], ans;
vector<int> e[MAX_N];

void dfs1(int u, int fa) {
    siz[u] = 1;
    for (int v : e[u]) {
        if (v == fa)
            continue;
        dfs1(v, u);
        dp[u] += dp[v] + siz[v];
        siz[u] += siz[v];
    }
}

void dfs2(int u, int fa) {
    for (int v : e[u]) {
        if (v == fa)
            continue;
        f[v] = f[u] + n - siz[v] * 2;
        dfs2(v, u);
    }
}

signed main() {
    cin >> n;
    for (int i = 1, u, v; i < n; i++) {
        cin >> u >> v;
        e[u].push_back(v);
        e[v].push_back(u);
    }

    dfs1(1, 0);
    f[1] = dp[1];
    dfs2(1, 0);
    for (int i = 1; i <= n; i++)
        if (f[i] > f[ans])
            ans = i;
    cout << ans;
    return 0;
}

根据这道题的解决方案,可以得出换根DP的使用条件:

  • 给定的是一颗无根树,且要求以任意一个节点为根时的答案
  • 可以快速的求出 \(u\)(父亲)到 \(v\)(儿子)的 \(f\) 变化量

下面是几道换根DP的例题:
洛谷P2986(模板题双倍经验)
CF1187E(换根DP建模)
CF1324F(换根DP建模)
洛谷P10974(边的流量限制)
洛谷P6419(换根DP+最长链)
洛谷P3761(枚举+树的直径+最长链)

注:在使用换根DP时,函数返回值能写void就写void,不然很可能喜提玄学爆栈:1234567

posted @ 2026-05-19 12:24  xubaichuan  阅读(16)  评论(0)    收藏  举报