E换根 DP

中心 / 偏心距

树的直径·核

本课摘要

中心 / 偏心距课程回答“偏心距与树中心如何由向下和向上信息共同得到”。内容以树的直径·核为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断中心 / 偏心距的适用条件与状态边界
  • 围绕“树的直径·核”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

给每个点算「到最远点有多远」

换根的第三类目标是偏心量:对每个点 uu,求它的偏心距 ecc[u]\mathrm{ecc}[u] = 「uu 到树上最远点的距离」。偏心距最小的点就是树的中心,最小值叫半径; 所有偏心距里的最大值 = 树的直径(最长链长度)。

又是「对每个点都要一个答案」的形状。朴素做法照旧 O(n2)O(n^2)(每点各 BFS 求最远)。 换根 DP 把它压到 O(n)O(n):每个点的最远点,要么在它的子树里(向下),要么在子树外(经父亲向上)——两支取较大。

1e42e33e24e35e46e3绿圈=偏心距最小(2)=树的中心e = 偏心距直径 = 最长链(1↔5,长 4)
每点标 e = 偏心距(到最远点的边数)。绿圈是偏心距最小的中心;虚线是直径(最长链 1↔5,长 4)。

与 F 部分·直径/重心 DP 的分工

「树的直径」本身有一套固定根一遍 DFS 的经典求法(子树最深链 + 次深链拼出过点最长路径), 那是 F 部分·直径 / 重心 DP 的主场,含完整推导。本页站在换根视角:不止求「一条直径」,而是给每个点都算出偏心距(二次扫描逐点求最远),两页互补——先在 F 学会「一条最长链」,再来这里学「每点的最远」。

两遍扫描:向下最长链 + 向上最长链

第一遍 · 后序,求「向下最长链」。对每个点 uu,记它子树内向下的最长链 down1[u]\mathrm{down1}[u] 和次长链 down2[u]\mathrm{down2}[u](次长必须来自与最长不同的孩子)。为什么要次长?换根时可能要「避开某个孩子」,届时就得退而求其次。

第二遍 · 前序,求「向上最长链」。孩子 vv 经父 uu 往上能走多远? 两条候选:走 uu 自己的向上链 up[u]\mathrm{up}[u],或走 uu 的「避开 vv 那支」的向下最长链—— 若 vv 恰是贡献 down1[u]\mathrm{down1}[u] 的那个孩子,就只能用 down2[u]\mathrm{down2}[u],否则用 down1[u]\mathrm{down1}[u]。取较大再加这条边:

up[v]=max⁡(up[u], down-exceptv[u])+w(u,v)\mathrm{up}[v]=\max\big(\mathrm{up}[u],\ \mathrm{down\text{-}except}_v[u]\big)+w(u,v)

合成偏心距:

ecc[u]=max⁡(down1[u], up[u])\mathrm{ecc}[u]=\max\big(\mathrm{down1}[u],\ \mathrm{up}[u]\big)
up子树内 down[u]:第一遍后序备好子树外 up[u](父方向)= 父的全部 − 朝自己子树那部分第二遍前序由父传子dist[u] = down[u] + up[u]
和内外合并同构:down(子树内向下)第一遍备好,up(父方向向上)第二遍由父传子。偏心距取两者较大。

本质

「每点偏心距」就是「每点的最远」这个换根问题。第一遍备好向下最长/次长两条链, 第二遍把向上最长链沿边传下去;「避开自己那支」正是换根一贯的「父贡献减去本孩子子树」—— 只不过这里的聚合是取 max⁡\max 而非求和。有了每点偏心距,中心 / 半径 / 直径一并落袋。

跟着算一遍

主链 1−2−3−4−51-2-3-4-5 加一个分支 3−63-6(无权)。固定根 1,逐步:

1
第一遍 down1。叶子 5、6 的 down1=0\mathrm{down1}=0;down1[4]=1, down1[3]=max⁡(1+1, 0+1)=2\mathrm{down1}[4]=1,\ \mathrm{down1}[3]=\max(1{+}1,\,0{+}1)=2(走 4 那支);down1[2]=3, down1[1]=4\mathrm{down1}[2]=3,\ \mathrm{down1}[1]=4。点 3 的 down2[3]=1\mathrm{down2}[3]=1(来自分支 6)。
2
根 up = 0。up[1]=0\mathrm{up}[1]=0,ecc[1]=max⁡(4,0)=4\mathrm{ecc}[1]=\max(4,0)=4(最远到点 5)。
2
换根往下传 up。up[2]=max⁡(up[1], 0)+1=max⁡(0,0)+1=1\mathrm{up}[2]=\max(\mathrm{up}[1],\,0)+1=\max(0,0)+1=1(式中第二项 00 = 节点 1 避开 2 那支的向下最长链);up[3]=max⁡(up[2],0)+1=2\mathrm{up}[3]=\max(\mathrm{up}[2],0)+1=2;ecc[3]=max⁡(down1[3],up[3])=max⁡(2,2)=2\mathrm{ecc}[3]=\max(\mathrm{down1}[3],\mathrm{up}[3])=\max(2,2)=2。
3
找中心。算完全部:偏心距为 ecc=[4,3,2,3,4,3]\mathrm{ecc}=[4,3,2,3,4,3],最小在点 3(ecc=2\mathrm{ecc}=2)—— 树的中心,半径 2;最大偏心距 4 = 直径长度。
下面点任一节点,看它的偏心距如何由向下 down 与向上 up 两支较量决出,绿圈标出全树中心。

看每点偏心距与中心

点任意节点,看它的偏心距(到最远点的距离)= max(向下最长链 down, 向上最长链 up)。 绿圈是偏心距最小的点 = 树的中心(半径 2), 全树最大偏心距 = 直径 4。
1e42e33e24e35e46e37e48e4
向下最长链 down1[3]
2
向上最长链 up[3](父方向)
2
偏心距 = max(down, up)
max(2, 2) = 2
节点 3 到最远点的距离是 2。 它由两支较量决出:往子树里最深走 down\mathrm{down},或经父亲往树的其余部分最远走 up\mathrm{up},取较大者。up\mathrm{up} 正是换根第二遍求的——把「父的最长链(避开自己这支)」加一条边传下来。 它就是当前的中心(偏心距最小)。

逐点统计是换根的通用形

回头看:距离和、距离分层点权和、偏心距——它们表面差别很大,但换根的骨架完全一样: 第一遍后序把「子树内的某个聚合」备好,第二遍前序用「父的信息减去本孩子那份,再沿边合并」把「子树外」补齐, 每点答案 = 内 + 外。变的只是聚合方式:求和(距离和 / 点权和)还是取 max⁡\max(偏心距)。

像「医院设置」(n≤100n\le100)这类,本质就是逐点距离和统计:换根一遍求出每个点作医院的总代价,再取最优。 换根让你把「对每个候选点各评估一次」的 O(n2)O(n^2) 收成 O(n)O(n)——这正是换根 DP 最通用的用途。

易错点

求偏心距务必维护次长链 down2\mathrm{down2} 与「贡献最长链的孩子编号」:换根到那个孩子时必须避开它自己、改用次长,否则会把「自己走出去又走回来」的假链算进最远。 「避开自己那支」是所有 max⁡\max 型换根的通病,格外小心。

例题

P1099[NOIP2007 提高组] 树网的核NOIP 2007提高+/省选-
题意
在树的某条直径上取一段长度不超过 ss 的路径(「核」),使全树到这段核的最大距离(偏心距)最小。
换个视角(本页站换根/逐点偏心距)
本题集直径 + 中心 + 最小偏心距于一身。直径三件套的完整推导在 F 部分·直径 / 重心 DP; 这里我们用换根的眼光看它的另一半——二次扫描求「每个点到最远点的距离」(偏心距), 为「核」的偏心量评估提供逐点数据。两页互补:F 讲怎么求出那条最长链,本页讲怎么对每个点都得到偏心距。
转移 · 复杂度
两遍 DFS 求 down1/down2/up\mathrm{down1}/\mathrm{down2}/\mathrm{up} → 每点 ecc[u]=max⁡(down1[u],up[u])\mathrm{ecc}[u]=\max(\mathrm{down1}[u],\mathrm{up}[u]); 结合直径上滑动取核,整体 O(n)O(n)~O(nlog⁡n)O(n\log n)。
参考代码(换根求每点偏心距 · 中心与半径)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 100005;
int n;
vector<int> g[N];
int down1[N], down2[N], best[N], up[N];   // 向下最长/次长链、贡献最长的孩子、向上最长链
int ecc[N];                                // 偏心距 = max(down1, up)

// 第一遍:后序求每点向下最长/次长链
void dfs1(int u, int fa)
{
    down1[u] = down2[u] = 0;
    best[u] = -1;
    for (int v : g[u])
    {
        if (v == fa) continue;
        dfs1(v, u);
        int cand = down1[v] + 1;      // 经孩子 v 向下最长
        if (cand > down1[u])
        {
            down2[u] = down1[u];
            down1[u] = cand;
            best[u] = v;
        }
        else if (cand > down2[u])
            down2[u] = cand;
    }
}

// 第二遍:前序求向上最长链 up[],合成偏心距
void dfs2(int u, int fa)
{
    for (int v : g[u])
    {
        if (v == fa) continue;
        // v 往上:父的 up 或父『避开 v 这支』的最长向下链,取大 + 1
        int uDown = (best[u] == v) ? down2[u] : down1[u];
        up[v] = max(up[u], uDown) + 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);
    }

    dfs1(1, 0);
    up[1] = 0;
    dfs2(1, 0);

    int center = 1;
    for (int i = 1; i <= n; i++)
    {
        ecc[i] = max(down1[i], up[i]);        // 每点到最远点的距离
        if (ecc[i] < ecc[center]) center = i; // 偏心距最小 = 树的中心
    }

    cout << center << " " << ecc[center] << endl;  // 中心及其偏心距(半径)
    return 0;
}
P1364医院设置洛谷原生普及/提高-
题意
带居民数的树,选一点设医院使加权距离和最小。
为什么再选它
换根「逐点统计」用途的最小样例:一遍换根算出每个点作医院的距离和,再逐点取最优。n≤100n\le100,还能与 O(n2)O(n^2) 暴力对拍,收束整个 E 部分「每点一个答案」的主线。
转移 · 复杂度
f[v]=f[u]+(W−2 sz[v])f[v]=f[u]+(W-2\,\mathrm{sz}[v]) 逐点求距离和;两遍 DFS,O(n)O(n)。
参考代码(换根逐点距离和)
#include <iostream>
#include <vector>
using namespace std;
typedef long long ll;

const int N = 105;
int n;
ll c[N];
vector<int> g[N];
ll sz[N], f[N], W;

void dfs1(int u, int fa, ll dep)
{
    sz[u] = c[u];
    f[1] += c[u] * dep;
    for (int v : g[u]) if (v != fa) { dfs1(v, u, dep + 1); sz[u] += sz[v]; }
}

void dfs2(int u, int fa)
{
    for (int v : g[u]) if (v != fa)
    {
        f[v] = f[u] + (W - 2 * sz[v]);   // 逐点距离和:换根一次性求全
        dfs2(v, u);
    }
}

int main()
{
    cin >> n;
    for (int i = 1; i <= n; i++)
    {
        int l, r;
        cin >> c[i] >> l >> r;
        W += c[i];
        if (l) { g[i].push_back(l); g[l].push_back(i); }
        if (r) { g[i].push_back(r); g[r].push_back(i); }
    }
    dfs1(1, 0, 0);
    dfs2(1, 0);

    ll ans = f[1];
    for (int i = 2; i <= n; i++) ans = min(ans, f[i]);   // 逐点统计后取最优
    cout << ans << endl;
    return 0;
}

练习

P3574[POI2014] FAR-FarmCraft拔高:遍历顺序 + 换根最小化『最晚装好』的瓶颈。子树内先算最优遍历代价,换根定每点作起点的答案。在洛谷打开
P1364医院设置先写 O(n²) 暴力拿分,再换根 O(n) 对拍——亲手确认逐点统计的两法一致。在洛谷打开
P1099[NOIP2007 提高组] 树网的核自测:先按 F 部分求出直径与每点偏心距,再在直径上滑动窗口取长度≤s 的核。在洛谷打开

已进入 中心 / 偏心距 · 换根 DP · DP大师