动态规划

什么是动态规划?

首先给出一道例题:

给定一个矩阵网格,一个机器人从左上角出发,每次可以向下或向右走一步。
A:求有多少种方式走到右下角。
B:输出所有走到右下角的路径。
那么哪道题可以用动态规划来解呢?
答案是A。
动态规划一般求出来的是最优解或者是方式数。

动态规划题目的特点

1.计数
-有多少种方式走到右下角
-有多少种方法选出k个数使得和是sum
2.求最大最小值
-从左上角走到右下角路径的最大数字和
-最长上升子序列长度
3.求存在性
-取石子游戏,先手是否必胜
-能不能选出k个数使得和是sum

接下来给出具体的几道例题的解题方案

第一题:Coin Change(最值型)

你有三种硬币,分别面值是2元,5元和7元,每种硬币都有足够多,卖一本书需要27元,如何用最少的硬币组合正好付清,不需要对方找钱。
通过最少两个字可以知道这是一道求最大最小动态规划。
接下来是解题步骤
一:确定状态

简单的说,解决动态规划的时候需要开一个数组,数组的每个元素f[i]或"f[i,j]代表什么

  • 类似于解数学题中,X,Y,Z代表什么

  • 确定状态需要两个意识(最后一步和子问题)

  • 虽然我们不知道最优的策略是什么,但是最优的策略肯定是K枚硬币a1,a2,…ak,面值加起来是27

  • 所以一定有一枚最后的硬币ak

  • 除掉这枚银币,前面的硬币面值加起来是27-ak
    关键点1:我们不关心前面K-1枚硬币是怎么拼出27-ak的(可能有一种拼法,可能有100种拼法),而且我们现在甚至不知道ak和K,但我们确定前面的硬币拼出了27-ak。
    关键点2:因为是最优策略,所以拼出27-ak的硬币数一定要最少,否则这就不是最优策略了。

  • 所以我们就要求:最少用多少枚硬币可以拼出27-ak

  • 原问题是最少用多少枚硬币拼出27

  • 我们将问题一样规模变小的问题叫做子问题,因此我们将原问题转换成了一个子问题,而且规模更小:27-ak

  • 为了简化定义,我们设状态f(X)=最少用多少枚硬币拼出X

  • 现在我们还不知道最后的那枚硬币ak面值是多少

  • 最后那枚硬币只可能是2,5, 7

  • 如果ak是2,f(27)应该是f(27-2)+1

  • 如果ak是5,f(27)应该是f(27-5)+1

  • 如果ak是7,f(27)应该是f(27-7)+1

  • 除此之外没有其他可能

  • 需要求最少的硬币数,所以:

  • f(27) = min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
    上面的式子是可以递归计算的,但是会进行大量的重复计算,效率低下
    因此,我们可以将结果保存下来,并改变计算顺序。

二:转移方程

  • 设状态f[X] = 最少用多少枚硬币拼出X
  • 对于任意X
  • f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]+1}

三:初始条件和边界情况

  • f[X] = min{f[X-2]+1,f[X-5]+1,f[X-7]}
  • 两个问题:X-2,X-5或者X-7小于0怎么办?什么时候停下来?
  • 如果不能拼出Y,就定义f[Y] = 正无穷,例如:f[-1]=f[-2]=…=正无穷
  • 所以f[1] = min{f[-1]+1,f[-4]+1,f[-6]+1} = 正无穷,表示拼不出来1
  • 初始条件f[0] = 0(就是用转移方程算不出来的,需要手工定义)
  • 边界条件就是不要数组越界

四:计算顺序

  • 初始条件:f[0] = 0
  • 然后计算f[1],f[2],…,f[27]
  • (一般一维的计算顺序都是由小到大,二维是从左向右,从上向下)
  • 顺序原则其实很简单,就是当我们计算f[X]时,f[X-2],f[X-5],f[X-7]都已经得到结果了
public class Solution {
    /**
     * @param coins: a list of integer
     * @param amount: a total amount of money amount
     * @return: the fewest number of coins that you need to make up
     */
    public int coinChange(int[] coins, int amount) {
        // write your code here
        
        // 定义动态规划需要的数组
        int[] f = new int[amount+1];
        
        // 特殊值
        f[0] = 0;
        
        for(int i=1;i<=amount;i++){
            f[i] = Integer.MAX_VALUE;
            for(int j=0;j<coins.length;j++){
                
                if( i-coins[j]>=0 && f[i-coins[j]]!=Integer.MAX_VALUE){
                    f[i] = Math.min(f[i-coins[j]]+1,f[i]);
                }
                
            }
        }
        
        if(f[amount]==Integer.MAX_VALUE){
            f[amount] = -1;
        }
        
        return f[amount];
    }
}

第二题 不同的路径(计数型)

有一个机器人的位于一个 m × n 个网格左上角。

机器人每一时刻只能向下或者向右移动一步。机器人试图达到网格的右下角。

问有多少条不同的路径?
解题步骤

最后一步:无论机器人用何种方式到达右下角,总有最后挪动的一步:向右或者向下

  • 右下角坐标设为(m-1,n-1)
  • 那么前一步机器人一定是在(m-2,n-1)或者(m-1,n-2)
  • 那么,如果机器人有X种方式从左上角走到(m-2,n-1),有Y种方式从左上角走到(m-1,n-2),则机器人有X+Y种方式走到(m-1,n-1)
  • 问题转化为,机器人有多少种方式从左上角走到(m-2,n-1)和(m-1,n-2)
  • 所以找到子问题了
    状态:设f[i,j]为机器人有多少种方式从左上角走到(i,j)
    所以对于任意一个格子(i,j):
f[i][j] = f[i-1][j] + f[i][j-1]

初始条件和边界情况:

  • 初始条件:f[0,0] = 1,因为机器人只有一种方式到左上角
  • 边界情况:i=0或j=0,则前一步只能有一个方向过来f[i,j] = 1
    计算顺序
  • 从0行到m-1 最后答案f[m-1,n-1]
public class Solution {
    /**
     * @param m: positive integer (1 <= m <= 100)
     * @param n: positive integer (1 <= n <= 100)
     * @return: An integer
     */
    public int uniquePaths(int m, int n) {
        // write your code here
        int f[][] = new int[m][n];
        f[0][0] = 0;
        for(int i=0;i<m;i++){
            for(int j=0;j<n;j++){
                if(i==0||j==0){
                    f[i][j] = 1;
                }else{
                    f[i][j] = f[i-1][j] + f[i][j-1];
                }
                
            }
            
        }
        return f[m-1][n-1];
    }
}

第三题 Jump Game

给出一个非负整数数组,你最初定位在数组的第一个位置。

数组中的每个元素代表你在那个位置可以跳跃的最大长度。

判断你是否能到达数组的最后一个位置。

确定状态
最后一步:如果青蛙能跳到最后一块石头n-1,我们考虑它跳的最后一步

  • 这一步是从石头i跳过来的,i<n-1
  • 这需要两个条件同时满足:
    1.青蛙可以跳到石头i
    2.最后一步不超过跳跃的最大距离:n-1-i<=ai
    **状态:**设f[j]表示青蛙能不能跳到石头j
    在这里插入图片描述

初始条件和边界情况
初始条件:f[0] = True,因为青蛙一开始就在石头0
计算顺序:
从左到右,从小到大
最后返回f[n-1]

public class Solution {
    /**
     * @param A: A list of integers
     * @return: A boolean
     */
    public boolean canJump(int[] A) {
        // write your code here
        
        boolean[] f = new boolean[A.length]; 
        
        
        f[0] = true;
        
        for(int j=1;j<A.length;j++){
            
            for(int i=0;i<j;i++){
                
                 if(f[i]==true && i+A[i]>=j){
                    f[j] = true;
                }
            }
            
        }
        
        return f[A.length-1];
        
    }
}
  • 15
    点赞
  • 73
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值