动态规划
概念¶
“ Dynamic programming is a technique that is sometimes considered the opposite of recursion.”
- 递归 自顶向下,假设整个问题已解决,再解决小问题;
- 动态规划 自底向上,先解决小问题,然后把它们组合起来,成为整个问题的解。
思想¶
一个问题的最优解,是由它各个子问题的最优解决定的。
- 将一个大问题,拆成若干子问题
- 计算每个子问题的最优解
- 根据这些解,得出大问题的最优解
问题属性¶
- 最优子结构 Optimal Substructure
- 状态转移方程 f(n)
- 重叠子问题 Overlapping Sub-problems
求解关键问题¶
- 如何定义 f(n)
- 如何通过 f(1), f(2), ..., f(n - 1) 推导出 f(n),即 状态转移方程。
300. 最长上升子序列¶
状态转移方程:
f(n) = max{ f(i) } + 1,其中 (1 <= i <= n - 1 且 nums[i - 1] < nums[n - 1])
递归实现 Recursion¶
/**
* @param {number[]} nums
* @return {number}
*/
var lengthOfLIS = function(nums) {
let solution = {};
f(nums, nums.length, solution);
// console.log(solution.list);
return solution.list.length;
};
// 状态转移函数
function f(nums, n, solution) {
if (n === 0) {
solution.list = [];
return [];
}
let maxHere = [nums[n - 1]];
if (n === 1) {
solution.list = maxHere;
return maxHere;
}
for (let i = 1; i <= n - 1; i++) {
let result = f(nums, i, solution);
if(nums[i - 1] < nums[n - 1] && maxHere.length < result.length + 1) {
maxHere = [...result, nums[n - 1]];
}
}
if (solution.list.length < maxHere.length) {
solution.list = maxHere;
}
return maxHere;
}
- 时间复杂度计算 T(n) = T(n - 1) + T(n - 2) + ... T(1)
- T(n) = O(2^n)
记忆化实现 Memorization¶
- 将已经计算好的结果,保存起来
问题分类¶
- 线性规划 Linear Programming
- 区间规划 Interval Programming
- 约束规划 Constraint Programming
线性规划¶
- 子问题的规模以线性方式存在;
- 可用一维线性表(数组,哈希表),保存子问题的最优状态或结果;
- 通常 dp[i] 表示第 i 个位置的结果,或从 0 到 1 的最优状态。
198. 打家劫舍¶
- 状态转移方程
f(n) = max{ f(n-1), f(n - 2) + nums[n] }
- 递推公式
dp[i] = max{ dp[i - 1], dp[i - 2] + nums[i] }
/**
* @param {number[]} nums
* @return {number}
*/
var rob = function(nums) {
let n = nums.length;
if (n === 0) {
return 0;
}
if (n === 1) {
return nums[n - 1];
}
const dp = [];
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
for(let i = 2; i < n; i++) {
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i])
}
return dp[n - 1];
};
62. 不同路径¶
- 递推公式:
dp[i][j] = dp[i - 1][j] + dp[i][j - 1]
/**
* @param {number} m
* @param {number} n
* @return {number}
*/
var uniquePaths = function(m, n) {
// TO DO
};
区间规划¶
- 子问题的规模,由不同的区间来决定;
- 可用二维数组,保存子问题的最佳状态或结果;
516. 最长回文子序列¶
假设
d[i][j]
表示从字符 i 到 j 之间的最长回文。递推公式,若首位字符相等,
dp[0][n - 1] = dp[1][n - 2] + 2
;否则,
dp[0][n - 1] = max{ dp[0][n - 2], dp[1][n - 1] }
。
/**
* @param {string} s
* @return {number}
*/
var longestPalindromeSubseq = function(s) {
let n = s.length;
let dp = Array.from({ length: n }).map(i => Array.from({ length: n }));
// 对角线元素设为1,表示单个字符回文长度为1
for(let i = 0; i < n; i++) {
dp[i][i] = 1;
}
for(let i = n - 1; i >= 0; i--) {
for (let j = i + 1; j <n; j++) {
if (s[i] === s[j]) {
// 若首尾相等,则加2
dp[i][j] = (dp[i + 1][j - 1] || 0) + 2;
} else {
// 若不等,则在左下两个矩阵中(更小规模的字符串中),取最大值
dp[i][j] = Math.max(dp[i + 1] && dp[i + 1][j] || 0, dp[i][j - 1] || 0);
}
}
}
return dp[0][n - 1];
};
约束规划¶
- 非决定性多项式
- 0-1 背包问题
经典问题¶
1. Fibonacci 数列¶
递归解法¶
- 递归函数多次被调用,形成一个递归树,事实上在内存中运行效率极其低下。
function recurFib(n) {
if (n < 2) {
return n;
} else {
return recurFib(n-1) + recurFib(n-2);
}
}
动态规划解法¶
- 使用
val
数组来存储中间结果
// 动态规划解法
function dynFib(n){
const val = Array(n).fill(0);
if (n === 1 || n === 2) {
return 1;
}
val[1] = 1;
val[2] = 2;
for (var i = 3; i <= n; ++i) {
val[i] = val[i - 1] + val[i - 2];
}
return val[n - 1];
}
2. 最长公共子串¶
- 寻找两个字符串中最长公共子串 (Longest Common Substring) 是动态规划的经典问题。
- 算法思想: 用二维数组记录两个字符串在相同位置的字母的比较结果。
- 1) 二维数组初始化为全0;
- 2) 每次两个字符串相同位置匹配时,二维数组相应位置加1,否则置0。
function lcs(word1, word2){
var max = 0; // 公共子串最大长度
var index = 0; // 标记公共子串位置
var lcsarr = new Array(word1.length); // 声明 lcsarr 矩阵,并初始化
for(var i = 0; i < word1.length; i++){
lcsarr[i] = new Array(word2.length);
for(var j = 0; j < word2.length; j++){
lcsarr[i][j] = 0;
}
}
for(var i = 0; i < word1.length; i++){
for(var j = 0; j < word2.length; j++){
if(word1[i] == word2[j]) {
if (i == 0 || j == 0) {
lcsarr[i][j] = 1;
} else {
lcsarr[i][j] = lcsarr[i-1][j-1] + 1;
}
if (max < lcsarr[i][j]) {
max = lcsarr[i][j];
index = i;
}
}
}
}
//console.log(lcsarr)
return word1.substr(index - max + 1, max);
}
3. 背包问题¶
-
背包问题 (Knapsack Problem):一堆物品,各有 value 和 size 两个属性,背包限定抓取物品 size 的总大小,如何让所选之物总值 value 最大。
-
假设有如下物品:
var capacity = 16; // 背包大小
var n = 5; // 物品数量
var value = [4,5,10,11,13]; // 物品价值
var size = [3,4,7,8,9]; // 物品大小
递归解法¶
/**
* Return 背包内物品价值总和
* */
function knapsack(capacity, n, size, value) {
// 递归终止条件
if(capacity == 0 || n == 0) {
return 0;
}
// 若装不下,则弃之
if(size[n-1] > capacity) {
return knapsack(capacity, n-1, size, value)
}
const a = knapsack(capacity, n-1, size, value);
const b = knapsack(capacity - size[n-1], n-1, size, value) + value[n-1];
return a > b ? a : b;
}
动态规划解法¶
-
算法思想:用二维矩阵记录背包和物品在扩张过程中的最优解。
-
矩阵用二维数组
K[i][j]
表示,i ∈ [0, …, n],j ∈ [0, …, capacity]
。
function dKnapsack(capacity, n, size, value){
const K = []; // 声明矩阵并初始化
for(var i = 0; i <= n; i++) {
K[i] = [];
}
for(var i = 0; i <= n; i++){
for(var j = 0; j <= capacity; j++){
if(i == 0 || j == 0) {
K[i][j] = 0;
} else if (j < size[i-1]) {
K[i][j] = K[i-1][j]
} else {
const a = K[i-1][j];
const b = K[i-1][j-size[i-1]]+value[i-1];
K[i][j] = a > b ? a : b;
}
}
}
return K[n][capacity];
}