动态规划算法详解(附带背包问题、机器人不同路径、跳台阶)

前言

关于动态规划的一些名词、概念,比如像什么动态转移方程,无后效性,最优子结构啦。我想我们可以先不用看百度出来的解释或者是书本上对这些概念的解释,过于复杂难懂的话,可能就没入门就放弃了。我们以三个例子为基础,将概念带入例子并进行总结,进而初识动态规划。

1、跳台阶

经典蛙兄跳台阶:

蛙兄正在上楼梯,楼梯有 n 阶台阶,蛙兄一次可以跳 1 阶或者跳2 阶,计算跳n阶台阶共有多少种方式。

分析:

我们假设f(n)表示跳n阶可以有多少种方式,而n阶是由n-1阶或者n-2阶跳上去的,而n-1阶又是由n-2阶或者n-3阶跳上去的。所以我们可以使用递归的方式来求得跳上n阶台阶共有多少种方式:

public class DPSkip {
    public int skip(int num){
        if (num<=2){
            return num;
        }
        return (num-1)+(num-2);
    }
}

但是这种方法的复杂度是很高的,为啥这么说呢?我们看一下就知道了,假设n为5:(毕竟n太大我也画不下。。。。)
在这里插入图片描述
这还只是n为5的情况,如果n更大的话,那么计算结果会更加复杂。

那么有没有复杂度小点的方法呢?肯定是有的,比如今天要看的动态规划。

因为我们上面就知道了,要想得到f(n),那么就要得到f(n-1)与f(n-2),这一点应该是都没有疑问的。什么?你有疑问?OK,同学你再回到上面看一看。

最优子结构与无后效性

f(n)作为我们要解决的问题,经过上面的分析,它的子问题是什么?哎,对,是f(n-1),f(n-2)这些,而这些子问题也被称为子结构。f(n)的值只与f(n-1)、f(n-2)有关,那f(n-1)的值与谁有关呢?没错,它只与f(n-2)与f(n-3)有关。发现了么?问题的最终解可以由子问题的解得到,而子问题的最优解又可以由子问题的子问题得到,本质上其实也就是递归。

通过子问题的最优解得到稍大点的子问题的最优解,而稍大点的子问题的最优解又可以得到更大问题的最优解。如果通过这样递归下去,子问题的最优结果能够推出更大问题的最优结果,我们就说这个问题符合最优子结构

什么是无后效性呢?f(n-1)、f(n-2)作为f(n)的子问题,我们只需要知道f(n-1)与f(n-2)的值即可,至于他们俩的值是怎么来的,对于我们后面的计算是无所谓的,这就是无后效性

最优子结构无后效性都是属于DP状态。

OK,了解了DP状态后,我们接着递归方法来看。在整个运算过程中,像这个f(3)、f(2)、f(1)这些都是计算了很多次, 我们可否只计算一次,将计算后的结果存储起来,用的时候直接使用:
在这里插入图片描述我们将每次得到的结果存储起来,从小到大依次计算,在这个跳台阶问题中,我们根据方程f(n)=f(n-1)+f(n-2)(动态转移方程) 来进行计算的。
所以用代码表示就是:

public class skip {
    public int skip(int i){
        if (i==1 || i==2){
            return i;
        }
        int skip[]=new int[i+1];
        skip[1]=1;
        skip[2]=2;
        for (int n=3;n<=i;n++){
            skip[n]=skip[n-1]+skip[n-2];
        }
        return skip[i];
    }
}

其实可以大概分为三步:
1、确定动态转移方程:本题中就是f(n)=f(n-1)+f(n-2)。通过一个方程式,能够将问题表达出来。倒是有些类似于我们高中学习过的数列。
2、从小到大计算:意思是说我们将问题分割之后,从最小的问题出发开始计算,不断地增大,直到最后解决问题。
3、保存每一步的结果:还是我们这个跳台阶,如果我们将f(1)、f(2)、f(3)等等,将这些结果保存之后,我们再后面用到的时候不就可以直接使用了么?不必花时间再去计算。

2、机器人不同路径

一个机器人位于一个 m x n 网格的左上角 (起始点在下图中标记为 “Start” )。
机器人每次只能向下或者向右移动一步。机器人试图达到网格的右下角(在下图中标记为 “Finish” )。

在这里插入图片描述
这个问题如果使用我们上面的那三步:确定动态转移方程(或者通俗说通用方程式)、从小到大计算、缓存每一步结果。这三步在这个问题该怎么描述呢?可以先思考一会。。。。。
在这里插入图片描述

这个呢,我们还是先倒推,如果要走到最后一格f(m,n),从图上直观来看,只有f(m-1,n)与f(m,n-1)这两格是可以走到的:
在这里插入图片描述
也就是说只能由上边那一格与左边那一格走过去。由此我们继续递推,除了最上面的一行与最左边的那一行,别的格子是都可以这样走过去的。

我们新建一个二维数组,

所以我们的方程就可以这样写:f(m,n)=f(m-1,n)+f(m,n-1)。

最关键的一步已经解决,剩下的就好办了。

  public int robot(int i,int j){
        if (i==1 || j==1){
            return 1;
        }
        int result[][]=new int[i+1][j+1];
        result[2][1]=1;
        result[1][2]=1;
        for (int m=2;m<=i;m++){
            for (int n=2;n<=j;n++){
                result[m][n]=result[m-1][n]+result[m][n-1];
            }
        }
        return result[i][j];
    }

3、01背包问题

假如你是探险家伊泽瑞尔,你来到野外进行探险,一不小心掉下了悬崖,发现了一大堆宝石,此时你有一个背包,容量为k,要从这n个宝石中选择最值钱的珠宝塞到你的背包,每个宝石都有自己的重量与价值,你需要从中选择最合适的组合放到背包中,从而使得你的背包中珠宝的总价值最大。

那好啊,我都装起来赶紧跑路啊。
在这里插入图片描述

装太多你都出不去了,得在最大容量范围内找到价值最大的组合才好。

我们假设有4个物品,他们的重量分别为:w1(2),w2(3),w3(5),w4(5)。
价值分别为:v1(2),v2(4),v3(3),v4(7)。我们假设最大容量k为10,ci表示第i个选或者不选,那么我们就可以得到这样一个方程式:

c1w1+c2w2+c3w3+c4w4<=k

ci是1或者0,1表示选,0表示不选。我觉得这也就是为啥是01背包吧,要么选,要么不选。

我们就还按照我们之前的那三步来进行分析。现在最终的问题是如何将n个物品放到容量为k的背包中,那么它的子问题我们可以看作是将i个物品放到容量为j(j<=k)的背包中。

而子问题中,是可以分为两种情况的:

1、当当前容量不足以装下第i个宝石时,那么i个物品、j容量的背包能够放下的宝石的最大价值就与i-1个物品,j容量的背包是一样的。因为第i个装不了嘛,所以要和前面的那种保持一致了。
2、当前背包可以装下第i个宝石,如果你选择了装第i个宝石,因为第i个宝石的重量是wi,所以前面i-1个宝石能够装到背包里面的总重量就是j-wi。我们以f(i,j)表示i个宝石、j容量背包时的最大价值。那么此时的表达时就可以写成:f(i,j)=f(i-1,j-wi)+vi。
但是可以装下第i个宝石,不代表装了第i个宝石,价值最大。对吧,假如你前面i-1个的时候就已经找到最优解了,你强行要装第i个,那不就错了嘛。所以需要在f(i-1,j)与f(i-1,j-wi)+vi中找到他们的最大值。

我们来画个表格看一下,分析配合上表格更加容易理解:

在这里插入图片描述
左侧没什么好说的,w与v的不同值。之后的横坐标表示当前有i个宝石,纵坐标表示j容量。

这个时候你会问了,这个表格看起来眼花,我怎么知道光看你这个图表就可以得到要那个宝石要选,哪个宝石不选呢?哎,莫慌,在上面我们知道,如果第i个宝石没有选的话,那么f(i-1,j)=f(i,j),如果选了第i个宝石,那么第i-1个宝石所在的位置就是f(i-1,j-wi)。这样我们就可以得到最终的选择结果:

在这里插入图片描述
所以最优解就是1、3、4这三个,最大价值就是13。

 //n为总数量,k为负重,v为宝石的价值的集合,w是宝石重量的集合
    public int dpPackage(int n,int k,int[] v,int[] w){
       //新建二维数组存放查询出来的数据
        int[][] valueSum = new int[n+1][k+1];
        //将第一行与第一列置为0
        for (int i = 0; i <= n; i++){
            valueSum[i][0] = 0;
        }
        for (int i = 0; i <= k; i++){
            valueSum[0][i] = 0;
        }
        for (int i = 1; i <= n; i++){
            for (int j = 1; j <= k; j++){
                if (j < w[i]){
                    // 装不进去
                    valueSum[i][j] = valueSum[i-1][j];
                } else {
                    //可以装进去,取最大值
                    valueSum[i][j] = Math.max(valueSum[i-1][j-w[i]] + v[i],valueSum[i-1][j]);
                }
            }
        }
        return valueSum[n+1][k+1];
    }

总结

经过上面三个例子,我们可以看到动态规划的基本思想就是:最终问题的最终解能够通过子问题的解得到。我们通过对子问题一步一步求解,并将子问题的解缓存起来供以后使用,直至得到最终问题的解。

至于像DP状态、动态转移方程这些概念,我想结合这三个例子,应该是可以理解的。不过动态规划问题也比较多,我想不是一篇文章就能够彻底明白的,还是需要多做题、总结,才能够更好的掌握。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值