前端必会数据结构与算法系列之动态规划(十三)

487 阅读6分钟

1. 什么是动态规划

动态规划(dynamic programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。

用动态规划解决问题时,要遵循三个重要步骤

  • (1) 定义子问题;
  • (2) 实现要反复执行来解决子问题的部分(状态定义)
  • (3) 识别并求解出基线条件。(dp方程)

定义:en.wikipedia.org/wiki/Dynami…

动态规划 = 分治+最优子结构

动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)

  • 共性:找到重复子问题
  • 差异性:最优子结构、中途可以淘汰次优解

动态规划问题的⼀般形式就是求最值。⽐如求最⻓递增⼦序列、最⼩编辑距离等。

核⼼问题是什么呢?求解动态规划的核⼼问题是穷举

  1. 动态规划的穷举有点特别,因为这类问题存在「重叠⼦问题」,如果暴⼒穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。

  2. 动态规划问题⼀定会具备「最优⼦结构」,才能通过⼦问题的最值得到原问题的最值

  3. 虽然动态规划的核⼼思想就是穷举求最值,但是问题可以千变万化,穷举所有可⾏解其实并不是⼀件容易的事,只有列出正确的「状态转移⽅程」才能正确地穷举

DP三部曲

  1. 子问题
  2. 状态定义
  3. DP方程

模版

// 初始化 base case
dp[0][0][...] = base case
// 进行状态转移
for 状态1 in 状态1所有取值
    for 状态2 in 状态2所有取值
        for ...
            dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...)

2. 常见问题

1.求斐波那契数列

使用递归法,画出树状图如下: image.png

我们使用缓存优化(记忆化缓存): image.png

加缓存后: image.png

2.路径计数

每次走一格,只能向下或向右(不能向上或向左),黄色代表障碍物,从start到end有多少条路径

image.png

小绿人可以到B或A点,所以问题分为从B/A到end有多少条路径,同理,再分下去,就是一个斐波那契数列。

image.png

image.png

自底向下倒推,数字代表到达该点有几种方式(下面加上右边的数)

image.png

动态规划关键点

  1. 最优子结构opt[n] = best_of(opt[n-1], opt[n-2], ...)
  2. 储存中间状态: opt[i]
  3. 递推公式(美其名日:状态转移方程或者DP方程) Fib: opt[i] =opt[n-1] + opt[n-2] 二维路径: opt[i, j] = opt[i+1][j] + opt[i][j+1] (且判断a[i,j]是否空地)

dp五步

  1. 分治 define subproblems
  2. 猜递推方程 guess(part of solution)
  3. 合并子问题的解 relate subproblems solution
  4. 递归和记忆化 recurse & memorize
  5. 解决原始问题solve original problem

2. 常见问题

1. 最少硬币找零问题

是找到 n 所需的最小硬币数。但要做到这一点,首先得找到对每个x < n 的解。然后,我们可以基于更小的值的解来求解

function minCoinChange(coins, amount) { 
    const cache = [];
    const makeChange = (value) => {
        if (!value) {// 若 amount 不为正(< 0),就返回空数组
            return []; 
        } 
        if (cache[value]) { // 若结果已缓存,则直接返回结果
            return cache[value]; 
        } 
        let min = []; 
        let newMin; 
        let newAmount; 
        for (let i = 0; i < coins.length; i++) {
            const coin = coins[i]; 
            newAmount = value - coin;
            if (newAmount >= 0) { 
                newMin = makeChange(newAmount);// 计算找零结果
            } 
            if ( 
                newAmount >= 0 &&
                (newMin.length < min.length - 1 || !min.length) &&
                (newMin.length || !newAmount)
            ) { 
                min = [coin].concat(newMin);
                console.log('new Min ' + min + ' for ' + amount); 
            }
        } 
        return (cache[value] = min);
    }; 
    return makeChange(amount);
}

2. 背包问题

给定一个固定大小、能够携重量 W 的背包,以及一组有价值和重量的物品,找出一个最佳解决方案,使得装入背包的物品总重量不超过W,且总价值最大

function knapSack(capacity, weights, values, n) { 
     const kS = []; 
     for (let i = 0; i <= n; i++) {// 初始化将用于寻找解决方案的矩阵
         kS[i] = []; 
     } 
     for (let i = 0; i <= n; i++) { 
         for (let w = 0; w <= capacity; w++) { 
             if (i === 0 || w === 0) { // 忽略矩阵的第一列和第一行,只处理索引不为 0的列和行
                 kS[i][w] = 0; 
             } else if (weights[i - 1] <= w) {// 物品 i 的重量必须小于约束
                 const a = values[i - 1] + kS[i - 1][w - weights[i - 1]]; 
                 const b = kS[i - 1][w]; 
                 kS[i][w] = a > b ? a : b; // 选择价值最大的那个
             } else { 
                 kS[i][w] = kS[i - 1][w];
             }
         }
     }
     findValues(n, capacity, kS, weights, values);
     return kS[n][capacity];
}

function findValues(n, capacity, kS, weights, values) { 
    let i = n; 
    let k = capacity; 
    console.log('构成解的物品:'); 
    while (i > 0 && k > 0) { 
        if (kS[i][k] !== kS[i - 1][k]) { 
            console.log(`物品 ${i} 可以是解的一部分 w,v: ${weights[i - 1]}, ${values[i - 1]}`); 
            i--; 
            k -= kS[i][k]; 
        } else { 
            i--; 
        } 
    } 
}

3. 最长公共子序列

找出两个字符串序列的最长子序列的长度。最长子序列是指,在两个字符串序列中以相同顺序出现,但不要求连续(非字符串子串)的字符串序列

image.png

function lcs(wordX, wordY) {
  const m = wordX.length;
  const n = wordY.length;
  const l = [];
  const solution = [];
  for (let i = 0; i <= m; i++) {
    l[i] = [];
    solution[i] = [];
    for (let j = 0; j <= n; j++) {
      l[i][j] = 0;
      solution[i][j] = '0';
    }
  }
  for (let i = 0; i <= m; i++) {
    for (let j = 0; j <= n; j++) {
      if (i === 0 || j === 0) {
        l[i][j] = 0;
      } else if (wordX[i - 1] === wordY[j - 1]) {
        l[i][j] = l[i - 1][j - 1] + 1;
        solution[i][j] = 'diagonal';
      } else {
        const a = l[i - 1][j];
        const b = l[i][j - 1];
        l[i][j] = a > b ? a : b; // max(a,b)
        solution[i][j] = l[i][j] === l[i - 1][j] ? 'top' : 'left';
      }
    }
    // console.log(l[i].join());
    // console.log(solution[i].join());
  }
  return printSolution(solution, wordX, m, n);
}

比较背包问题和 LCS 算法,我们会发现两者非常相似。像背包问题算法一样,这种方法只输出 LCS 的长度,而不包含 LCS 的实际结果。要提取这个信息,需要对算法稍作修改,声明一个新的 solution 矩阵

function printSolution(solution, wordX, m, n) {
  let a = m;
  let b = n;
  let x = solution[a][b];
  let answer = '';
  while (x !== '0') {
    if (solution[a][b] === 'diagonal') {
      answer = wordX[a - 1] + answer;
      a--;
      b--;
    } else if (solution[a][b] === 'left') {
      b--;
    } else if (solution[a][b] === 'top') {
      a--;
    }
    x = solution[a][b];
  }
  return answer;
}

image.png

image.png

image.png

推荐阅读

www.bilibili.com/video/BV1Kx…
www.bilibili.com/video/BV1Ni…
www.bilibili.com/video/BV1Fs…

3. leetcode常见考题

3.1 easy

1. 爬楼梯

难度:简单

题解:爬楼梯(dp,递归多解法)

2. 最大子序和

难度:简单

题解: 最大子序和(DP)

3.2 medium

1. 不同路径

难度:中等

题解:不同路径(DP)

2. 不同路径 II

难度:中等

题解:不同路径 II(DP)

3. 最长递增子序列

难度:中等

题解:最长递增子序列(DP+二分法)

4. 乘积最大子数组

难度:中等

题解: 乘积最大子数组(DP)

5. 最长公共子序列

难度:中等

题解: 最长公共子序列(DP)

6. 最长回文子序列

难度:中等

题解:最长回文子序列(DP)

7. 最长回文子串

难度:中等

8. 三角形最小路径和

难度:中等

高赞题解

题解:三角形最小路径和(DP)

9. 零钱兑换

难度:中等

题解:零钱兑换(DP)

10. 零钱兑换 II

难度:中等

11. 打家劫舍

难度:中等

题解:打家劫舍

image.png

12. 打家劫舍 II

难度:中等

题解:打家劫舍II

13. 打家劫舍 III

难度:中等

题解:打家劫舍III

14. 目标和

难度:中等

题解:目标和(回溯/DP)

3.3 hard

1. 编辑距离

难度:困难

题解:编辑距离(穷举—>记忆画搜索,DP)

2. 让字符串成为回文串的最少插入次数

难度:困难

题解:让字符串成为回文串的最少插入次数(DP)

3. 正则表达式匹配

难度:困难

题解:正则表达式匹配(DP)

4. 鸡蛋掉落

难度:困难

题解:鸡蛋掉落(DP+二分)

5. 戳气球

难度:困难

题解:戳气球

6. 俄罗斯套娃信封问题

难度:困难

题解:俄罗斯套娃信封问题(DP)

4. 买卖股票问题

买卖股票的最佳时机

买卖股票的最佳时机 II

买卖股票的最佳时机 III

买卖股票的最佳时机 IV

买卖股票的最佳时机含手续费

题解

🍥前端食堂题解,超好理解,带你一口气团灭6道股票算法

3.4 推荐题目(middle)

1. 最小覆盖子串

难度:困难

2. 跳跃游戏

难度:中等

3. 跳跃游戏 II

难度:中等

4. 最小路径和

难度:中等

3.5 延伸扩展

完全平方数

难度:中等

最长有效括号

难度:困难

解码方法

难度:中等

最大正方形

难度:中等

矩形区域不超过 K 的最大数值和

难度:困难

青蛙过河

难度:困难

分割数组的最大值

难度:困难

任务调度器

难度:中等