F树形 DP

树上背包

二叉苹果树·选课

本课摘要

树上背包课程回答“树上选取数量约束如何在子树间做背包合并”。内容以二叉苹果树·选课为主线,配合逐步推导、可编辑演示、例题与练习形成可复查的学习闭环。

  • 判断树上背包的适用条件与状态边界
  • 围绕“二叉苹果树·选课”推导转移与计算顺序
  • 用演示、复杂度分析和配套题目校验实现
本课目录 · 0 节

正在整理目录…

当「选子必先选父」遇上容量限制

上一类的 f[u][0/1]f[u][0/1] 只问「选不选」。但很多问题还带一个预算:一棵长满苹果的树, 只能保留 QQ 条树枝,且——要留一条枝,它上面那截主干必须先留住(否则这条枝就从树上掉了)。 在预算 QQ 下让保留的苹果最多,这是树上背包。

「容量受限的取舍」正是背包的本行,但这里多了一层依赖:孩子想被选中,得先为「连接它的那条边」付出一份容量。 于是每个点的状态要带上容量维:

f[u][j]: u ; j f[u][j]:\ u\ \text{;}\ j\

读作 f[u][j]f[u][j] = 在 u 的子树里恰好保留 j 条边时的最大苹果数。答案在 f[root][Q]f[root][Q]。

1523344152金色序号 = 处理次序:叶子 4、5 先算好,父亲 2 才能合并;根 1 最后收口。
仍是后序:先把每个孩子子树的 f[c][⋅]f[c][\cdot] 整张小表算好,父亲再把孩子们「分组背包」式地并进来。

把孩子当成一「组」物品

怎么合并?关键一步:把每个孩子子树看成分组背包里的一「组」。给孩子 cc 分配 tt 条边(t≥1t\ge1), 意味着——先用掉 1 条去接通「u 到 c 的边」(拿到它的边权),再把剩下 t−1t-1 条留给 c 的子树内部去最优,即 f[c][t−1]f[c][t-1]。

uc连 c 的边先花 1 条容量给孩子 c 这一组分 t 条边收益 = 边权(c)+ dp[c][t−1](1 条连边 + t−1 条子树内)每个孩子是一「组」物品,父亲对孩子们做分组背包——这就是有依赖背包的树上形态。
选孩子 c,先花 1 条容量接通「连 c 的边」;这条边的存在,正是「有依赖背包」的依赖。

于是父亲对每个孩子做一次分组背包合并(jj 倒序,避免一组被算两次):

f[u][j]=max⁡1≤t≤j(f[u][j−t]+(wu,c+f[c][t−1]))f[u][j]=\max_{1\le t\le j}\Big(f[u][j-t]+\big(w_{u,c}+f[c][t-1]\big)\Big)

这里 wu,cw_{u,c} 是 u 到孩子 c 的边权。注意 tt 从 1 起步——要碰孩子子树里任何一条边,就必须先付这条连边,这就是依赖被自然编码进转移的方式。

本质

树上背包 = 子树维背包 + 依赖。「选子必选连父的边」不是额外判断,而是把 tt 的下界设成 1、并把边权算进那一步——依赖就免费融进了分组背包的转移里。复杂度是每个点 O(szu2)O(sz_u^2),全树合起来 O(n2)O(n^2)(经典的「树上背包是平方」结论)。

跟着算一遍

小树:根 11,两个孩子 22(连边苹果 2)、33(连边苹果 5);22 再带两片叶 44(连边 3)、55(连边 4)。保留 Q=3Q=3 条边:

1
叶子 4、5、3 子树内没有边:f[⋅][0]=0f[\cdot][0]=0,更高列不存在(sz=0sz=0)。
2
节点 2(孩子 4、5,连边 3、4)。并入 4:f[2][1]=w2,4+f[4][0]=3f[2][1]=w_{2,4}+f[4][0]=3。再并入 5:f[2][1]=max⁡(3, 4)=4f[2][1]=\max(3,\,4)=4,f[2][2]=w2,4+w2,5=3+4=7f[2][2]=w_{2,4}+w_{2,5}=3+4=7。得 f[2]=[0,4,7]f[2]=[0,4,7]。
3
根 1(孩子 2、3,连边 2、5)。并入 2:给它 tt 条 → w1,2+f[2][t−1]w_{1,2}+f[2][t-1],得 f[1][1..3]=[2,6,9]f[1][1..3]=[2,6,9]。再并入 3(sz=0sz=0,只能给 1 条 w1,3=5w_{1,3}=5):f[1][3]=max⁡(9, f[1][2]+5)=max⁡(9,6+5)=11f[1][3]=\max(9,\ f[1][2]+5)=\max(9,6+5)=11。
✓
答案 f[1][3]=11f[1][3]=11——留「1-3」这条边(5)+「1-2」「2-5」两条(2+4),共 3 条边、苹果 11。
下面的演示画出这棵苹果树,点任意节点看它的小背包表 f[u][j]f[u][j];改边权或保留数 QQ,所有表实时重算。

每个节点一张小背包表

改每条边的苹果数(边权)
2
连 1–2 的边
2
3
连 1–3 的边
5
4
连 2–4 的边
3
5
连 2–5 的边
4
保留边数 K
3
点节点看它的小背包表 dp[u][j] = u 子树保留 j 条边的最大苹果数。答案在根 dp[1][3] = 11。
253412345
节点 4 · 子树 0 条边
j
0
dp
0
节点 5 · 子树 0 条边
j
0
dp
0
节点 2 · 子树 2 条边
j
0
1
2
dp
0
4
7
节点 3 · 子树 0 条边
j
0
dp
0
节点 1(根) · 子树 4 条边
j
0
1
2
3
dp
0
5
7
11
每个孩子当作一组物品:给它分 t 条边就得到 边权 + dp[孩子][t−1] 的苹果, 在父亲的背包里做分组背包合并。选子必先选连它的那条边——这正是「有依赖背包」的树上形态。

依赖成森林:接一个虚根

再看「选课」:nn 门课,有的课要先修另一门才能选;没有先修课的课可以直接选。选 mm 门,最大化学分。 「先修」关系画出来是一片森林(多棵依赖树),不是单棵树——DFS 从哪开始?

技巧极简:造一个虚根 00,把每棵树的树根都挂到它下面。森林瞬间变成一棵以 00 为根的树,前面那套树上背包直接套用。 只是要记得——选 mm 门真课,等价于在含虚根的树里选 m+1m+1 个点(虚根白占一个名额),最后读 f[0][m+1]f[0][m+1]。

0虚线 = 虚根 0 补出的边:三棵依赖树瞬间变成一棵以 0 为根的树,选 m 门真课 = 含虚根选 m+1 个点。
三棵依赖树各自的根,用虚根 0 的虚线边挂起来——森林化为一棵树,树上背包直接套用。

常见陷阱:虚根的「+1」不能漏

接虚根后,容量要留给虚根那一门。转移时 jj 上界写 m+1m+1、每个点的 f[u][1]f[u][1] 先塞自己(占 1 门),答案读 f[0][m+1]f[0][m+1]。漏掉这个 +1,会把真课数当成点数,答案系统性偏小一门。

例题

P2015二叉苹果树洛谷原生普及+/提高
题意
一棵带边权(苹果数)的二叉苹果树,共 nn 个节点。只保留 QQ 条树枝(保留的枝必须与根连通),求最多保留多少苹果。
对应关系
f[u][j]f[u][j] = u 子树保留 j 条边的最大苹果数。二叉限制让每个点至多两个孩子,转移最清爽,是树上背包最佳入门。
转移 · 复杂度
f[u][j]=max⁡t(f[u][j−t]+wu,c+f[c][t−1])f[u][j]=\max_t(f[u][j-t]+w_{u,c}+f[c][t-1]);一遍 DFS,O(nQ)O(nQ) 级。
参考代码(边权分组背包)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 105;
struct E { int to, w; };
vector<E> g[N];               // 邻接表带边权(苹果数)
int f[N][N];                  // f[u][j]:u 子树里保留 j 条边的最大苹果数
int sz[N];                    // sz[u]:u 子树内的边数(dp 第二维上界)
int Q;

void dfs(int u, int fa)
{
    for (E e : g[u])
    {
        if (e.to == fa) continue;
        dfs(e.to, u);
        sz[u] += sz[e.to] + 1;          // 加上「连孩子的边」和孩子子树里的边
        for (int j = min(sz[u], Q); j >= 1; j--)      // ★分组背包:容量倒序
            for (int t = 1; t <= sz[e.to] + 1 && t <= j; t++) // 给这个孩子分 t 条边
                f[u][j] = max(f[u][j], f[u][j - t] + f[e.to][t - 1] + e.w);
    }
}

int main()
{
    int n;
    cin >> n >> Q;
    for (int i = 1; i < n; i++)
    {
        int a, b, w;
        cin >> a >> b >> w;
        g[a].push_back({b, w});
        g[b].push_back({a, w});         // 无向,dfs 里用 fa 挡回边
    }

    dfs(1, 0);
    cout << f[1][Q] << endl;
    return 0;
}
P2014[CTSC1997] 选课CTSC 1997提高+/省选-
题意
nn 门课,每门有先修课(0 表示无)与学分。选 mm 门使学分最大,且选一门必须先选它的先修课。
为什么选它
有依赖背包的标准母题:先修关系成森林,用虚根 0 合成一棵树后,就是「点数背包」。它把「依赖 → 树上背包」的转承讲得最透。
转移 · 复杂度
f[u][j]=max⁡k(f[u][j−k]+f[c][k])f[u][j]=\max_k(f[u][j-k]+f[c][k]),选 m+1m+1 个点(含虚根);O(nm)O(nm) 级。
参考代码(虚根 + 点数背包)
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;

const int N = 305;
vector<int> g[N];             // g[u]:以 u 为先修课的那些课
int s[N];                     // s[i]:第 i 门课的学分
int f[N][N];                  // f[u][j]:在 u 子树里选 j 门课的最大学分
int m;

void dfs(int u)
{
    f[u][1] = s[u];           // 选了 u 子树里的课,必先选 u 自己(占 1 门)
    for (int v : g[u])
    {
        dfs(v);
        for (int j = m + 1; j >= 2; j--)        // ★+1:0 号虚根也算一门,容量留够
            for (int k = 1; k <= j - 1; k++)    // 给孩子 v 这一组分 k 门
                f[u][j] = max(f[u][j], f[u][j - k] + f[v][k]);
    }
}

int main()
{
    int n;
    cin >> n >> m;
    for (int i = 1; i <= n; i++)
    {
        int fa;
        cin >> fa >> s[i];
        g[fa].push_back(i);   // fa == 0 表示无先修课,挂到虚根 0 下
    }

    dfs(0);                   // ★森林接一个虚根 0,问题化为一棵树
    cout << f[0][m + 1] << endl;   // 选 m 门真课 + 虚根 1 门 = m+1
    return 0;
}

练习

P1273有线电视网叶子是用户(带收视费),内部点转发有成本。f[u][j] = u 子树覆盖 j 个用户的最大净收益;边权取负成本,用户数当容量。在洛谷打开
P3177[HAOI2015] 树上染色拔高:把 k 个点染黑,f[u][j] = u 子树染 j 个黑点。转移时每条边的贡献 = 边权 × (两侧黑点对数),边贡献型树上背包。在洛谷打开
P1064[NOIP2006] 金明的预算方案主件带 ≤2 附件的依赖背包:把「主件 + 其附件的子集」枚举成一组物品做分组背包。是树上背包退化到「深度 1」的特例。在洛谷打开

已进入 树上背包 · 树形 DP · DP大师