动态规划案例(python版本)

最近几天一直在看有关动态规划的算法,整理了一些常见案例,主要是求最长公共子序列最长公共子串最长递增子序列最长回文子串硬币的组合数硬币的最少组合方法最小编辑距离背包问题(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]的值只可能来自一下三种情况,三种可能的值中,选择最大的值即可

  1.  情况一:可能是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]
  2. 情况二:同理可知,dp[i][j]的值也可能是dp[i][j-1]
  3. 情况三:如果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
代码:

  1. def findLongest(self, A, n, B, m):
  2. #新建一个m行n列的矩阵
  3. matrix = [0] * m * n
  4. #1、矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0...n]的最长公共子串.
  5. # str2[0]只有一个字符,所以matrix[i][0]最大为1
  6. for i in range(n):
  7. if A[i] == B[0]:
  8. for j in range(i,n):
  9. matrix[j] = 1
  10. #2、矩阵的第一列,matrix[i][0]最大为1
  11. for i in range(m):
  12. if B[i] == A[0]:
  13. for j in range(i,m):
  14. matrix[j*n] = 1
  15. #3、其他位置,matrix[i][j]有三种情况,matrix[m][n]即为所求的最长公共子序列长度
  16. for i in range(1,m):
  17. for j in range(1,n):
  18. if B[i] == A[j]:
  19. matrix[i*n+j] = max(matrix[(i-1)*n+j-1]+1,matrix[(i-1)*n+j],matrix[i*n+j-1])
  20. else:
  21. matrix[i*n+j] = max(matrix[(i-1)*n+j],matrix[i*n+j-1])
  22. return matrix[m*n-1]


案例二:求最长公共子串(连续)

Q:给定两个序列,找出在两个序列中同时出现的最长子序列的长度。子串的意思是要求为连续的子序列

分析:

矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0…n]的最长公共子串.

与案例一中的前两步相同,只是最后一步不同。

代码:

  1. def findLongest(self, A, n, B, m):
  2. #新建一个m行n列的矩阵
  3. matrix = [0] * m * n
  4. #1、矩阵的第一行,即matrix[0][i],代表str1[0]与str2[0...n]的最长公共子串.
  5. # str2[0]只有一个字符,所以matrix[i][0]最大为1
  6. for i in range(n):
  7. if A[i] == B[0]:
  8. matrix[i] = 1
  9. #2、矩阵的第一列,matrix[i][0]最大为1
  10. for i in range(m):
  11. if B[i] == A[0]:
  12. matrix[i*n] = 1
  13. #3、其他位置
  14. max = 0
  15. for i in range(1,m):
  16. for j in range(1,n):
  17. if B[i] == A[j]:
  18. matrix[i*n+j] = matrix[(i-1)*n+j-1]+1
  19. if max<matrix[i*n+j]:
  20. max = matrix[i*n+j]
  21. 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])}


代码:

  1. def findLongest(self, A, n):
  2. dp = [0] * n
  3. dp[0] = 1
  4. for i in range(1, len(A)):
  5. l = [1]
  6. for j in range(0, i):
  7. if A[i] > A[j]:
  8. l.append(dp[j] + 1)
  9. dp[i] = max(l)
  10. 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

代码:

  1. def manacher(self,s):
  2. #建立一个二维数组
  3. maxlen = 0
  4. start = 0
  5. dp = [[0 for i in range(len(s))] for i in range(len(s))]
  6. for i in range(len(s)):
  7. dp[i][i] = 1
  8. if i+1<len(s) and s[i] == s[i+1]:
  9. dp[i][i+1] = 1
  10. maxlen = 2
  11. start = i
  12. for i in range(3,len(s)+1): #i表示回文子串长度,从3开始,最长为len(s)
  13. for j in range(len(s)-i+1): #j表示指针移动的起点
  14. k = i+j-1 #k表示终点
  15. if dp[j+1][k-1]==1 and s[j]==s[k]:
  16. dp[j][k] = 1
  17. if i>maxlen:
  18. start = j
  19. maxlen = i
  20. if maxlen>=2:
  21. return s[start:start+maxlen]
  22. return None

案例五:硬币最少数量(凑齐n元最少需要几枚硬币

Q:假设有 1 元,3 元,5 元的硬币若干(无限),现在需要凑出 11 元,问如何组合才能使硬币的数量最少?

分析:

我们先假设一个函数 d(i) 来表示需要凑出 i 的总价值需要的最少硬币数量。

  1. 当 i = 0 时,很显然我们可以知道 d(0) = 0。因为不要凑钱了嘛,当然也不需要任何硬币了。注意这是很重要的一步,其后所有的结果都从这一步延伸开来
  2. 当 i = 1 时,因为我们有 1 元的硬币,所以直接在第 1 步的基础上,加上 1 个 1 元硬币,得出 d(1) = 1
  3. 当 i = 2 时,因为我们并没有 2 元的硬币,所以只能拿 1 元的硬币来凑。在第 2 步的基础上,加上 1 个 1 元硬币,得出 d(2) = 2
  4. 当 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. 假设最后加上的是 1 元硬币,那 d(i) = d(j) + 1 = d(i - 1) + 1
  2. 假设最后加上的是 3 元硬币,那 d(i) = d(j) + 1 = d(i - 3) + 1
  3. 假设最后加上的是 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 )

代码:

  1. def findLeast(self, n):
  2. # write code here
  3. l = [0,1,2,1,2,1]
  4. for i in range(6,n+1):
  5. l.append(min(l[i-1]+1,l[i-3]+1,l[i-5]+1))
  6. return l[n]


案例六:硬币组合种类数(凑齐n分钱有多少种方法)

Q:有数量不限的硬币,币值为25分、10分、5分和1分,请编写代码计算n分有几种表示法。给定一个int n,请返回n分有几种表示法。保证n小于等于1000,为了防止溢出,请将答案Mod 1000000007。

分析:

  1. dp[i][sum]表示用前i种硬币构成sum的所有组合数,本题实际上就是求dp[n][sum]
  2. coins = [1,5,10,25]
  3. 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])
  4. 上一步化简后:dp[i][sum] = dp[i-1][sum-k*coins[i]]求和 (k = 0…sum/coins)

代码:

  1. def coinsWays(self, n):
  2. coins = [1,5,10,25]
  3. dp = [[0 for i in range(n+1)] for i in range(5)]
  4. for i in range(5):
  5. dp[i][0] = 1
  6. for i in range(1,5):
  7. for j in range(1,n+1):
  8. for k in range(j/coins[i-1]+1):
  9. dp[i][j] += dp[i-1][j-k*coins[i-1]]
  10. return dp[4][n]


案例七:最小编辑距离

Q:给定一个长度为m和n的两个字符串,设有以下几种操作:替换(R),插入(I),删除(D)且都是相同代价的操作。寻找到转化一个字符串插入到另一个需要修改的最小(操作)数量。

分析:

  1. dp[i][j] 表示长度为i的字符串A替换到长度为j的字符串B所付出的代价
  2. 当两个字符串的大小为0,其操作距离为0。
  3. 当其中一个字符串的长度是零,需要的操作距离就是另一个字符串的长度. 


代码:

  1. def editDist(self,s1,s2):
  2. #思路:
  3. #dp[i][j] 表示长度为i的字符串A替换到长度为j的字符串B所付出的代价
  4. len1 = len(s1)
  5. len2 = len(s2)
  6. dp = [[0 for i in range(len2+1)]for i in range(len1+1)]
  7. for i in range(len1+1):
  8. dp[i][0] = i
  9. for i in range(len2+1):
  10. dp[0][i] = i
  11. for i in range(1,len1+1):
  12. for j in range(1,len2+1):
  13. #如果当前两个字符串指针所指向的字符相等时,
  14. if s1[i-1]==s2[j-1]:
  15. dp[i][j] = dp[i-1][j-1]
  16. else:
  17. dp[i][j] = min(dp[i-1][j],dp[i][j-1],dp[i-1][j-1])+1
  18. 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]])

  1. for (i = 1; i <= n; i++) #从1开始是因为这涉及到dp[i-1][j],从0开始会越界
  2. for (m = v; j >= weight[i]; j--)//在这里,背包放入物品后,容量不断的减少,直到再也放不进了
  3. dp[i][m] = max(dp[i-1][m],dp[i-1][m-weight[i]+value[i]])

仔细分析就会发现,这种二维数组开销很大,因此有了下面的滚动数组,说白了只是把所有的物品都跑一遍,然后到最后一个物品的时候输出答案,那么过程值只是计算的时候用一次,没必要存下来,所以用一个数组去滚动存储,然后用后一个状态的值去覆盖前一个状态。

  1. for(int i=1; i<=n; i++)//对每个数判断,可反
  2. {
  3. for(int j=m; j>=weight[i]; j--)//这里这个循环定死,不能反,反了就是完全背包
  4. {
  5. dp[j]=max(dp[j],dp[j-weight[i]]+value[i]);//其实不断在判断最优解,一层一层的
  6. }
  7. }
其实就是规定从m开始循环,保证了选择这个物品时,肯定不会重复使用状态。

关于完全背包:

完全背包每个物品都是无限,认死了选性价比最高的,不一定是完全填满背包的。(其实就是01背包一维数组中把j倒置)

这里的二维数组就不如一维数组了

  1. for(int i=0;i<n;i++){
  2. for(int j=node[i].b;j<=m;j++){//这样就是完全背包
  3. dp[j]=max(dp[j],dp[j-node[i].b]+node[i].a)
关于多重背包:

首先把物品拆开,把相同的num[i]件物品看成价值和重量相同的num[i]件不同的商品,那么,就转化成了一个规模稍微大一点的01背包了。

  1. for(int i=1; i<=n; i++)//每种物品
  2. for(int k=0; k<num[i]; k++)//其实就是把这类物品展开,调用num[i]次01背包代码
  3. for(int j=m; j>=weight[i]; j--)//正常的01背包代码
  4. dp[j]=max(dp[j],dp[j-weight[i]]+value[i])


以上八种案例为动态规划的经典案例,后序还会进行不定期更新!

参考文献:
1、https://blog.csdn.net/lq_lq314/article/details/79172578 2018.8.6

阅读更多

扫码向博主提问

JohnieLi

深度学习小白,寻找一起学习的道友
  • 擅长领域:
  • 图像处理
  • 深度学习
去开通我的Chat快问
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页