最近几天一直在看有关动态规划的算法,整理了一些常见案例,主要是求最长公共子序列,最长公共子串,最长递增子序列,最长回文子串,硬币的组合数,硬币的最少组合方法,最小编辑距离,背包问题(01背包,完全背包,多重背包)等方面的经典案例求解。
这些案例大部分都是用python实现的动态规划算法。
案例一:求最长公共子序列(不一定连续)
Q:给定两个序列,找出在两个序列中同时出现的最长子序列的长度。一个子序列是出现在相对顺序的序列,但不一定是连续的。
分析:
- 假设str1的长度为M,str2的长度为N,生成的大小为M*N的矩阵dp。dp[i][j]的含义是str[0…i]与str2[0…j]的最长公共子序列的长度。
- 矩阵dp第一列,即dp[i][0],代表str1[0…i]与str2[0]的最长公共子序列长度。str2[0]只有一个字符,所以dp[i][0]最大为1,如果str[i] == str2[0],则令dp[i][0]为1,一旦dp[i][0]被设为1,则令dp[i+1…M][0]全部为1
- 矩阵dp第一行,即dp[0][j],与步骤1同理。如果str1[0]==str[j],则令dp[0][j]为1,一旦dp[0][j]被设为1,则令dp[0][j+1…N]全部为1
- 其他位置,dp[i][j]的值只可能来自一下三种情况,三种可能的值中,选择最大的值即可:
- 情况一:可能是dp[i-1][j]的值,这代表str1[0….i-1]与str2[0…j]的最长公共子序列长度。 举例:str1 = “A1BC2”, str2 = “AB34C” str1[0..3]为”A1BC”,str2[0…4]为”AB34C”,这两部分最长公共子序列为”ABC”,即dp[3][4]为3. str1整体和str2整体最长公共子序列也是”ABC”,所以dp[4][4]可能来自dp[3][4]
- 情况二:同理可知,dp[i][j]的值也可能是dp[i][j-1]
- 情况三:如果str1[i]==str2[j],还可能是dp[i-1][j-1]+1的值。 举例:比如str1 =”ABCD”, str2 = “ABCD”. str1[0…2]即“ABC”与str2[0…2]即“ABC”的最长公共子序列为”ABC”,也就是dp[2][2]为3。因为str1和str2的最后一个字符都是”D”,所以dp[i][j] = dp[i-1][j-1]+1
-
def findLongest(self, A, n, B, m):
-
#新建一个m行n列的矩阵
-
matrix = [
0] * m * n
-
#1、矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0...n]的最长公共子串.
-
# str2[0]只有一个字符,所以matrix[i][0]最大为1
-
for i
in range(n):
-
if A[i] == B[
0]:
-
for j
in range(i,n):
-
matrix[j] =
1
-
#2、矩阵的第一列,matrix[i][0]最大为1
-
for i
in range(m):
-
if B[i] == A[
0]:
-
for j
in range(i,m):
-
matrix[j*n] =
1
-
#3、其他位置,matrix[i][j]有三种情况,matrix[m][n]即为所求的最长公共子序列长度
-
for i
in range(
1,m):
-
for j
in range(
1,n):
-
if B[i] == A[j]:
-
matrix[i*n+j] = max(matrix[(i
-1)*n+j
-1]+
1,matrix[(i
-1)*n+j],matrix[i*n+j
-1])
-
else:
-
matrix[i*n+j] = max(matrix[(i
-1)*n+j],matrix[i*n+j
-1])
-
return matrix[m*n
-1]
案例二:求最长公共子串(连续)
Q:给定两个序列,找出在两个序列中同时出现的最长子序列的长度。子串的意思是要求为连续的子序列
分析:
矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0…n]的最长公共子串.
与案例一中的前两步相同,只是最后一步不同。
代码:
-
def findLongest(self, A, n, B, m):
-
#新建一个m行n列的矩阵
-
matrix = [
0] * m * n
-
#1、矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0...n]的最长公共子串.
-
# str2[0]只有一个字符,所以matrix[i][0]最大为1
-
for i
in range(n):
-
if A[i] == B[
0]:
-
matrix[i] =
1
-
#2、矩阵的第一列,matrix[i][0]最大为1
-
for i
in range(m):
-
if B[i] == A[
0]:
-
matrix[i*n] =
1
-
#3、其他位置
-
max =
0
-
for i
in range(
1,m):
-
for j
in range(
1,n):
-
if B[i] == A[j]:
-
matrix[i*n+j] = matrix[(i
-1)*n+j
-1]+
1
-
if max<matrix[i*n+j]:
-
max = matrix[i*n+j]
-
return max
案例三:最长递增子序列
Q:给定一个序列,找到最长子序列的长度,使得子序列中的所有元素被排序的顺序增加。比如arr = [2,1,5,3,6,4,8,9,7], 最长递增子序列为[1,3,4,8,9],所以返回这个子序列的长度5。给定数组arr,返回数组arr,返回arr的最长递增子序列长度。比如arr =[2,1,5,3,6,4,8,9,7],最长递增子序列为[1,3,4,8,9],所以返回这个子序列的长度5
分析:
dp[i]表示在必须以arr[i]结尾的情况下,arr[0 … i]中的最大递增子序列长度,
dp[i] = max{ dp[j]+1 (0<=j<i , arr[j]<arr[i])}
代码:
-
def findLongest(self, A, n):
-
dp = [
0] * n
-
dp[
0] =
1
-
for i
in range(
1, len(A)):
-
l = [
1]
-
for j
in range(
0, i):
-
if A[i] > A[j]:
-
l.append(dp[j] +
1)
-
dp[i] = max(l)
-
return max(dp)
案例四:最长回文子串
Q:给一个字符串,找出它的最长的回文子序列LPS的长度。例如,如果给定的序列是“BBABCBCAB”,则输出应该是7,“BABCBAB”是在它的最长回文子序列。
分析:
dp[i][j] = 1表示字符串s从i到j是回文串 dp[i][j] = 0表示字符串s从i到j不是回文串
如果dp[i][j] = 1, 那么dp[i+1][j-1] = 1
代码:
-
def manacher(self,s):
-
#建立一个二维数组
-
maxlen =
0
-
start =
0
-
dp = [[
0
for i
in range(len(s))]
for i
in range(len(s))]
-
for i
in range(len(s)):
-
dp[i][i] =
1
-
if i+
1<len(s)
and s[i] == s[i+
1]:
-
dp[i][i+
1] =
1
-
maxlen =
2
-
start = i
-
for i
in range(
3,len(s)+
1):
#i表示回文子串长度,从3开始,最长为len(s)
-
for j
in range(len(s)-i+
1):
#j表示指针移动的起点
-
k = i+j
-1
#k表示终点
-
if dp[j+
1][k
-1]==
1
and s[j]==s[k]:
-
dp[j][k] =
1
-
if i>maxlen:
-
start = j
-
maxlen = i
-
if maxlen>=
2:
-
return s[start:start+maxlen]
-
return
None
案例五:硬币最少数量(凑齐n元最少需要几枚硬币)
Q:假设有 1 元,3 元,5 元的硬币若干(无限),现在需要凑出 11 元,问如何组合才能使硬币的数量最少?
分析:
我们先假设一个函数 d(i) 来表示需要凑出 i 的总价值需要的最少硬币数量。
- 当 i = 0 时,很显然我们可以知道 d(0) = 0。因为不要凑钱了嘛,当然也不需要任何硬币了。注意这是很重要的一步,其后所有的结果都从这一步延伸开来。
- 当 i = 1 时,因为我们有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = 1。
- 当 i = 2 时,因为我们并没有 2 元的硬币,所以只能拿 1 元的硬币来凑。在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = 2。
- 当 i = 3 时,我们可以在第 3 步的基础上加上 1 个 1 元硬币,得到 3 这个结果。但其实我们有 3 元硬币,所以这一步的最优结果不是建立在第 3 步的结果上得来的,而是应该建立在第 1 步上,加上 1 个 3 元硬币,得到 d(3) = 1。
- …
接着就不再举例了,我们来分析一下。可以看出,除了第 1 步这个看似基本的公理外,其他往后的结果都是建立在它之前得到的某一步的最优解上,加上一个硬币得到。得出:
d(i) = d(j)+1
这里j<i。通俗的将,我们需要凑出 i 元,就在凑出 j 的结果上再加上某一个硬币就行了。那这里我们加上的是哪个硬币呢。嗯,其实很简单,把每个硬币试一下就行了:
- 假设最后加上的是 1 元硬币,那 d(i) = d(j) + 1 = d(i - 1) + 1。
- 假设最后加上的是 3 元硬币,那 d(i) = d(j) + 1 = d(i - 3) + 1。
- 假设最后加上的是 5 元硬币,那 d(i) = d(j) + 1 = d(i - 5) + 1。
我们分别计算出d(i - 1) + 1,d(i - 3) + 1,d(i - 1) + 1的值,取其中的最小值,即为最优解,也就是d(i)。
最后公式
d(i) = min( d(i - 1) + 1,d(i - 3) + 1,d(i - 5) + 1 )
代码:
-
def findLeast(self, n):
-
# write code here
-
l = [
0,
1,
2,
1,
2,
1]
-
for i
in range(
6,n+
1):
-
l.append(min(l[i
-1]+
1,l[i
-3]+
1,l[i
-5]+
1))
-
return l[n]
案例六:硬币组合种类数(凑齐n分钱有多少种方法)
Q:有数量不限的硬币,币值为25分、10分、5分和1分,请编写代码计算n分有几种表示法。给定一个int n,请返回n分有几种表示法。保证n小于等于1000,为了防止溢出,请将答案Mod 1000000007。
分析:
- dp[i][sum]表示用前i种硬币构成sum的所有组合数,本题实际上就是求dp[n][sum]
- coins = [1,5,10,25]
- dp[i][sum] = dp[i-1][sum-0*coins[i]] + dp[i-1][sum-1*coins[i]]+….+dp[i-1][sum-k*coins[i]](k = sum/coins[i])
- 上一步化简后:dp[i][sum] = dp[i-1][sum-k*coins[i]]求和 (k = 0…sum/coins)
代码:
-
def coinsWays(self, n):
-
coins = [
1,
5,
10,
25]
-
dp = [[
0
for i
in range(n+
1)]
for i
in range(
5)]
-
for i
in range(
5):
-
dp[i][
0] =
1
-
for i
in range(
1,
5):
-
for j
in range(
1,n+
1):
-
for k
in range(j/coins[i
-1]+
1):
-
dp[i][j] += dp[i
-1][j-k*coins[i
-1]]
-
return dp[
4][n]
案例七:最小编辑距离
Q:给定一个长度为m和n的两个字符串,设有以下几种操作:替换(R),插入(I),删除(D)且都是相同代价的操作。寻找到转化一个字符串插入到另一个需要修改的最小(操作)数量。
分析:
- dp[i][j] 表示长度为i的字符串A替换到长度为j的字符串B所付出的代价
- 当两个字符串的大小为0,其操作距离为0。
- 当其中一个字符串的长度是零,需要的操作距离就是另一个字符串的长度.
代码:
-
def editDist(self,s1,s2):
-
#思路:
-
#dp[i][j] 表示长度为i的字符串A替换到长度为j的字符串B所付出的代价
-
len1 = len(s1)
-
len2 = len(s2)
-
dp = [[
0
for i
in range(len2+
1)]
for i
in range(len1+
1)]
-
for i
in range(len1+
1):
-
dp[i][
0] = i
-
for i
in range(len2+
1):
-
dp[
0][i] = i
-
for i
in range(
1,len1+
1):
-
for j
in range(
1,len2+
1):
-
#如果当前两个字符串指针所指向的字符相等时,
-
if s1[i
-1]==s2[j
-1]:
-
dp[i][j] = dp[i
-1][j
-1]
-
else:
-
dp[i][j] = min(dp[i
-1][j],dp[i][j
-1],dp[i
-1][j
-1])+
1
-
return dp[len1][len2]
案例八:背包问题(01背包,完全背包,多重背包)
这里只写出了c++的写法
首先分别解释一下三种背包的含义:
- 01背包:有n种物品与承重为m的背包。每种物品只有一件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大
- 完全背包:有n种物品与承重为m的背包。每种物品有无限件,每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大
- 多重背包:有n种物品与承重为m的背包。每种物品有有限件num[i],每个物品都有对应的重量weight[i]与价值value[i],求解如何装包使得价值最大
关于01背包:
为什么叫01背包,因为装进去就是1,不装进去就是0,所以针对每个物品就有两种状态,装?不装?所以这个背包只要有足够大的空间,这个物品都是有可能被装进去的。
所以有状态转移方程
dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])
-
for (i =
1; i <= n; i++) #从
1开始是因为这涉及到dp[i
-1][j],从
0开始会越界
-
-
for (m = v; j >= weight[i]; j--)
//在这里,背包放入物品后,容量不断的减少,直到再也放不进了
-
-
dp[i][m] = max(dp[i
-1][m],dp[i
-1][m-weight[i]+value[i]])
仔细分析就会发现,这种二维数组开销很大,因此有了下面的滚动数组,说白了只是把所有的物品都跑一遍,然后到最后一个物品的时候输出答案,那么过程值只是计算的时候用一次,没必要存下来,所以用一个数组去滚动存储,然后用后一个状态的值去覆盖前一个状态。
-
for(
int i=
1; i<=n; i++)
//对每个数判断,可反
-
{
-
for(
int j=m; j>=weight[i]; j--)
//这里这个循环定死,不能反,反了就是完全背包
-
{
-
dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);
//其实不断在判断最优解,一层一层的
-
}
-
}
其实就是规定从m开始循环,保证了选择这个物品时,肯定不会重复使用状态。
关于完全背包:
完全背包每个物品都是无限,认死了选性价比最高的,不一定是完全填满背包的。(其实就是01背包一维数组中把j倒置)
这里的二维数组就不如一维数组了
-
for(
int i=
0;i<n;i++){
-
-
for(
int j=node[i].b;j<=m;j++){
//这样就是完全背包
-
-
dp[j]=max(dp[j],dp[j-node[i].b]+node[i].a)
关于多重背包:
首先把物品拆开,把相同的num[i]件物品看成价值和重量相同的num[i]件不同的商品,那么,就转化成了一个规模稍微大一点的01背包了。
-
for(
int i=
1; i<=n; i++)
//每种物品
-
for(
int k=
0; k<num[i]; k++)
//其实就是把这类物品展开,调用num[i]次01背包代码
-
for(
int j=m; j>=weight[i]; j--)
//正常的01背包代码
-
dp[j]=max(dp[j],dp[j-weight[i]]+value[i])
以上八种案例为动态规划的经典案例,后序还会进行不定期更新!
参考文献:
1、https://blog.csdn.net/lq_lq314/article/details/79172578 2018.8.6