学习笔记——树形DP
前言
学完树形\(DP\),\(NOIP\)会考的所有\(DP\)我们就都学完了,所以让我们一鼓作气,开始树形\(DP\)的学习之旅吧!
一、树形\(DP\)的基本概念
顾名思义,树形\(DP\)就是在树这种数据结构上进行\(DP\)(这不是废话吗?),通过有限次遍历树,记录相关信息,以求解问题。因为树形\(DP\)是建立在树上的,而树中的父子关系本身就是一个递归结构(满足子问题重叠性),所以自然而然地就衍生出了两种\(DP\)顺序:
1.叶子节点 $\Rightarrow $ 根节点:就是将叶子节点的信息向上传到父亲节点,在父亲节点处进行信息的整合。最后由根节点记录的信息得到最优解。
例如本题可以将下属来不来得到的权值作为信息传递到上级(题解)(我的代码)
2.根节点 $\Rightarrow $ 叶子节点:取所有点作为一次根节点进行求值,此时父节点得到整棵树的信息,只需要去除这个儿子节点\(DP\)值的影响,然后再转移给这个儿子。(此做法一般不用,但有时可能有奇效)
树形\(DP\)的顺序:一般按照\({\color{Red}\colorbox{White}{后续遍历}}\)的顺序(先处理儿子节点再处理当前节点(这样才符合\(DP\)的三个基本条件))
实现方式:树形\(DP\)一般用\({\color{Red}\colorbox{White}{记忆化搜索}}\)实现(既然是记搜当然用的是递归了)(类似于树剖的第一个dfs)
时间复杂度:一般的树形\(DP\)的时间复杂度是\(O(N)\)(因为每一个节点只会被遍历一次),如果有附加维\(M\),则复杂度为\(O(N\times M)\)。
二、树形\(DP\)的几种类型
1.背包类树形\(DP\)
既然我们前面将背包和树形\(DP\)联系在一起学了,那我们就先从我们熟悉的背包类入手吧!(这样能增加我们的信心,减少我们对树形\(DP\)的畏惧心理)
通过本题,我们就可以大致了解树形\(DP\)的思路--从根节点出发不断向子树进行搜索,再将子树的信息合并到该节点上。(注意:本题要考虑只选该节点与子树连接的这一条边的情况)
本题我们又学会了一种思想--建虚点。因为本题是在森林中选一定的节点(选完父节点才可以选子节点),但我们又不可以只由一棵树得到最优解,所以我们可以考虑建一个虚点(\(0\)),令它向所有树的根节点连一条边,接着只要对以虚点为根节点的子树进行树形\(DP\)求出最优解即可。
通过本题,我们要学会在树形\(DP\)时注意细节--该节点与它儿子节点相连的边到底可不可取,以后做树形\(DP\)题推导状态转移方程时要仔细考虑这个问题,才能更正确的做出树形\(DP\)题。
4.状态转移方程含义比较新奇的题(内层循环顺序讲解清楚的题解)(我的代码)
本题告诉我们:
(1)树形\(DP\)的状态转移方程记录的可以不只是一个最优解,还可以记录该状态对最终答案的贡献。
(2)我们可以通过建立双向边的方式将一棵树强行转化为以1号节点为根节点的树,然后通过遍历顺序重新确立父子节点。
(3)我们要注意该状态有没有被更新过,可以通过思考将一些不可能更新答案的状态排除(本题中还是初始值-1的状态),加快程序的运行效率。
先吐槽一下:普及\(DP+IOI\)输入\(=\)提高难度
好了好了,来总结一下本题的收获
(1)联想已学:遇到另类的输入时,可以联想一下我们已学算法中有没有类似的结构。例如本题可以用类似线段树的建树方式来解决输入问题。
(2)根据题意合理运用填表法和刷表法:
还是先简单解释一下这两个词吧!
填表法 :就是一般的动态规划,当前点的状态,可以直接用状态方程,根据之前点的状态推导出来。
刷表法:由当前点的状态,更新其他点的状态。
刷表法需要注意:只用于每个状态所依赖的状态对它的影响相互独立。
这么说大家是不是还是一头雾水,举个例子:如果状态\(A\)对状态\(B\)有影响,状态\(C\)也对状态\(B\)有影响,当状态\(A\)的影响和状态\(C\)的影响相互不影响,就可以运用刷表法。
本题中就记录到达该走廊末尾需要花费的时间(设为\(tim\)),令\(j\)表示分配给左儿子的时间,\(k\)表示分配给右儿子的时间,然后更新以\(u\)为根节点的树花\(tim+j+k\)秒可以取得的最大价值。这样可以极大地减少思考难度。
继续挖坑 (挖坑太多后面会不会填不完啊)
2.普通树形\(DP\)
经历过树形背包的洗礼,我们应该了解到了树形\(DP\)的基本思路,接下来就开始真正的树形\(DP\)吧!
本题作为树形\(DP\)的经典例题,当然会在各种场合出现。本题要求父子节点二选一,使总价值最大,自然是一道典型的最大权独立集问题板子。做这类题,我们可以在\(DP\)数组上多开一维,这一维只有\(0\)或\(1\)组成,表示该节点选或者不选,然后在根节点处统计答案最大值即可。
本题要求 在给定的树上取最小的节点数,使所有边都至少有一个端点在选中的集合中。 我们还可以仿照第一问的思路继续解题-- 在\(DP\)数组上多开一维,表示该节点选或者不选,然后在根节点处统计答案最小权值 。所以,只要我们深刻理解树形\(DP\)的方法,就可以如法炮制做其他题。
本题看上去像例题2的加强版,但其实本题要求守点,而不是像例题2一样守边。就是这一个看似细小的差距,却导致了代码的千差万别。例如下面这张图:
如果我们只守1、4号点,那这一条链上的点就都可以守到,但2<->3这一条边明显没有守到。所以本题中我们要考虑三种情况:
1.\(x\)节点被自己覆盖,即选择\(x\)点来覆盖\(x\)点
2.\(x\)节点被儿子\(y\)覆盖,即选择\(y\)点来覆盖\(x\)点
3.\(x\)节点被父亲\(fa\)覆盖,即选择\(fa\)点来覆盖\(x\)点
然后分类讨论进行状态转移,最后在根节点处取\(min(f[1][0],f[1][1])\)作为代价的最小值。(因为根节点没有父亲节点,所以不用讨论该情况)
本题想到\(DP\)的思路并不难,但主要恶心在输出每个点是否可能成为最优解,这就导致一道本来黄\(\sim\)绿的树形\(DP\)题强行升紫。
思路:
我们用dp[u][0]表示以\(u\)为根节点的子树将所有叶子节点控制的最小代价,用
dp[u][1]表示以\(u\)为根节点的子树中还剩一个叶子节点未控制的最小代价。
-
\(dp[u][1] = \sum\limits dp[v][0] - max(dp[v][0] - dp[v][1])\)(我们可以贪心选择一颗子树,使\(dp[u][1]\)的代价最小)
-
\(dp[u][0] = min(sum[u],dp[u][1] + val[u])\)(显然,我们可以不选当前节点取尽\(u\)的所有子树,也可以选当前节点)
我们再建一堆辅助数组,用于输出答案,用g[u]表示\(u\)的儿子节点中dp[v][0]-dp[v][1]的最大值,用
num[u]表示dp[u][0]可以有多少个dp[v][1]转移而来,用sum[u]表示所有dp[v][0]之和,再用can[u][1]表示可以通过dp[u][1]转移到其父亲节点can[u][0]表示可以通过dp[u][0]转移到其父亲节点。
-
如果\(can[u][0]=1\)
-
\(\circ\)转移到\(can[v][0]\)
-
\(\circ\) \(\circ\) 如果\(num[u]>1\),则\(can[v][0]=1\),因为\(v\)节点不一定传递到\(dp[u][1]\)
-
\(\circ\) \(\circ\) 如果\(g[u]!=dp[v][0]-dp[v][1]\),即\(v\)节点一定传递到\(dp[u][0]\),\(can[v][0]=1\)。
-
\(\circ\) \(\circ\) 如果\(dp[u][0]==sum[u]\),即\(v\)节点一定传递到\(dp[u][0]\),\(can[v][0]=1\)。
-
\(\circ\)转移到\(can[v][1]\)
-
\(\circ\) \(\circ\) 如果\(sum[u]-dp[v][0]+dp[v][1]+val[u]==dp[u][0]\),即贪心选择的子树是\(v\)节点,显然\(can[v][1]=1\)。
-
如果\(can[u][1]=1\)
-
\(\circ\)转移到\(can[v][0]\)
-
\(\circ\) \(\circ\) 如果\(num[u]>1\),则\(can[v][0]=1\),因为\(v\)节点不一定传递到\(dp[u][1]\)
-
\(\circ\) \(\circ\) 如果\(g[u]!=dp[v][0]-dp[v][1]\),即\(v\)节点一定传递到\(dp[u][0]\),\(can[v][0]=1\)。
-
\(\circ\)转移到\(can[v][1]\)
-
\(\circ\) \(\circ\) 如果\(g[u]==dp[v][0]-dp[v][1]\),即贪心选择的子树是\(v\)节点,显然\(can[v][1]=1\)。
又到了扔题时间
1.P4084 [USACO17DEC]Barn Painting G
4.P3574 [POI2014]FAR-FarmCraft
3.换根树形\(DP\)
1.基本思路
-
先以\(1\)号节点,进行一遍\(dfs\),令\(f[u]\)表示以\(u\)为根节点的子树的最优解。
-
将根节点转移到其儿子节点上,并不断更新\(f[to]\)(假设\(to\)为\(u\)的儿子节点)
-
将第二步推而广之,逐渐更新完整颗树
由上面的操作可以看出,换根\(DP\)其实就是不断利用以\(1\)为根节点得到的信息更新以其他点为根节点得到的信息。
1.来一道入门题--找使所有结点的深度之和最大的根节点(题解)(我的代码)
作为入门题,当然是要让我们熟悉换根\(DP\)基本思路的题。本题可以令\(f[i]\)表示以\(i\)号节点为根节点的树的深度和,用根节点从\(u\)移动到\(to\)(\(u\)的儿子节点)时以\(to\)为根节点这颗子树的所有节点的深度\(-1\),而其他节点的深度\(+1\)进行状态转移。
一样的板子和套路,不过因为极大值比较大,所以先以\(f[1]\)为极大值再更新其他值即可。(白白WA40pts)
本题思路比较好想(改造该点子树中最大节点数不超过\(\frac{n}{2}\)的子树),但是码代码时一定要养成好习惯,不要打出int to=e[i].nxt这样的细节错误 (关键这种错误找了一个下午+一个晚上才找出来) 。
因为要去考\(NOIP\),所以还是挖坑 (估计也不会填了,毕竟考完还要补文化)
我回来填坑了\(!\)



浙公网安备 33010602011771号