树上 DP(树形 DP & 换根 DP)
以前咋这么多鸽子呃呃呃啊啊啊啊啊啊啊啊
树形 DP
一些理论理解:
1.关于状态:f[u] 表示 u 为根的子树内,xxx 值。这样可以把一颗子树的信息压到一个点内。
2.关于转移:
在计算 f[u] 时,u 通过孩子 v 转移过来更新就好,此时 v 对 u 更新,相当于是点对点的更新(都把子树压成了点)。
儿子 v 对 u 的贡献独立(可能绝大多数但不完全是),考虑一颗子树如何转移过来即可,其余同理。
另外,转移可以理解为每次都是加入一颗子树,和先前若干棵子树以及根的结果合并在一起,也相当于对于两颗子树合并。
常见的普通树形 DP
一些基本的树形 DP 例题(还有以前写的,但是修过一遍,完全是能看的):树形dp - cn是大帅哥886 - 博客园
和没有上司的舞会(也称最大独立集)一模一样。很水的题。
其实很水,但是想了很久还不会。
具体些,定 f[u][0/1/2] 表示 u 的染色情况,然后考虑子结点 v 的染色情况。如果 u,v 同色那么 v 的染色就是不必要的,可以减去 v 染色的代价 1。
1.转移方程定的比较模糊,在转移时多从状态 & 决策出发思考,考虑转移点可能是哪些状态,转移过来。
2.还需要注意,转移的时候并不代表前驱状态一定是固定选择,而是可以进行多种决策,而随决策变化而变化的。相当于有了“撤销”操作。
可以思考下,是否需要换根?
考虑让 u 为根节点,换根成 u 的子结点 v 为根结点,并考虑这样做的影响。
具体需要分类讨论 u,v 两结点的关系,以及可以用调整法等等的技巧。
如果 u,v 两结点换根了对答案没有影响,就可以归纳法到一般情况,证明换根就是无影响的。
1.在这题中,就讨论 u,v 的颜色关系,u,v 染色与没染色,共 4 种情况,自己手玩下是易证对答案无影响的。具体证明的时候需要借助调整法。
2.没有上司的舞会一题中,仅仅考虑的是父子关系,就算换了根,父子关系也不会变,所以换不换根对答案无影响。
先递归建树。对于递归多模拟几次,多想几次,就会明白如何实现的。
然后又是染色问题,对于一颗子树,我们只关心其根结点的染色情况,这样就能把树的状态压到根节点上,然后转移了。
所以记录 f[u][0/1/2] 表示 u 结点染成 0/1/2,u 子树内最多 0 的数量。
然后因为是二叉树,且限制比较多,大力分讨就好。
那如果对于多叉树呢?转移的时候相当于每次合并两颗子树,分别是子树 v 和先前若干子树合并的结果 u,那其实就是相当于二叉树的情况。
思考个问题:如果扩展到 k 叉树,m 种颜色如何做?
那像上面一样记录 m 种颜色,似乎很难分讨。
首先比较显然的,k>=m,一定无解。
这里的染色其实不关心具体如何染色。只关心 0 的数量,以及 0 的合法性。
然后又有 k<m,所以只要 0 合法,其余的没染成 0 的一定可以构造出一种合法的情况。
然后这题再改一下变成计数,也是能做的。
一个简单树形 DP 计数题。对于加法原理,乘法原理,先考虑两颗子树的情况。然后对于多颗子树,就是相当于两颗子树的情况,就是上一题的 trick。
树上背包
是树形 DP 的变式。
可以把若干棵树看成物品,于是有了 dp 定义,f[u][i][j]:u 子树内,前 i 棵子树,选了重量为 j 的最大价值。
但是树形 DP 是顺次遍历儿子, i 这一维可以滚动掉。状态就变成了:f[u][j]。
考虑转移,就是类似分组背包的做法,对于 u 的子树 v,把 v 当成一组物品,在不同重量下有不同价值,然后正常背包就做完了。
这里可以延伸,树形 DP 计算 f[u] 每次相当于加入一颗子树 v,和先前的若干个子树合并贡献到 u,在没更新 u 之前,先前的若干棵子树算出的结果即为 f[u]。
此处又顺应了上面说的,所以只需要考虑单个儿子 v 对 u 会有什么贡献即可。
故有写法:
void dfs(int u, int fa)
{
f[u][1]=w[u];
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i];
if (v==fa) continue;
dfs(v, u);
for (int k=m; k>=1; k--)
for (int p=0; p<k; p++)
f[u][k]=max(f[u][k], f[u][k-p]+f[v][p]);
}
}
注意需要倒序循环,原因就是滚动了一维后,需要确保 f[u][k-p] 是合并 v 之前的结果,于是倒序循环,类似分组背包的倒序循环。
显然是 O(nm^2) 的复杂度,加一点点上下界优化可以变为:
void dfs(int u, int fa)
{
f[u][1]=w[u];
size[u]=1;
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i];
if (v==fa) continue;
dfs(v, u);
size[u]+=size[v];
for (int k=min(m, size[u]); k>=1; k--)
for (int p=0; p<k && p<=size[v]; p++)
f[u][k]=max(f[u][k], f[u][k-p]+f[v][p]);
}
}
但复杂度仍是假的,为 O(n^3),考虑成一条链的情况,就可以知道复杂度假没假了。
假的原因就是每次 u 子树的大小会先加上自己儿子 v 的子树的大小,然后再转移。然后极端情况下也就是链的情况就爆炸了。
但是如果 u 子树大小不是先加上自己儿子 v 的子树大小,在极端情况下复杂度就不会假了。
也就是考虑用刷表法,相当于枚举 u 在合并 v 前的若干子树大小,和 v 子树大小。
void dfs(int u, int fa)
{
f[u][1]=w[u];
size[u]=1;
for (int i=0; i<a[u].size(); i++)
{
int v=a[u][i];
if (v==fa) continue;
dfs(v, u);
for (int k=min(m, size[u]); k>=1; k--)
for (int p=1; p<=min(m-k, size[v]); p++)
f[u][k+p]=max(f[u][k+p], f[u][k]+f[v][p]);
size[u]+=size[v];
}
}
此时复杂度没问题,可以证明其复杂度就是其数组大小,O(nm)。
复杂度证明:树上背包时间复杂度证明 - 洛谷专栏
和选课差不多,把边权转成点权就好了。然后问题等价于保留 m+1 个点,点权最大。
当然直接对边讨论也是可行的。
不过要注意树上边权转点权的时候,只能在 dfs 中处理,不能在输入边的时候处理。因为输入边的过程中无法确定父子的先后关系!!!
这里需要返回根结点。那不妨稍稍改变状态,定 $f_{u, i}$ 表示 u 子树内走了 $i$ 分钟,最终回到 $u$ 结点拿到的最多糖果数。
然后你发现每次要回到 $u$ 结点,相当于所有边都经过两次。因为每个点只有一个父亲,相当于走了一条路径后,想要返回 $u$ 结点只能原路返回。
所以可以直接所以边边权乘 2,然后就是树形背包板子了,不过是带了边权。
树形背包容易越界,需要注意下。
求经过 K 条边的路径,但是可以重复经过边,也就是可返回,可不返回 $u$ 结点。
和上题类似。定 f[u][i][0/1] 表示 u 子树内,走了 i 条边,最后不返回/返回 u 的最多苹果数。
这里可以直接考虑对边分讨,而不是把边转到点上,不然有来回的情况,放点上就太不形象了,转移比较麻烦。
然后转移就是分讨两颗子树的情况而已,因为转移的时候可以理解为是二叉树合并。
一点细节:因为可以返回 u,所以经过的边数上界是 min(2*sz[u], m),要和普通的树形背包不同。
P5054 [COCI 2017/2018 #7] Dostavljač(Code)
因为只有一个人,所以需要考虑是否返回。
然后就和上题代码完全一样,不过思路可能有点不同,但都一个套路。
对树形背包上下界优化时,一定要注意有没有假,不要反向优化了,这里不太好优化,又发现 n^3 完全能过,就可以干脆不优化了。
Find Metal Mineral - HDU 4003(Code)
这题有多个人,每个人都可以返回当前点或不返回。于是考虑记 f[u][i][j] 表示,u 子树内原来有 i 个人,现在有 j 个人没有返回 u。
注意这里记的是原来有多少人,而不能记成子树内共有 i 人。因为严格来说这子树内的 i 人最开始都是在 u 上,所以必须按照前者具体描述。
状态要把整个局面完整刻画出来。不然状态定义是模糊的,转移时会出现歧义。
然后考虑转移,就考虑让 u 分一些人到 v 上,然后这些人在 v 子树遍历,然后再返回 u,再去 u 的其余子树遍历。
显然复杂度有问题,这里直接贴代码了:Source code - Virtual Judge
朴素的 DP 复杂度不接受,可以通过观察性质优化 DP。
发现 u 上原来有 i 人,这 i 人都不回到 u 肯定最优, 因为会少一些折返。
于是定 f[u][i]:u 子树内,原来有 i 个机器人,现在 i 个机器人不在 u 上,遍历完 u 子树内所有点的最小代价。
然后考虑转移,就是正常树形背包去做就好。也是相当于让 u 先分 k 个人到 v 上,再让人在子树内遍历,就有:
$$f[u,j] = \min_{v \in son_u,1<k\le j} \{ \ f[u,j],\ f[u,j-k]+f[v,k]+k*w \} $$
但是转移出现 f[v][0] 的情况显然不合法,原来 0 个人不可能遍历完整棵树。我们期望是想要遍历完这棵树才合法。
就是当 k=0 时,我们可以让 u 分 j 个人到 v 上,遍历完 v 这棵子树再让 j 个人回到 u,继续遍历 u 除去 v 的若干子树。
所以我们特别考虑一下,定 f[u][0] 表示原来 x 个人,现在这 x 个人遍历完整棵树后全在 u 上。
这里我们并不在意原来的人数,因为想要 u 子树所有点遍历完,肯定是派一个人下去遍历后回来最优。
那么就有 $f[i,0] = \sum_{j \in son_i} f[j,0] + 2*w$。(其实就是和上面转移一样的)
此时 f[u][0] 相当于算遍历完 u 子树的代价,但不一定最优,但一定要确保遍历完子树才行。
所以转移时应该先假设让 u 遍历完 v,再考虑具体如何更优,要先保证 u 遍历完了 v 子树才行。
然后根据这一点,转移考虑填表是容易实现的,刷表算不了,因为顺序配不上,所以上下界优化也用不上。
所以在确定正确性的前提下再考虑上下界优化树形背包,这里刷表的正确性是假的,就不用考虑优化了。
只有大头有限制,考虑对于有限制的东西 DP 就好。
因为树是二分图,发现头数 m>=3 时,另外至少两个头一定可以不吃到树枝。所以只需要考虑大头。但如果头数为 2,就不一定了,所以需要特别考虑下。
然后考虑需要记录什么。发现记一下吃的果子数,还有当前果子吃不吃,才能转移。
定 f[u][i][0/1]:u 子树内,大头吃了 i 个果子,u 果子大头吃或不吃。
转移就是考虑树上背包,合并两子树时分类讨论下。
然后发现 m=2 的情况可以直接融入转移,因为如果 u,v 都不吃,那么一定是给第二个头吃了。
一点细节:在转移的时候,必须开辅助数组。
因为刚开始初始化使 f[u][0][0]=0,此时有 f[v][0][0]=0,如果 m=2,一定会让第二个头吃,也就是有转移式 f[u][0][0]=min{f[u][0][0]+f[v][0][0]+w},但是 f[u][0][0] 为 0,一定不会被更新到了。
发现合并子树的时候,不然转移不满足最优性就不转移了,所以要开一个新的辅助数组,存合并的结果,才满足最优性。
树形 DP 转移的时候,要考虑是否开个辅助数组存合并的结果(具体看转移顺序,以及合并时是否满足最优性而定),容易发现就算开了也不会影响复杂度。
不仅要记录子树内的信息,为了方便转移,还需考虑记录子树内向外延伸的信息
需要和 P2016 战略游戏 好好区分一下。本质是考虑当前记录的状态会被哪些状态影响。
P2016:树上,每个点有代价,选了一个点,就能让这个点能覆盖到和这个点相邻的边,要求全部边被覆盖最小代价。
考虑边 (u,v),在考虑到 u 点时,想要边被覆盖,只可能是选 u 或选 v。于是考虑 u 或者 u 的儿子 v 选不选即可。
P2458(最小支配集):树上,每个点有代价,选了一个点,就能让这个点能覆盖到和这个点相邻的点,要求全部点被覆盖最小代价。
在考虑到 u 点时,想要 u 被覆盖,只可能是选 v 或选 fa 或他自己。于是考虑 u 或者 u 的儿子或 u 的父亲选不选即可。
所以会定 f[u][0/1/2] 表示 u 子树内的点都被覆盖所花费仅考虑在子树内的最小代价,其中 u 是被自己/儿子/父亲 覆盖的。
然后注意一下边界就好。
设计转移方程时,要考虑树形 DP 本质,根据状态定义,不断划分子问题。仍然考虑 v 转移到 u。
即使 u 有涉及到 fa,但不能用 fa 的代价更新 u。不然后续再算 fa 的代价时会出问题。这也是为什么状态定义强调的是子树内的最小代价。
即考虑成这样一个问题:
为什么状态这样定,定成只考虑子树内?
如果状态定成 f[u][0/1/2] 表示 u 子树内的点都被覆盖所花费的最小代价,其中 u 是被自己/儿子/父亲 覆盖的。
然后当 u 被父亲覆盖时,把选父亲的代价加上。
为什么这样就错了?
因为选父亲的代价在父亲的位置自然被计算。你现在在 u 的时候就加了,之后再算 u 的父亲又加一遍,相当于加重了,当然就错了。因为之后在父亲位置时自然会被转移方程考虑到。
所以计算当前子树不用考虑向外延伸的代价。只需要考虑子树内的代价,因为向外延伸的代价在之后自然会被考虑。
上题带了计数版本。在转移的时候顺带计数就好。
也是一样,为了避免重复考虑代价或者计数,我们仅考虑子树内的情况。
也就是定 f[u][0/1/2] 表示 u 子树内的点都被覆盖所花费仅考虑在子树内的最小代价,其中 u 是被自己/儿子/父亲 覆盖的。
定 g[u][0/1/2] 表示 u 子树内的点都被覆盖所花费仅考虑在子树内的方案数,其中 u 是被自己/儿子/父亲 覆盖的。
但是有重复的情况按此状态会多统计,即一个点 u 既被父亲 fa,又被儿子 v 覆盖。
这样在这个点 g[u][1] 和 g[u][2] 都会把 g[v][0] 的贡献算进来,相当于 g[v][0] 贡献了两次,多统计了方案数。
所以,为了防止重复统计,在定状态的时候应该把状态考虑成互斥的情况,避免有相同情况重复统计方案。
具体点,我们把状态改成:f[u][k=0/1/2] 表示 u 子树内的点都被覆盖所花费仅考虑在子树内的最小代价。
k=0,表示 u 自己覆盖自己。
k=1,表示 u 被儿子覆盖。
k=2,表示 u 被父亲覆盖,但一定不被儿子覆盖。
对于 g 的定义同理,然后再统计方案就不会出现重复统计的情况了。
容易发现这样定义只是把先前可能重复的情况拆成互斥的情况,所以对于最值 f 不会有任何影响。
然后考虑转移,f[u][0] 和 f[u][2] 是容易的。
同样的,f[u][1] 中,至少要有一个儿子选择自己覆盖自己。这里用类似 DP 套 DP 的技巧,可以再开一个 DP 维护。
记 p[u][0/1] 表示 u 在 v 之前的所有儿子,是否有选 v 自己覆盖自己的情况。
但是通过观察可以发现,g[u][2] 和 p[u][0] 是等价的。然后我们可以让 f[u][1] 的定义改为 p[u][1] 的定义,即把 f[u][1] 的定义改为,u 被儿子覆盖,且至少有一个儿子选择自己覆盖自己。
这样就省去了一个额外的 DP。但道理是一样的,不过会省去很多边界条件,更加简洁。
类似背包,但是时间空间不允许。发现 u 连出的边一定是有1或0条 (u, fa) 和若干条 (u, v),所以记 f[u][0/1],表示 u 有没有连向父亲的边。然后贪心转移就好。
刚开始想的差不多了,但差一点,如果状态非法可以直接设成 INF,而不是说不从该状态转移,这样可能会有些问题。
换根 DP
何时用换根?可以现场分析一下,答案是否跟根是哪个结点有关。
对于根可能变的问题如果只是路径上的问题那么跟根没有关系不需要换根,如果是子树之类与根关联的问题就要换根。
比如说问每个节点深度和,这个问题和哪个点当根关联性很大,需要换根。
你会发现换根这是相当于改变了原来树的父子关系。
当要求的信息都包括 u 点,不妨考虑成是以 u 为根时的答案。
学习笔记看这个,很详细了:换根dp - cn是大帅哥886 - 博客园
鸽子
没错还是鸽子。
再挂一次:图树模板 - 题单 - 洛谷 | 计算机科学教育新生态
树形 DP+换根 题单:云剪贴板 - 洛谷 | 计算机科学教育新生态
NFLS 树形DP+换根 题目+代码:

浙公网安备 33010602011771号