E换根 DP

子树内外合并

距离≤k 点权和

本课摘要

子树内外合并课程回答“子树内外贡献如何在第二遍 DFS 中合并”。内容以距离≤k 点权和为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断子树内外合并的适用条件与状态边界
  • 围绕“距离≤k 点权和”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

每个点的答案 = 子树内 + 子树外

前两节的距离和,换根系数是一句 n−2 szn-2\,\mathrm{sz} 就搞定的标量。但很多换根题的「答案」是一个更复杂的东西—— 比如 「距离不超过 kk 的点权和」,它天然分成两截:

固定一个根后,任一点 uu 的答案 = 子树内贡献 down[u]\mathrm{down}[u](uu 往下的部分) + 子树外贡献 up[u]\mathrm{up}[u](uu 经父亲往「树的其余部分」的那截)。 子树内的一截,第一遍后序就能直接算;难点全在子树外那一截怎么 O(1)O(1) 补上。

up子树内 down[u]:第一遍后序备好子树外 up[u](父方向)= 父的全部 − 朝自己子树那部分第二遍前序由父传子dist[u] = down[u] + up[u]
u 的答案分两块:向下的 down[u](第一遍后序备好)+ 父方向的 up[u](第二遍前序由父传子补上)。

父贡献减去「自己那一份」:换根的核心操作

换根到孩子 vv 时,它的「子树外」up[v]\mathrm{up}[v] 要从父亲 uu 借。 但不能直接把 uu 的全部信息给 vv——因为 uu 的信息里,有一部分正是「朝着 vv 这棵子树」的, 对 vv 来说那属于「子树内」,会重复计算。

核心操作就一句话:

父方向给 vv 的贡献 = (父 uu 的完整信息) − (朝 vv 子树的那一份)

以「距离分层点权和」dp[u][j]dp[u][j](子树内距 uu 恰为 jj 的点权和)为例,换根 u→vu\to v 分三步:

1234567第一遍后序求 sz第二遍前序换根合计 O(n)
仍是两遍 DFS:第一遍后序把每层的子树内点权和堆好,第二遍前序换根时「先扣重复、再合并、后复原」。

① 扣重复:父 uu 距 jj 的层里,混进了「从 uu 走到 vv 再拐回 vv 子树、距 j−2j-2」的点,即 dp[v][j−2]dp[v][j-2];先减掉。
② 合并下推:此刻的 dp[u][⋅]dp[u][\cdot] 已是「vv 看出去的父方向」,把它的 j−1j-1 层加到 dp[v][j]dp[v][j]——父方向的点距 vv 要多走一步。
③ 复原:把 ① 减掉的加回去,让 dp[u]dp[u] 恢复成「完整的 uu」,供 uu 的其它孩子换根时继续用。

本质

子树内外合并型换根,把 「父的完整信息」减去「本孩子子树贡献」 得到父方向,再合并进孩子—— 这就是「up[v]\mathrm{up}[v] 由 uu 回推」的一般套路。第一遍备好子树内,第二遍用「减一份、加一层、复原」把子树外沿边传下去。

跟着算一遍

用距离和把「内 + 外」看清楚(它是分层点权和最简的一维版)。链 1−2−31-2-3,无权,固定根 1:

1
第一遍 down(子树内距离和)。down[3]=0\mathrm{down}[3]=0(叶);down[2]=down[3]+sz[3]=0+1=1\mathrm{down}[2]=\mathrm{down}[3]+\mathrm{sz}[3]=0+1=1;down[1]=(down[2]+sz[2])=1+2=3\mathrm{down}[1]=(\mathrm{down}[2]+\mathrm{sz}[2])=1+2=3。
2
根的 up = 0。根没有子树外,up[1]=0\mathrm{up}[1]=0,故 dist[1]=down[1]+up[1]=3\mathrm{dist}[1]=\mathrm{down}[1]+\mathrm{up}[1]=3。
2
换根 1→2,补 up[2]。父 1 的「除去 2 子树」= 只剩点 1 自己。它距 2 为 1,且这条边外还有 sz[1]−sz[2]=1\mathrm{sz}[1]-\mathrm{sz}[2]=1 个点。 于是 up[2]=up[1]+2=0+2=2\mathrm{up}[2]=\mathrm{up}[1]+2=0+2=2(式中 +2+2 即扣掉节点 2 子树后剩下的量),dist[2]=down[2]+up[2]=1+2=3\mathrm{dist}[2]=\mathrm{down}[2]+\mathrm{up}[2]=1+2=3。
3
换根 2→3。同理 up[3]\mathrm{up}[3] 把「除 3 子树的其余两点」补进来,dist[3]=down[3]+up[3]=0+4=4\mathrm{dist}[3]=\mathrm{down}[3]+\mathrm{up}[3]=0+4=4。 与直接换根系数法算出的 3,3,43,3,4 完全一致——两种视角同解。
下面的演示固定根后,点任一点就把它的距离和拆成 down(子树内,青)+ up(子树外,父方向)两块,直观看「父方向 = 全局 − 自身子树」。

看「内 + 外」怎么拼出答案

固定根 = 节点 1。点任意节点,把它的距离和拆成两块:子树内(向下,down) + 子树外(父方向,up)。
1外23外4内5内6内7外8外
子树内 down[2](向下)
4
子树外 up[2](父方向)
9
距离和 = 内 + 外
4 + 9 = 13
换根到 节点 2 时,它的「子树外」up[u]\mathrm{up}[u] 要从父亲 节点 1 那里回推:父亲的全部信息里,减去「本来朝着自己这棵子树」的那部分, 剩下的就是 uu 的父方向贡献。子树内 down\mathrm{down} 在第一遍后序里已备好, 子树外 up\mathrm{up} 在第二遍前序里由父传子——两者一合并,dist[2] = 13。

多一维状态:距离分层

「距离 ≤k\le k 的点权和」比标量距离和多一维:dp[u][j]dp[u][j] 记录子树内距 uu 恰为 jj 的点权和(jj 从 00 到 kk)。 第一遍合并子树:dp[u][j]+=dp[v][j−1]dp[u][j]\mathrel{+}=dp[v][j-1](子树 vv 里距 vv 为 j−1j-1 的点,距 uu 就是 jj)。

换根时对每一层 jj 都做一次「减一份、加一层、复原」。最终点 ii 的答案 = ∑j=0kdp[i][j]\sum_{j=0}^{k}dp[i][j]。 复杂度 O(nk)O(nk)——每条边换根时扫 O(k)O(k) 层。

易错点

换根三步的顺序与循环方向是关键:扣重复用 dp[v][j−2]dp[v][j-2],下推用 dp[u][j−1]dp[u][j-1],两处下标错位不同; 且「合并下推」会改到 dp[v]dp[v],务必先扣父、再推子、最后复原父,否则同一个父的多个孩子会互相污染。 分层数组第二维只需开到 k+1k+1(本题 k≤20k\le20)。

例题

P3047[USACO12FEB] Nearby Cows GUSACO 2012提高+/省选-
题意
树上每点有点权,给定 kk,对每个点求「距它不超过 kk 的所有点的点权和」。
为什么选它
子树内外合并的标准训练题:状态 dp[u][j]dp[u][j] 按距离分层,换根必须做「父贡献减去自身子树贡献」这一核心操作, 把「内 + 外」讲得最透。k≤20k\le20 让分层维很小,focus 在换根逻辑本身。
转移 · 复杂度
合并 dp[u][j]+=dp[v][j−1]dp[u][j]\mathrel{+}=dp[v][j-1];换根「减 dp[v][j−2]dp[v][j-2]、加 dp[u][j−1]dp[u][j-1]、复原」;答案 ∑jdp[i][j]\sum_j dp[i][j];O(nk)O(nk)。
参考代码(分层换根 · 减一份/加一层/复原)
#include <iostream>
#include <vector>
using namespace std;

const int N = 100005;
int n, k;
vector<int> g[N];
long long val[N];             // 点权
long long dp[N][21];          // dp[u][j]:只在 u 子树内,距 u 恰为 j 的点权和

// 第一遍:子树内的分层点权和(后序)
void dfs1(int u, int fa)
{
    dp[u][0] = val[u];
    for (int v : g[u])
    {
        if (v == fa) continue;
        dfs1(v, u);
        for (int j = 1; j <= k; j++)
            dp[u][j] += dp[v][j - 1];   // 子树 v 里距 v 为 j-1 的点,距 u 就是 j
    }
}

// 第二遍:换根,把『父方向』的分层点权补进来(前序)
void dfs2(int u, int fa)
{
    for (int v : g[u])
    {
        if (v == fa) continue;
        // ★先扣除重复:父 u 距 j-2 的层里,含了『经 v 又回来』的 dp[v][j-2]
        for (int j = k; j >= 2; j--)
            dp[u][j] -= dp[v][j - 2];   // 撤销自身子树对父这一层的贡献
        for (int j = 1; j <= k; j++)
            dp[v][j] += dp[u][j - 1];   // 再把父方向(此刻的 dp[u])下推给 v
        // 复原 dp[u],供 u 的其它孩子换根时仍是『完整的 u』
        for (int j = 2; j <= k; j++)
            dp[u][j] += dp[v][j - 2];
        dfs2(v, u);
    }
}

int main()
{
    cin >> n >> k;
    for (int i = 1; i < n; i++)
    {
        int a, b;
        cin >> a >> b;
        g[a].push_back(b);
        g[b].push_back(a);
    }
    for (int i = 1; i <= n; i++) cin >> val[i];

    dfs1(1, 0);
    dfs2(1, 0);

    for (int i = 1; i <= n; i++)
    {
        long long s = 0;
        for (int j = 0; j <= k; j++) s += dp[i][j];   // 距 i 不超过 k 的点权和
        cout << s << endl;
    }
    return 0;
}
P1395会议洛谷原生普及+/提高
题意
树上选一点,使所有点到它的距离和最小。
换个视角
换个角度重看第一节的「会议」:把每点距离和写成 dist[u]=down[u]+up[u]\mathrm{dist}[u]=\mathrm{down}[u]+\mathrm{up}[u],down\mathrm{down} 第一遍后序求,up[v]\mathrm{up}[v] 由父回推——正是「子树外距离回推」的最简一维实例。 它和「换根系数 n−2 szn-2\,\mathrm{sz}」是同一件事的两种写法,互相印证。
转移 · 复杂度
down[u]=∑(down[v]+sz[v])\mathrm{down}[u]=\sum(\mathrm{down}[v]+\mathrm{sz}[v]);up[v]=up[u]+(down[u]−(down[v]+sz[v]))+(n−sz[v])\mathrm{up}[v]=\mathrm{up}[u]+(\mathrm{down}[u]-(\mathrm{down}[v]+\mathrm{sz}[v]))+(n-\mathrm{sz}[v]);O(n)O(n)。

练习

P6419[COCI2014-2015#1] Kamp拔高:每点作起点送客到所有关键点的最短耗时,内外两遍换根 + 『来回一条边只走单程』的直径式修正。在洛谷打开
P1395会议用 down/up 分解重写一遍,和换根系数法对拍——两种视角结果必须一致。在洛谷打开
P3478[POI2008] STA-Station深度和最大:同样可拆成 down + up,验证换根不止一种推法。在洛谷打开

已进入 子树内外合并 · 换根 DP · DP大师