重拾 DP(一):区间类 DP 和划分类 DP
__biu_biu_biu__
·
·
算法·理论
零、写在前面
最近几场比赛总会被 DP 干爆,我深深的认识到了自己的 DP 能力与做其他类型的题目的能力相差甚远,为了让 CSP2026 考出一个好成绩,我决定重新学习 DP。
那么就让我们一起重新回顾 DP 以及它的奇技淫巧。
一、区间 DP
区间 DP 的复杂度一般为 O(n^3),但是可以用四边形不等式优化到 O(n^2),这个后面在讲。
一般来说,如果一个题的范围无法支持 n\ge 10^4,则这个题一般不是区间 DP。
区间 DP 的定义很简单,一般就是 dp_{i,j} 表示区间 [i,j] 的贡献。
区间 DP 的转移一般有两种:一种是枚举单个断点,还有一种是枚举两个断点。
对于枚举一个断点的情况,相当于两个段合并起来需要用的最小代价,转移为 dp_{i,j}=dp_{i,k}+dp_{k,j}+f(i,k,j) 或者 dp_{i,j}=dp_{i,k}+dp_{k+1,j}+f(i,k,j)。
对于枚举两个断点的情况,相当于就是相当于在一段内截取一个子段,然后剩下两个部分会产生一定的贡献,这种题目一般会在删除子段时剩下的会拼到一起的题目背景下出现,转移为 dp_{i,j}=dp_{i,l}+dp_{r,j}+f(i,l,r,j),此时的复杂度为 O(n^4)。
对于枚举两个断点的情况,会有一下几种特殊情况:
- 在子段两边截取相同的长度:dp_{i,j}=dp_{i+k,j-k}+dp_{i,i+k-1}+dp_{j-k+1,j}+f(i,j,k);
- 在子段中间截取 x 长度的子段 dp_{i,j}=dp_{i,k}+dp_{k+x+1,j}+f(i,j,k);
在转移的过程中,我们不能按枚举左端点再枚举右端点的顺序来进行,因为会出现 dp_{i,j} 需要计算但转移中的 dp_{k,j} 还未计算的问题。所以我们需要按长度从小到大遍历,再遍历起点,才能使得转移不漏且过程中的值均已求出。
for(len=1;len<=n;len++){
for(i=1;i+len-1<=n;i++){
int j=i+len-1;
//此时[i,j]就是需要去计算的区间
}
}
区间 DP 的题目的转移难点一般在 f 函数的定义上,下面我们举几个例题进行分析:
1、P1775 石子合并(弱化版)
有 n 堆石子,第 i 堆有 a_i 个,合并 i 堆 和 j 堆的代价是 a_i+a_j,每次只能合并相邻两堆,求合并为一堆的最小代价,1\le n\le 300。
考虑 dp_{i,j} 表示将第 i\sim j 堆合并为一堆时的答案。
我们思考要合并为一堆的前提条件是只剩了两堆,由于你合并过程是连续的(即不会存在合并了 i 和 i+2 堆却没有 i+1 堆的情况)所以我们可以枚举断点:将 i\sim k 堆和 k+1\sim j 堆合并后,在合并新的两堆。所以我们得到转移式:dp_{i,j}=\min\limits_{i\le k< j} dp_{i,k}+dp_{k+1,j}+\sum\limits_{i\le x\le j}a_x。
时间复杂度 O(n^4),可以使用前缀和优化,最终时间复杂度 O(n^3)。
2、P3102 [USACO14FEB] Secret Code S
定义一次加密操作为将字符串 S 的一个前缀或一个后缀加到 S 前面或者后面。求加密若干次(至少 1 次)后字符串为 S 的不同操作序列的个数,答案模 2014,1\le n\le 100。
考虑 dp_{i,j} 表示加密 S_{i\sim j} 的方案数。
我们分析 S_{i\sim j} 是怎么得来的:
- 没有经过任何操作,方案数为 1;
- 添加了一个前缀 T,则这个前缀 T 一定是 S-T 的前缀或者后缀,方案数为 \sum dp_{k,j},其中 k 要满足 S_{i\sim k-1}=S_{k\sim 2k-i-1} 或 S_{i\sim k-1}=S_{j+i-k+1\sim j}。
- 添加了一个后缀 T,则这个后缀 T 一定是 S-T 的前缀或者后缀,方案数为 \sum dp_{i,k},其中 k 要满足 S_{k+1\sim j}=S_{i\sim i+j-k-1} 或者 S_{k+1\sim j}=S_{2k+1-j\sim k}。
所以我们有转移 dp_{i,j}=1+\sum\limits_{S_{i\sim k-1}=S_{k\sim 2k-i-1}\lor S_{i\sim k-1}=S_{j+i-k+1\sim j}} dp_{k,j}+\sum\limits_{S_{k+1\sim j}=S_{i\sim i+j-k-1}\lor S_{k+1\sim j=S_{2k+1-j\sim k}}} dp_{i,k}。
如果你直接暴力匹配可以做到 O(n^4),在 n=100 的情况下勉强通过。
当然匹配也可以使用哈希算法,可以做到 O(n^3)。
3、P3146 [USACO16OPEN] 248 G
你可以将两个相邻且相等的元素 a_{i},a_{i+1} 合并为一个元素 a_i+1,求最后可能出现的最大整数。n\le 248。
考虑 dp_{i,j} 表示数 i\sim j 可以合并得到的最大整数。
但是这个东西会很不好合并,因为你根本无法确定你 i\sim j 中的最大值在头、中间还是结尾,这样的话你是无法合并的。
所以我们可以钦定 dp_{i,j} 表示 i\sim j 合并到只剩一个数时的最大整数。如果无法合并到一个数,则 dp_{i,j}=0。
所以这样我们就比较方便转移了:dp_{i,j}=\max\limits_{dp_{i,k}=dp_{k+1,j}>0} dp_{i,k}+1,否则 dp_{i,j}=0。
4、P4170 [CQOI2007] 涂色
每次可以将颜色涂一段,要求将模板涂成指定颜色的最小步数。1\le |S| \le 50。
考虑 dp_{i,j} 表示将 i\sim j 染成目标颜色的指定颜色的最小步数。
首先,我们染色区间省步数的方案一定是将一些颜色相同的区间一起染了,这个东西有一个前提:及不存在多个交叉的像这样省步数的区间染色,则我们相当于找最外层的区间,将其染色来减少染色步数,对于中间被染上的错误颜色,就相当于未染色考虑。
这个步骤相当于如果 s_i=s_j,则 dp_{i,j}=\min(dp_{i+1,j}+dp_{i,j-1}),否则 dp_{i,j}=dp_{i,k}+dp_{k+1,j}。这样一定可以不重不漏的枚举。
时间复杂度 O(n^3)。
5、P2466 [SDOI2008] Sue 的小球
有 n 个点,第 i 个点初始在 (x_i,y_i),每秒下落 v_i 的时间,你一开始在 x_0,每秒可以走 1 的距离,如果你在某个点的正下方,则可以获得这个点此时高度的价值,如果一个点的高度为负的,则这个点就消失了。你需要在获得所有点的情况下,获得的最大价值为多少,答案乘 10^{-3} 后输出,1\le n\le 1000。
由于获得点的价值不需要消耗时间,所以在三个点 l,x,r 满足 l \le x\le r 的时候,你肯定会按照 l\to x\to r 或者 r\to x\to l 的顺序去接。也就是说,你不会在有点可以获得价值的时候不获得,但不代表你不会去走回头路。
我们考虑按 x 排序,然后考虑 dp_{i,j} 表示将点 i\sim j 接完后能产生的最大价值。
但这样的话你是无法转移的,因为你根本不知道你现在在哪里,怎么走到下一个位置。
根据我们发现的性质,你是不会在 l\le x\le r 的时候停留在 x 的,我们扩展一下,发现对于点 [l,r],我们接完这些点一定只会停留在 l 或 r 上。
所以我们可以升维,变成 dp_{i,j,0/1} 表示在接完点 i\sim j 后停留在点 i 或点 j 的位置。
此时我们有另一个问题:你的贡献产生是需要依靠时间的,但是如果你的区间 DP 在加一个时间维度显然会爆炸,所以我们可以采用一种类似费用提前计算的方式,每走一步额外增加你其他点落下的损耗,因为你这些点一定会接,所以贡献在哪里计算都无影响。
那么 dp_{i,j,0} 可以由 i+1 点或 j 号点到达,所以有 dp_{i,j,0}=\min(dp_{i+1,j,0}+(x_{i+1}-x_i)\times (sum_{1,i-1}+sum_{j+1,n}),dp_{i+1,j,1}+(x_j-x_i)\times (sum_{1,i-1}+sum_{j+1,n}))。
同理,dp_{i,j,1}=\min(dp_{i,j-1,0}+(x_{j}-x_i)\times (sum_{1,i-1}+sum_{j+1,n}),dp_{i+1,j,1}+(x_j-x_{j-1})\times (sum_{1,i-1}+sum_{j+1,n}))。
### 6、P4766 [CERC2014] Outer space invaders
> 有 $n$ 个怪兽,每个怪兽的出现时间为 $[a_i,b_i]$,重量为 $v_i$。你可以花费 $x$ 的代价去击败目前已经出现了的怪兽切重量 $\le x$ 的所有怪兽。问要击败所有怪兽最少要花费多少代价。$1\le n\le 300,1\le a_i\le b_i\le 10000,1 \le v_i\le 10000$。
$dp_{i,j}$ 表示击败第 $i\sim j$ 个怪兽所需要的代价。但这样很不好做,所以我们可以改为 $dp_{i,j}$ 表示击败所有 $i\le a_i\le b_i\le j$ 的所有怪兽的最小代价。
首先,对于区间 $[i,j]$ 中出现的重量最大的外星人 $x$,我们一定会有一次代价是去击败它的。不然的话就没有办法击败它了。
那么我们可以选择在 $[a_x,b_x]$ 中任选一个时间 $t$ 去击败它,同时我们还可以击败在该时刻出现的所有怪兽,因此还需要击败 $[i,t-1]$ 和 $[t+1,j]$ 范围内的怪兽,即 $dp_{i,j}=v_x+\min\limits_{a_x\le k\le b_x} dp_{i,k-1}+dp_{k+1,j}$,时间复杂度 $O(V^3)$。
由于 $a_i,b_i \le 10000$,你直接跑区间 DP 肯定不行,但是 $n$ 很小,所以你可以离散化!这样离散化以后 $a_i,b_i$ 的范围只有 $600$,足够我们支撑 $O(V^3)$ 的时间复杂度。
实现可以先在 $1\sim n$ 中找到最大的 $y$ 使得 $i\le a_y\le b_y\le j$ 且 $v_j$ 最大,然后再枚举 $a_y\le k\le b_y$。
### 7、P3592 [POI 2015 R3] 洗车 Car washes
> 你需要给一个长度为 $n$ 的序列 $a$ 附上值,有 $m$ 条限制,第 $i$ 条限制是区间 $[l_i,r_i]$ 的最小值不能超过 $x_i$,要求在满足所有限制的情况下,求 $\sum\limits_{1\le i\le m} \min\limits_{l_i\le j\le r_i} a_j$ 的最大值。$1\le n\le 50,1\le m\le 4000,1\le x_i\le 5\times 10^5$。
和上一个题目类似,我们用 $dp_{i,j}$ 表示所有 $i\le l_i\le r_i\le j$ 的限制都满足的情况下,能获得的最大值。
但是这样的话,你会发现你无法转移,因为对于两个区间 $l_1\le l_2\le r_2\le r_1$ 的话,有可能你区间 DP 的时候 $dp_{l_2,r_2}$ 的值填的特别大,导致无法满足区间 $dp_{l_1,r_1}$。
所以我们必须要再添一维,即 $dp_{i,j,x}$ 表示区间 $[i,j]$ 内的所有限制都满足的情况下,且最小值不超过 $x$,所能产生的最大代价。
则可以枚举 $dp_{i,j}$ 最小值的位置,所以有 $dp_{i,j,x} = \min\limits_{i< k< j} c\times x + dp_{i,k-1,x}+dp_{k+1,j,x}$,其中 $c$ 为穿过 $k$ 点的顾客的数量,可以预处理实现。
然后做完了,时间复杂度 $O(n^2\max x_i)$,无法通过。
我们可以做一点小贪心:我们是只会选择所有出现过的 $x_i$ 填到序列上的,不然你不一定最优,所以可以离散化,时间复杂度 $O(n^ 2m)$。
### 8、CF149D Coloring Brackets
> 给出一个配对的括号序列,一个括号可以染成红色、蓝色或者不染色。一对匹配的括号需要且只能将其中一个染色。相邻两个括号颜色不能相同(但都可以不染色),求染色方案,对 $1000000007$ 取模。$2\le |S| \le 700$。
考虑 $dp_{i,j}$ 表示一个合法括号区间 $i\sim j$ 的染色方案。
但是这个东西好像很不好转移,因为你不知道边缘括号的颜色,所以你可以加维,用 $dp_{i,j,0/1/2,0/1/2}$ 表示:
- 一个合法括号序列 $i\sim j$;
- $i$ 没染色,染红色,染蓝色;
- $j$ 没染色,染红色,染蓝色;
- 且该染色方案合法时的方案数。
因此,我们发现,有两种情况,第一种情况是 $S_i+T+S_j$,其中 $S_i$ 和 $S_j$ 是合法括号序列,则根据定义我们可以由 $T$ 转移过来,即 $dp_{i,j,x,y}=dp_{i+1,j-1,x',y'}$,由于转移的限制过多,就不在此处列出,但只需要遵守题目条件进行转移即可。
对于第二种情况,是 $S_i+T_1+S_k+S_{k+1}+T_2+S_j$,其中 $i$ 和 $k$ 是一对匹配的括号,$k+1$ 和 $j$ 是另一对匹配的括号,则可以通过 $dp_{i,k,x,y}$ 和 $dp_{k+1,j,x',y'}$ 转移,即 $dp_{i,j,x,y'}=dp_{i,k,x,y}+dp_{k+1,j,x',y'}$,限制条件根据题目判断即可。
## 二、划分类 DP
划分类 DP 是指将原序列划分成若干段,每一段有贡献,然后求最大或最小贡献的问题。
这类问题通常可以定义 $dp_{i,j}$ 表示前 $i$ 个元素划分成 $j$ 段,能得到的最大或最小贡献, 通过预处理其他区间贡献可以做到 $O(n^2)$ 的复杂度。
### 1、CF833B The Bakery
> 将原序列划分成 $k$ 段,每段的价值为该段不同元素的数量,求划分方法使得价值之和最大。$1\le n\le 35000,1\le k\le 50$。
考虑 $dp_{i,j}$ 表示将前 $i$ 个划分成 $j$ 段的最大价值,则有 $dp_{i,j}=\max\limits_{1\le x\le i}dp_{x-1,j-1}+f(x,i)$,其中 $f(x,i)$ 表示区间 $[x,i]$ 的不同元素的数量。时间复杂度 $O(n^2k)$。
我们考虑快速维护区间范围内的不同数量,我们知道该元素是区间中第一次出现其实等价于这个元素上一次出现的时候 $pre_i$ 是 $<k$ 的,所以我们可以在转移的时候将 $[pre_i+1,i]$ 的位置上加上 $1$ 的贡献,然后再将上一次的所有 $dp_{x-1,j-1}$ 先存在线段树里,这样的话就可以直接快速地求出 $dp_{x-1,j-1}+f(x,i)$ 了,之后取 $\max$ 即可。时间复杂度 $O(nk\log n)$。
### 2、P1848 [USACO12OPEN] Bookshelf G
> 将原序列划分成若干段,要求每段的 $a$ 之和不超过 $k$,每段的价值为该段 $b$ 的最大值,求价值之和最小为多少。$1\le n\le 10^5,1\le k\le 10^9,1\le b\le 10^6$。
考虑 $dp_{i}$ 表示将前 $i$ 个数划分的得到的最小代价,则有 $dp_i=\min\limits_{1\le j<i\land \sum\limits_{j+1\le x\le i}a_x\le k}dp_j+\max\limits_{j+1\le x\le i}b_x$。通过双指针和 ST 表可以优化成 $O(n^2)$,但无法通过此题。
我们考虑 CF833B 的思路,考虑单个 $b_i$ 会在什么时候取到最大。
对于区间 $[l_i,i]$ 的最大值为 $b_i$,则说明区间 $[l_i,i]$ 中的 $\max b_x$ 这一部分已经定了下来,可以通过线段树区间赋值 $[l_i,i]$。
然后对于 $dp_j$,我们相当于在求出后线段树上单点赋值。
对于求出 $dp_j$,我们相当于在合法区间内找最小值。
线段树解决,时间复杂度 $O(n\log n)$。
## 3、P8239 [AGM 2022 资格赛] 分裂
> 将原序列划分成 $k$ 段,每段的价值为最大值的 $b_i$ 次方减去最小值的 $b_i$ 次方,求划分方案的最大价值。$1\le k\le n\le 5000$。
考虑 $dp_{i,j}$ 表示将前 $i$ 个分为 $j$ 段的最大价值,$dp_{i,j}=\max\limits_{1\le k<i}dp_k+(\max\limits_{k+1\le x\le i}a_x)^{b_j}-(\min\limits_{k+1\le x\le i} a_x)^{b_j}$,使用 ST 表只能做到 $O(n^2k)$,因为你在转移状态的时候需要枚举 $k$。
而且由于每个题的 $b$ 不一样,无法使用数据结构优化。
由于我们是要求方案的最大价值,我们不妨将这道题目转换为在区间中任选两个数 $x_1,x_2$,然后价值为 $x_1^{b_j}-x_2^{b_j}$。这样写的话是不影响的,因为你贪心发现去到最大仍然是取 $\max$ 和 $\min$ 的时候。
所以我们考虑再加一维状态 $dp_{i,j,0/1/2}$,表示前 $i$ 段分为 $j$ 段,其中第 $j$ 段取完了,还没取 $x_1$,还没取 $x_2$。
这里没有第 $j$ 段还没有贡献时的状态是没有必要的,因为你没有贡献的话这些数都可以转换到上一段去。
所以 $dp_{i,j,0}$ 可以从 $dp_{i-1,j,1}$ 和 $dp_{i-1,j,2}$ 转移,即 $dp_{i,j,0}=\max(dp_{i-1,j,1}-a_i^{b_j},dp_{i-1,j,2}+a_i^{b_j},dp_{i-1,j,0},dp_{i-1,j-1,0})$。
而 $dp_{i,j,1}$ 可以从 $dp_{i-1,j-1,0}$ 转移,即 $dp_{i,j,1}=\max(dp_{i-1,j-1,0}+a_i^{b_j},dp_{i-1,j,1})$。
$dp_{i,j,2}$ 可以从 $dp_{i-1,j-1,0}$ 转移,即 $dp_{i,j,1}=\max(dp_{i-1,j-1,0}-a_i^{b_j},dp_{i-1,j,2})$。
## 三、后记
本篇文章到这里就结束了,下一期(如果还有的话)应该是字符串 DP 和树形 DP,欢迎大家观看!