树形 DP 的通用套路就是定根 → 设状态(含语义与边界)→ 按子树合并转移,再用 换根(rerooting)/树形背包/计数与期望等模板覆盖高频题型,必要时配合小到大合并、长链剖分或虚树解决复杂度问题。
一、通用三步法?
- 定根:任选 1 为根(或题意指定/重心/直径端点),把无根树变成有根树。
- 设状态:让
dp[u][*]的每一维都有清晰语义(如选/不选、已用容量、涂色数、路径是否经过 u 等),明确可空子树的返回值(常用 0、1、−INF)。 - 合并转移:对子节点逐个合并(“多子树分组背包”),注意顺序与幂等:
- 点/边独立:
min/max + Σ child型。 - 路径/距离:
size、sum两件套; - 树形背包:二维/一维滚动合并,复杂度常见
O(n k^2),用“大合并小/分治/单调性”优化。
- 点/边独立:
二、典型模型与“拿来即用”的状态设计
1. 点独立集 / 覆盖 / 匹配(最常考)
- 最大独立集:
f[u][0/1]表示 u 不选/选 的最优。 转移:f[u][1] = val[u] + Σ f[v][0]``f[u][0] = Σ max(f[v][0], f[v][1]) - 最小点覆盖与最大匹配互补;边版本同理改成边状态或儿子之间两两配对。
2. 距离与路径贡献
- 子树信息:
sz[u]子树大小,g[u]为 u 到其子树所有点距离和:g[u] = Σ (g[v] + sz[v])。 - 换根(全局距离和):
ans[1]=g[1],向下:ans[v] = ans[u] + n - 2*sz[v]。 同构思可做经过 u 的路径数/长度和、点/边权累加等。
3. 树形背包
f[u][j]表示在 u 的子树拿 j 个(或容量 j)能取得的最优。- 合并孩子时做分组背包:
f[u]= merge(f[u], f[v]),双循环j与t。 - 常见优化:
- 大合并小把均摊复杂度降到
O(n k); - 若转移是 min-convolution,可用分治/单调队列/Knuth等(依题目结构)。
- 大合并小把均摊复杂度降到
4. 染色与计数(方案数 / 期望)
- 树上 k 染色(相邻不同色):
f[u] = (k-1) * Π f[v]或用f[u][same/diff]细分。 - 计数题注意:模数、空子树=1,与乘法在合并时机。
5. Rerooting(换根 DP)
把“以 u 为根的答案”设为聚合函数:
- 自底向上得到每条“向父贡献”的值;
- 用前后缀预处理(或 AtCoder 式模板)把去掉某子树的外部贡献 O(1) 拿到,再把根换到该子树。 适用:距离和、最大值带方向、子树答案转全局答案、边双向属性等。
6. 进阶
- DSU on tree(小到大):离线统计“关于 u 子树的查询”(颜色数、频次、莫队思想),不是 DP,但常与“每个 u 的答案”搭配。
- 虚树 DP:关键点集上的 DP,把原树缩成含关键点与其 LCA 的稀疏树,复杂度
O(k log n)建 +O(k)DP。 - 长链剖分:把线性转移挂到主链上,优化合并。
- 答案二分 + 可行性 DP:如“最多选 k 条边使代价 ≤ X”。
三、合并技巧与复杂度
- 两层循环谁在外? 通常“容量/数量在外,孩子在内”。
- 大合并小:总移动次数 ≤ 每元素被搬运
O(log n)次,降维打击树背包的常数。 - 前后缀:把“去掉某个孩子的合并结果” O(1) 取到,为换根模板与“除去某子树的贡献”提供捷径。
- 空集定义:计数设 1,max 设
-INF,min 设+INF,避免把“没选到东西”的状态误参与比较。
四、易错
- 点权/边权别混:统一在转移的入口处“落点”。
- 模数:乘法合并先取模;0 方案注意。
- 重根时别二次计入:
ans[v] = ans[u] + … - 子树贡献 + 外部贡献,公式要写干净。 - 空子树/叶子:提前给初值;
max型要允许“一个孩子也不选”。 - 递归深度:极端链用迭代或
std::vector<int>().shrink_to_fit()无关,这里更建议栈大小或尾递归的替代。
五、模板
1) 最大独立集(点权)
// 题意&思路:树上选点,任意相邻不能同选;dp[u][0/1] 表示不选/选u的最优。
// 转移:选u则孩子必不选;不选u则孩子取max。
// 复杂度O(n)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=200005;int n,hd[N],to[N<<1],nx[N<<1],ec;
ll w[N],f[N][2];void ad(int u,int v){ to[++ec]=v; nx[ec]=hd[u]; hd[u]=ec; }
void df(int u,int p){f[u][1]=w[u];for(int e=hd[u];e;e=nx[e]){int v=to[e]; if(v==p) continue;df(v,u);f[u][1]+=f[v][0];f[u][0]+=max(f[v][0],f[v][1]);}
}
int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n;for(int i=1;i<=n;i++) cin>>w[i];for(int i=1,u,v;i<n;i++){ cin>>u>>v; ad(u,v); ad(v,u); }df(1,0);cout<<max(f[1][0],f[1][1])<<"n";
}
2) 距离和(换根 DP)
// 题意&思路:求每个点到所有点的距离和。先算子树距离和g与子树大小sz;
// 再换根:ans[v]=ans[u]+n-2*sz[v]。总O(n)。
#include<bits/stdc++.h>
using namespace std;
using ll=long long;
const int N=200005;int n,hd[N],to[N<<1],nx[N<<1],ec,sz[N];
ll g[N],ans[N];void ad(int u,int v){ to[++ec]=v; nx[ec]=hd[u]; hd[u]=ec; }
void d1(int u,int p){sz[u]=1; g[u]=0;for(int e=hd[u];e;e=nx[e]){int v=to[e]; if(v==p) continue;d1(v,u);sz[u]+=sz[v];g[u]+=g[v]+sz[v];}
}
void d2(int u,int p){for(int e=hd[u];e;e=nx[e]){int v=to[e]; if(v==p) continue;ans[v]=ans[u]+(ll)n-2*sz[v];d2(v,u);}
}
int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n;for(int i=1,u,v;i<n;i++){ cin>>u>>v; ad(u,v); ad(v,u); }d1(1,0); ans[1]=g[1]; d2(1,0);for(int i=1;i<=n;i++) cout<<ans[i]<<(i==n?'n':' ');
}
六、思考什么?
-
我需要子树信息还是全局每点答案?若后者,十有八九要 rerooting。
-
状态是否可合并且无顺序依赖?若有顺序依赖,考虑前后缀或链式优化。
-
是否存在“选/不选”二值语义?能否把复杂约束转化为“选了谁就限制谁”的树独立/覆盖模型?
-
复杂度落在
O(n)/O(n log n)/O(n k)可承受区间了吗?不行就:大合并小 / 降维 / 分治 / 单调。 -
边界:空子树值、模数、叶子初始化、取 max/min 的“无效值”。
模型/目的 状态设计(示例) 合并与转移 常见复杂度 易错点 最大独立集 f[u][0/1]选 u:儿子取 0;不选:取maxO(n)重复计数、负权初值 最小覆盖/匹配 f[u][0/1/2]或边匹配相邻限制 + 枚举儿子态 O(n)覆盖定义与可行性 距离/路径和 sz[u], g[u]g[u]=Σ(g[v]+sz[v]);换根ans[v]=ans[u]+n-2sz[v]O(n)sz与方向号树形背包 f[u][j]儿子分组背包合并 O(n k^2)→ 优化到O(n k)空集初值、转移顺序 染色/计数 f[u]或f[u][same/diff]乘法合并 O(n)模数与 0/1 设定 Rerooting 子树 DP + 前后缀 去掉某子树的外部贡献再加回 O(n)不要重复计入