[关闭]
@adamhand 2019-04-02T09:01:19.000000Z 字数 5518 阅读 918

算法


动态规划

基本思想

动态规划是通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。对于一个复杂的问题,可以将其分解成很多小问题,再合并子问题的解以得出原问题的解。通常许多子问题非常相似,为此动态规划法试图仅仅解决每个子问题一次,从而减少计算量: 一旦某个给定子问题的解已经算出,则将其记忆化存储,以便下次需要同一个子问题解之时直接查表。 这种做法在重复子问题的数目关于输入的规模呈指数增长时特别有用。

动态规划和分治的思想很像,主要区别和联系如下:

所以,动态规划的核心就是记住已经解决过的子问题的解从而减少重复计算

例子

下面看一个例子。

  1. 给定一个三角形,寻找一条从顶部到底边的路径,使得路径上所经过的数字之和最大。路径上的每一步都只能往左下或 右下走。只需要求出这个最大和即可,不必给出具体路径。 三角形的行数大于1小于等于100,数字为 0 - 99
  2. 7
  3. 3 8
  4. 8 1 0
  5. 2 7 4 4
  6. 4 5 2 6 5

思路:首先,肯定得用二维数组来存放数字三角形;然后用D( r, j) 来表示第r行第 j 个数字(r,j1开始算),用MaxSum(r,j)表示从D(r,j)到底边的各条路径中,最佳路径的数字之和。

因此,此题的最终问题就变成了求 MaxSum(1,1)

递归

上述问题可以用递归来解决。从D(r,j)出发,下一步只能走D(r+1,j)或者D(r+1, j+1)。故对于N行的三角形,我们可以写出如下的递归式:

  1. if ( r == N)
  2. MaxSum(r,j) = D(r,j)
  3. else
  4. MaxSum( r, j) = Max{ MaxSum(r1,j), MaxSum(r+1,j+1) } + D(r,j)

完整代码如下:

  1. /**
  2. * n表示二维矩阵的行数
  3. */
  4. public int maxSum(int i, int j, int n, int[][] matrix){
  5. if (i == n){
  6. return matrix[i][j];
  7. }
  8. int x = maxSum(i+1, j, n, matrix);
  9. int y = maxSum(i+1, j+1, n, matrix);
  10. return matrix[i][j] + Math.max(x, y);
  11. }

但是用递归会存在存在很多重复计算的问题,如下图所示:



比如第三行数字1,当计算从第2行的数字3开始的MaxSum时会计算出从1开始的MaxSum,当计算从第二行的数字8开始的MaxSum的时候又会计算一次从1开始的MaxSum

记忆递归型的动态规划:(自顶向下)

如果每算出一个MaxSum(r,j)就保存起来,下次用到其值的时候直接取用,则可免去重复计算。

代码如下:

  1. /**
  2. * maxSum是用来记忆的数组,元素值初始化为-1
  3. */
  4. public int maxSum_1(int i, int j, int n, int[][] matrix, int[][] maxSum){
  5. if (maxSum[i][j] != -1){
  6. return maxSum[i][j];
  7. }
  8. if (i == n){
  9. maxSum[i][j] = matrix[i][j];
  10. }else {
  11. int x = maxSum_1(i+1, j, n, matrix, maxSum);
  12. int y = maxSum_1(i+1, j+1, n, matrix, maxSum);
  13. maxSum[i][j] = matrix[i][j] + Math.max(x, y);
  14. }
  15. return maxSum[i][j];
  16. }

迭代式的动态规划:(自底向上)

递归总是需要使用大量堆栈上的空间,很容易造成栈溢出,现在就要考虑如何把递归转换为递推。

首先需要计算的是最后一行,因此可以把最后一行直接写出,如下图:



现在开始分析倒数第二行的每一个数,现分析数字22可以和最后一行4相加,也可以和最后一行的5相加,但是很显然和5相加要更大一点,结果为7,此时就可以将7保存起来。

然后分析数字77可以和最后一行的5相加,也可以和最后一行的2相加,很显然和5相加更大,结果为12,因此将12保存起来。以此类推,可以得到下面这张图:



然后按同样的道理分析倒数第三行和倒数第四行,最后分析第一行,可以依次得到如下结果:





根据上面的推断,就可以写出递推型动态规划程序:

  1. public int maxSum_2(int i, int j, int n, int[][] matrix, int[][] maxSum){
  2. for (int m = 1; m < n; m++){
  3. maxSum[n][m] = matrix[n][m];
  4. }
  5. for (int m = n - 1; m >= 1; m--){
  6. for (int p = 1; p <= m; p++){
  7. maxSum[m][p] = Math.max(maxSum[m+1][p], maxSum[m+1][p+1]) + matrix[m][p];
  8. }
  9. }
  10. return maxSum[1][1];
  11. }

继续进行空间优化

其实完全没必要用二维maxSum数组存储每一个MaxSum(r,j),只要从底层一行行向上递推,那么只要存储一行的MaxSum值就可以。

对于空间优化后的具体递推过程如下:













程序如下:

  1. public int maxSum_3(int i, int j, int n, int[][] matrx, int[] maxSum){
  2. maxSum = matrx[n];
  3. for (int m = n-1; m >= 1; --m){
  4. for (int p = 1; p <= m; p++){
  5. maxSum[p] = Math.max(maxSum[p], maxSum[p+1]) + matrx[m][p];
  6. }
  7. }
  8. return maxSum[1];
  9. }

总结

动规解题的一般思路

1. 将原问题分解为子问题

2.确定状态

3.确定一些初始状态(边界状态)的值

以“数字三角形”为例,初始状态就是底边数字,值就是底边数字值。

4. 确定状态转移方程

定义出什么是“状态”,以及在该“状态”下的“值”后,就要找出不同的状态之间如何迁移――即如何从一个或多个“值”已知的 “状态”,求出另一个“状态”的“值”(递推型)。状态的迁移可以用递推公式表示,此递推公式也可被称作“状态转移方程”。

数字三角形的状态转移方程:



能用动规解决的问题的特点

例题1:0-1背包问题

描述:有 N 件物品和一个容量为 C 的背包。第 i 件物品占用的空间是 w[i],价值是 v[i]。求解将哪些物品装入背包可使这些物品的费用总和不超过背包容量,且价值总和最大。

思路:先定义子问题的状态:f[i][v]表示在面对第i件物品,且背包容量为v时所能获得的最大价值 。对于每一件物品,都有两种选择:放入不放入。对于第i件物品:

  1. if (背包体积j小于物品i的体积)
  2. f[i][v] = f[i-1][v] //背包装不下第i个物体,目前只能靠前i-1个物体装包
  3. else
  4. f[i][v] = max{f[i-1][v], f[i-1][v-w[i]]+p[i]}

程序如下:

  1. public int test(){
  2. int V = 10; //背包容量
  3. int N = 5; //物品个数
  4. int[] values = {0,8,10,4,5,5}; //第i个物品的价值,下标为0的位置不是物品
  5. int[] weight = {0,6,4,2,4,3}; //第i个物品占用的空间
  6. int[][] f = new int[N+1][V+1];
  7. for (int i = 0; i < N+1; i++){
  8. for (int j = 0; j < V+1; j++){
  9. f[i][j] = 0;
  10. }
  11. }
  12. for (int i = 1; i <= N; i++){
  13. for (int j = 1; j <= V; j++){
  14. if (weight[i] > j){
  15. f[i][j] = f[i-1][j];
  16. }else {
  17. f[i][j] = Math.max(f[i-1][j], f[i-1][j-weight[i]]+values[i]);
  18. }
  19. }
  20. }
  21. return f[N][V];
  22. }

图解如下:



填表的时候自左向右、自上向下填。比如图中红色的10,填这个位置的时候,weight4,包容量为5,说明如果包为空的话,这个物品能够放下。那么就要考虑放和不放两种情况哪种能够使得背包中的物品的值更大。

比较一下105,显然是10更大,所以选择不放,此时的价值为10

最大连续子序列之和

描述:给定K个整数的序列{ N1, N2, ..., NK },其任意连续子序列可表示为{ Ni, Ni+1, ..., Nj },其中 1 <= i <= j <= K。最大连续子序列是所有连续子序中元素和最大的一个, 例如给定序列{ -2, 11, -4, 13, -5, -2 },其最大连续子序列为{ 11, -4, 13 },最大和为20

思路:
令状态 dp[i] 表示以 A[i] 作为末尾的连续序列的最大和,那么只有两种情况:

由此可得状态转移方程为:

  1. dp[i] = max{A[i], dp[i-1]+A[i]}

边界为 dp[0] = A[0]

程序如下:

  1. public int test(){
  2. int[] nums = {-2, 11, -4, 13, -5, -2};
  3. int len = nums.length;
  4. int[] dp = new int[len];
  5. dp[0] = nums[0];
  6. for (int i = 1; i < len;i ++){
  7. dp[i] = Math.max(nums[i], dp[i-1]+nums[i]);
  8. }
  9. int max = dp[0];
  10. for (int i = 1; i < len; i++){
  11. max = max > dp[i] ? max : dp[i];
  12. }
  13. return max;
  14. }

最长递增子序列(LIS)

动态规划(三)最长递增子序列LIS、最大连续子序列和、最大连续子序列乘积
动态规划:最长上升子序列(LIS)

最长公共子序列(LCS)

参考

动态规划
教你彻底学会动态规划——入门篇
算法-动态规划 Dynamic Programming--从菜鸟到老鸟
动态规划——最大连续子序列和
经典算法问题 - 最大连续子数列和
动态规划经典五题

回溯

回溯是一种算法思想。它的主要思想是:通过对所做的选择构造一颗状态空间树,按照深度优先的策略,从根开始深度搜索状态空间树。当搜索到某一结点,先判断该节点是否可以构成完整解(剪枝),如果可以就继续向下搜索;否则就逐层返回回溯,尝试其他路径。

基本思路

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注