zoukankan      html  css  js  c++  java
  • JS 动态规划入门

    动态规划入门

    动态规划题目特点

    1. 计数

      • 有多少种方式走到右下角
      • 有多少种方法选出k个数使得和是Sum
    2. 求最大值最小值

      • 从左上角走到右下角路径的最大数字和
      • 最长上升序列长度
    3. 求存在性

      • 取石子游戏, 先手是否必胜
      • 能不能选出k个数使得和是Sum

    解题步骤

    1. 确定步骤

    状态在动态规划中的作用属于定海神针。

    简单来说, 解动态规划的时候需要开一个数组, 数组的每个元素f【i】或者f【i】【j】代表什么

    确定状态需要两个意识:

    • 最后一步
    • 子问题
    2. 转移方程

    f【x】 = f【x-1】

    3. 初始化条件和边界情况

    初始条件, 用转移方程算不出来, 需要手动定义
    边界情况, 数组不要越界,向上越界,向下越界

    4. 计算顺序

    当我们计算到f【x】时, 右边的都已经得到结果了

    求最值

    题目一

    • 你有三种硬币, 分别面值2元, 5元和7元, 每种硬币都有足够多
    • 买一本书需要27元
    • 如何用最少的硬币组合正好付清, 不需要对方找钱
    最后一步

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

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

    除掉这枚硬币, 前面的面值加起来是27 - ak

    关键点

    我们不关心前面的K-1枚硬币是怎么拼出27-ak的(可能有一种拼法, 可能有100中拼法), 而且我们现在甚至还不知道ak和K, 但是我们确定前面的硬币拼出了27-ak

    因为是最优策略, 所以拼出27-ak的硬币数一定要最少, 否则这就不是最优策略了

    子问题

    所以我们就要求: 最少用多少枚硬币可以拼出27-ak
    原问题是最少用多少问题拼出27
    我们将原问题转化成了一个子问题, 而且规模更小: 27-ak
    为了简化定义, 我们设状态f(x) = 最少用多少枚硬币拼出x

    等等,我们还不知道最后那一枚硬币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【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】+1 }
    两个问题

    • 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

    计算顺序

    从小到大,一个for循环搞定

    /**
     * 
     * @param {int []} A 
     * @param {int} M 
     */
    function coinChange(A, M){
        var f = new Array(M+1);
        // f.fill(0,0, f.length) // 用0初始化数组
        var n = A.length // number of kinds of coins
    
        // initialization
        f[0] = 0;
    
        var i, j;
        // f[1], f[2], .... f[27]
        for(i = 1; i <= M; ++i){
            f[i] = Number.MAX_VALUE;
            // last coin a[j]
            for(j = 0; j <= n; ++j){
                if(i >= A[j] && f[i - A[j]] != Number.MAX_VALUE){
                    var a = f[i - A[j]] + 1;
                    var b = f[i];
                f[i] = Math.min.apply(null, [a, b])
                }
                
            }
        }
        if(f[M] == Number.MAX_VALUE){
            return -1;
        }
        return f[M]
    }
    

    小结

    求最值规划组成的部分:

    1. 确定状态

      • 最后一步(最优策略中使用的最后一枚硬币ak)
      • 化成子问题(最少的硬币拼出更小的面值27-ak)
    2. 转移方程

      • f【x】 = min{ f【x-2】+1, f【x-5】+1, f【x-7】+1 }
    3. 初始条件和边界情况

      • f【0】= 0,如果不能拼出Y, f【Y】 = 正无穷
    4. 计算顺序

      • f【0】,f【1】,f【2】。。。。, 因为计算f【2】时需要f【1】f【0】都计算完毕

    动态规划:
    消除冗余, 加速计算

    计数型动态规划

    题目二

    给定m行n列的网格, 有一个机器人从左上角(0,0)出发, 每一步可以向下或者向右走一步。
    问有多少种不同的方式走到右下角。

    确定状态

    最后一步: 无论机器人用何种方式到达右下角, 总有最后挪动的一步: 向右或者向下
    右下角坐标设为(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)

    对于任意一个格子(i,j)
    f【i】【j】=f【i-1】【j】 + f【i】f【j-1】
    机器人有多少种方式走到(i,j) = 机器人有多少种方式走到(i-1,j) + 机器人有多少种方式走到(i,j-1)

    初始条件和边界情况

    初始条件:f【0】【0】=1,因为机器人只有一种方式到左上角
    边界情况:i = 0 或 j = 0, 则前一步只能有一个方向过来->f【i】【j】= 1

    计算顺序

    f【0】【0】 = 1
    计算第0行: f【0】【0】, f【0】【1】
    答案是f【m-1】【n-1】

    代码
    function uniquePaths(m, n) {
      var f = new Array(m);
      f.fill(new Array(n).fill(0, 0), 0);
      var i, j;
      for (i = 0; i < m; ++i) {
        // row
        for (j = 0; j < n; ++j) {
          // column
          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];
    }
    
    

    存在性动态规划

    青蛙跳石头

    有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 < n -1

    这需要两个条件同时满足

    • 青蛙可以跳到石头i
    • 最后一步不超过跳跃的最大距离: n - 1 -i <= ai
    子问题

    状态: 设f【j】表示青蛙能不能跳到石头j
    f【j】= OR(f【i】AND i + a【i】>= j)
    0 <= i < j: 枚举上一个跳到的石头i
    f【i】: 青蛙能不能跳到石头i
    i + a【i】>= j:最后一步的距离不能超过ai

    初始条件和边界情况

    初始条件: f【0】 = True, 因为青蛙一开始就在石头0
    没有边界情况: 因为枚举不存在越界情况

    计算顺序

    从左到右
    答案是f【n-1】
    时间复炸度:O(N^2),空间复杂度数组大小:O(N)

    代码
    function conJump(A) {
      var n = A.length;
      f = new Array(n);
      f[0] = true;
      var i, j;
      for (j = 1; j < n; ++j) {
        f[j] = false;
        for (i = 0; i < j; ++i) {
          if (f[i] && i + A[i] >= j) {
            f[j] = true;
            break;
          }
        }
      }
      return f[n - 1];
    }
    var r1 = conJump([2, 3, 1, 1, 4]);
    var r2 = conJump([3, 2, 1, 0, 4]);
    console.log(r1);
    console.log(r2);
    

    动态规划入门总结

    四个组成部分

    确定状态
    • 研究最优策略的最后一步
    • 化为子问题
    转移方程
    • 根据子问题定义直接得到
    初始条件和边界情况
    • 细心,考虑周全
    计算顺序
    • 利用之前的计算结果
  • 相关阅读:
    转--后台开发人员的技术栈
    hadoop +streaming 排序总结
    python 的tempfile学习
    hadoop学习日志
    shell sort 排序大讨论
    系统吞吐量、TPS(QPS)、用户并发量、性能测试概念和公式
    推荐系统评测指标--准确率(Precision)和召回率(Recall)、F值(F-Measure)
    shell 数组
    leecode第七百四十六题(使用最小花费爬楼梯)
    leecode第四百七十五题(供暖器)
  • 原文地址:https://www.cnblogs.com/xiaoxu-xmy/p/13679095.html
Copyright © 2011-2022 走看看