[复习资料]树形dp-杂题选讲

树形dp,一般指的是在树上的dp,一般情况下,树形dp完全没有重叠子问题,只是单纯地记录一个值罢了,但是我们还是习惯性地称它们为树形dp。

树形dp的状态设置都是很有套路的,在一般情况下我们都把状态设为 \(dp_{u,sta}\) ,表示考虑以 \(u\) 的根为子树,其余状态为 \(sta\) 的答案(或其他),然后转移时一般会忽略掉父节点对其的影响,只考虑自己子树内的点对自己的影响(举个例子:在没有上司的舞会中, \(sta\) 就是设置为该节点是否被选中, \(dp\) 值本身的含义就是选中点最多为多少)。

树形dp在转移时通常只会一棵一棵子树地考虑,每次考虑用儿子的dp值来更新自己的。


先从最基础的模型讲起吧:

树形背包

树形背包和普通背包一样,时间复杂度为 \(\mathcal O(NV)\) ,这里不给出详细证明。

例题:[JSOI2018]潜入行动

给定一棵有 \(n\) 个节点的无向树,问标记恰好 \(k\) 个节点后满足每个节点相邻节点中至少一个被标记(不含自己)的方案数。

\(1\le n\le 10^5,1\le k\le \min(n,100)\)

相邻点被标记以下简称被覆盖。

状态不难设出来,设 \(dp_{i,j,0/1,0/1}\) 表示以 \(i\) 为根的子树中标记了 \(j\) 个点,其中 \(i\) 号节点是否被标记,是否被覆盖的方案数,考虑将子树 \(v\) 并入 \(u\)

\(u\) 没被标记并且没被覆盖: \(v\) 必须被覆盖,同时 \(v\) 不能被标记:

\[dp_{u,x+y,0,0}\gets dp_{u,x,0,0}\times dp_{v,y,0,1} \]

\(u\) 没被标记但被覆盖了:如果之前 \(u\) 就是没被标记并且被覆盖了,那么这次 \(v\) 可以标记或不标记,但是必须被覆盖;如果之前 \(u\) 没有被标记并且没有被覆盖,那么这次 \(v\) 必须被标记,并且必须被覆盖。

\[dp_{u,x+y,0,1}\gets dp_{u,x,0,1}\times dp_{v,y,0/1,1}+dp_{u,x,0,0}\times dp_{v,y,1,1} \]

\(u\) 被标记了但是没有被覆盖: \(v\) 必须被标记,可以选择被覆盖或不被覆盖。

\[dp_{u,x+y,1,0}\gets dp_{u,x,1,0}\times dp_{v,y,0,0/1} \]

\(u\) 被标记了并且被覆盖了: 如果之前 \(u\) 已被标记且被覆盖,那么这次 \(v\) 可以选择标记或不标记,可以选择覆盖或不被覆盖;如果 \(u\) 之前已被标记且没被覆盖,那么 \(v\) 必须被标记,可以选择覆盖或不被覆盖。

\[dp_{u,x+y,1,1}\gets dp_{u,x,1,1}\times dp_{v,y,0/1,0/1}+dp_{u,x,1,0}\times dp_{v,y,1,0/1} \]

时间复杂度为 \(\mathcal O(nk)\)

总结

如果题目转化后变成类似背包合并这样,那么就可能要用树形背包,一般遇到这种树形背包题,在草稿纸上大力分类讨论一波一般都不会错,所以这种题的套路就是:理清思路,在草稿纸上写下dp转移


换根dp

换根dp属于树形dp的一种特殊的dp,一般情况下,如果题目经过转化后相当于是要求以每个点为根时的答案,那么换根dp就派上用场了。

换根dp之所以快,就是因为它利用了之前求出的答案,如果 \(u,v\) 相邻,那么当根节点从 \(u\) 变成 \(v\) 时,由于树形dp在dp时只考虑了子树,所以只会有两个节点的dp值改变,而其他值都是不变的,同时也正因如此,换根dp的关键就是考虑在根节点改变时考虑对这两个节点dp值的影响,同样的,换根dp的复杂度也取决于一次换根的复杂度。

例题:[APIO2014]连珠线

不会简化题意,直接把原题描述写下来了。

在达芬奇时代,有一个流行的儿童游戏称为连珠线。当然,这个游戏是关于珠子和线的。线是红色或蓝色的,珠子被编号为 \(1\)\(n\)。这个游戏从一个珠子开始,每次会用如下方式添加一个新的珠子:

\(\rm Append(w, v)\) :一个新的珠子 \(w\) 和一个已经添加的珠子 \(v\) 用红线连接起来。

\(\rm Insert(w, u, v)\) :一个新的珠子 \(w\) 插入到用红线连起来的两个珠子 \(u,v\) 之间。具体过程是删去 \(u,v\) 之间红线,分别用蓝线连接 \(u,w\)\(w,v\)

每条线都有一个长度。游戏结束后,你的最终得分为蓝线长度之和。

给你连珠线游戏结束后的游戏局面,只告诉了你珠子和链的连接方式以及每条线的长度,没有告诉你每条线分别是什么颜色。

你需要写一个程序来找出最大可能得分。即,在所有以给出的最终局面结束的连珠线游戏中找出那个得分最大的,然后输出最大可能得分。

\(1\le n\le 200000\)

首先转化题意,两个操作可以转化为:

  1. \(\rm Append(w, v)\) :一个新的珠子 \(w\) 和一个已经添加的珠子 \(v\) 用红线连接起来。
  2. \(\rm Insert(w, u, v)\) :一个新的珠子 \(v\) 和一个已经添加的珠子 \(v\) 用蓝线连接起来,同时一个新的珠子 \(w\)\(v\) 用蓝线连接起来。

如果确定了刚开始拥有的点将其作为根,那么题意再次转化:

给定一棵有根树,每次可以选择依次相连的三个深度依次递增的点,并删去连接着这三个点的两条边,执行若干次后,最终被删除的边边权和最大是多少。

考虑dp,设 \(dp_{u,0}\) 表示考虑完以 \(u\) 为根的子树,其中 \(u\) 到它父亲的边不必被删去能获得的最大权值; \(dp_{u,1}\) 表示考虑完以 \(u\) 为根的子树,其中 \(u\) 到它父亲的边必须被删去能获得的最大权值,设 \(w_u\) 表示 \(u\) 和他父亲连边的边权,转移就比较显然了:

\[dp_{u,0}=\sum_{v\ is\ u's\ son}\max(dp_{v,0},dp_{v,1}+w_v) \]

\[dp_{u,1}=dp_{u,0}-\min_{v\ is\ u's\ son}(\max(dp_{v,0},dp_{v,1}+w_v)-(dp_{v,0}+w_v)) \]

方便起见,再设一个 \(dp_{u,2}=\max(dp_{u,0},dp_{u,1}+w_{u})\) ,那么转移:

\[dp_{u,0}=\sum_{v\ is\ u's\ son}dp_{v,2} \]

\[dp_{u,1}=dp_{u,0}-\min_{v\ is\ u's\ son}(dp_{v,2}-(dp_{v,0}+w_v)) \]

\[dp_{u,2}=\max(dp_{u,0},dp_{u,1}+w_u) \]

最后答案就是 \(dp_{root,0}\)

这是在根确定的时候我们得到的转移方程,接下来的问题就是换根时如何改变dp值了。

假设当前根是 \(u\) ,然后根要从 \(u\) 换为 \(v\) (其中 \(v\)\(u\) 的儿子)首先改变 \(w\) ,然后更新dp值: \(dp_{u,0}\) 直接减去 \(dp_{v,2}\) 即可,对于处理 \(dp_{u,1}\) 时需要的取 \(\min\) 操作,我们可以记录一个最小值和一个次大值,如果 \(dp_{v,2}-(dp_{v,0}+w_v)\) 是最小值,那么更新 \(dp_{u,1}\) 是就用次小值更新,否则就用最小值,然后更新 \(dp_{u,2}\) ,接着用 \(dp_{u,2}-(dp_{u,0}+w_u)\) 来更新 \(v\) 的最小次小值,最后更新 \(v\) 的dp值即可实现换根。

还有要注意的是,换完根后要换回来。

求出以每个点作为根时的dp值,那么最终答案就是每个点作为根时答案的最大值。

总结

如果题目转化后是要求对于每个点作为根时的答案,就可能是要用换根dp,换根dp首先会求出把某个点作为根时的dp值,然后再考虑换根对dp值的影响


长链剖分

长链剖分优化dp也是一种套路,一般情况下,可以用长链剖分优化的树形dp题第二维状态都是和深度有关的,使用长链剖分可以把 \(\mathcal O(n^2)\) 的复杂度降为 \(\mathcal O(n)\)

长链剖分优化实现方法:先找到所有点的重儿子,利用指针 \(\mathcal O(1)\) 继承重儿子信息,其余轻儿子全部扫一遍暴力转移。

为什么长链剖分优化和深度有关的dp可以做到 \(\mathcal O(n)\) ?可以这样考虑:每个点只有在其所在链链顶合并时会对时间复杂度造成 \(\mathcal O(1)\) 的贡献,所以总复杂度就是 \(\mathcal O(n)\)

例题:[POI2014]HOT-Hotels 加强版

给出一棵有 \(n\) 个点的树,求有多少组点 \((i,j,k)\) 满足 \(i,j,k\) 两两之间的距离都相等。

\((i,j,k)\)\((i,k,j)\) 算作同一组。

\(1\le n\le 10^5\)

\(f_{i,j}\) 表示在以 \(i\) 为根的子树内,离 \(i\) 距离为 \(j\) 的节点个数;设 \(g_{i,j}\) 表示在以 \(i\) 为根的子树内,满足 \(x,y\) 在以 \(i\) 为根的子树内,且若有一个节点 \(z\) 在以 \(i\) 为根的子树外且到 \(i\) 的距离为 \(j\) 时, \(x,y,z\) 两两距离相等的点对 \((x,y)\) 的数目。

考虑一棵子树一棵子树地合并,将 \(v\) 并入 \(u\)

\[ans\gets ans+f_{u,x}\times g_{v,x+1}+f_{v,x}\times g_{u,x+1} \]

\[g_{u,x+1}\gets g_{u,x+1}+g_{v,x+2}+f_{u,x+1}\times f_{v,x} \]

\[f_{u,x+1}\gets f_{u,x+1}+f_{v,x} \]

不难发现 \(f\)\(g\) 的第二维都是和树的深度有关的,用长链剖分优化,唯一要注意的就是空间要开大还有 \(f\)\(g\) 直接继承时是反着的。

总结

当最终所设状态第二维和深度有关时,可能可以用到长链剖分优化。


重链剖分

既然长链剖分可以优化dp,重链剖分可不可以呢?重链剖分可以用来解决另一类树上问题,就是 \(\rm dsu\ on\ tree\)

\(\rm dsu\ on\ tree\) 可以用来解决这一类问题:只有对子树的询问,没有修改,其时间复杂度可以做到 \(\mathcal O(n\log_2 n)\)

\(\rm dsu\ on\ tree\) 实现方法:需要两个搜索函数,不妨设为 \(\rm dfs0\)\(\rm dfs1\) ,先找到所有节点的重儿子,然后从根节点开始 \(\rm dfs0\) ,先扫所有轻儿子执行 \(\rm dfs0\) ,回溯时消去所有轻儿子对答案造成的影响,然后再扫重儿子执行 \(\rm dfs0\),回溯时不消去重儿子对答案造成的影响,然后再 \(\rm dfs1\) 一遍遍历到所有除重儿子子树外的点,同时更新答案。

很暴力对不对?但是复杂度是真的。证明方法如下:考虑每个点会被遍历几次,显然 \(\rm dfs0\) 会恰好遍历所有点一次,而如果 \(\rm dfs1\) 遍历到了点 \(u\) ,说明这次 \(\rm dfs1\) 是从 \(u\) 到根的路径上的一条轻边开始的,而重链剖分有个特性:所有点到根的路径经过的轻边数量是 \(\log_2 n\) 级别的,所以复杂度是 \(\rm O(n\log_2n)\)

例题:Lomsat gelral

一棵以 \(1\) 为根的 \(n\) 个节点的树,每个节点有一个权值,求每棵子树众数的和。

\(1\le n\le 10^5\)

\(\rm dsu\ on\ tree\) 的模板,算答案需要维护两个值:众数出现次数,众数和。然后直接套模板即可。

总结

如果题目中只有对子树地查询且没有修改,可能要用到 \(\rm dsu\ on\ tree\) ,唯一需要考虑的就是加入或删去一个节点对答案的影响。

其他dp-杂题选讲

例题:一道考试题

考虑一种奇怪的有根树,这种有根树是在普通有根树的基础上,添加了儿子之间的顺序,也就是说我们可以把一个点的所有儿子们看成一个序列,两棵有根树相同,当且仅当它们点数相同,且每个点的儿子序列相同。

现在给定 \(n\) 和数组 \(a_{1\dots n}\) ,你需要计算有几种不同的奇怪有根树,满足对于 \(1\le i\le n\) ,有 \(dep_i\le a_i\) ,其中 \(dep_i\) 表示点 \(i\) 到根节点的距离,也就是有几条边。

\(1\le n\le 100\)

这题dp状态设置和其他题不太一样,也可以算是一种套路,考虑到如果节点 \(u\) 的深度可以为 \(x\) ,那么其深度肯定也可以为 \(x-1\dots 0\) ,于是便考虑从深度大的地方开始放节点。设 \(dp_{i,j,k}\) 表示考虑完深度 \(i\dots n\) ,其中第 \(i\) 层有 \(j\) 个节点,一共用了 \(k\) 个节点的方案数,转移就考虑枚举当前层放几个节点(假设满足 \(a_y\ge x\)\(y\)\(sum_x\) 个):

\[dp_{i,j,k}=\sum_{l=0}^{j-k}dp_{i+1,j-k}k!C_{l+k-1}^{k-1}C_{sum_i-(j-k)}^k \]

例题:[NOIP2018模拟赛] 小P的技能

问深度大于 \(k\)\(n\) 个节点的二叉树有多少个。

\(1\le n\le 500\)

像之前那样按深度dp?显然复杂度是不好的,考虑另一种dp状态设计:设 \(dp_{i,j}\) 表示 \(i\) 个节点深度为 \(j\) 构成二叉树的方案数,转移考虑枚举左子树和右子树的大小和深度:

\[dp_{x+y,\max(a,b)+1}\gets dp_{x+y,\max(a,b)+1}+dp_{x,a}\times dp_{y,b} \]

时间复杂度 \(\mathcal O(n^4)\) ,如何优化?转移也可以写成这样:

\[dp_{i,j}=\sum_{a=0}^{i-1}(\sum_{b=0}^{j-1}dp_{a,j-1}\times dp_{i-a-1,b}+\sum_{b=0}^{j-2}dp_{a,b}\times dp_{i-a-1,j-1}) \]

\[\to=\sum_{a=0}^{i-1}(dp_{a,j-1}\times\sum_{b=0}^{j-1}dp_{i-a-1,b}+dp_{i-a-1,j-1}\times\sum_{b=0}^{j-2}dp_{a,b}) \]

比较显然的前缀和优化,设 \(sum_{i,j}=\sum_{l=0}^jdp_{i,l}\)

\[\to=\sum_{a=0}^{i-1}(dp_{a,j-1}sum_{i-a-1,j-1}+dp_{i-a-1,j-1}sum_{a,j-2}) \]

时间复杂度: \(\mathcal O(n^3)\)

例题:[APIO2016]烟火表演

给定一棵以 \(1\) 为根 \(n\) 个节点的树,每条边有边权,将边权为 \(w\) 的边边权改为 \(w^{'}\) 的花费是 \(\mid w-w^{'}\mid\) ,请用最小的花费使得根节点到所有叶子节点的距离相等。

\(1\le n\le 3\times 10^5\)

\(dp_{i,j}\) 表示以 \(i\) 号节点为根的子树到 \(i\) 的父亲的距离全部修改为 \(j\) 所需要的最小花费。

转移:

\[dp_{u,s}=\min_{k=0}^s(\mid val_u-k\mid+\sum_{v\ is\ u'son}dp_{v,s-k}) \]

\(g_{u,k}=\sum_{v\ is\ u'son}dp_{v,k}\) ,那么:

\[dp_{u,s}=\min_{k=0}^s(\mid val_u-k\mid +g_{u,s-k}) \]

显然直接扫复杂度太劣了,如何优化?把 \(dp_{u,s}\) 看做是一个自变量为 \(s\) 的函数,观察这个函数的性质。

首先,叶子结点的函数图像一定是斜率为 \(-1\) 的一条折线加上斜率为 \(1\) 的一条折线。

然后从叶子节点手推一下,可以得到以下几个性质:

  1. \(dp_u\)\(g_u\) 为下凸函数。
  2. \(g_u\) 可以直接推得 \(dp_u\)
  3. \(dp_u\)\(g_u\) 函数下降段斜率最大为 \(-1\) ,上升段斜率最小为 \(1\)

假设函数 \(g_u\)\([L,R]\) 段取值为最小值,由 \(g_u\) 推得 \(dp_u\) 的方法如下:

  1. 将函数在 \([1,L]\) 的折线往上平移 \(val_u\) 个单位。
  2. 将函数在 \([L,R]\) 的折线往右平移 \(val_u\) 个单位。
  3. 将函数在 \([R,+\infty]\) 的折线往右平移 \(val_u\) 个单位并把斜率改为 \(-1\)

这时空出了一段斜率为 \(-1\) 的折线,直接连上即可。

还有个性质: \(g_u\) 最右段的斜率是 \(u\) 的儿子数。

但是还有个问题:如何表示这个函数?

我感觉这题最妙的地方就是这里:用转折点的横坐标表示!

我们规定从左往右每经过一个转折点斜率就加 \(1\) ,那么:

  1. 函数相加就是转折点的并(可以在纸上推一下)。
  2. \(g_u\) 转到 \(dp_u\) 相当于是先把横坐标最大的儿子数个的转折点弹出,然后把平行段的 \([L,R]\) 取出,插入 \([L+val_u,R+val_u]\) ,即再弹出两个横坐标最大的两个转折点,把它们加上 \(val_u\) 后再插回去。

那么要实现:

  1. 合并两个点集。
  2. 弹出权值最大的点。
  3. 插入点。

于是,便自然而然地想到可并堆,利用可并堆维护函数图像,最终还原即可得到答案。

posted @ 2021-01-04 21:22  xiaolilsq  阅读(179)  评论(0编辑  收藏  举报