【AcWing算法基础班】动态规划(二)学习笔记

一、线性DP

定义:有模糊的线性递推顺序的模型
例1 数字三角形问题:选择从上到下的一条路径,使得路径上的数字和最大
样例:
7
3 8
8 1 0
2 7 4 4
4 5 2 6 5
考虑状态表示的是哪一个集合:所有从起点走到(i,j)的路径
属性:所有这些路径上的数字之和的最大值
状态计算:集合划分为从左上方来的一类和从右上方来的一类
来自左上:f[i-1][j-1]+a[i][j]
来自右上:f[i-1][j]+a[i][j]
动态规划问题的时间复杂度一般是状态数量×状态转移的计算量
注意初始化边界条件
例2 最长上升子序列问题:给定一个长度为N的数列,求数值严格单调递增的子序列长度的最大值
样例:3 1 2 1 8 5 6
答案:4
状态表示f[i],集合:所有以数列中第i个数为结尾的上升子序列
属性:所有上升子序列长度的最大值
集合划分:以a[i]为结尾的上升子序列中a[i]的前一个数可能是a[0]、a[1]、a[2]、……、a[i-1],即j在0、1、2、……、i-1中取值,且满足a[j] 状态计算:f[i]=max{f[j]+1,a[j] 实现方式:二重循环,枚举所有a[j] 若要输出最长上升子序列,则利用并查集的思想,每次更新状态时,记录第i个节点的父节点。最后找到最长上升子序列最后一个数在数列中的位置,循环按照根节点向前输出即可。
练 最长上升子序列Ⅱ 若数据加强,怎么做?
考虑贪心的思想。结尾更大的上升子序列必定不如结尾更小的上升子序列要好,因为结尾越小,能接在该上升子序列后面的上升子序列越多,适用范围越广。因此我们可以存所有长度的最长上升子序列结尾的最小值。
结论:上升子序列的最小结尾值随长度增加而严格单调递增。
证明:假如有长度是k的结尾值最小的上升子序列。由于其严格单调递增,因此该上升子序列中第k-1个数一定比第k个数要小。那么我们就找到了一个长度是k-1的上升子序列,它的结尾值比长度是k的结尾值最小的上升子序列的结尾值小。如果长度是k-1的结尾值最小的上升子序列结尾值大于等于长度是k的结尾值最小的上升子序列结尾值,那么它也一定大于我们找到的这个长度是k-1的上升子序列的结尾值。这就矛盾了。因此,长度是k-1的结尾值最小的上升子序列结尾值一定小于长度是k的结尾值最小的上升子序列结尾值。
思考:如何求以a[i]为结尾的最长上升子序列的长度?
假设长度是k的结尾值最小的上升子序列结尾值大于等于a[i],但长度是k-1的结尾值最小的上升子序列结尾值小于a[i],那么a[i]是一定不能接到长度大于等于k的上升子序列后面的,但可以接到长度是k-1的结尾值最小的上升子序列后面。此时,就产生了一个以a[i]为结尾值且结尾值更小的长度是k的上升子序列。我们不断更新长度是k的结尾值最小的上升子序列的结尾值即可。这个过程需要用到二分,在以不同长度的最小结尾值构成的有序序列中查找最大的小于a[i]的那个值的位置。这是一种插入的思想,即找到a[i]在有序序列中应该插入的准确位置,结尾值就一定更新。注意初始化条件,最初有序序列还是空的,a[i]前面没有接任何数,也要假定一个极小值接上,来满足二分的过程。
例3 最长公共子序列问题:给两个长度分别为M和N得字符串A和B,求既是A的子序列又是B的子序列的字符串的长度最长是多少
样例:
acbd
abedc
答案:3
状态表示f[i][j],集合:第一个序列的前i个字母和第二个序列的前j个字母的所有公共子序列
属性:所有这些子序列长度的最大值
集合划分:划分为四类,即最长公共子序列中a[i]、b[j]都选了,只选了a[i]没选b[j],只选了b[j]没选a[i],以及a[i]和b[j]都没选。
☆a[i]和b[j]都没选:f[i-1][j-1],不用考虑进代码里,原因:f[i-1][j]和f[i][j-1]两类已经包含了f[i-1][j-1]
☆a[i]、b[j]都选:f[i-1][j-1]+1,限制条件:a[i]等于b[j]
★只选了b[j]没选a[i]:这种情况被f[i-1][j]包含,因为f[i-1][j]表示第一个序列的前i-1个字母和第二个序列的前j个字母的所有公共子序列长度的最大值,不一定以b[j]结尾。而这一类是说,a[i]不出现在最长公共子序列当中,b[j]一定出现在其中。综上,f[i-1][j]一定包含只选了b[j]没选a[i]的这种情况,但不等价。
★f[i][j]包含f[i-1][j]这种情况,f[i-1][j]又包含只选了b[j]没选a[i]的这种情况,要求只选了b[j]没选a[i]这一类情况的最大值,我们是可以用f[i-1][j]来代表这一类情况的最大值。虽然这样产生了重复,但终归是包含在f[i][j]里的,并不妨碍。因为我们求的是最大值,是可以重复的。比如,求A,B,C的最大值,我们可以先求A和B的最大值,再求B和C的最大值,B重叠了,但没有关系,只要我们没有漏掉某一个元素,就可以求出来三者的最大值。但是如果是求数量,那就不能有重复了,这只是针对最大值,可以不一一对应。
★只选了a[i]没选b[j]:同理,用f[i][j-1]代表
练 编辑距离问题:用最少的字符操作次数将字符串A转换为字符串B。字符操作共三种,即删除一个字符、插入一个字符、将一个字符改为另一个字符
状态表示f[i][j],集合:所有将 a [ 1 ] a[1] a[1] a [ i ] a[i] a[i]变成 b [ 1 ] b[1] b[1] b [ j ] b[j] b[j]的操作方式
属性:这些操作方式所需操作次数中的最小操作次数
集合划分:
①若删除A的最后一个字符 a [ i ] a[i] a[i]后A和B匹配,即原来 a [ 1 ] a[1] a[1] a [ i − 1 ] a[i-1] a[i1] b [ 1 ] b[1] b[1] b [ j ] b[j] b[j]匹配,即 f [ i ] [ j ] = f [ i − 1 ] [ j ] + 1 f[i][j]=f[i-1][j]+1 f[i][j]=f[i1][j]+1
②若在A后面增加一个字符 a [ i + 1 ] a[i+1] a[i+1]后A和B匹配,即原来 a [ 1 ] a[1] a[1] a [ i ] a[i] a[i] b [ 1 ] b[1] b[1] b [ j − 1 ] b[j-1] b[j1]匹配,即 f [ i ] [ j ] = f [ i ] [ j − 1 ] + 1 f[i][j]=f[i][j-1]+1 f[i][j]=f[i][j1]+1
③若将A的最后一个字符 a [ i ] a[i] a[i]改成 b [ j ] b[j] b[j]后A和B匹配,即原来 a [ 1 ] a[1] a[1] a [ i − 1 ] a[i-1] a[i1] b [ 1 ] b[1] b[1] b [ j − 1 ] b[j-1] b[j1]匹配,即 f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + 1 f[i][j]=f[i-1][j-1]+1 f[i][j]=f[i1][j1]+1,限制条件: a [ i ] ≠ b [ j ] a[i]≠b[j] a[i]=b[j];若 a [ i ] = b [ j ] a[i]=b[j] a[i]=b[j],则 f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] f[i][j]=f[i-1][j-1] f[i][j]=f[i1][j1],即无需进行修改操作
状态计算: f [ i ] [ j ] = m i n ( f [ i − 1 ] [ j ] + 1 , f [ i ] [ j − 1 ] + 1 , f [ i − 1 ] [ j − 1 ] + ( a [ i ] ! = b [ j ] ) ) f[i][j]=min(f[i-1][j]+1,f[i][j-1]+1,f[i-1][j-1]+(a[i]!=b[j])) f[i][j]=minf[i1][j]+1f[i][j1]+1f[i1][j1]+a[i]!=b[j]

二、区间DP

定义:状态表示的是某一个区间的模型
例 石子合并问题:将N堆石子合并为一堆,每次只能合并相邻两堆石子,不同合并顺序代价不同,求最小的合并代价
状态表示f[i][j]:第i堆石子到第j堆石子这一合并区间
集合:所有将第i堆石子到第j堆石子合并成一堆石子的合并方式
属性:所有合并方式的代价的最小值
集合划分:最后一次分界线的位置,可以在i到j之间的j-i个位置,即1、2、3、……、k-2、k-1,其中k=j-i+1
状态计算:f[i][j]=min{f[i][k]+f[k+1][j]+s[j]-s[i-1],k=i~j-1}
首先计算前缀和,然后用区间DP时,枚举区间起点和区间长度,并每次计算区间左右端点的值,区间长度是1不需要做合并,长度从2开始,用状态计算公式枚举区间计算即可。注意f[i][j]要先初始化,否则每次求最小值都是0。

三、计数类DP

定义:状态表示的是选法数量的模型
例 整数划分:一个正整数可以表示为若干个正整数之和,如n=n1+n2+……+nk,其中n1>=n2>=……>=nk,k>=1,将这样的一种表示称为正整数n的一种划分。给定一个正整数n,求n有多少种不同的划分方法。
问题转化为完全背包模型:容量是n的背包,有n个物品,物品的体积分别是从1~n,每种物品有无限个,求恰好装满背包的方案数。
状态表示f[i][j],集合:从前i件物品中选,总体积等于j的所有选法
属性:所有选法的数量
集合划分:第i件物品不选、选1、2、3、……、s件
状态计算:
∵ f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i − 1 ] [ j − i ] + f [ i − 1 ] [ j − 2 i ] + … … + f [ i − 1 ] [ j − s i ] ∵f[i][j]=f[i-1][j]+f[i-1][j-i]+f[i-1][j-2i]+……+f[i-1][j-si] f[i][j]=f[i1][j]+f[i1][ji]+f[i1][j2i]++f[i1][jsi]
f [ i ] [ j − i ] = f [ i − 1 ] [ j − i ] + f [ i − 1 ] [ j − 2 i ] + … … + f [ i − 1 ] [ j − s i ] f[i][j-i]= f[i-1][j-i]+f[i-1][j-2i]+……+f[i-1][j-si] f[i][ji]=f[i1][ji]+f[i1][j2i]++f[i1][jsi]
∴ f [ i ] [ j ] = f [ i − 1 ] [ j ] + f [ i ] [ j − i ] ∴f[i][j]=f[i-1][j]+f[i][j-i] f[i][j]=f[i1][j]+f[i][ji]
最终版本: f [ j ] = f [ j ] + f [ j − i ] f[j]=f[j]+f[j-i] f[j]=f[j]+f[ji],j从小到大循环
法2:状态表示f[i][j],集合:所有总和是i,并且恰好表示成j个数的和的方案
属性:所有方案的数量
集合划分:最小值是1的所有方案,和最小值大于1的方案
最小值是1:与把所有方案中都去掉一个1是等价的,即f[i-1][j-1]
最小值大于1:与把所有方案中的所有数都减1是等价的,即f[i-j][j]
∴ f [ i ] [ j ] = f [ i − 1 ] [ j − 1 ] + f [ i − j ] [ j ] ∴f[i][j]=f[i-1][j-1]+f[i-j][j] f[i][j]=f[i1][j1]+f[ij][j]
注意初始化f[1][1]=1。

你可能感兴趣的:(动态规划,算法,动态规划,acm竞赛,icpc,程序设计)