DP 复习
DP 复习
本文写于2025.10.28,旨在复习做过的 DP 题目,以及启发的思想。
一、DP 概念
1. 概念
DP 是 Dynamic Programming 的简称,专指动态规划算法。
2. 性质
能用 DP 求解的问题,必须满足如下三个性质:
-
最优子结构
指该问题的最优解依赖于其子问题的最优解。
上述信息同时表明,具有最优子结构的问题可能也适用贪心算法。
-
无后效性
已经求解的子问题,不依赖于后续将要求解的子问题。
-
子问题重叠
如果某些子问题是相同的,那么可以通过记忆化存储的方式避免求解相同的子问题。
3. 求解思路
- 定义状态:将问题划为多个阶段,每个阶段对应若干子问题,归纳这些子问题的共有特征,并以某种统一的方法来表示这些子问题(即状态);
- 值得注意的是,一些复杂的 DP 问题可能需要使用多个状态定义模式;
- 推式子:寻找状态之间的依赖关系,即状态转移方程,通常是一个严谨的数学表达式;
- 一个问题的状态转移方程可能不止一个,这导致多个状态转移方程的触发条件有别;
- 找边界:声明状态转移时的起点;
- 边界通常与极小数(如0)有关;
- 找答案:弄清答案对应哪个状态;
- 按顺序求解。
4. DAG 与 DP
DAG,即有向无环图。
如果把每个状态对应图上的一个节点,决策对应连边,这样问题就转变成了在 DAG 上寻找最长/最短路的问题。
二、贪心与 DP
我们在解决一道求最优方案的问题时,常会陷入一种纠结:这道题适用贪心还是 DP?接下来,我将对贪心算法与动态规划算法进行细致区分。
1. 相同点
都利用了历史信息进行求解,即利用之前已经计算过的答案更新下一步的答案。
2. 不同点
(1)贪心
贪心算法的实质是,每一步都进行当前局面下最优的决策,使用局部最优推导全局最优,不存在回溯和反悔。
贪心算法的每一步都包含上一步的最优解,这导致其利用的历史信息有且仅有上一步的最优解。
(2)DP
由于子问题重叠和最优子结构的性质,DP 可以使用多个局部最优解求解全局最优解。
三、例题
1. 线性 DP
(1)P1028 [NOIP 2001 普及组] 数的计算(常规线性 DP)
如果把 n 称为生成因子,我们可以定义 dp[i] 表示以i为生成因子的合法序列总数。
-
对于偶数
i:dp[i] = dp[i-1] + dp[i/2];-
dp[i-1]:不选择i作为起点,继承前一个数的方案数;下一个可加数的最大值为(i-1)/2,严格小于i/2; -
dp[i/2]:选择i作为起点,那么下一个可以加的数最大是i/2,补全第一种决策转移的空缺;
-
-
对于奇数 i:
dp[i] = dp[i-1];- 因为
i是奇数,i/2向下取整,所以方案数与i-1相同。
- 因为
例题:
-
考虑影响决策的因素:①序列长度②公差;于是将这两个因素融入状态定义,定义
f[i][d]表示长度为i的、公差为d的等差序列个数。考虑
f[i][d]的转移来源。我们知道,长度为i的等差序列来源于长度为i-1的等差序列后添加一个数值等于a[j]+d的元素,即f[i][d]=Σf[j][d]+1,其中1<=j<i且a[i]=a[j]+d。求 DP 问题的状态转移方程时,一定要思考:“我从哪儿来。”
使用一个极大数
D(一般为数值上限的一半)施加于每个数组下标上,可以让数组实现负数下标。因为有一些不存在的
d被枚举,所以上面的解法超时。考虑仅转移实际存在的d即f[i][a[i]-a[j]]=Σf[j][a[i]-a[j]]+1,其中j<i。答案为所有
f[j][a[i]-a[j]]在状态转移过程中的累计求和。由此,需要说明:求解 DP 问题时,一定要看答案所求的是什么量。
-
考虑到这道题最少的加法次数跟字符串长度和目标和有关,于是融入状态定义:
f[i][s]表示字符串前i位达到目标和s的最少加法次数。f[i][s]=min(f[j][s-a[j+1->i]])+1,其中j<i。如下图所示。-------- + ======= 1 j j+1 ia[j+1->i]指的是原数字从第j+1位到第i位所分离出的数值。这就启示我们要预处理。因为我们需要的是原数字从第
j+1位到第i位所分离出的数值,所以j索引越大,该数值越小。因此我们需要让j反向遍历。特别地,如果a[j+1->i]>n,直接终止遍历。在求解 DP 问题时,若遇到下标位置处发生减法运算(如背包 DP 问题),一定要判断这个下标会不会为负数。在最新的 C++ 标准中,负数下标不会报错,而会返回一个特殊的值,这会干扰我们的调试。
-
首先这道题的部分分非常好得:定义
f[i]表示枚举到第i位时的合法最长序列,然后按照 \(O(n^2)\) LIS 的思路推式子即可。让我们思考优化。题目中说,\(a\) 的一个子序列 \(b\) 是合法的,当且仅当 \(\forall b_i \& b_{i-1} ≠ 0\)。这句话可以被理解为,一个数
cur_b可以被接在某一个序列后面,当且仅当这个序列最后一位的二进制的某一位与cur_b均为1。所以我们可以定义:
f[j]表示以第j位为1的数字结尾的最长子序列长度。对于每个输入的数字
a:- 检查当前数字
a的每一位; - 如果
a的第j位为1,说明它可以接在以第j位为1的数字结尾的子序列后面; f[j]+1表示接上后的新长度;- 将上面的
f[j]+1取最大值,得到当前数字能形成的最长子序列长度。
然后,对于当前数字
a的所有为1的位,更新f[j]为当前数字形成的最长子序列长度。这样后续数字如果与当前数字有公共位,就能接上这个子序列。这样,我们巧妙利用位运算的特性,将时间复杂度降到了 \(O(n)\) 的。
在解决 DP 问题时,一定要先想最朴素、最暴力的 DP 做法,哪怕它的时间复杂度很高。正如那句话所说,“过早的优化是万恶之源。”
- 检查当前数字
-
高度对于这道题一点用也没有,只是用来排序的。
一定要辨清题中哪些条件是没用的。
由于序列长度和去掉书本的数量都会影响状态,故将它们融入状态定义。
思维转换:去掉
k本即留下n-k本。f[i][l]表示前i本书留下l本获得的最小不整齐度。考虑决策来源:对于前
j(j<i)本书,如果留下l-1本,而[j+1,i-1]内的书全都不留,那么产生|a[i].w-a[j].w|的贡献,可以推出状态转移方程:f[i][l]=min(f[i][l],f[j][l-1]+abs(a[i].width-a[j].width)),其中l从1到min(i,k)。 -
由于从不同方向走过来的方案会影响下一轮的决策,导致贡献有所不同,所以开一个第二维,存”是从左边走过来的还是从右边走过来的“。然后分讨即可。
如果存在一个形如"...是否..."或仅有两种可能性的的状态维度,直接将其作为一个布尔类型,融入状态定义。这个技巧经常会用。
(2)P1164 小A点菜(背包)
0-1 背包问题。我们可以认为 0-1、完全、多重背包问题都属于一维线性 DP 问题。
每个物品只能选一次,所以考虑倒序遍历第二层循环。
定义 f[j] 表示背包容量为 j 时的方案总数。对于第 i 个物品,如果选它,那么背包现在所装物品的容量总和会比原来少 a[i],即 f[j] 的状态完全依赖于 f[j-a[i]]。所以可以推导出状态转移方程。
例题:
-
每个纸币看作一个物品,体积为其面值,不考虑其价值,跑完全背包求方案数。
-
将每个物品的体积和价值都看作其体积,定义
f[j]表示j容量下最多能装的体积,然后跑 0-1 背包板子,输出V-f[v]即可。 -
P2340 [USACO03FALL] Cow Exhibition G
将智商看作体积,情商看作价值,加入负数偏移量(很重要的技巧,可以使负数下标变得合法),跑 0-1 背包板子。
-
P1466 [USACO2.2] 集合 Subset Sums
对于一个 \(1—n\) 的连续整数集合 \(A\),如果要把 \(A\) 分成等和的两集合 \(M\) 和 \(N\),那么对于每一个 \(M\),对应的 \(N\) 都是确定且唯一的。所以我们只需要求 \(M\) 集合的个数即可。
同时又注意到,\(ΣM=ΣN=\frac{ΣA}2=\frac{\frac{n(n+1)}2}2=\frac{n(n+1)}4\)
所以我们只需要选取集合 \(A\) 中的若干元素,使得它们的和等于 \(\frac{n(n+1)}4\) 即可。
于是对于每个元素,只有选或不选两种选择,那就是 0-1 背包求方案数,将每个数的数值看作其价值。
注意,当当 \(\frac{n(n+1)}2\)为奇数时,\(\frac{n(n+1)}4\) 必定带有小数部分,而整数部分凑不出来小数部分,所以这种情况没答案,直接输出 \(0\)。
-
P2851 [USACO06DEC] The Fewest Coins G
本题分为两步:①拿钱;②找钱。
由于我们有一套货币系统,而且我们拥有的特定种类的货币数量是有限的,所以拿钱时,跑多重背包(甚至不需要二进制优化),定义
f[j]记录支付j元钱所需的最少纸币数。找钱时,由于钦定售货员的纸币数是无限多的,所以我们使用完全背包,使用
g[j]记录找回j元钱所需的最少纸币数。根据题意,求的是
f[j]+g[j-t]的最小值。这道题细节很多,尤其是边界问题。
-
P2736 [USACO3.4] “破锣摇滚”乐队 Raucous Rockers
CD数目是有限的,而且根据题意,最后一张CD所存的乐曲时长,会影响当前所枚举的乐曲的存储方案。
不妨将上面两个因素融入状态定义:
f[j][k]表示前j张光碟,最后一张光碟存储k分钟乐曲,所能存储的乐曲最大数。对于第
i个乐曲,有三种选择:- 不选,
f[j][k]=f[j][k]; - 选,但最后一张放不下当前枚举的乐曲,所以需要新开一个CD,有
f[j][k]=f[j-1][T]+1; - 选,但最后一张能放下当前枚举的乐曲,有
f[j][k]=f[j][k-a[i]]+1。
上面三种情况取最大值。
- 不选,
-
通过这道题,我深刻体会到打暴力的重要性。首先写一个暴力背包(只能得到 20 pts),将每个数字的数值作为价值,木棍数作为体积,跑完全背包。
然后输出前若干项的答案,发现一些明显的规律,然后按照规律输出即可。
(3)P1020 [NOIP 1999 提高组] 导弹拦截(LIS)
此题是 LIS(最长上升子序列)板子。
-
对于 LIS \(O(n^2)\) 复杂度的解法:
- 使用一维状态定义
f[i]表示对于前i个数的 LIS 长度。 - 枚举到
i这个位置时,前一个决策应当保证j严格小于i且a[j]严格小于或等于a[i]且f[j]为最大。因此,我们有f[i]=max(f[j]+1)。 i与j的双重遍历导致复杂度变为 \(O(n^2)\) 的。
- 使用一维状态定义
-
对于 LIS \(O(n\log n)\) 的做法:
- 定义
f[i]表示长度为i的 LIS 的末尾元素的最小值。容易证明,该数组始终保持严格递增; - 在
f数组中找到第一个大于或等于a[i]的位置,并更新该位置的值为a[i]; - 在每次循环中更新答案。
- 定义
LIS 的优化还有很多种方式(如树状数组优化)。
例题:
-
维护一个正序的 LIS 和一个倒序的 LIS,找最大值即可。
(4)P1439 两个排列的最长公共子序列(两个排列的 LCS)
由于给定的是两个排列,所以可以建立 a 序列的一个键值映射表 ind,其中 ind[i] 表示键值 i 在 a 序列中出现的位置。
因为容易证明在位置序列中的递增子序列恰好等价于在两个原序列中的公共子序列,所以该问题转变成在映射序列内(即 ind[b[1]], ind[b[2]], ... , ind[b[n]] )求 LIS 的问题。
(5)P2679 [NOIP 2015 提高组] 子串(双序列一维线性 DP)
录入这道题旨在说明:凡是遇到两个或多个序列的 DP 问题,一般都要在状态定义中融入所有序列目前枚举到的下标。
格外注意:LCS 问题之所以不需要开二维数组,正是因为其”排列“的特殊性质,导致两个序列的键值能一一对应。
定义 f[i][j][p][0/1] 表示 a 串枚举到第 i 位,b 串枚举到第 j 位,且恰好在 a 串中使用 p 个子串,位置 a[i] 是否纳入子串的方案总数。
枚举 i 和 j ,考虑两种情况:
- 当
a[i]==b[j]时:- 若使用字符
a[i]进行匹配,决策来源于:- 将当前字符纳入前一个子串,且使用前一个字符,即
f[i-1][j-1][p][1]; - 将当前字符纳入新的子串,且不使用前一个字符,即
f[i-1][j-1][p-1][0]; - 将当前字符纳入新的子串,且使用前一个字符,即
f[i-1][j-1][p-1][1]; - 将以上三者求和,转移至
f[i][j][p][1];
- 将当前字符纳入前一个子串,且使用前一个字符,即
- 若不使用字符
a[i]进行匹配,则对于前一个字符,只有选或不选两种选择;且b串的枚举位置不变,子串数目也不变,即f[i][j][p][0]=f[i-1][j][p][1]+f[i-1][j][p][0];
- 若使用字符
- 当
a[i]!=b[j]时,不能使用字符a[i]进行匹配,即f[i][j][p][1]=0;否则,第四维为0,那么有f[i][j][p][0]=f[i-1][j][p][1]+f[i-1][j][p][0]。
注意到第一维 i 依赖且仅依赖于 i-1 ,那么就可以使用滚动数组优化掉。值得注意的是,滚动数组不能优化多维这样的转移。
例题:
-
双序列问题,将两个串的状态融入状态定义。
定义f[i][j]表示字符串a的前i位经过增删改恰好变为字符串b的前j位所需的最少操作次数。
考虑f[i][j]的来源:
1.f[i][j-1]+1 将a第i位删除 op1
2.f[i-1][j]+1 增加a的第i位 op2
3.f[i-1][j-1]+(a[i]!=b[j]) a和b的最后一位是否需要匹配上 op3
只需求出三者最小值即可。
-
定义
dp[i][j]表示a字符串前i个字符与b字符串前j个字符匹配的最大相似度,后面基本一样了。
(6)P1854 [IOI 1999] 花店橱窗布置(输出方案)
题意:给你一个矩阵,每行只能选一个数,每列所选数字的列索引必须递增,求最大的贡献和。
这是很简单的二维 DP,但是题目要求你输出方案。这就要求我们在每次状态转移时用一个数组记录转移的前驱,最后递归输出这些前驱即可。
(7)P1004 [NOIP 2000 提高组] 方格取数(二维线性 DP)
和一维线性 DP 基本一致,只需要融入对于每一个点,融入两个维度的状态即可。
f[i][j][k][l]表示第一边走到点(i,j),第二遍走到点(k,l)取到数字的最大值
转移为四种走过情况的最大值,加上a[i][j]与a[k][l]的数字
注意特判:若i==k&&j==l则减去方格数字。
例题:
-
分层二维,将层数融入状态定义,然后分讨。
-
分层二维,将从上/从下转移而来的情形融入状态定义,然后分讨。注意倒序遍历的情况。
-
分层二维,将修改多少次
'?'作为第三维,分讨即可。
值得注意的是这道题的滚动数组优化。状态转移的第一维 i 最多依赖于 i-1 的决策,那么我们就可以用 0/1 表示所依赖的下标是 i 还是 i-1。
对于状态仅从当前决策和上一决策转移而来的转移方式,可以使用滚动数组优化,即,将所有的 i 替换为 i&1,将所有的 i-1 替换为 (i+1)&1 即可。注意,这种转移方式最多能优化一个维度。
(8)P1725 琪露诺(单调队列优化)
显然有 f[i]=max(f[j])+a[i]。注意到所求的最大值的区间长度是一定的,想到滑动窗口优化。用一个滑动窗口维护f区间内的最值即可。
例题:
-
P3572 [POI 2014] PTA-Little Bird
和 P1725 完全一致。
-
虽然并非 DP 问题,但是仍然具有代表性。
题意简述:给你一个环,权值可正可负,求一个环上的路径,使得其权值和最大。
首先我们要断环为链,求出双倍序列的前缀和。
对于 \(\forall x \in [1,2n]\),确保长度不大于 \(n\) 的前提下,维护一个长度为 \(n\) 的、基于前缀和数组的滑动窗口;
找出最小的前缀和,然后作差,再取最大值。
注意特判:如果权值全为负,输出最大的那个负数。
(9)P6394 樱花,还有你(前缀和优化)
设f[i][j]表示走到第i格,恰好收集到j朵樱花的方案数。
显然f[i][j]+=f[i-1][j-k],其中1<=k<=a[i]。
注意到两个事情:一、i在转移过程中没用,可以滚掉;二、所累加的f[j-k]中的k是连续的,所以累加的值也是连续的,可以用前缀和优化。
开一个前缀和数组s[j]表示f[1]到f[j]的和。
状态转移方程显然为f[j]=s[j]-s[j-a[i]-1],当且仅当j>a[i];如果不,则f[j]=s[j]。
每次循环枚举i的时候,都要将f[n]累加到ans中,并重新预处理前缀和数组。
例题:
-
注意:贡献转化
f[i]=min(f[i],f[j]+1)当且仅当1<=j<=i且序列j~i可以分到一个机房如果1的贡献是
1,2的贡献是-1,那么可以用计算前缀和,当且仅当abs(s[j]-s[i-1])==i-j+1 || abs(s[j]-s[i-1])<=m
(10)P8742 [蓝桥杯 2021 省 AB] 砝码称重(布尔类型 DP / bitset)
DP数组可以用布尔类型记录状态:用f[i][j]表示枚举到第i个砝码时,能否称到重量j。
i枚举砝码序号,j从1枚举到所有砝码的质量总和,即所能称到的最大重量。
边界:f[i][w[i]]=1,因为对于每个砝码i,只放自己,肯定能称到自己的重量。
状态转移必须分类讨论。
- 当第
i个砝码没放时:f[i][j]=f[i-1][j]; - 当第
i个砝码被放到右盘时:f[i][j]=f[i-1][j-w[i]],但仔细想想,考虑到前一个砝码被放到左盘或右盘的情况不同,所以加上绝对值,得到f[i][j]=f[i-1][abs(j-w[i])]; - 当第
i个砝码被放到左盘时:f[i][j]=f[i-1][j+w[i]]。
答案为f[n][j]的求和。
但是这种方法太麻烦了。因为我们状态定义的的总是一些布尔量,于是我们使用 bitset。
开一个bitset b,其中b[j]表示重量j能否称到。
边界显然是b[0]=1。
对于每一个w[i],b的每一位都应左移w[i]位,表示某一位所代表的砝码称重方案再加上第i个方案所构成的新方案所能称量的重量。
然后b的每一位再右移w[i]位,表示放在左盘,减去对应砝码的重量。注意左右位移的运算需要分别循环操作。
最后把重量0算上了,得减掉。
例题:
-
先把数组从小到大排序,然后遍历每一种货币。如果使用前
i种货币能够凑到i后面(不包含i)的货币面值,那么这个被凑到的货币面值是无用的。维护一个答案变量
ans,初始值为面值种类数n,若出现一个无用的变量则自减。
(11)P1450 [HAOI2008] 硬币购物(容斥原理)
这题可以先处理完全背包,然后用凑出金额s的不加限制的总方案数减去不合法的方案数。这就涉及到容斥原理。
定义f[j]。显然f[j]可以从所有的f[j-c[i]]转移而来,并累加求和,相当于f*1的乘法原理。
所以如果第i种硬币使用超限,那么一定用了大于等于(d[i]+1)个i种硬币,相当于状态从f[j-c[i]*(d[i]+1)]转移而来。
超限时,硬币数大于d[i]+1的情况则一定会被包含在f[j-c[i]*(d[i]+1)]里,所以我们只需要考虑f[j-c[i]*(d[i]+1)]即可。
但是我们想到,同时计算四种金币的不合法方案数可能会有算重的情况,于是我们考虑容斥原理(可以画Venn图来形象理解):不加限制的总方案数 - (一种金币超限的方案数 - 两种金币超限的方案数 + 三种金币超限的方案数 - 四种金币超限的方案数) = 答案(即奇加偶减)。
后面括号里的运算结果就是所有不合法的方案总数。
例题:
-
题意简述:给你一个矩阵,每行至多选一个数,每列选数的个数不能超过所选数字个数总数的一半,求选数的总方案数。
容易发现一个性质:如果某一种方案不合法,那么必定有且仅有一列的选数个数超过数字总数的一半。
该性质可以反证法证明:假设存在一种不合法的方案,使得这种方案中存在两列的选数个数超过数组总数的一半,那么显然地,所选数字的总个数一定大于矩阵中所存在的数字总数。所以这种不合法的方案不存在。进而该性质得以证明。
那么,合法方案总数等于:每行选至多一个的总方案数(记为<1>),减去每行选至多一个但某列选了超过一半的方案数(记为<2>)。
接下来考虑<1>。
定义
f[i][j]表示前i行,在每行选至多一个的条件下,总共选了j个数的总方案数(即每列选多少个都行的方案)。如果第
i行不选,那么所选数字的个数不变,即:f[i][j]=f[i-1][j]如果选,那么可以选这一行的任意一个数,那么有:
f[i][j]=f[i-1][j-1]*a[i][1] + f[i-1][j-1]*a[i][2] + ... + f[i-1][j-1]*a[i][m]进一步地,不妨维护一个预处理数组
s[i],表示第i行所有数的和。那么我们有:上式=f[i-1][j-1]*s[i]综上,把上两式求和,有:
f[i][j]=f[i-1][j]+f[i-1][j-1]*s[i]因为我们求的是前
n行的总方案数,那么选数的个数在区间[1,n]内,即<1>式的结果是Σf[n][1->n]。这一步骤的时间复杂度是 \(O(n^2)\) 的。接下来考虑<2>。
定义
g[i][j][k]表示前i行,在每行至多选一个的条件下,在所枚举的第l列(不将其融入状态定义中,因为它没有状态的转移性)中选了j个数,而在矩阵内其他地方选了k个数的总方案数。对于第
i行的数,可以不选任何一个数,那么有:g[i][j][k]=g[i-1][j][k]对于第
i行的数,如果所选的那一个数位于第l列(即a[i][l]),那么:g[i][j][k]=a[i][l]*g[i-1][j-1][k]对于第
i行的数,如果所选的那一个数不位于第l列(即非a[i][l]),那么:g[i][j][k]=(s[i]-a[i][l])*g[i-1][j][k-1]把上三式求和,有:
g[i][j][k]=g[i-1][j][k]+a[i][l]*g[i-1][j-1][k]+(s[i]-a[i][l])*g[i-1][j][k-1]重新阅读题面,发现当且仅当
j>k时,g[n][j][k]不合法。所以不合法的方案总数即为Σg[n][⌊k/2⌋->k][k]。它的时间复杂度是 \(O(mn^3)\) 的。总的来说,答案为
Σf[n][1->n]-Σg[n][⌊k/2⌋->k][k]的值。这一做法能得到84pts。Re:我们考虑优化
g的转移。我们知道,对于一个不合法的方案,总有
j>k,即j-k>0。所以我们只需要维护j与k的差(记为d),当d>0时不合法,否则合法。值得注意的是,d可以为负数。重新定义
g[i][d]表示前i行,第l列所选数字个数与其他列所选数字个数之差为d时的方案总数。根据上面的转移方程,显然有:
g[i][d]=g[i-1][d]+g[i-1][d-1]*a[i][l]+g[i-1][d+1]*s[i][l],其中d为n-i -> n+i。不合法的方案总数即为
Σg[n][n+(1->j)]。注意此时
g数组的第二维应开2*MaxM大小。
2. 区间 DP
1. P1775 石子合并(弱化版)(区间 DP)
对于一类区间决策的问题,可以使用区间 DP 解决。区间 DP 的状态定义一般要融入左端点和右端点。
定义 f[l][r] 表示合并序列区间 [l,r] 的最大贡献。
对于区间 [l,r],可以将其分成区间 [l,k] 以及区间 [k+1,r],然后再把这两个区间内的子段和累积到贡献里。这里,我们将 k 叫做区间断点。
一般地,我们在循环最外层遍历区间长度 len,其最大值为 n;再遍历区间起始顶点 i,然后再在循环内计算出来区间右端点 j,这样可以避免计算重复区间。在这些循环里面,再遍历一个区间断点 k。总的来说,时间复杂度是 \(O(n^3)\) 的。
很容易推出状态转移方程:f[l][r]=min(f[l][k]+f[k+1][r])+sum[l][r],其中 sum[l][r] 表示区间 [l,r] 内数组元素的和,可以 \(O(n^2)\) 预处理得到(使用前缀和也行)。
例题:
-
与字符串结合。状态定义很好想,然后分类讨论
a[l]与a[r]相同与否时的转移方式。 -
需要一个逆向思考:定义在区间
[i,j]内,从给定答案涂成同色所需的最少涂色次数为f[i][j]。然后仍然分类讨论
a[l]与a[r]相同与否时的转移方式。因为我们定义的是同色,所以最后还需要染色一次,才能让它变为空。所以答案是
f[1][n]+1。 -
这道题的题目名就具有迷惑性,会让人认为这是一个二维 DP,实则不然。注意到每一行的决策转移模式是完全独立、不会互相影响的,因此只需要求出每一行的最优决策,然后再累积求和即可。
左右取数,显然区间 DP。对于一个区间
[l,r],可以取它的左边,也可以取它的右边,这就产生两种转移方式。注意贡献因子(即题中所说的 \(2^i\))的计算,应为1<<(n-len+1)(可以推导)。 -
一个分层区间 DP。对于每个人,总有两种操作,往左放或往右放。
定义
f[i][j][0/1]表示区间[i,j]中,第i个人从左边进来 / 第j个人从右边进来的方案数。然后分四种情况讨论:左左、左右、右左、右右,推式子即可。
答案需要累积第三维的两种状态。
-
P2890 [USACO07OPEN] Cheapest Palindrome G
状态定义很常规。我们考虑一个区间
[l,r],其子区间[l+1,r]或[l,r-1]。对于那个被排开的字符,可以把它加到一侧,也可以从另一侧删除,这就产生了两种决策。特别地,当左右两侧字符相同,且区间长度不为
2时,直接转移即可。 -
先写一个函数
cnt返回数x的位数,再写一个函数check判断某区间以某步长为循环节长度是否能压缩。状态定义是常规的。
讨论子串
[l,r]是否能被折叠的两种情况。通常情况下,
f[i][j]=min(f[i][j],f[i][k]+f[k+1][j])。当序列能被压缩时,即
len%slen==0,其中len为序列长度,slen为循环节长度时,有f[i][j]=min(f[i][j],cnt(len/slen)+f[i][k]+2)。 -
f[i][j][c]表示原串的子串[i,j]能否被字符c替换。边界:
f[i][i][s[i]]=1(自己肯定能替换自己)。将子串
[i,j]拆分成两部分,如果这两部分都能分别替换自己的字母且这两个字母能被替换成一个字母,那么这两部分合起来的部分也能被替换成一个字母,即f[i][j][c]|=f[i][k][x] && f[k+1][j][y] && vis[x][y][c]。其中
vis[x][y][c]表示:如果字符x和y可以合成字符c,返回真,否则为假,可以预处理得到。
2. P1880 [NOI1995] 石子合并(环形 DP)
这道题和弱化版基本相同,只是序列变成了环。这要求我们把原序列复制两边,变成一个双倍序列(这种操作称为断环为链)。在这个双倍的序列上就可以取到所有环上能取到的序列了。
注意答案,要统计每一个长度为 n 的序列的贡献最值。
例题:
首先断环为链。
然后注意到数组元素值可能为负数,乘法运算会干扰最大值的判断,所以要维护区间操作的最小贡献数组 g,分类讨论四种乘法运算的情况(最大/最小 × 最大/最小)。
第一问显然是 f[i][i+n-1] 的最大值。而第二问则是当 f[i][i+n-1] 等于第一问答案时,所有数组索引的升序排列,用一个数组维护即可。
3. 树上 DP
-
一个节点如果被选,那么它的孩子就不能被选。
对于每个节点,只有选或不选两个操作,因此将其融入状态定义:
f[u][0/1]表示节点u选/不选情形下,u子树的最大贡献。然后对于第二维分讨即可。
-
节点的转移没有任何限制,直接定义一维的 DP 数组即可。
但是当
f[v]为负时,其贡献对状态转移没有任何积极作用。所以当且仅当f[v]为正时,我们才进行转移。 -
定义
f[u]表示u子树中,从u出发的最长路径。显然,状态来源于它的子节点,即f[u]=max(f[v]+g[u][v])。树的直径,实质上是某个节点出发的两条不同路径的最大值。因此,只需在状态转移的过程中,更新
ans=max(f[u]+f[v]+g[u][v])即可。 -
题意简述:给定一棵树,可以选择一些节点,使得与这些节点直接相连的边被点亮;要使所有的边都被点亮,求最少选择的节点数。
对于每个节点,都有选或不选两个情况,这就和舞会题一样了。状态转移稍微改一下就行。
-
子树的根和保留的树枝个数都会影响状态,所以定义
f[i][j]表示以i为根的子树,保留j根树枝,能留住的最多苹果数。分类讨论:考虑选左子树上的边与选右子树上的边两种决策,可以同真同假,也可以一真一假,那么它们实际可以构成一个背包,因此有三种转移:
-
左右子树的边都保留,有
f[i][x+y+2]=max(f[i][x+y+2],f[l][x]+f[r][y]+g[i][l]+g[i][r]) -
仅保留左子树的边,
f[i][x+1]=max(f[i][x+1],f[l][x]+g[i][l]) -
仅保留右子树的边,
f[i][y+1]=max(f[i][y+1],f[r][y]+g[i][r])
-
-
树上背包问题的很好体现。
状态定义和上一道题大体思想一致,这里不说了。
以节点
u为根的子树上选j个节点,相当于以节点u的各个孩子v为根的子树上,选k个节点的状态值,加上以节点u为根的子树上选j-k个节点的状态值。这题输入的是一片森林,所以我们构造一个超级源点
0(学分为0),统领整片森林。注意构造完超级源点之后,必须将m变量自增1,因为多建了一个点。 -
题意简述:给定一个带边权的树,每个叶子节点有一个权值。对于每个叶子节点,它对答案的贡献等于其点权与它到根的路径上的所有边权和的差值。求最大的叶子节点数,使得答案非负。
注意到这道题所选节点数和子树的根都会影响状态,所以融入状态定义。
定义
f[u][j]表示以u为根的子树,选取j个叶子节点,所获得的最大利润,也就是上面说的差值。这里的利润是指:收入-成本,即叶子节点支付的钱数总和与路径边权的差值。
其转移一定来源于其子树根的子节点。
u子树选了j个子节点,可以分割为v子树选了k个子节点和u子树选了j-k个子节点,再获得-g[u][v]的消极贡献。当利润大于
0时,则证明不亏本,所以可以作为答案使用。 -
仍记得艺术节当天这道题硬控我一个小时多。题意:保证给定的树严格拥有以下性质:
- 每个非叶子节点都有两个子节点;
- 对于每个非叶子节点,要么修它的左子边,要么修它的右子边,即不能两者都修;
- 每个叶子节点到根的贡献由且仅由三元组 \((a,b,c)\) 决定。
本题的输入方式需要仔细斟酌并处理细节,这里不阐述了。
注意到,未修缮的左边或右边的数量,会产生不同贡献,所以我们将它们融入状态定义。
因为贡献仅由叶子节点产生,所以可以定义
f[u][i][j]表示在u子树中,从u到根的路径上存在i条未修缮左边,j条未修缮右边时,u子树中所有叶子节点产生的最小贡献和。然后进行分讨:①当
u为叶子节点时的情况;②当u非叶子节点时的情况,分为两种子情况:a. 修缮左边,则多出来一条未修缮的右边;b. 修缮右边,则多出来一条未修缮的左边。优化:注意到对于每个节点
u,有效的状态转移仅仅来源于u+1和u+2两个节点,这样可以大大减少递归次数,避免RE。 -
注意下列性质:
-
对于任何两个节点,都存在且仅存在一条通路,这启示我们本题中的图实际上是一棵树。
-
对于树上的任何一个节点,都可以将此节点作为根节点,重新建一颗新树。所以本题中的激发器,即是树的根。
-
我们的操作只能将边权加
1而不能减。那么容易证明,对于一列边权,想使其所有元素都相等,最少操作次数即为该序列内元素的最大值与每一元素差值的求和。
维护一个数组
tim[u]表示让u子树达到时态同步的操作次数最少时,从u到任意一个叶子节点需要的时间。根据第三条性质,有tim[u]=max(tim[v]+g[u][v])。再定义
f[u]表示u子树达到时态同步的最少操作次数。tim[v]+g[u][v]为v到u本来的权和,其与tim[u]的差值tim[u]-(tim[v]+g[u][v])正为u到v的操作次数。然后求和,有f[u] += f[v]+(tim[u]-(tim[v]+g[u][v]))。其实正常的思路应当是先定义出数组
f的,但是我们在进行f的转移过程中,发现需要用到tim数组所表示的量,所以才定义出了tim数组。这里,tim数组更像一个预处理,而非 DP。 -
-
数据范围为
3e5,启示我们不能开二维DP数组,最好开一维。定义
f[u]表示u子树内,以u为头的最长毛毛虫的节点数最大值(并非题目所求答案)。有时,DP 数组不一定要定义成答案所表示的量。
定义
deg[u]表示u节点的度数,这是可以预处理的。当
u为叶子节点时,f[u]=1,否则f[u]=f[v]+deg[u]-(u!=1)(因为根节点没有父亲,不用减去1)。对于每个节点,考虑一个贪心:找出其子节点的最优解(
f[v1])和次优解(f[v2]),加起来就一定是最优解。然后对于
u拥有子节点的数目进行分讨:u没有子节点,ans=deg[u]+1;u有一个子节点,ans=f[v1]+deg[u];u有两个子节点,ans=f[v1]+f[v2]+deg[u]-1。
-
可以默认所有点刚开始都是白点,操作为:把某一点染成黑点。
这样的话,状态定义可以设
f[u][j]表示u子树内,染了j个点,所获得的最大收益。首先预处理出
siz数组,然后根据计数原理,考虑边 \((u,v)\) 的流量,则存在两个贡献:g[u][v]*k*(m-k)表示:染成黑色的点,两两之间经过边 \((u,v)\) 产生的总贡献。g[u][v]*(siz[v]-k)*(n-m-siz[v]+k)表示:染成白色的点,两两之间经过边 \((u,v)\) 产生的总贡献(一个点不是黑就是白)。 -
P4084 [USACO17DEC] Barn Painting G
题意简述:给定一棵树,可以把节点染成
0/1/2三种颜色,互相连通的两点的颜色不能相同,求有多少种染色方案。定义很显然,把子树的根和根的颜色融入状态定义:
f[u][c]表示以节点u为根的子树,把节点u染成颜色c的方案总数。因为一个节点只能被染一种颜色,所以当某个节点被指定上色后,它染另外两种颜色的方案数为
0。然后按照染色规则转移即可。根据计数原理,有
f[u][c] = f[u][c] * Σf[v][d],其中颜色d与颜色c严格不相同。答案需要对
c求和。
4. 状压 DP
例题:
-
可以用一串二进制数存储每一行国王的状态,然后根据国王吃子的原则判断状态是否可行即可。
因为国王最多影响一行之外,所以状态定义之中只需要传一行的状态即可,即定义
f[i][j][s]表示已经放了i行,用了j个国王,第i行的状态为s的放置方案数。然后枚举先前的状态和现在的状态,再枚举国王个数,简单写一个转移即可。
-
与上一题基本类似,但没有使用炮兵数量的限制,所以不用往状态定义里传炮兵数量这个参量了。
但是炮兵的攻击方式会影响两格之外,所以我们在状态定义中传两行的状态,然后按照炮兵吃子的规则进行转移即可。注意炮兵只能放在指定的格子内。
这里推荐把所有的合法状态存到一个数组里,便于查询。
-
P1879 [USACO06NOV] Corn Fields G
与国王题基本完全一样。
-
旅行商 DP 问题。
首先使用两点间距离公式建图,然后使用状压记录走过的点。
-
如果枚举每一个可能的排列,去判断是否合法,肯定会超时。注意到x+y+z<=17,故考虑状压DP。
直接做会算重,所以考虑容斥。
我们每新枚举一个数位的时候,都会产生
(原来排列的位数+1)个新的子段,这就产生了新的子段和。那么只需要判断这些子段和里是否有连续的x、y、z即可。所以我们维护后缀和,这样每加进来一个新数,都会使得后缀和全部发生变化,这样就实现了统计所有子段和的功能。因为直接统计容易算重(
x、y、z可以被任意拆解,导致组合出连续的和为x、y、z的子段方案较多,进而一个合法的方案会被算多次),所以定义f[i][st]表示枚举到第i位 ,后缀和状态为st时的不合法方案总数。其中,st的第j位若为1,表示后缀和中存在j这个数。显然,最大状态为
(1<<(x+y+z))。因为数据范围限制,所以我们只需要判断st的1~17位。多余的位虽然能凑到,但不予考虑。枚举每个数的数值
j,在枚举状态st时,加入数值j,那么目前的状态nowst:- 先将
st左移j位,表示后缀和数组全部加上j;
- 先将
-
再将
1左移(j-1)位,表示后缀和数组的最后一位添加了j这个数;
3. 再将结果与上(maxst-1),表示取范围内的二进制位,多余的位不予考虑。对于一个目前的状态
st,当且仅当:第z位、第(y+z)位、第(x+y+z)位为1时,表示存在一个以最后一位结束的连续的子段,使其可以被分成三段,且这三段的子段和分别为x、y、z时,状态合法,不能转移。当上述情况不满足时,状态不合法,可以转移。显然有:
f[i+1][nowst]=∑f[i][st]。答案为
10^n-∑f[n][st]。记得取模。快速幂也要取模!

浙公网安备 33010602011771号