换根基础模型
二次扫描骨架
本课摘要
换根基础模型课程回答“一次定根结果如何在线性时间转移到所有根”。内容以二次扫描骨架为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。
- 判断换根基础模型的适用条件与状态边界
- 围绕“二次扫描骨架”推导转移与计算顺序
- 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节
正在整理目录…
同一个量,要对「每个点」都算一遍
换根 DP 解决这样一类问题:给一棵无根树,要对每一个点,都算出「把它当根时的某个量」—— 比如「它到所有其它点的距离之和」「它的子树深度和」。注意关键词是每一个点:不是求一个全局最优,而是要 个答案。
先看最朴素的想法:枚举每个点当根,各自跑一遍遍历。以某点为根做一次 BFS/DFS,就能得到它到全树的距离和,。 但要对 个点都这么做,总共就是 。
时 直接爆炸。可这 遍遍历里藏着大量重复: 相邻两个点当根,绝大多数点到它们的距离只差了「一步」。换根 DP 正是要抓住这个「只差一步」, 让相邻根之间 递推,把 压回 。
两遍 DFS:先立地基,再顺边换根
换根 DP 的骨架是两遍 DFS,以「深度和 / 距离和」为例(每条边长 1、每个点算 1):
第一遍 · 后序,固定一个根(记作 1)。求出每个点的子树大小 (= 它自己 + 各孩子子树大小之和)。因为父要用到子的结果,必须子先于父,所以是后序。 顺手把固定根的距离和 也累加出来——这是唯一一个「老实一层层加」得到的答案,作为换根的起点。
第二遍 · 前序,从根出发把根「挪」给每个孩子。关键是想清楚:根从 挪到相邻的孩子 时,距离和怎么变?
把根从 移到孩子 ,相当于所有点相对根「整体挪了一条边」:落在 子树里的 个点,离新根近了 1;其余 个点,离新根远了 1。于是
一次加法就把 算出来了。沿树前序递归,每条边做一次这样的 更新,走完就得到所有点的答案。 边界:起点 由第一遍给出。
本质
换根 DP = 「固定根的一份答案」+「相邻根之间的 增量」。第一遍 DFS 花 立好地基( 与起点 ), 第二遍 DFS 用 把答案沿边「传染」出去。把 次独立遍历,换成一次遍历里 个相互推导的增量。
跟着算一遍
用一条 5 个点的链 手推(无权,求每点距离和)。固定根取 1:
看两遍扫描跑起来
- 来源参与当前转移的依赖状态
- 当前正在计算或观察的状态
- 确定 / 最优已进入当前最优解或确定结果
- 已处理已经扫描、当前不再活跃的状态
- 非法越界、冲突或不可达状态
暴力的 是「每个点都从头 BFS 一遍」;换根只走两遍 DFS 就把全部 个点的答案填满。
写法要点:无根树、任取一根、别回头
换根题的输入几乎都是无根树(只给 条无向边)。实现时统一套路: 用 存双向邻接表,DFS 时传一个 (父亲)参数, 遇到 就跳过——这样就把无向图当有根树遍历,不会走回头路。
两遍 DFS 都从固定根 1 出发: 后序累加 (递归返回后再 子树), 前序换根(先算 再递归进 ,保证父答案已就绪)。 时递归可能栈深较大——洛谷默认栈够用,实在担心可手写栈迭代。
易错点
换根的增量必须用「相对固定根 1」算出的 (第一遍那套), 而不是「相对当前根」。第二遍前序时每个 都是固定值,别在换根途中去改它。 另外距离和常常爆 ( 时和可达 ),全程开 。
例题
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 1000005;
int n;
vector<int> g[N]; // 邻接表
ll sz[N]; // sz[u]:以 1 为根时 u 的子树节点数
ll f[N]; // f[u]:以 u 为根时的深度和(距离和)
ll dep[N]; // dep[u]:以 1 为根时 u 的深度
// 第一遍:后序求子树大小 sz[],顺带累加 f[1] = Σ dep[i]
void dfs1(int u, int fa)
{
sz[u] = 1;
for (int v : g[u])
{
if (v == fa) continue;
dep[v] = dep[u] + 1;
dfs1(v, u);
sz[u] += sz[v]; // 子必先算好——所以后序
}
}
// 第二遍:前序换根 f[v] = f[u] + (n - 2*sz[v])
void dfs2(int u, int fa)
{
for (int v : g[u])
{
if (v == fa) continue;
f[v] = f[u] + (n - 2 * sz[v]); // ★O(1) 换根:子树内 sz 个近 1,其余远 1
dfs2(v, u);
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dep[1] = 0;
dfs1(1, 0);
for (int i = 1; i <= n; i++)
f[1] += dep[i]; // 以 1 为根的深度和 = 起点
dfs2(1, 0);
int best = 1;
for (int i = 1; i <= n; i++)
if (f[i] > f[best]) best = i; // 本题求深度和最大的点
cout << best << endl;
return 0;
}#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;
const int N = 50005;
int n;
vector<int> g[N];
ll sz[N], f[N], dep[N];
void dfs1(int u, int fa)
{
sz[u] = 1;
for (int v : g[u])
{
if (v == fa) continue;
dep[v] = dep[u] + 1;
dfs1(v, u);
sz[u] += sz[v];
}
}
void dfs2(int u, int fa)
{
for (int v : g[u])
{
if (v == fa) continue;
f[v] = f[u] + (n - 2 * sz[v]);
dfs2(v, u);
}
}
int main()
{
cin >> n;
for (int i = 1; i < n; i++)
{
int a, b;
cin >> a >> b;
g[a].push_back(b);
g[b].push_back(a);
}
dfs1(1, 0);
for (int i = 1; i <= n; i++) f[1] += dep[i];
dfs2(1, 0);
int best = 1;
for (int i = 1; i <= n; i++)
if (f[i] < f[best]) best = i; // 会议:求距离和最小的点
cout << best << " " << f[best] << endl;
return 0;
}
