线性dp(线性dp分类与应用)
一、线性DP基础
对于一个题目,我们要使用DP,需要满足以下三个特征:
-
最优子结构,可以从子问题的解推出全局解。
-
无后效性,后面的状态对之前的状态没有影响。
-
有重叠状态,这样才可以通过DP Table降低复杂度。
一个动态规划问题的解决,有三个步骤:
设计状态。对于基础线性DP,一般由题意直接得出状态。或者有一些套路,如设 \(dp_i\) 表示前 \(i\) 个的答案。
状态转移。按照题意手推。要注意状态转移方程的正确。
递推。有两种大方向:填表或刷表。有些时候不同的方向可能造成思维难度的差异。
e.g.#1 零钱兑换
题意:有 \(n\) 种不同面额的钱,其中第 \(i\) 种面额为 \(a_i\),求凑出 \(m\) 块钱的最少钱币数量。
样例:
输入
3
2 3 7
11
输出
3
发现对于每一个子问题,都可以再加一张钱达到另一个状态。后面状态的改变也对之前的状态没有影响。
按照题意,设 \(dp_i\) 表示凑出 \(i\) 块钱时的最少钱币数量。当得到一个状态 \(dp_i\) 的答案时,可以通过加 \(n\) 种钱中的一种 \(a_j\) ,到达另一个状态 \(dp_{i+a_j}\)。取最小即可。
转移方程(刷表):
其中 \(i\in [1,m],j\in [1,n]\),最后答案在 \(dp_m\)。这是按照题意直接设状态的例子。
e.g.#2 LIS
题意:给出长度为 \(n\) 的数组 \(a\),求最长上升子序列(LIS)长度。
样例:
输入
7
1 3 6 3 1 8 9
11
输出
4
设 \(dp_i\) 表示以 \(a_i\) 结尾的最长上升子序列长度。由于子序列不一定是连续的,所以对于每一个状态,都可以在之前的状态中寻找最小的,再加上本数即可。
转移方程:
其中 \(i\in [0,n],j\in [0,i)\),答案是 \(\max{\{dp_i\}},i\in [1,n]\)。对于这样的序列问题,可以考虑把 \(dp_i\) 设为“前 \(i\) 个的情况。
练习题:
e.g.#3 最大子数组和
题意:给出长度为 \(n\) 的数组 \(a\),求最大子数组和。
样例:
输入
6
-1 1 4 5 -1 4
输出
13
效仿LIS,我们设 \(dp_i\) 表示以 \(a_i\) 结尾的最大子数组和。
对于每一个 \(dp_i\),当 \(dp_{i-1}\le0\) 时,因为字数组是连续的,前面序列的和对最大和没有用反而会减小,应放弃之前的值只用当前的值作为答案;反之则应该用之前的值加上当前的值。
转移方程:
其中 \(i\in[1,n]\),答案是 \(\max{\{dp_i\}},i\in [1,n]\)。
二、复杂的线性DP
此类dp问题比起裸的简单题的区别主要有几点,本部分主要通过几道例题总结了一些应对此类问题的套路和分析方式:
- 状态难找或维护的信息较多;
- 子问题的划分不明显或明显但情况较多。
e.g.#1 乘积最大子数组
给你一个整数数组 \(num\),请你找出数组中乘积最大的非空连续子数组,并返回该子数组所对应的乘积。子数组是数组的连续子序列。
思路:难点很明显在于数组中的数的符号,两个负得很多的数乘起来就很大了。于是,我们从正负两方面同时维护,即将单一点的状态信息扩展到两个,用数组 \(f\) 和 \(d\) 分别进行运算,有方程
还有一个细节,便是至少得有一个数,于是都得与 \(a_i\) 的大小进行比较,防止 \(0\) 对答案正确性的影响。
e.g.#2 友好城市
两岸各有 \(N\) 个城市,且友好城市一一对应。现将友好城市连线,求最大能连多少使连线两两不相交?
思路:不难发现,若同时选择了 \(a\) 位置连向 \(b\) 位置的连线和 \(c\) 向 \(d\) 的,假使 \(c\) 大于 \(a\),则一定有 \(d\) 大于 \(b\)。即一边有序后,另一边所选也应有序,即严格递增。显然是 \(LIS\),解法也了然了:
e.g.#3 不同的二叉搜索树
求有n个节点的二叉搜索树有多少种不同的形态。
思路:由于是二叉树,显然可以分为左子树,根,右子树三部分。根自不必说,只有单一的一个节点,所以剩下的左右子树的节点个数之和便为 \(n-1\),有左 \(0\) 右 \(n - 1\),左 \(1\) 右 \(n - 2\),…,左 \(n-1\) 右 \(0\) 这 \(n\) 种搭配。设有 \(n\) 个节点的二叉搜索树有 \(f_x\) 种不同的形态,则有 $f_x = $ 所有 \(f_a * f_{x-a-1}\) 的和。便是方程了。
让我们从上面的几道例题中来尝试提取一下其思维之精华:由例1,我们可以在一个数组不是特别方便转换时扩展所转移的信息的数量,从而让更多的信息得以联系,进而方程更易推出且更简单;由例2,便可看出很多看似挺难的问题实则是基础模型的变型,但对状态空间进行抽象的关键便是对每一个条件及信息的深入分析,将其的性质与自己熟悉的模型的性质结合便能推出;最后是例3,看似是静态的求解但还是可以用方程帮助递推过程的思考,这种情况难点往往是对子问题的正确划分。但是技巧毕竟是其次,下来读者最好再自行寻找题目认真练习,这才是主要。
三、字符串线性DP
状态设计
对于只给定一个字符串进行\(DP\)的题目有以下三种设计方案:
-
\(dp[i]\) 表示区间 \(0\thicksim i\) 中题目所求内容
-
\(dp[i]\) 表示区间 \(i\thicksim n\) 中题目所求内容
-
\(dp[i][j]\) 表示区间 \(i\thicksim j\)中题目所求内容
对于只给定两个字符串 \(s\) 和 \(t\) 进行\(DP\)的题目有以下两种设计方案:
-
\(dp[i][j]\) 表示 \(s\) 从 \(0\thicksim i\) , \(t\) 从 \(0\thicksim j\) 的匹配情况
-
\(dp[i][j]\) 表示 \(s\) 从 \(i\thicksim n\) , \(t\) 从 \(j\thicksim m\) 的匹配情况
注意在某些情况下可以对于状态进行压缩,不仅可以节省空间有时甚至可以降低状态转移方程和代码实现复杂度
\(e.g.\)
T252309 0x51-线性DP-分班之殇
题目描述
给定三个字符串 \(s1\)、\(s2\)、\(s3\),判断 \(s3\) 是否由 \(s1\) 和 \(s2\) 交错构成。
对于两个字符串 \(s\) 和 $t,交错的定义为:
将 \(s\) 分为 \(s_1+s_2+s_3+......+s_n\),将 \(t\) 分为 \(t_1+t_2+t_3+......+t_m\),则两字符串的交错为 \(s_1+t_1+s_2+t_2+s_3+t_3+......\) 或 \(t_1+s_1+t_2+s_2+t_3+s_3+......\)。其中 \(s\) 和 \(t\) 的连续字串交错出现,因此 \(|n-m|≤1\)。
思路
将题目中 \(s3\) 字符串匹配数一维压缩后的状态转移方程如下,既节省了空间还将进行转移时的边界判断省去了
有了以上的设计方法就可以进行字符串 \(DP\) 的状态设计和状态转移方程设计了
e.g.#1 最长有效括号
题目描述
给你一个只包含 \('('\) 和 \(')'\) 的字符串,找出最长有效(格式正确且连续)括号子串的长度。
样例
输入
(()
输出
2
思路
以常规的字符串 \(DP\) 状态表示,\(dp_i\) 表示以第 \(i\) 位结束的最长有效括号子串的长度。接下来分类讨论:
如果 \(s_i = '('\),则 \(dp_i = 0\) ;如果 \(s_i = ')'\),再次分类讨论;
当 \(s_{i - 1} = '('\) 时, \(s_i\) 与 \(s_{i - 1}\) 将组成一个完整的括号,而在这个括号之前则会有其它完整的括号序列和 \(s_{i-1}\) 和 \(s_i\) 拼接,所以此时 \(dp_i = dp_{i - 2} + 2\);
当 \(s{i - 1} = ')'\) 时,\(s_i\) 只能与更前面的一个 \('('\) 匹配,这个括号的位置应在 \(s_{i - 1}\) 为结尾的最长有效括号子串的前一个位置,即 \(i - dp_{i - 1} - 1\),此时从 \(i - dp_{i - 1} - 1\) 到 \(i\) 的子串已经是一个有效括号序列,但是以 \(i - dp_{i - 1} - 2\) 为结尾的括号序列也能和它拼接在一起,所以 \(dp_i = dp_{i - 1} + dp_{i - dp_{\ i - 1}\ \ - 2} + 2\)。
e.g.#2 字符串
P8256 [NOI Online 2022 入门组] 字符串
题目描述
有三个字符串 \(S\),\(R\) 和 \(T\)。其中 \(S\) 长度为 \(n\),且只由 0, 1, - 三种字符构成(注:这里的第三种字符是减号),\(R\) 初始时为空。
进行n次操作。每次操作中,取出 \(S\) 的第一个字符(记为 \(c\)),并将其从 \(S\) 中删去。如果 \(c = \texttt{-}\),则删去 \(R\) 的开头字符或结尾字符(数据保证删去后 \(R\) 不为空)。否则,将 \(c\) 加入到 \(R\) 的末尾。
请求出将 \(R\) 变得与 \(T\) 相同的操作方法数量。我们定义两种方案不同,当且仅当在某种方案的某次操作中,删去了 \(R\) 的开头字符。而在另一种方案的这次操作中,删去了 \(R\) 的结尾字符。
答案对 \(1e9+7\) 取模。
多测。
样例
输入
3
6 2
10-01-
01
7 3
010-1-1
101
6 4
111-00
1100
输出
2
1
2
思路
可以设 \(dp[i][j][k][l]\) 表示已经考虑到字符串 \(S\) 的第 \(i\) 个字符, 字符串 \(R\) 中有 \(j\) 个字符已匹配,前面和后面分别有 \(k\) , \(l\) 个字符需要被删除时的方案总数
如果 \(S_i="-"\) ,那么可以在前面或后面删去一个字符,即转移到 \(dp_{i+1,j,k-1,l}\) 和 \(dp_{i+1,j,k,l-1}\)
否则,就可以加在后面充当一个在后面的转移中从后面删去的字符,即转移到 \(dp_{i+1,j,k,l+1}\)
如果此时全为要从前面删去的字符\(
(j=0\And l=0)\) ,可以转移到 \(dp_{i+1,0,k+1,0}\)
如果 \(S_i=T_{j+1}\And l=0\) ,那就可以成为新的一个已确定的字符,转移到 \(dp_{i+1,j+1,k,0}\)
练习题
P2890 [USACO07OPEN]Cheapest Palindrome G
四、二维平面上的线性DP
前面的题目都是针对数组的。我们思考如何把线性DP推广到平面上。
e.g.#1 最大正方形
题意:给出长 \(n\) 宽 \(m\) 的 01 矩阵,求其中只包含 1 最大的正方形面积。
例子:
手推可得答案是 4。
效仿LIS的方法。LIS记录的是前 \(i\) 个数的答案。所以设 \(dp_{i,j}\) 表示以 \((i,j)\) 为右下角的最大正方形的边长。分类讨论
-
当 \(a_{i,j}=0\) 时,显然不可能有符合要求的正方形,此时 \(dp_{i,j}=0\)。
-
当 \(a_{i,j}=1\) 时,要让当前状态的矩形最大,取决于 \(dp_{i,j}\) 与 \(dp_{i-1,j}\)、\(dp_{i,j-1}\) 和 \(dp_{i-1,j-1}\)。举几个例子。
如上代表DP数组,红色表示当前的状态。发现当前状态的答案是左边、上面、左上位置答案的最小值加一。
转移方程:
其中 \(i\in [1,n],j\in [1,m]\)。答案为 \(\max{\{dp_{i,j}\}}^2,i\in[1,n],j\in[1,m]\)。
由此可见,对于二维平面,线性DP的状态变得更复杂,转移方程需要更多思考。
五、DP的特殊方法:画表
画表应用的类型主要是复杂、难以分析的 \(DP\) 题,譬如你无法较为轻松地划分出子问题或无法轻易地分析出状态转移方程,你就可以尝试画表,通过画表寻找规律和分析状态转移方程。对于复杂的 \(DP\),最理想的方法是画表,画表能更加直观的反映出状态与状态之间的关系和状态的转移,所以在对 \(DP\) 题没有思路时就采取画表的方法。另外画表的主要过程就是模拟,然后通过表格找规律,画的时候画较特殊的情况,通常画两到三种情况即可。
e.g.#1 最低票价
在一个火车旅行很受欢迎的国度,你提前一年计划了一些火车旅行。在接下来的一年里,你要旅行的日子将以一个名为 \(days\) 的数组给出。每一项是一个从 \(1\) 到 \(365\) 的整数。
火车票有 三种不同的销售方式:
一张为期一天的通行证售价为 \(costs_0\) 美元;
一张为期七天的通行证售价为 \(costs_1\) 美元;
一张为期三十天的通行证售价为 \(costs_2\) 美元。
通行证允许数天无限制的旅行。 例如,如果我们在第 \(2\) 天获得一张 为期 \(7\) 天的通行证,那么我们可以连着旅行 \(7\) 天:第 \(2\) 天、第 \(3\) 天、第 \(4\) 天、第 \(5\) 天、第 \(6\) 天、第 \(7\) 天和第 \(8\) 天。
返回 你想要完成在给定的列表 \(days\) 中列出的每一天的旅行所需要的最低消费。
分析:刚拿到这道题没有很清晰的思路,但可以先确定状态 \(dp_{i,j}\) 表示前 \(i\) 项上次购买的天数还剩下 \(j\) 天,然后可以通过画表发现状态转移至于 \(0\), \(6\), \(29\) 这三天有关,于是我们可以得到如下的状态转移方程:
所以说画表对 \(DP\) 状态转移的归纳与分析是有质的帮助的,所以在做 \(DP\) 时,可以考虑画表来帮助分析。

浙公网安备 33010602011771号