树形 DP

简单的树上 dp 其实已经在普及组涉及过:自上而下和自下而上传递的性质。
现在我们需要研究更复杂的树上 dp,比如换根 dp 等等。

【树上 dp】

最大子树和

给出一棵带点权的树,求这棵树中的最大权连通块。

因为是无根树,我们人为规定 1 号结点为根。

dp[i] 表示以 i 为根的子树内,必须包含 i 的最大权连通块。

我们发现,“最大权连通块的点权和” 是一个可以自下而上传递的属性。对于每一个结点 udp[u] 初值 a[u]
我们先递归处理 u 的所有子结点。然后对于每一个子结点 v,若 dp[v]>0,则 dp[u]+=dp[v]

现在我们求出了 dp 数组,dp[1] 就是必须包含 1 的最大权连通块。但是现在还有一个问题,dp[1] 只是包含 1 的最大权连通块,真正的最大权连通块可能并不包含 1。

对于这个问题,我们的处理是:ansmaxi=1n(dp[i])

为什么正确呢?因为我们考虑任何一个连通块,无论这棵树以谁为根,它一定有一个深度最浅的点 x,并且只有一个(如果有两个,一个结点有两个父亲)。那么这个连通块就被 dp[x] 代表了。

战略游戏

给定一颗无根树。如果我们在 i 上放一个士兵,那么所有与 i 相连的边都会被瞭望。要使所有边都被瞭望,求最小士兵数。

我们令 1 号结点为根。

dp[i][0] 表示以 i 为根的子树中瞭望所有边,i 不放士兵,所需的最小士兵数。

dp[i][1] 定义类似,但是 i 放士兵。

显然所有 dp[i][0] 初值 0,dp[i][1] 初值 1。

下考虑一个结点 udp 咋求。

  1. dp[u][0]。因为 u 不放,所以与 u 相连的边都需要靠 u 的子结点瞭望。所以 dp[u][0]=fa[v]=udp[v][0]

  2. dp[u][1]u 放了,那么子结点就可以考虑放与不放。显然每个子结点的子树之间没有关系。所以 dp[u][1]=fa[v]=umin(dp[v][0],dp[v][1])

选课

这题有点像树上背包,但并不是树上背包。

这题相当于给出了一片 n 个点的森林。要求选一个点必须选其父节点。现在问你最多选 m 个点,最大点权和是多少。

我们可以考虑建一个 0 号点,作为每棵树的根的父节点,同时也是根节点,权值设为 0。

定义:

dp[i][j] 表示在以 i 为根的子树中,选 j 个点,并且必须包含 i 这个点,得到的最大点权和。

初值:

dp[i][0]=0,dp[i][x(x0)]=

递推:

对于一个结点 u,我们先计算它的子结点的 dp。然后我们枚举子结点 vu 的子树里面取多少个 jv 的子树里面取多少个 k

dp[u][j]=max(dp[u][j],dp[u][jk]+dp[v][k])。(注意:这里 j 需要从大到小枚举,不然就会出现用当前层更新当前层的情况

可是这样写感觉很奇怪,因为 dp[u] 里面应该包含了 dp[v]。但是我们说这没问题,因为 dp[u] 只包含枚举过的子结点的子树,并不包含现在正在枚举的子树。而每当我们枚举完一个结点 v 之后,就相当于把 “可以在 v 的子树中选点”的情况加入了 dp[u] 了。

注意,这里我们递推完 dp[u] 了,但是现在的 dp[u] 是 不包含 u 的。
所以,我们最后还需要枚举 jm1,使所有 dp[u][j]=dp[u][j1]+s[u].

答案是 dp[0][m+1],这意味着我们上面枚举的时候要枚举到 m+1

【换根 dp】

对于一颗无根树,我们可以考虑固定一个结点为根来求出以这个点为根的答案。但是我们可能需要知道每一个点为根的答案,如果每一个点做根都重新求一遍答案,这就太慢了。
我们可以考虑 “换根 dp”。

具体而言,当我们当前做根的结点是 u,现在尝试以 u 的子结点 v 作为根。
这个时候我们发现一件事:对于所有 v 的子结点,我们在 u 做根的时候求出的答案依然可以用,唯一变化的是父节点子树。
而父节点子树变化的地方在于:增加了 u 的子树,但排除 v 的子树。
如果我们能快速计算这一部分产生的贡献,并将其传递给子结点,我们就可以让子结点快速计算变化的量。

Accumulation Degree

给出一颗无根树,每条边有一个容量,所有叶结点(度为 1)都是汇点。求以哪个点为源点(根)时,从每个汇点流出的水量之和最大。当然汇点不能同时做源点。

先假设 1 是根节点(源点)。

我们令 dp[i] 表示以 i 为源点流向 i 的子树,最大流量是多少。

显然对于每一个点 xdp[x]=vson[u]min(dp[v],c(u,v))c(u,v) 表示 u 通向 v 的边的容量。

接下来我们考虑 a[i]:表示以 i 为根的最大出水量。假设 a[u] 已经算完,现在求 a[v],vson[u]

a[v]= vv 的子树的流量 + vu 的子树(v 的父节点子树)的流量。
vv 的父节点子树的流量 =min{c(v,u),uu 的父节点子树的流量 + uu 的“所有除了 v 所在子树的子树” 的流量 }

vv 的子树的流量 =dp[v]uu 的父节点子树的流量 =a[u]dp[u]uu 的“所有除了 v 所在子树的子树” 的流量 =dp[u]min(c(u,v),dp[v])

所以a[v]=dp[v]+min{c(v,u),(a[u]dp[u])+(dp[u]min(c(u,v),dp[v]))}

最后还有一个问题,就是关于叶结点当根的问题。我们可以让 dp[]=0,但是求 dp[叶的父亲] 的时候不要和 dp[]min,直接返回 c(叶的父亲,)


一般换根 dp 都会有两次循环,一次计算以一个点为根的答案,另一次用换根计算每一个点为根的答案。

code

Centroids

给出一棵树,可以删去一条边再补上一条边,请问对于每一个点,是否能通过操作使其变成中心。

显然对于一个点,如果它不是重心,意味着它恰有(不恰有,加起来大于 n)一个规模大于 n2 的子树。

显然,我们要从最大的子树中,切下一个规模 n2 但尽可能大的子树,然后接到根节点上。
我们关心一颗子树中 一个规模 n2 但尽可能大的子树 规模最大是多少,因为这样我们就可以相减,求出切掉之后是否还大于 n2

dp[i] 表示 i 向下(i 下方所有边)切出 n2 的最大子树的大小。a[i] 表示 i 向上(i 的父节点子树加上通往父节点的边)切出 n2 的最大子树的大小。
考虑如何用 a[i]a[j],json[i]

dp[i] 时,枚举子结点 j。若 sz[j]n2dp[i]=max(dp[i],sz[j]);否则,dp[i]=max(dp[i],dp[j])

a[j]a[i] 多出来的切法是 直接与 i 相连的边和 i 所有不是 j 的子树中包含的边。

a[j]=max{a[i],切与 i 相连的边的答案,i 的其他子树的答案}

"切与 i 相连的边的答案" 分两种,如果是 j 连向 i 的边,答案是 nsz[j];否则答案是 sz[v],vi 的其他子结点。

“切 i 的其他子树的答案”,相当于把 i 的除了 j 的子树的 dp 都取 max,这可以用前后缀最大值来做。

还有一个要注意的点:我们要去掉子结点通向父节点的边。

code

【环形 dp】

Naptime G(这个是环形 dp,为后面基环树 dp 做准备)

注:如果只睡一个时间,不增加效用值。如果睡了连续多个时间,从第二个时间开始计算效用值。

我们从简单情况开始,考虑我们在线性怎么做这个问题:

dp[i][j][k] 表示前 i 段时间睡 j 段,第 i 段时间睡不睡。(1睡 0不睡)

之所以要开第三维 k,是因为第 i 段睡不睡可能会影响到下一段如果睡的话统不统计效用值,有后效性。

dp[i][j][0]=max(dp[i1][j][0],dp[i1][j][1])
dp[i][j][1]=max(dp[i1][j1][1]+Ui,dp[i1][j1][0])

接下来我们考虑环形,因为环形交界处在 n1,不妨讨论第 n 段睡不睡。

  1. n 段不睡,正常线性,取 dp[n][B][0](第 n 段必须不睡);

  2. n 段睡,唯一的影响是如果第 1 段睡就会变成有效睡眠,取 dp[n][B][1](第 n 段必须睡)。

因为 n 是接在 1 后面的,在 dp 里我们以 0 开始递推,所以我们可以把 n 视作 0

这两种情况我们可以做两次 dp,唯一的区别是如果第 n 段不睡,初值的设定是 dp[0][0][0]=0,其他 (不可以在第一段统计贡献);如果第 n 段睡,初值的设定是 dp[0][0][0]=dp[0][0][1]=0(可以在第一段统计贡献,是 dp[0][0][1] 而非 dp[0][1][1] 是因为我们把 n 视作 0,但是并没有占用睡觉时间,真正的睡觉时间应该在 dp[n][B][1] 的时候才计算)。

code

【基环树 dp】

基环树:一棵树,加上一条边(n 条边的连通图)。可以看成一个环吊着好几棵树。

城市环路

给出一棵基环树,每个点有点权。相邻两点不能同时选。选出若干个点使得点权和最大。

首先,我们可以找出这个环。然后我们枚举环上每一个点,这个点下方吊着一棵树。而在树上这个问题是很简单的,类似 “战略游戏”。

对于环上每个点都做完这个操作之后,我们可以用环上每个点代表这个点吊着的树,可以这么做的原因是环上每个点是否选择,并不影响环上其他点吊着的树。

于是我们就可以做一次环形 dp,第 x 个数如果选,有 dp[x][1] 的贡献;如果不选,有 dp[x][0] 的贡献。

上面的题类似,我们可以讨论最后一个数选还是不选,做两次 dp。

还有一个问题:怎么 O(n) 找环。我们知道,当我们在深搜的时候如果试图进入一个已访问的结点,那么我们就找到了一个环。我们可以倒着退回去,把环存进链表里面。

code

这里还要注意一个点:可能出现一种只有两个点两条边的环,这种情况我们需要特判。虽然这题没有,但是如果遇到了,建议去掉通向父节点的边。

注:基环树还有另一种处理方法,那就是在环上找一条边删掉,用树的方法处理。

posted @   FLY_lai  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· 单线程的Redis速度为什么快?
· SQL Server 2025 AI相关能力初探
· AI编程工具终极对决:字节Trae VS Cursor,谁才是开发者新宠?
· 展开说说关于C#中ORM框架的用法!
点击右上角即可分享
微信分享提示