zoukankan      html  css  js  c++  java
  • DP 从入土到入门

    emm随便错

    树形DP

    例题

    没有上司的舞会

    没有上司的舞会
    定义 (f[x][0/1]) 表示x这个点选或者不选能够带来的最大的快乐值。

    因为每一个人的顶头上司肯定不能和他一起选,也就是说,如果当前这个人选了,那么他的直接下属不能选,但是当前这个人不选对快乐值没有影响,也就是。

    [egin{cases} f_{x, 1} = f_{to, 0} \ f_{x, 0} = ext{max}(f_{to, 1}, f_{to, 0}) end{cases} ]

    然后题目就做完了,一个 dfs 解决问题

    [HAOI2009]毛毛虫


    题面自己看吧,简化不动...


    传送门
    我们令 (f_x) 表示以 x 为根的子树内最大的毛毛虫的大小,而且 x 为毛毛虫的头,那么 (f_x) 的转移方程就很显然了。
    (cnt_x) 为 x 的儿子大小。
    (f_x = max_{to in son_x} f_{to} + 1 + max(0, cnt_{to} - 1))

    [ZJOI2007]时态同步

    传送门


    给你一棵树,边有边权,然后问你每次可以将一条边的边权加一,
    问你至少多少次操作之后,1 这个根节点到叶子节点的路径长度都相同。


    可以让所有叶子节点同时发出信号,然后这些信号同时到达根节点。于是我们可以自下而上的进行维护,使得每一节点所有子节点的信号同时到达该节点。

    我们从根节点开始搜索,搜索到叶子节点,回溯的时候进行维护,先维护节点的所有子节点到该节点最大边权(边权为叶子节点到同时到达它所需要时间)。然后维护答案,答案为最大边权减去所有到子节点的边权。然后维护父节点的边权,父节点边权为该节点子节点的 最大边权+父节点到该节点的时间。然后就回溯,重复操作,到根节点为止。

    换根法

    也叫二次扫描法
    基本思路是先指定一个根结点,然后第一次dfs求出根节点的权值,然后第二次dfs的时候可以搞一个转移方程由根结点转移过去。

    STA-Station

    传送门


    给定一个 (n) 个点的树,请求出一个结点,使得以这个结点为根时,所有结点的深度之和最大。
    一个结点的深度之定义为该节点到根的简单路径上边的数量。


    (dp_i) 为第 i 个结点作为根节点时的深度之和,我们不妨令根结点为 1,然后进行第一次dfs,求出根结点为 1 时的深度之和,然后考虑如何转移。

    以样例为例

    8
    1 4
    5 6
    4 5
    6 7
    6 8
    2 4
    3 4
    


    我们可以知道 (dp_4)(12) 考虑如何换到5这个结点。

    因为换成 5 的时候相当于 5 所在的子树所有的结点的深度都减一,剩下的结点的深度都加一,所以我们令 (siz_x) 记录 x 这个结点的子树的大小,(point) 为所有子树的大小,然后(x, to) 为 x 结点与 x 结点所到的 to( x 为 to 的父亲结点)。
    那么 (point - siz_{to}) 为除了以 to 为根的子树之外的有多少个点。

    [dp_{to}=dp_x+(point-siz_{to}) - siz_{to} ]

    Great Cow Gathering G

    传送门


    给你一棵树,边有边权,让你求出一个到其他所有点的路径和最小的那个点的路径和。


    和上一个题差不多,就是每每条边有边权,也就是说我们可以通过让 siz 数组维护的值改变一下来 AC 这道题。

    考虑 siz 数组中存贮每个牛棚的以及他的子树中,一共有多少头牛,剩下的转移就和上边那个题差不多了。

    Choosing Capital for Treeland

    传送门


    给你一棵树,但是树上的边都是单向边,问以 i 点为根的时候,为了能够遍历到每个点,需要修改多少条边的方向。


    建图的时候按照双向边建图,正向和反向边分别标记,然后可以很轻松的求出 1 为根时的答案,转移的时候就判断 x 与 to 之间连的边是正向边还是反向边就行了。

    Nearby Cows G

    传送门
    给你一棵 (n) 个点的树,点带权,对于每个节点求出距离它不超过 (k) 的所有节点权值和 (m_i)


    我们用 (f_{i,j}) 表示距离 i 点为 j 的所有点的点权和,可以很容易得到下边的递推式

    (f_{i,j} = sum f_{x,j-1}),其中 x 为 i 的儿子结点,第一遍 dfs 的时候求出这个即可。

    考虑如何第二遍 dfs 搞出其他点的 DP 值,可以发现 (f_{x,j} = f_{i,j - 1}) ,然后我们发现还有算重的部分,所以可以简单容斥一下。

    清北学堂 某题

    传送门

    树点染色类

    Cell Phone Network G

    传送门
    双倍经验

    一开始的时候还以为和黑白染色差不多的那种,但是仔细一想就会发现不对劲。

    我们设 (f_{x,0/1/2}) 分别为:

    • (f_{x,0}) 表示 i 被自己覆盖 的最小花费
    • (f_{x,1}) 表示 i 被儿子覆盖 的最小花费
    • (f_{x,2}) 表示 i 被父亲覆盖 的最小花费

    可以发现,x 自己覆盖的时候,也就是 x 自己建信号塔的时候,他的儿子们建或不建都可,所以转移方程就是 (f_{x,0} += min { f_{son,0}, f_{son,1},f_{son,2}})

    可以发现,自己被儿子覆盖的时候只需要被其中一个儿子覆盖就可以了,所以转移方程就是: (f_{x.1} = f_{sx.0} + sum ig(min (f_{son,0}, f_{son,1})ig ))

    可以发现,他自己如果被父亲覆盖,说明他的儿子一定没有被他自己覆盖,所以状态转移方程就是: (f_{x,2} += min { f_{son. 0}, f_{son.1}})

    树形背包

    选课

    洛谷
    简化题面
    一共有 n 门课,但是你只能选其中的 m 门,而且这些课中有的有依赖,比如学数学之前先学数数。


    思路
    不妨设 (f_{now, j, k}) 表示以 now 为根节点的子树,考虑前 j 个节点选 k 门课的方案数
    因为 1 号节点是根节点,显然递推起点 (f_{now, 1, 1} = val_{now})

    这样很容易得到状态转移方程 (f_{now,j,k} = max {f_{now,j-1,k},f_{son,所有节点数,l}+f_{now,j-1,k-l}});

    然后我们观察等式两边的特点,哪些是我们已知的?

    在对 now 求解前,我们至少已经处理完了前面的子树,所以 (f_{son,所有节点数,l})是可以直接用的,然后在处理第 j 个节点前,前 j-1 个节点是我们已经处理过的,所以 (f_{now,j-1,k})(f_{now,j-1,k-l}) 也不用考虑循环顺序问题。

    但是问题来了,这样开三维数组不会炸空间吗,也许本题不会,但是我们可以很显然的发现,空间是可以优化的,只要稍稍改变循环顺序即可,我要用到 j-1 的内容,都是满足 l<k 的,所以倒着循环 (k) 这样就可以使我们在一个数组中当前值和上面我们用到的值完全不影响。(类似 01 背包)
    code
    洛谷

    基环树DP

    城市环路

    传送门
    一个基环树上的最大点权独立集。


    题目中告诉我们每个点有点权且任意相邻的两个点不能同时选取,而且整个城市可以看做一个 n 个点,n 条边的单圈图(保证图连通)。

    很明显这是一个基环树上最大权独立集问题,和没有上司的舞会类似。

    我们可以根据基环树的良好性质把这个问题拆分成两个问题。

    • 我们把在环上的点都看做一种根节点,不考虑它们连向环上的点,它可以连出一棵外向的子树。在这棵树上,我们可以做树上的最大权独立集问题。

    • 在环上的点,把它们当做环形 DP 问题求解就好了。

    大体思路就是这样。因为是基环树,所以第一步肯定是找环(妈妈我会tarjan

    这里介绍一种 dfs + toposort 的做法。

    首先我们在加边的时候记录一下每个点的入度,显然入度为 1 的点都是叶子节点。我们就从这些叶子节点开始 toposort 。

    拓扑排序完成之后,会发现环上的点的入度都变成了 2。我们就找到第一个在环上的点,从这个点开始 dfs,只走当前入度为 2 的点,同时记录每一个点,这样就找到了所有环上的点。

    然后就是树上的最大点独立集问题,就和没有上司的舞会一样,设 (f_{x,0/1}) 表示在以 x 为根的子树内,x 这个点选或不选能够达到的最大价值,显然有以下转移方程。

    [egin{aligned} & f_{x,0} = sum_{to in son_x} max Big{ f_{to,0}, f_{to,1}Big} \ & f_{x,1} = sum_{to in son_x} f_{to, 0} + w_x end{aligned} ]

    求出每个环上的点选或不选的最大权值之后,可以在环上进行环形 DP。

    我们令 (g_{i. 0/1}) 为前 i 个点中,第 i 个点选或不选时能够达到的最大价值,所以也有一个很显然的式子。

    [egin{aligned} & g_{i,0} = max Big{ g_{i - 1,0}, g_{i - 1,1}Big} + f_{x.0}\ & g_{x,1} = g_{i - 1, 0} + f_{x,1} end{aligned} ]

    初始化的时候要把 g 数组都赋成 -inf 然后需要钦定 (g_1) 选或不选的两种情况,分别进行 DP。

    时间复杂度 : (operatorname{O}(n))

    状压DP

    棋盘类

    [SCOI2005]互不侵犯

    洛谷

    简化题意
    让你在一个棋盘上放置 k 个棋子,每个棋子的八个方位不能有其他的棋子。


    (f_{i, j, s}) 表示,第 i 行放置的方式为 j,放置了多少个棋子。
    (sit_x) 表示 x 中 1 的个数,可以预处理,以及当前状态下放置的国王的个数 gs 。

    那么 (f_{i,j,s} = sum f_{i-1, k, s - gs_j})

    转移的条件就是:

    • (sit_j & sit_k) 及上下有重复的棋子。
    • ((sit_j << 1) & sit_k) 及左上右下有重复的棋子。
    • (sit_j & (sit_k << 1)) 及左下右上有重复的棋子。

    以上条件如果都不满足,那么可以转移。

    code
    loj

    炮兵阵地

    洛谷
    简化题意
    也可以看成放旗子,但是只能把棋子放到空地上,而且棋子上下各两个不能有其他棋子,左右各两个不能有其他棋子。


    (f_{l,s,i}) 表示上一行的状态为 l,当前这一行的状态为 s,现在是第 i 行时能放置的最多的炮兵数,那么转移方程可以很容易的推出,(sum_s) 表示当前的状态 s 中包含多少个 1。

    (f_{l,s,i} = max(f_{l,s,i}, f[fl,l,i-1]+sum_s))

    下边只需要判断那里能放那里不能放就行了。

    可以用一个数组 (a_i) 来表示第 i 行中,山的位置是 1,平地为 0。

    判断当前状态有没有在平原上可以直接 (s & a_i) 就行了。
    code
    洛谷

    Corn Fields G

    洛谷
    简化题意
    其实和上边的问题都差不多,只是 prework 有些不懂而已。

    ** code **
    洛谷

    蒙德里安的梦想

    AcWing
    题意分析
    rua,可以分割棋盘,但是我们发现分割棋盘之后会有好多长方形被拦腰折断,所以可以没被折断的看做 0,折断的看做 1,那么下一行中那些折断的就必须看做 0, 剩下的可以是1 也可以是 0。


    (f_{i,j}) 表示第 i 行的形态 为 j 时,前 i 行的分割方案的总数。

    (i-1) 的形态 k 转移到第 i 行的 j 形态,当且仅当:

    • j 和 k 的 (&) 结果是 0, 这保证了每个数字 1 下方必须是数字 0 ,代表继续补全上比方竖着的长方形。
    • j 和 k 的 (ig|) 每一段连续的 0 必须有偶数个,代表了横着放的长方形。

    我们可以 DP 求出 ([0, 2^M - 1]) 内所有满足“二进制表示下每一段都是连续的0有偶数个”的整数,记录下来。

    不难发现

    [displaystyle f_{i.j} = sum_{j & k= 0 且 j ig| k in s} f_{i-1, k} ]

    code
    AcWing

    奇怪的状压

    [AHOI2009]中国象棋

    洛谷
    简化题意
    在一个 (n imes m) 的棋盘上最多可以放置多少个炮,试任意的两个炮不能相互打到。


    感觉这也不像是一个状压DP,但是标签上就是这么写的..

    可以发现,让任意的两个炮不能相互打到就是,任意的一行一列都不能有两个炮。

    (f_{i,j,k}) 表示前 i 行中 j 列有一个炮,k 列有两个炮的最大方案数。

    可以发现,可以由一下状态转移而来。

    • (f_{i-1,j, k}),可以一个都不放,那就是上一个的方案数。
    • (f_{i-1, j-1,k}),从一个炮都没有放的列转移而来,因为有 (m - (j - 1) - k) 个没有放任何炮的列,所以最后要乘上这个系数。
    • (f_{i-1,j+1,k-1}),这个表示从 j+1 个放了一个炮的,在这些列中任意放上一个,让放一个的变成放两个的,因为那些列可以随便取,所以要乘以 (j+1)
    • (f_{i-1,j - 2, k}), 当前这一行放两个,都放到没有放过的地方,这个时候没有放过的地方有 (m - (j - 2) - k) 个,从这些中任取两个,就是 ({m-(j - 2) - k}choose 2),乘上这个系数。
    • (f_{i - 1, j, k-1}),当前这一行放两个,放到同一列中,让那一列变成两个炮,因为当前没放过的有 (m - j -(k -1))
    • (f_{i-1,j+2, k - 2}),从原本有一个炮的列中任意选两个,各放上一个炮,({j+2} choose 2),乘上这个系数。

    code
    洛谷

    DP 优化

    单调队列优化DP

    loj 最大连续和

    loj
    简化题意
    给你 n 个数的序列,找出一段连续的长度不超过 m 的非空子序列,使得这个序列的和最大。
    (1 leq n , m leq 2e5)


    思路
    有一个非常显然的 DP,(F_i = max Big{ sum_{j = i - k + 1}^i A_j ig| k in [1,m]Big})

    然而这样做是 (operatorname{O}(nm)) 的,并不优秀。

    我们令 (s_i = sum_{j = 1}^i A_j),则:

    [egin{aligned} F_i&=max Big{ sum_{j = i - k + 1}^i A_j ig| k in [1,m] Big} \ F_i&=max Big{ s_i - s_{i - k} ig| k in [1, m]Big}\ F_i&=s_i - minBig{ s_{i-k} ig| kin[1,m]Big} end{aligned} ]

    考虑用队列来维护决策值 (s_{i-k}) ,每次只需要在队首删除 (s_{i-k-1}),在队尾加入 (s_{i-1}),但是取最小值的操作还是需要 (operatorname{O}(m)) 实现。

    考虑在添加 (s_{i-1}) 的时候,设现在队尾元素为 (s_k) ,由于 (k < i-1),所以 (s_k) 必然比 (s_{i-1}) 先出队。若此时 (s_{i-1} leq s_k),则 (s_k) 这个决策就永远不会再以后用到,可以将 (s_k) 从队尾删除,此时的队尾就形成了一个类似栈的一个东西。

    同理,若队尾的两个元素 (s_i)(s_j) ,若 (i < j)(s_i geq s_j),则我们可以删掉 (s_i),因为 (s_i) 永远都不会用到了,此时的队列中的元素构成了一个单调递增的序列,即 (s_1 < s_2 < dots < s_k)

    维护的时候只需要这么做就行了。

    • 若当前队首元素 (s_x),有 (x < i - m) , 则 (s_x) 出队,知道队首元素 (x geq i-m) 为止。
    • 若当前队尾元素 (s_k geq s_{i-1}),则 (s_k) 出队,知道 (s_k < s_{i-1}) 为止。
    • 在队尾插入 (s_{i-1})
    • 取出队列中的最小值,更新一下答案。
      代码
      code

    Mowing the Lawn G

    洛谷
    简化题意
    给你一个 n,k 和 n 个整数 (a_i),从 a 中取出任意多个数,让取出得数的和最大,并且相邻的取出的数的个数不能超过 k 个。


    思路
    (f_{i, 0/1}) 表示以 i 这个数结尾, i 这个数取或者不取,能够到达的最大价值。

    容易得到下边的转移方程。

    [egin{aligned} f_{i.1}&= max_{i-l leq j < i} Big{ f_{j,0} + sum_i - sum_jBig}\ f_{i,0}&= max ig{f_{i-1, 0}, f_{i-1, 1}ig} end{aligned} ]

    可以上边那个式子可以转化为 (f_{i.1}= max Big{ f_{j,0}- sum_jBig} + sum_i)

    式子上边的那个 max 可以单调队列优化。
    code
    洛谷

    绿色通道

    loj
    双倍经验
    简化题意
    题目描述挺清楚的。


    思路
    让最长的空段至少有多长,emm显然可以二分答案。

    可以二分一个最长的空段,check 的时候找到最小的时间。

    (f_i) 表示前 i 个数中选取的最小价值。
    易得 (displaystyle f_i = min_{i-m leq j < i} f_j + a_i)
    前边那个 (min), 可以单调队列优化。

    然后你就做完了,大水题,双倍经验就不需要二分了,比这个还水。

    code
    loj

    [SCOI2010]股票交易

    简化题意
    三个数字 T, maxP, w 分别代表,一共有 T 天,任何时候手中不得超过 maxP 张股票,买卖股票必须在上次买卖之后的 w + 1 天。

    然后给你每天的 买入,卖出,最多能买,最多能卖的股票数,分别为 AP, BP,AS,BS。


    思路
    看看数据范围,二位状态没多大问题,不妨就设 (f_{i, j}) 为第 i 天时,手中有 j 张股票的时候最多能赚的钱数。

    转移的话,有下边几种情况。

    1. 凭空买。

    (f_{i, j} = -ap_i imes jig(j in [0, as_i]ig)),挺好理解,就是这一天买了 j 张股票,前边几天没买也没卖。

    1. 不买也不卖。

    显然就是上前一天的状态, (f_{i,j} = f_{i-1, j})

    1. 在此基础上买股票。

    题目中说,两次交易之间至少间隔 w 天,当前是第 i 天,也就是说可以从 i - w - 1 转移过来,设第 i 天的时候一共有 j 个股票, 第 i-w-1 天的时候有 k 张股票,所以转移方程就是:

    [f_{i. j} = max_{j - as_i leq k < j} Big{ f_{i. j}, f_{i-w-1, k} - (j - k) imes ap_i Big} ]

    as 为今天最多能买多少股票,应该很好理解。

    1. 在此基础上卖股票。

    和上一个差不多,但是卖股票,钱要加上 (k - j imes bp_i),所以转移方程就是:

    [f_{i. j} = max_{j < k leq j + bs_i} Big{ f_{i. j}, f_{i-w-1, k} + (k - j) imes bp_i Big} ]

    这样你就有 60 分的好成绩了。


    考虑如何单调队列优化,我们把上边的式子拆开看看。

    [egin{aligned} f_{i, j} &= max_{j - as_i leq k < j} Big{ f_{i. j}, f_{i-w-1, k} - (j - k) imes ap_i Big} \ f_{i, j} &= max_{j - as_i leq k < j} Big{ f_{i. j}, f_{i-w-1, k} - j imes ap_i + k imes ap_i Big} \ f_{i, j} &= max_{j - as_i leq k < j} Big{ f_{i. j}, f_{i-w-1, k}+ k imes ap_i Big} - j imes ap_i end{aligned} ]

    前边那个 max 可以单调队列优化掉,第四种转移的时候同理。

    code
    loj

    围栏

    简化题意
    rua
    自己看


    可以先将其按照 (s_i) 排序,使我们能够按照一定的顺序进行DP。

    (f_{i, j}) 表示前 i 个工人中,已经涂了前 j 块的最大价值,可以有不涂的。

    有两个非常明显的转移方程,即:

    • (f_{i, j} = f_{i-1, j}), 第 i 个工人可以不干,所以可以承接上一个人的状态。
    • (f_{i,j} = f_{i. j - 1}),第 j 块可以不涂嗯。

    根据题意,该工匠的粉刷长度不能超过 (L_i),且必须粉刷 (s_i) 。不妨设一个 k ,令 (f_{i-1, k}), 为上一个人粉刷的最大价值,可以发现 (j - l_i leq k leq s_i leq j),并且 (j - k leq L_i) ,有下面的转移方程。

    [f_{i, j} = max_{j - l_i leq k leq s_i leq j} Big{ f_{i-1.k} + p_i imes ig(j - k ig)Big} ]

    化简上边的式子,按照这么一个思路,我们枚举 (j,k) 的时候可以把带着 i 的都看做一个常量,想办法把 (j,k) 分开就行了。

    [egin{aligned} f_{i,j}&= max_{j - l_i leq k leq s_i leq j} Big{ f_{i-1.k} + p_i imes j - p_i imes k Big}\ f_{i, j} &= max_{j - l_i leq k leq s_i leq j} Big{ f_{i-1.k} - p_i imes k Big} + p_i imes j end{aligned} ]

    这样 max 里边都是 k 的,外边都是 j 的,只需要找出一个中间 max 最大就行了,可以发现,当 j 增大时上限不变,下限增大,不妨设两个决策 (k_1 < k_2 leq s_i - 1)

    因为 (k_2)(k_1) 靠后,所以随着 j 的增加,(k_1) 一定会比 (k_2) 更早的从 ([j-L_i,S_i -1]) 中删除,而且如果满足 (f_{i-1,k_1} - P_i imes k_1 leq f_{i-1,k_2} - P_i imes k_2),那就说明 (k_1) 就是一个无用的状态。

    你会发现这个东西像极了单调队列,所以我们只需要维护一个 k 单调递增,(f_{i-1, k} - P_i imes k) 单调递减的队列就行。
    code
    AcWing

  • 相关阅读:
    关于C++顺序容器一致初始化的问题
    44. 通配符匹配(Wildcard Matching)
    76. 最小覆盖子串(Minimum Window Substring)
    72. 编辑距离(Edit Distance)
    首入大观园
    删除链表的倒数第N个节点
    目标和
    克隆图
    最长回文子串
    旋转矩阵
  • 原文地址:https://www.cnblogs.com/zzz-hhh/p/all_DP.html
Copyright © 2011-2022 走看看