算法第二章 动态规划

241 阅读8分钟

概念

状态转移方程、最优子结构、边界

初探

leetcode 63 不同路径

leetcode 63 不同路径

image.png 动态规划解题三部曲

  • 1 最优子结构:从最后的状态确定最优子结构,从下图可以看到在最后一步到达终点只有两种可能,从上面向下或者从左边往右边。这个就是最优子结构,然后题目中说有障碍物,因此如果当前的坐标有障碍物,那么步数就是0 image.png
  • 2 状态转移方程:基于最优子结构写出状态转移方程,在m行*n列的网格种从finish上面往下走就是f(m-1,n)第二种情况从左往右,f(m,n-1),将这两种可能的步数相加就是当前坐标可能的步数,所以方程为dp(m,n)=dp(m-1,n)+dp(m,n-1)
  • 3 边界,在应用动态规划时要注意边界值的问题,在这题中第一列,和第一行显示不能用状态转移方程,因为已经越界,所以在此前要把边界处理好,由于机器人一次只能向下或者向右,所以第一列和第一行的步数只能为1,注意在处理边界时,要考虑障碍物的情况,有障碍物则后面的路就不通路径就为0
var uniquePathsWithObstacles = function(obstacleGrid) {
        let n = obstacleGrid.length, m = obstacleGrid[0].length;
        // n行,m列
        // dp矩阵初始化
        let dp = Array(n)
        for(let i = 0; i < n; i++) {
           dp[i] = Array(m).fill(0)
        }
        for(let i = 0; i < dp.length && obstacleGrid[i][0] === 0; i++) {
            dp[i][0] = 1
        }
        for(let i = 0; i < dp[0].length && obstacleGrid[0][i] === 0; i++) {
            dp[0][i] = 1
        }
        // 计算dp
        for(let i = 0; i < n; i++) {
            for(let j = 0; j < m; j++){
                if(obstacleGrid[i][j] === 1){
                    dp[i][j] = 0
                    continue
                }
                if(i > 0 && j > 0 && obstacleGrid[i][j] !== 1){
                    dp[i][j] = dp[i-1][j] + dp[i][j-1]
                }
            }
        }
        return dp[n-1][m-1];
    }

leetcode k站中转内最便宜的航班

leetcode k站中转内最便宜的航班

image.png

/**
 * @param {number} n
 * @param {number[][]} flights
 * @param {number} src
 * @param {number} dst
 * @param {number} k
 * @return {number}
 */
var findCheapestPrice = function(n, flights, src, dst, k) {
    const find = (flights,src,dst,k) => {
    let last = flights.filter(item => item[1] === dst)
    let arr = Math.min.apply(null,last.map(item => {
        if(item[0] === src) {
            return item[2]
        }else if(k <= 0 && item[0] !== src){
            return Number.MAX_SAFE_INTEGER
        }else {
            return item[2] + find(flights,src,item[0],k-1)
        }
    }))
    return arr
    }
    let res = find(flights,src,dst,k)
    return res >= Number.MAX_SAFE_INTEGER ? -1 : res
};

问道

动态规划是一种分阶段求解决策问题的数学思想

类型

计数

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

步骤

  • 从最后的状态确定最优子结构
  • 状态转移方程,基于最优子结构写出状态转移方程
  • 边界值和初始值,处理边界值和初值 找最优子结构心得
  • 从最后的状态确定最优子结构,最优子结构就是前一步到最后一步最优的结构
  • 前一步的往往不只有一种可能,有时候可以列举出前一步中的所有可能性
  • 在推的过程中,可以是前n-1步的结果也就是f(n-1),因为动态规划就是递推的过程 找边界初值心得
  • 根据题目的语境猜测一开始的值,然后再代入得出的公式,如果公式处理不了,而后一个数可以,那么这里就是要处理的边界和初值

计数类型

爬楼梯

leetcode

image.png 思路

  • 题目字眼有多少种,有多少种方式,第一反应动态规划
  • 找最优子结构,从最后一步往前推一步,比如现在要爬4层,那么往前推一步,前一步到第三层有多少种方法,第一种爬一阶从3层往上,第二种爬两阶从2层往上。这就是最优结构,也就是爬到3层的方法数加上爬到2层的方法数
  • 写状态转移方程,从最优子结构可以看出,当前层的方法数=前一层的方法数+前前一层的方法数。变成表达式就是fn = f(n-1) + f(n-2)。不要想太多,这个公式一定是成立的而且动态规划中的转移方程有一个特点就是它的右侧往往是前一步的结果,所以有点像递推
  • 处理边界和初值,根据题目猜测初值应该是第一层和第二层,将0,1,2代入公式可以看到公式得不出正确答案,这个就是需要处理的初值,而公式的边界是从3层才开始起作用。
  • 在这道题的解法中还用到了滚动数组的思想,因为每次起作用的其实就三个值,所以每次得出答案后可以把fn1的值去掉,把新的值放上来
var climbStairs = function(n) {
    let fn1 = 1, fn2 = 2,fn = 0
    if(n < 3) return n <= 1 ? fn1 : fn2
   for(let i = 3; i <= n; i++) {
        fn = fn1 + fn2
        fn1 = fn2
        fn2 = fn
   }
    return fn
};

青蛙跳2

leetcode

image.png 这题有点特特殊,因为它取模了,参考评论区解释
PS : 为什么要模1000000007(跟我念,一,八个零,七)。参考www.liuchuo.net/archives/64…

  1. 大数相乘,大数的排列组合等为什么要取模
  • 1000000007是一个质数(素数),对质数取余能最大程度避免结果冲突/重复
  • int32位的最大值为2147483647,所以对于int32位来说1000000007足够大。
  • int64位的最大值为2^63-1,用最大值模1000000007的结果求平方,不会在int64中溢出。
  • 所以在大数相乘问题中,因为(a∗b)%c=((a%c)∗(b%c))%c,所以相乘时两边都对1000000007取模,再保存在int64里面不会溢出。
  1. 这道题为什么要取模,取模前后的值不就变了吗?
  • 确实:取模前 f(43) = 701408733, f(44) = 1134903170, f(45) = 1836311903, 但是 f(46) > 2147483647结果就溢出了。
  • _____,取模后 f(43) = 701408733, f(44) = 134903163 , f(45) = 836311896, f(46) = 971215059没有溢出。
  • 取模之后能够计算更多的情况,如 f(46)
  • 这道题的测试答案与取模后的结果一致。
  • 总结一下,这道题要模1000000007的根本原因是标准答案模了1000000007。不过大数情况下为了防止溢出,模1000000007是通用做法,原因见第一点。
var numWays = function(n) {
    let dp1 = 1, dp2 = 2, dp = 0
    if(n <= 2) return n <= 1 ? 1 : 2
    for(let i = 3; i <= n; i++) {
        dp = (dp1 + dp2) % 1000000007
        dp1 = dp2
        dp2 = dp
    }
    return dp
};

青蛙跳进阶

剑指 image.png 题意是说:跳4级台阶时,可以跳青蛙可以跳1,2,3,4级台阶

  • 之前跳一级,两级时是从后往前推1阶和2阶,现在所有级都可以跳,所以从最后一个台阶的角度,自然是把所有台阶的可能性都加起来就是总的方法数
  • 状态转移方程,再次体现数学的重要性
f(n)=f(n-1)+f(n-2)+...+f(1)
f(n-1)=f(n-2)+...f(1)
得:f(n)=2*f(n-1)
function jumpFloorII(number)
{
    // write code here
    let dp1 = 1, dp2 = 2, dp = 0
    if(number <= 2) {
        return number <= 1 ? dp1 : dp2
    }
    for(let i = 3; i <= number; i++) {
        dp = 2*dp2
        dp2 = dp
    }
    return dp
    
}
module.exports = {
    jumpFloorII : jumpFloorII
};

矩形覆盖

剑指 image.png

新启发

对于这道题,一开始是从大矩形里最后一个找规律,看了答案发现,原来是通过举例出前4个n,然后再观察规律,所以总结来说,找规律时如果是可以列举出来的,不妨也可以从这个角度去想想可能性

function rectCover(number)
{
    // write code here
    let fn1 = 1, fn2 = 2, fn = 0
    if(number <= 2)return number
    for(let i = 3; i <= number; i++){
        fn = fn1 + fn2
        fn1 = fn2
        fn2 = fn
    }
    return fn
}
module.exports = {
    rectCover : rectCover
};

求最大最小值类型

连续子数组最大和

剑指

image.png

var maxSubArray = function(nums) {
    let dp = 0,  max = -Number.MAX_VALUE
    nums.forEach(item => {
        dp = Math.max(dp + item , item)
        max = Math.max(max, dp)
    })
    return max
};

买卖股票的最佳时机

leetcode

image.png 思路

  • 这题只求结果不求具体的数,显然是动态规划的题
  • 问题中卖出要在买入的同一天或者同一天之后
  • 从最后一个状态出发,得出结果的关键是卖出-买入,然后和之前的最优解做比较
  • 可以得出状态转移方程 dp[i] = Math.max(dp[i - 1], nowp[i] - minp) 感悟:对于最值问题状态转移方程中肯定要和之前的最优解进行比较,如果有比之前最优解更好的解就放进dp的结果中。而除之前的最优解之外,另一个才是计算这道题的关键,可以是和之前最优解没有关系的计算,也可以借助之前的最优解接着往下算
var maxProfit = function(prices) {
    let minp = prices[0]
    let dp, dp0 = 0
    for(let i =0; i < prices.length; i++) {
        minp = Math.min(minp, prices[i]) 
        dp = Math.max(dp0, prices[i] - minp)
        dp0 = dp
    }
    return dp
};

多少种方法类型总结

  • 如果局部找不出解,可以试着从整体出发看看有没有可能性

最值类型总结

  • 对于求最值问题来说,从最后的状态往前推,往往也是要前面结果的最大或最小,加上最后一个数,而我们的任务就是制定在什么条件下,把下一个数加入到结果中。
  • 对于最值问题状态转移方程中肯定要和之前的最优解进行比较,如果有比之前最优解更好的解就放进dp的结果中。而除之前的最优解之外,另一个才是计算这道题的关键,可以是和之前最优解没有关系的计算,也可以借助之前的最优解接着往下算