动态规划(一)

一.初入动态规划

1.什么是动态规划?

动态规划(一)_第1张图片

  • A:求有多少种方式走到右下角
  • B:输出所有走到右下角的路径,哪个可以用动态规划?
  • 题A是计数问题,属于动态规划题型的一种,而题B则是DFS

2.动态规划特点

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

3.例1 零钱兑换(leetcode 322)

  • 给定不同的硬币,比如分别为2元,5元,7元,每种硬币足够多
  • 买一本书需要27元
  • 如何用最少的硬币组合正好付清,不需要对方找钱?

重点是最少,明显属于最值问题,那么如何达到最少的目的?

  • 试试直觉,最少的硬币组合->用面值大的硬币(算是贪心的思想),7+7+7=21 21+5=26,还有1块钱凑不出来…
  • 改算法,尽量用面值大的硬币,最后如何可以用一种硬币付清就行,7+7+7=21 21+2+2+2=27,6枚
  • 然而答案是5枚,7+5+5+5+5=27
动态规划组成部分一:确定状态
  • 状态在动态规划中的作用属于定海神针
  • 简单的说,解动态规划的时候需要开一个数组,数组的每个元素f[i]或者f[i][j]代表什么(类似解数学题中,x,y,z代表什么)
  • 确定状态需要两个意识,最后一步、子问题
最后一步
  • 虽然不知最优策略是什么,但是最优策略肯定是K枚硬币a1,a2,…ak面值加起来是27
  • 所以一定有一枚最后的硬币:ak
  • 除掉这枚硬币,前面硬币面值加起来是27-ak
    动态规划(一)_第2张图片
  • 我们不关心前面的k-1枚硬币是怎么拼出27-ak(可能有1种拼法,也可以能有100种),而且我们现在不知道ak和k,但是我们知道前面的硬币拼出27-ak
  • 因为是最优策略,所以拼出27-ak的硬币数一定是最少,否则这就不是最优策略了,就矛盾了
  • 如下图
    动态规划(一)_第3张图片
  • 所以我们需要知道最少用多少枚硬币可以拼出27-ak,而原问题是最少用多少枚硬币拼出27
  • 我们将原问题转化成一个子问题,而且规模更小:27-ak
  • 为了简化定义,我们设状态f(x)=最少用多少枚硬币拼出x,而原来要求f(27),现在变成了f(27-k)
  • 但是此时我们不知道最后那枚硬币ak是多少,最后那枚硬币ak可能是2,5,7其中之一
    • 如果ak是2,f(27)=f(27-2)+1(加上最后这一枚硬币2)
    • 如果ak是5,f(27)=f(27-5)+1(加上最后这一枚硬币5)
    • 如果ak是7,f(27)=f(27-7)+1(加上最后这一枚硬币7)
  • 除此以外,没有其他的可能了
  • 需要求最少硬币,所以
  • f(27)=min{f(27-2)+1,f(27-5)+1,f(27-7)+1}
  • f(27)表示拼出27所需最少的硬币数
  • f(27-2)+1拼出25所需最少的硬币数,加上最后一枚硬币2
  • f(27-5)+1拼出25所需最少的硬币数,加上最后一枚硬币5
  • f(27-7)+1拼出25所需最少的硬币数,加上最后一枚硬币7
和递归有什么关系?

动态规划(一)_第4张图片

  • 用递归解法会有点问题,重复计算了很多次,效率低下,直接用数组在数组中计算不香嘛?
    动态规划(一)_第5张图片
  • 如何避免?
    • 可以将计算结果保存下俩,并改变计算顺序
动态规划组成部分二:转移方程
  • 设状态f[x]=最少用多少枚硬币拼出x
  • 对于任意x,f[x]=min{f[x-2]+1,f[x-5]+1,f[x-7]+1}拼出x所需最少的硬币数
  • f[x-2]+1表示拼出x-2所需最少的硬币数,加上最少一枚硬币2
  • f[x-5]+1表示拼出x-5所需最少的硬币数,加上最少一枚硬币5
  • f[x-7]+1表示拼出x-7所需最少的硬币数,加上最少一枚硬币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(初始条件用转移方程算不出来,要根据题目来定义)
动态规划组成部分四:计算顺序
  • 拼出X所需要的最少硬币数:f[X]=min{f[x-2]+1,f[x-5]+1,f[x-7]+1}
  • 初始条件:f[0]=0
  • 然后计算f[1],f[2],…f[27]
  • 当我们计算到f[x]时,f[x-2],f[x-5],f[X-7]都已经得到结果了
    • f[x]=非无穷数,表示最少用多少枚硬币拼出X
    • f[x]=正无穷,表示无法用硬币拼出X
      动态规划(一)_第6张图片
  • 每一步尝试三种硬币,一共27步,与递归算法比,没有任何重复计算
  • 算法时间复杂度(需要进行步数):273(nm)
  • 递归时间复杂度:>>27*3
编写代码
public int coinChange(int[] coins, int amount) {
            int [] dp = new int[amount+1];
           dp[0]=0;
           for(int i=1;i<=amount;i++){
               dp[i]=Integer.MAX_VALUE;
               for(int j=0;j<coins.length;j++){
                   if(i-coins[j]>=0&&dp[i-coins[j]]!=Integer.MAX_VALUE) {
                       dp[i] = Math.min(dp[i - coins[j]] + 1, dp[i]);
                   }
               }
           }
           if(dp[amount]==Integer.MAX_VALUE){
               return -1;
           }
           return dp[amount];
    }

4.例2 不同路径(leetcode 62)

  • m行n列网格,有一个机器人总左上角触发,每一步只能向下或向右一步,问有多少种方法走到右下角
    动态规划(一)_第7张图片
  • 典型的计数型动态规划
动态规划组成部分一:确定状态
  • 最后一步,不管何种方式到右下角,总有最后一步挪动,向右或者向下
  • 右下角坐标为(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),而原题有多少种方式从左上角走到(m-1,n-1)
  • 子问题
  • 状态:设f[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
动态规划组成部分四:计算顺序
  • f[0][0]=1
  • 计算第0行,计算第1行…
  • 计算第m-1行
  • 答案是f[m-1][n-1]
  • 时间复杂度(计算步数):O(MN),空间复杂度(数组大小):O(MN)
编写代码
public int uniquePaths(int m, int n) {
       int [][] dp = new int [n][m];//表示到(i,j)有dp[i][j]种方式
        for(int i=0;i<n;i++){
            for(int j=0;j<m;j++){
                if(i==0||j==0){
                    dp[i][j]=1;
                }else{
                    dp[i][j]=dp[i-1][j]+dp[i][j-1];
                }
            }
        }
        return dp[n-1][m-1];
    }

5.例3 青蛙跳石头(leetcode 403)

  • 有n块石头分别在x轴的0,1,…n-1位置,一只青蛙在石头0,想跳到石头n-1,如果青蛙在第i块石头上,它最多可以向右跳距离ai,问青蛙能否跳到石头n-1上
    例子:
    输入a=[2,3,1,1,4] 输出:true
    输入a=[3,2,1,0,4] 输出;false
第一步:确定状态
  • 最后一步:如果青蛙能跳到最后一块石头n-1,我们考虑它跳的最后一步

  • 这一步是从石头i跳过来,i

  • 需要两个条件同时满足

    • 青蛙可以跳到石头i
    • 最后一步不超过跳跃的最大距离:n-1-j<=ai
      动态规划(一)_第8张图片
  • 那么,我们需要知道青蛙能不能跳到石头i(i

  • 原来问题青蛙能不能跳到石头n-1

  • 子问题

  • 状态:设f[j]表示青蛙能不能跳到石头j上

第二步:转移方程

f[j]=OR(0<=i=j)

  • f[j]表示青蛙能不能跳到石头j
  • 枚举上一个跳到的石头i
  • f[i]表示青蛙能不能跳到石头i
  • 最后一步的距离不能超过ai
    动态规划(一)_第9张图片
第三步:初始条件和边界情况
  • 初始条件:f[0]=True,因为青蛙一开始就在石头0
  • 没有边界情况
第四步:计算顺序
  • 初始化f[0]=true
  • 计算f[1],f[2],…,f[n-1]
  • 答案是f[n-1]
  • 时间复杂度:O(n²),空间复杂度:O(n)
编写代码
 public boolean canJump(int[] nums) {
        int n = nums.length;
        boolean [] dp = new boolean[n];
        dp[0]=true;//dp[i]表示是否可以跳到第i个石头上,初始化青蛙在第0个石头上
        for(int i=1;i<n;i++){
           dp[i]=false;
           for(int j=0;j<i;j++){
               if(dp[j]&&j+nums[j]>=i){
                   dp[i]=true;
                   break;
               }
           }
        }
        return dp[n-1];
    }

6.例4 乘积最大子数组(leetcode 152)

  • 给你一个整数数组 nums ,请你找出数组中乘积最大的连续子数组(该子数组中至少包含一个数字),并返回该子数组所对应的乘积
    例子:
    输入: [2,3,-2,4] 输出:2*3=6
    输入:[-2,0,-1] 输出:0
第一步:确定状态
  • 题目是求连续最大的子数组对应的乘积,要考虑乘积就一定要分为两种情况,两个正数相乘为最大或者是两个负数相乘为最大
  • 最后一步:前面相乘的结果为sum,再乘以an,为数组中乘积最大的连续子数组
  • 按照乘法性质,如果你想让sum*an最大,无非两种可能性
    • an为负数,sum一定是前n-1位乘积最小的数
    • an为正数,sum一定是前n-1位乘积最大的数
  • 本来求整个数组中乘积最大的连续子数组的乘积(长度n),现在我们要求数组前(长度n-1)乘积最大和最小的连续数组的乘积
  • 子问题
  • 设imax[i]为前i位乘积最大的数,imin[i]为前i位乘积最小的数
第二步:转移方程
  • 当nums[i]小于0时
    imax[i]=Math.max(imin[i-1]*nums[i],nums[i])
    imin[i]=Math.min(imax[i-1]*nums[i],nums[i])
  • 当nums[i]大于等于0时
    imax[i]=Math.max(imax[i-1]*nums[i],nums[i])
    imin[i]=Math.min(imin[i-1]*nums[i],nums[i])
第三步:初始条件和边界情况
  • 初始条件:imax[0]=nums[0],imax[0]=nums[0]
  • 没有边界情况
第四步:计算顺序
  • 初始化imax[0]=nums[0],imax[0]=nums[0]
  • 计算从1到n-1
  • 时间复杂度:O(n),空间复杂度:O(n)[可以优化成O(2)]
第五步:编写代码
public int maxProduct(int[] nums) {
        int n = nums.length;
        int result = nums[0];
        int [] imax = new int[n];
        int [] imin = new int[n];
        imax[0]=nums[0];
        imin[0]=nums[0];
        for(int i=1;i<n;i++){
            if(nums[i]<0){
              imax[i] = Math.max(imin[i-1]*nums[i],nums[i]);
              imin[i] = Math.min(imax[i-1]*nums[i],nums[i]);
            }else{
            imax[i] = Math.max(imax[i-1]*nums[i],nums[i]);
            imin[i] = Math.min(imin[i-1]*nums[i],nums[i]);
            }
            result = Math.max(imax[i],result);
        }
        return result;
    }
  • 优化
    • 你会发现它i只会和i-1有关,所以我们可以对这题的空间进行优化
public int maxProduct(int[] nums) {
        int n = nums.length;
        int result = nums[0];
        int imax=nums[0],imin=nums[0];
        for(int i=1;i<n;i++){
            if(nums[i]<0){
              imax = imax^imin;
              imin = imax^imin;
              imax = imax^imin;
            }
            imax = Math.max(imax*nums[i],nums[i]);
            imin = Math.min(imin*nums[i],nums[i]);
            result = Math.max(imax,result);
        }
        return result;
    }
  • 常见动态规划类型
    • 坐标型
    • 序列型
    • 划分型
    • 区间型
    • 背包型
    • 最长序列型
    • 博弈型
    • 综合型

你可能感兴趣的:(ACM-dp(动态规划))