zoukankan      html  css  js  c++  java
  • [复习资料]树形dp-杂题选讲

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

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

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


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

    树形背包

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

    例题:[JSOI2018]潜入行动

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

    (1le nle 10^5,1le kle 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} imes dp_{v,y,0,1} ]

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

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

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

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

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

    [dp_{u,x+y,1,1}gets dp_{u,x,1,1} imes dp_{v,y,0/1,0/1}+dp_{u,x,1,0} imes 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)。这个游戏从一个珠子开始,每次会用如下方式添加一个新的珠子:

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

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

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

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

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

    (1le nle 200000)

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

    1. ( m Append(w, v)) :一个新的珠子 (w) 和一个已经添加的珠子 (v) 用红线连接起来。
    2. ( m 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)) 算作同一组。

    (1le nle 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)

    [ansgets ans+f_{u,x} imes g_{v,x+1}+f_{v,x} imes g_{u,x+1} ]

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

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

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

    总结

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


    重链剖分

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

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

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

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

    例题:Lomsat gelral

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

    (1le nle 10^5)

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

    总结

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

    其他dp-杂题选讲

    例题:一道考试题

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

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

    (1le nle 100)

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

    (1le nle 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} imes dp_{y,b} ]

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

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

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

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

    [ o=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) ,请用最小的花费使得根节点到所有叶子节点的距离相等。

    (1le nle 3 imes 10^5)

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

    转移:

    [dp_{u,s}=min_{k=0}^s(mid val_u-kmid+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-kmid +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. 插入点。

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

  • 相关阅读:
    退出状态、测试(test or [])、操作符、[]与[[]]区别
    shell中$(( ))、$( )、``与${ }的区别
    正则表达式
    vim常用快捷键
    hadoop综合大作业
    分布式文件系统HDFS 练习
    安装Hadoop
    Hadoop综合大作业
    分布式文件系统HDFS 练习
    安装hadoop
  • 原文地址:https://www.cnblogs.com/lsq147/p/14232463.html
Copyright © 2011-2022 走看看