动态规划的实现和特性
递归和分治
递归代码模板
function recur (level, param) {
// terminator
if (level > MAX_LEVEL) {
// process result
return;
}
// process current logic
process(level, param);
// drill down
recur(level + 1, newParam);
// restore current status
}
分治代码模板
function divide_conquer (problem, param1, param2, ...) {
// recursion terminator
if (!problem) {
// process result
return;
}
// prepare data
data = prepare_date(problem);
subproblems = split_problem(problem, data);
// conquer subproblems
subresult1 = divide_conquer(subproblems[0], p1, ...);
subresult2 = divide_conquer(subproblems[1], p2, ...);
subresult3 = divide_conquer(subproblems[2], p3, ...);
// process and generate the final result
result = process_result(subresult1, subresult2, subresult3, ...);
// revert current level status
}
人肉递归低效、很累;
找到最近最简方法,将其拆解为可重复解决的问题;
数学归纳法思维(抵制人肉递归诱惑)
本质:寻找重复性 => 计算机指令集只会 if else、for、recur
动态规划
动态规划 Dynamic Programming。
Divide & Conquer + Optimal substructure,分治 + 最优子结构。
动态规划定义 https://en.wikipedia.org/wiki/Dynamic_programming
"Simplifying a Complicated problem by breaking it down into simpler sub-problems" (in a recursive manner).
关键点
动态规划 和 递归 或者 分治 没有根本的区别(关键看有无最优子结构)。
共性:找到重复子问题。
差异性:最优子结构、中途可以淘汰次优解。
斐波那契数列
// 斐波那契数列是指数级的 2^n,不是 n^2
// 无优化
function fib (n) {
if (n <= 2) return n;
return fib(n - 1) + fib(n - 2);
};
// 记忆化搜索
function fib (n, memo = {}) {
if (n <= 2) return n;
if (!memo[n]) {
memo[n] = fib(n - 1, memo) + fib(n - 2, memo);
}
return memo[n];
};
// 迭代
function fib (n) {
if (n <= 1) {
return n;
}
let first = 1;
let second = 2;
for (let i = 3; i <= n; i++) {
let thrid = first + second;
first = second;
second = thrid;
}
return second;
};
路径计算问题(二维)
// opt[i, j] = opt[i + 1, j] + opt[i, j + 1]
// 递推伪代码,状态转移方程(DP 方程、动态规划方程)
if (a[i, j] === 'blank') {
opt[i, j] = opt[i + 1, j] + opt[i, j + 1]
} else {
opt[j, j] = 0;
}
总结
- 打破思维习惯,形成机器思维
- 理解复杂逻辑的关键
动态规划关键点
- 最优子结构:
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]
- Fib:
相关题目
不同路径 (Facebook、亚马逊、微软在半年内面试中考过)
不同路径 II (谷歌、美团、微软在半年内面试中考过)
最长公共子序列(字节跳动、谷歌、亚马逊在半年内面试中考过)
// 不同路径
/**
* 自底向上
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
for (let i = m - 1; i >= 0; i--) {
for (let j = n - 1; j >= 0; j--) {
if (i == m - 1 || j == n - 1) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i + 1][j] + dp[i][j + 1];
}
}
}
return dp[0][0];
};
/**
* 自顶向下
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
if (i == 0 || j == 0) {
dp[i][j] = 1;
} else {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
};
/**
* 算法优化
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < m; i++) {
dp[i][0] = 1;
}
for (let i = 0; i < n; i++) {
dp[0][i] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
return dp[m - 1][n - 1];
};
// 不同路径Ⅱ
/**
* @param {number[][]} obstacleGrid
* @return {number}
*/
var uniquePathsWithObstacles = function(obstacleGrid) {
const m = obstacleGrid.length,
n = obstacleGrid[0].length;
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
for (let i = 0; i < m && obstacleGrid[i][0] === 0; i++) {
dp[i][0] = 1;
}
for (let i = 0; i < n && obstacleGrid[0][i] === 0; i++) {
dp[0][i] = 1;
}
for (let i = 1; i < m; i++) {
for (let j = 1; j < n; j++) {
if (obstacleGrid[i][j] == 1) {
dp[i][j] = 0;
} else {
dp[i][j] = dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
}
}
}
return dp[m - 1][n - 1];
};
// 最长公共子序列
// s1 s2
// if (s1[length - 1] != s2[s2.length - 1]) lcs[s1, s2] = max(lcs[s1 - 1, s2], lcs[s1, s2 - 1]);
// lcs[s1, s2] = max(lcs[s1 - 1, s2], lcs[s1, s2 - 1], lcs[s1 - 1, s2 - 1])
// if (s1[length - 1] == s2[s2.length - 1]) lcs[s1, s2] = lcs[s1 - 1, s2 - 1] + 1
// lcs[s1, s2] = max(lcs[s1 - 1, s2], lcs[s1, s2 - 1], lcs[s1 - 1, s2 - 1], lcs[s1 -1, s2 - 1] + 1)
=>
// if (s1[length - 1] != s2[s2.length - 1]) lcs[s1, s2] = max(lcs[s1 - 1, s2], lcs[s1, s2 - 1]);
// if (s1[length - 1] == s2[s2.length - 1]) lcs[s1, s2] = lcs[s1 - 1, s2 - 1] + 1
/**
* @param {string} text1
* @param {string} text2
* @return {number}
*/
var longestCommonSubsequence = function(text1, text2) {
const m = text1.length,
n = text2.length;
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
const c1 = text1[i - 1];
for (let j = 1; j <= n; j++) {
const c2 = text2[j - 1];
if (c1 == c2) {
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
dp[i][j] = Math.max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
};
爬楼梯(阿里巴巴、腾讯、字节跳动在半年内面试常考)
三角形最小路径和(亚马逊、苹果、字节跳动在半年内面试考过)
最大子序和(亚马逊、字节跳动在半年内面试常考)
乘积最大子数组(亚马逊、字节跳动、谷歌在半年内面试中考过)
零钱兑换(亚马逊在半年内面试中常考)
打家劫舍(字节跳动、谷歌、亚马逊在半年内面试中考过)
打家劫舍 II (字节跳动在半年内面试中考过)
买卖股票的最佳时机(亚马逊、字节跳动、Facebook 在半年内面试中常考)
买卖股票的最佳时机 II (亚马逊、字节跳动、微软在半年内面试中考过)
买卖股票的最佳时机 III (字节跳动在半年内面试中考过)
最佳买卖股票时机含冷冻期(谷歌、亚马逊在半年内面试中考过)
买卖股票的最佳时机 IV (谷歌、亚马逊、字节跳动在半年内面试中考过)
股票问题系列通解https://leetcode-cn.com/circle/article/qiAgHn/
高级 DP 题目
完全平方数(亚马逊、谷歌在半年内面试中考过)
编辑距离 (重点)
跳跃游戏(亚马逊在半年内面试中考过)
跳跃游戏 II (亚马逊、华为字节跳动在半年内面试中考过)
不同路径(Facebook、亚马逊、微软在半年内面试中考过)
不同路径 II (谷歌、美团、微软在半年内面试中考过)
不同路径 III (谷歌在半年内面试中考过)
零钱兑换(亚马逊在半年内面试中常考)
零钱兑换 II (亚马逊、字节跳动在半年内面试中考过)