前端算法入门之路(十七)(递推算法及解题套路)

180 阅读2分钟

递推算法

数学归纳法

  1. 验证k0成立
  2. 证明如果ki成立,那么ki+1也成立
  3. 由第一步和第二步,证明由k0->ki也成立

如何求解递推问题

  1. 确定递推状态(重中之重)
    • 一个函数符号f(x),外加这个函数符号的含义描述
    • 一般函数所对应的值,就是要求解的值
  2. 确定递推公式(ki -> ki+1)
    • 确定f(x)究竟依赖哪些f(y)的值
  3. 分析边界条件(k0)
  4. 程序实现(循环||递归)

LeetCode肝题

    1. 爬楼梯
  • 递推状态f(n)指的是爬到n层楼梯所用的方法总数
  • 能跨到n层楼梯的方法有两个,一种是从n-1层楼梯跨一步,另一种是从n-2层楼梯跨两步,所以到n层楼梯的方法总数等于到n-1层楼梯的方法总数+到n-2层楼梯的方法总数
  • 由此推导出递推公式f(n) = f(n-1) + f(n-2)
  • 分析边界值到第1层的方式有一种,到第2层的方式有两种f(1) = 1,f(2)=2
var climbStairs = function(n) {
    let fn = Array(n + 1)
    fn[1] = 1
    fn[2] = 2
    for (let i = 3; i <= n; i++) fn[i] = fn[i - 1] + fn[i - 2]
    return fn[n]
};
    1. 使用最小花费爬楼梯
  • 动态规划问题比递推问题多了一个决策过程
  • 递推状态dp(n)指的是爬到n层楼梯所用的最小体力花费
  • 能跨到n层楼梯的方法有两个,也是n-1层和n-2层,此时要判断从0层到n-1层和从0层到n-2层那种方法花费的体力更小,这也就是决策过程
  • 由此推导出递推公式dp(n) = Math.min(dp(n-1) + dp(n-2)) + cost[n]
  • 分析边界值,到第0层需要花费cost[0],到第1层需要花费cost[1]
var minCostClimbingStairs = function(cost) {
    let n = cost.length, dp = new Array(n + 1)
    cost.push(0)
    dp[0] = cost[0]
    dp[1] = cost[1]
    for(let i = 2; i <= n; i++) dp[i] = Math.min(dp[i - 1], dp[i - 2]) + cost[i]
    return dp[n]
};
  1. 剑指 Offer II 091. 粉刷房子
  • 递推状态dp(n,i)指的粉刷第n个房子并且用第i种颜色的最小花费
  • 总共三种颜色,所以第n个房子用第i种颜色的最小花费为粉刷第n-1个房子另外两种颜色的花费+粉刷第n个房子三种颜色花费的最小值
  • 由此推导出递推公式dp(n,i) = Math.min(Math.min(dp(n-1,j), dp(n-1,k)) + costs[n][i])
  • 分析边界值,粉刷第0个房子用第0种颜色需要花费costs[0][0],第1种颜色需要花费costs[0][1],第2种颜色需要花费costs[0][2]
  • 本题dp[n]只依赖dp[n-1]和dp[n-2]的结果,所以可以使用滚动数组优化空间结构
var minCost = function(costs) {
    let n = costs.length, dp = [[costs[0][0], costs[0][1], costs[0][2]], []], ans = 0
    for(let i = 1; i < n; i++) {
        let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
        dp[ind][0] = Math.min(dp[pre_ind][1], dp[pre_ind][2]) + costs[i][0]
        dp[ind][1] = Math.min(dp[pre_ind][0], dp[pre_ind][2]) + costs[i][1]
        dp[ind][2] = Math.min(dp[pre_ind][0], dp[pre_ind][1]) + costs[i][2]
    }
    let ind = (n - 1) % 2
    ans = Math.min(dp[ind][0], dp[ind][1], dp[ind][2])
    return ans
};
    1. 三角形最小路径和
  • 使用从下往上查找的方法
  • 递推状态dp(n,i)指从第n层到第1层所途径的路径最小值
  • 除了第n层,其余每层的第i个位置依赖于n+1层i的值和i+1的值最小值 + triangle[n][i]
  • 由此推导出递推公式dp(n, i) = Math.min(dp(n+1, i),dp(n+1, i+1)) + triangle[n, i]
  • 边界值就是triangle[n-1]
  • 本题dp[n]只依赖dp[n+1]的结果,所以可以使用滚动数组优化空间结构
let minimumTotal = function(triangle) {
    let dp = new Array(2), n = triangle.length
    dp[0] = []
    dp[1] = []
    dp[(n - 1) % 2] = triangle[n - 1].slice()
    for(let i = n - 2; i >= 0; i--) {
        let ind = i % 2, next_ind = ind === 0 ? 1 : 0
        for (let j = 0; j <= i; j++) {
            dp[ind][j] = Math.min(dp[next_ind][j+1], dp[next_ind][j]) + triangle[i][j]
        }
    }
    return dp[0][0]
};
    1. 杨辉三角 II
  • 使用从下往上查找的方法
  • 递推状态f(n,i)指第0层第i位的值
  • 第1层=1,其余每层,除了第一位和最后一位等于1,i位等于上一层i位 + i-1 位
  • 推导出递推公式f(n, i) = f(n - 1, i - 1) + f(n - 1, i)
  • 边界值就是triangle[n-1]
var getRow = function(n) {
    let f = [new Array(n + 1).fill(1), new Array(n + 1).fill(1)]
    for(let i = 1; i <= n; i++) {
        let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
        for(let j = 1; j < i; j++) {
            f[ind][j] = f[pre_ind][j - 1] + f[pre_ind][j]
        }
    }
    return f[n % 2]
};
    1. 最大子序和
  • 递推状态dp(n)指第n位之前较大的连续和
  • dp[0] = nums[0],后面的位置等于前一位的值加上nums里当前位置的值,如果前一位的值为负,说明此次相加对结果不产生增益,所以对前一位和0取最大值
  • 推导出递推公式dp(n) = Math.max(dp[n-1], 0) + nums[n]
  • 边界值就是dp[0] = nums[0]
  • 最后在dp里取最大值
var maxSubArray = function(nums) {
    let dp = nums.slice()
    for (let i = 1; i < nums.length; i++) {
        dp[i] = Math.max(dp[i - 1], 0) + nums[i]
    }
    return Math.max(...dp)
};
空间优化,dp[n]只依赖于dp[n-1]的值
let maxSubArray = function(nums) {
    let n = nums.length, ans = nums[0], pre = nums[0]
    for(let i = 1; i < n; i++) {
        pre = Math.max(nums[i], pre + nums[i])
        ans = Math.max(pre, ans)
    }
    return ans
};
    1. 乘积最大子数组 记录两个数,当前值为负先交换最大最小值,然后把乘积和当前值的最小值放入min,为正把乘积和当前值最大值放入max,过程取max的最大值
var maxProduct = function(nums) {
    let ans = -Infinity, max = 1, min = 1
    for(let item of nums) {
        if (item < 0) [max, min] = [min, max]
        max = Math.max(item, item * max)
        min = Math.min(item, item * min)
        ans = Math.max(ans, max)
    }
    return ans
};
    1. 买卖股票的最佳时机 II
  • 脑筋急转弯问题,遇到递增的就把差值相加
var maxProfit = function(prices) {
    let ans = 0
    for (let i = 1; i < prices.length; i++) {
        if (prices[i] > prices[i - 1]) ans += prices[i] - prices[i - 1]
    }
    return ans
};
    1. 打家劫舍
  • 递推状态dp(n,i)指打劫到第n家所获取到的最大收获,i值0是不打劫1是打劫
  • 打劫到第n家的最大收获取决于(第n家打劫且第n-1家不打劫的收获)和(第n家不打劫且(第n-1家打劫和不打劫的最大值)的收获)的最大值
  • 推导出递推公式dp(n,i) = Math.max(dp[n-1][0] + nums[n], Math.max(dp[n-1][0], dp[n-1][1]))
  • 边界值就是dp(0,0) = 0,dp(0,1) = nums[0]
var rob = function(nums) {
    let n = nums.length, dp = [[], []]
    dp[0][0] = 0
    dp[0][1] = nums[0]
    for(let i = 1; i < n; i++) {
        let ind = i % 2, pre_ind = ind == 0 ? 1 : 0
        dp[ind][0] = Math.max(dp[pre_ind][1], dp[pre_ind][0])
        dp[ind][1] = dp[pre_ind][0] + nums[i]
    }
    let ind = (n + 1) % 2
    return Math.max(dp[ind][0], dp[ind][1])
};
    1. 零钱兑换
  • 递推状态dp(n)指拼凑n面额最少使用的硬币数量
  • 拼凑n面额最少的硬币数量取决于当前使用的面额coins[j],如果dp[n - coins[j]]合法,那么dp[n]等于所有(dp[n - coins[j]]+1)的最小值
  • 推导出递推公式dp(n) = Math.min(dp[coins[j]] + 1)
  • 边界值为凑齐0面额需要使用0个硬币,dp[0]=0
var coinChange = function(coins, amount) {
    let dp = new Array(amount + 1).fill(-1)
    dp[0] = 0
    for (let i = 1; i <= amount; i++) {
        for (let j = 0; j < coins.length; j++) {
            if (i < coins[j]) continue
            if (dp[i - coins[j]] == -1) continue
            if (dp[i] == -1 || dp[i] > dp[i - coins[j]] + 1) dp[i] = dp[i - coins[j]] + 1
        }
    }
    return dp[amount]
};
    1. 最长递增子序列
  • 递推状态dp(n)指以n位置作为结尾最长递增子序列的长度
  • dp[n]的值就取决于在nums里,n位之前所有小于nums[n]的位置,这些位置在dp数组中最小的值再加上1
  • 推导出递推公式dp(n) = Math.max(dp[j] + 1),其中j < n, nums[j] < nums[n]
  • dp所有位置初始化为1,因为递增子序列至少有一个是自己
var lengthOfLIS = function(nums) {
    let n = nums.length, dp = new Array(n).fill(1), ans = 1
    for(let i = 1; i < n; i++) {
        for(j = 0; j <= i; j++) {
            if (nums[i] <= nums[j]) continue
            dp[i] = Math.max(dp[i], dp[j] + 1)
        }
        ans = Math.max(ans, dp[i])
    }
    return ans
};