1. 什么是动态规划
动态规划(dynamic programming,DP)是一种将复杂问题分解成更小的子问题来解决的优化技术。
用动态规划解决问题时,要遵循三个重要步骤
- (1) 定义子问题;
- (2) 实现要反复执行来解决子问题的部分(状态定义)
- (3) 识别并求解出基线条件。(dp方程)
定义:en.wikipedia.org/wiki/Dynami…
动态规划 = 分治+最优子结构
动态规划和递归或者分治没有根本上的区别(关键看有无最优的子结构)
- 共性:找到重复子问题
- 差异性:最优子结构、中途可以淘汰次优解
动态规划问题的⼀般形式就是求最值。⽐如求最⻓递增⼦序列、最⼩编辑距离等。
核⼼问题是什么呢?求解动态规划的核⼼问题是穷举
-
动态规划的穷举有点特别,因为这类问题存在「重叠⼦问题」,如果暴⼒穷举的话效率会极其低下,所以需要「备忘录」或者「DP table」来优化穷举过程,避免不必要的计算。
-
动态规划问题⼀定会具备「最优⼦结构」,才能通过⼦问题的最值得到原问题的最值
-
虽然动态规划的核⼼思想就是穷举求最值,但是问题可以千变万化,穷举所有可⾏解其实并不是⼀件容易的事,只有列出正确的「状态转移⽅程」才能正确地穷举
DP三部曲
- 子问题
- 状态定义
- DP方程
模版
// 初始化 base case
dp[0][0][...] = base case
// 进行状态转移
for 状态1 in 状态1所有取值
for 状态2 in 状态2所有取值
for ...
dp[状态1][状态2][...] = 求最值(选择1, 选择2, ...)
2. 常见问题
1.求斐波那契数列
使用递归法,画出树状图如下:
我们使用缓存优化(记忆化缓存):
加缓存后:
2.路径计数
每次走一格,只能向下或向右(不能向上或向左),黄色代表障碍物,从start到end有多少条路径
小绿人可以到B或A点,所以问题分为从B/A到end有多少条路径,同理,再分下去,就是一个斐波那契数列。
自底向下倒推,数字代表到达该点有几种方式(下面加上右边的数)
动态规划关键点
- 最优子结构
opt[n] = best_of(opt[n-1], opt[n-2], ...) - 储存中间状态:
opt[i] - 递推公式(美其名日:状态转移方程或者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五步
- 分治 define subproblems
- 猜递推方程 guess(part of solution)
- 合并子问题的解 relate subproblems solution
- 递归和记忆化 recurse & memorize
- 解决原始问题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. 最长公共子序列
找出两个字符串序列的最长子序列的长度。最长子序列是指,在两个字符串序列中以相同顺序出现,但不要求连续(非字符串子串)的字符串序列
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;
}
推荐阅读
www.bilibili.com/video/BV1Kx…
www.bilibili.com/video/BV1Ni…
www.bilibili.com/video/BV1Fs…
3. leetcode常见考题
3.1 easy
1. 爬楼梯
难度:简单
2. 最大子序和
难度:简单
题解: 最大子序和(DP)
3.2 medium
1. 不同路径
难度:中等
题解:不同路径(DP)
2. 不同路径 II
难度:中等
题解:不同路径 II(DP)
3. 最长递增子序列
难度:中等
4. 乘积最大子数组
难度:中等
题解: 乘积最大子数组(DP)
5. 最长公共子序列
难度:中等
题解: 最长公共子序列(DP)
6. 最长回文子序列
难度:中等
题解:最长回文子序列(DP)
7. 最长回文子串
难度:中等
8. 三角形最小路径和
难度:中等
题解:三角形最小路径和(DP)
9. 零钱兑换
难度:中等
题解:零钱兑换(DP)
10. 零钱兑换 II
难度:中等
11. 打家劫舍
难度:中等
题解:打家劫舍
12. 打家劫舍 II
难度:中等
题解:打家劫舍II
13. 打家劫舍 III
难度:中等
题解:打家劫舍III
14. 目标和
难度:中等
题解:目标和(回溯/DP)
3.3 hard
1. 编辑距离
难度:困难
2. 让字符串成为回文串的最少插入次数
难度:困难
3. 正则表达式匹配
难度:困难
题解:正则表达式匹配(DP)
4. 鸡蛋掉落
难度:困难
题解:鸡蛋掉落(DP+二分)
5. 戳气球
难度:困难
题解:戳气球
6. 俄罗斯套娃信封问题
难度:困难
4. 买卖股票问题
买卖股票的最佳时机
买卖股票的最佳时机 II
买卖股票的最佳时机 III
买卖股票的最佳时机 IV
买卖股票的最佳时机含手续费
题解
3.4 推荐题目(middle)
1. 最小覆盖子串
难度:困难
2. 跳跃游戏
难度:中等
3. 跳跃游戏 II
难度:中等
4. 最小路径和
难度:中等
3.5 延伸扩展
完全平方数
难度:中等
最长有效括号
难度:困难
解码方法
难度:中等
最大正方形
难度:中等
矩形区域不超过 K 的最大数值和
难度:困难
青蛙过河
难度:困难
分割数组的最大值
难度:困难
任务调度器
难度:中等