刚讲完状压,我们来讲讲树形dp
1.基本概念
树形 d p dp dp,顾名思义就是在树上做动态规划,通过若干次的遍历树,得到有用的信息,解决问题
树形 d p dp dp的枚举顺序类似线性 d p dp dp,也有两个方向:
①:叶→根:根的子节点传递有用的信息给根,由根得出最优解
②:根→叶:需要取出所有点作为根节点求值,最后根节点获得整棵树的信息
动态规划的顺序一般是后序遍历,即当子节点处理完成再处理父节点
树形 d p dp dp一般通过记忆化搜索实现
普通的树形 d p dp dp的时间复杂度基本都是 O ( n ) O(n) O(n),若有附加维则为 O ( n ∗ m ) O(n*m) O(n∗m)
2.经典问题
①树的直径
给你一颗 n n n个节点的有权有根树,根节点编号为1,找到一条最长的路径
最长链不一定经过根,所以不能单纯的从根贪心
要解决这个问题我们可以使用树形 d p dp dp
设 d [ x ] d[x] d[x]表示从节点 x x x出发走向以 x x x为根的子树,能够到达的最远节点的距离
d [ x ] = max i ∈ s o n [ x ] d [ i ] + l e n g t h [ x ] [ i ] d[x]=\max_{i∈son[x]}d[i]+length[x][i] d[x]=maxi∈son[x]d[i]+length[x][i]
答案就是 max x ∈ 1... n max i ∈ s o n [ x ] d [ x ] + d [ i ] + l e n g t h [ x ] [ i ] \max_{x∈1...n}\max_{i∈son[x]}d[x]+d[i]+length[x][i] maxx∈1...nmaxi∈son[x]d[x]+d[i]+length[x][i]
void dp(int x) {
v[x] = 1 ;
for (int i = head[x]; i; i = e[i].nxt) {
int y = e[i].to ;
if (v[y]) continue ;
dp(y) ;
ans = max(ans, d[x] + d[y] + e[i].w) ;
d[x] = max(d[x], d[y] + e[i].w) ;
}
}
这个问题也可以通过两遍 b f s bfs bfs解决,作者在此就不多说了
②树的重心
树的重心的定义是这样的:若把树变成该点为根的有根树时,最大子树的节点数最小
这个问题也比较简单
随便选一个点作为根
设 s z [ x ] sz[x] sz[x]表示以 x x x为根的子树大小
不难发现 s z [ x ] = ∑ y ∈ s o n [ x ] s z [ y ] + 1 sz[x]=\sum_{y∈son[x]}sz[y]+1 sz[x]=∑y∈son[x]sz[y]+1
删除 x x x节点后最大连通块有多少节点? 子树最大有 m a x { s z [ y ] } max\{sz[y]\} max{ sz[y]}个,还有上面的它的祖先构成的树大小为 n − s z [ x ] n-sz[x] n−sz[x], d p dp dp时顺便更新答案
void dfs(int rt, int fat) {
sz[rt] = 1 ;
rep(i, 0, siz(e[rt]) - 1) {
int to = e[rt][i] ;
if (to == fat) continue ;
dfs(to, rt) ;
sz[rt] += sz[to] ;
dp[rt]= max(dp[rt], dp[to]) ;
}
dp[rt] = max(dp[rt]<