树形 DP

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

【树上 dp】

最大子树和

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

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

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

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

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

对于这个问题,我们的处理是:\(ans\leftarrow \displaystyle \max_{i=1\sim n}(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。

下考虑一个结点 \(u\)\(dp\) 咋求。

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

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

选课

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

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

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

定义:

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

初值:

\(dp[i][0]=0,dp[i][x(x\neq 0)]=-\infty\)

递推:

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

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

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

注意,这里我们递推完 \(dp[u]\) 了,但是现在的 \(dp[u]\) 是 不包含 \(u\) 的。
所以,我们最后还需要枚举 \(j\leftarrow m\sim 1\),使所有 \(dp[u][j]=dp[u][j-1]+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\) 的子树,最大流量是多少。

显然对于每一个点 \(x\)\(dp[x]=\displaystyle \sum_{v\in son[u]}\min(dp[v],c(u,v))\)\(c(u,v)\) 表示 \(u\) 通向 \(v\) 的边的容量。

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

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

\(v\)\(v\) 的子树的流量 \(=dp[v]\)\(u\)\(u\) 的父节点子树的流量 \(=a[u]-dp[u]\)\(u\)\(u\) 的“所有除了 \(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\))一个规模大于 \(\frac{n}{2}\) 的子树。

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

\(dp[i]\) 表示 \(i\) 向下(\(i\) 下方所有边)切出 \(\leq \frac{n}{2}\) 的最大子树的大小。\(a[i]\) 表示 \(i\) 向上(\(i\) 的父节点子树加上通往父节点的边)切出 \(\leq \frac{n}{2}\) 的最大子树的大小。
考虑如何用 \(a[i]\)\(a[j],j\in son[i]\)

\(dp[i]\) 时,枚举子结点 \(j\)。若 \(sz[j]\leq \frac{n}{2}\)\(dp[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\) 的边,答案是 \(n-sz[j]\);否则答案是 \(sz[v],v\)\(i\) 的其他子结点。

“切 \(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[i-1][j][0],dp[i-1][j][1])\)
\(dp[i][j][1]=\max(dp[i-1][j-1][1]+U_i,dp[i-1][j-1][0])\)

接下来我们考虑环形,因为环形交界处在 \(n\)\(1\),不妨讨论第 \(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\),其他 \(-\infty\)(不可以在第一段统计贡献);如果第 \(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 @ 2024-02-15 11:16  FLY_lai  阅读(8)  评论(0编辑  收藏  举报