动态规划 (dynamic programming)
基本思想: 动态规划是一种将问题实例分解为更小的/相似的子问题,并存储子问题的解,使得每个子问题只求解一次,最终获得原问题的答案,以解决最优化问题的算法策略。
重要概念:
- 最优子结构
- 边界
- 状态转移公式 -> 递推关系
过程:
- 1、根绝问题所求的那一项和变量的个数,确定是一维数组,二维数组或者多维数组。
- 2、写出初始值,一般是某个变量为1或者0 的特殊情况时候的解。
- 3、通过循环,一般是两个循环中间每一层循环表示一个变量的递增情况。
- 4、在循环中间写出对应的递推关系表达式,求出问题的每一项。
- 5、根据题意得到答案,一般是数组的最后一项。
常见问题:
- 计数
- 有多少种方式走到右下角
- 有多少种方式选出K个数使得和是Sum
- 求最值
- 从左上角到右下角路径的最大数字和
- 最长上升子序列
- 存在性
- 取石子游戏 先手是否必胜
- 能不能选出k个数组成Sum
复杂度:
- 时间复杂度O(n^2)
- 空间复杂度O(1)
解题思路:
- 一般可以利用递归来解
- 通过递归找到递推关系
- 找到递归的出口
- 获得结果
// 在一个数组中选择不相邻的数可以组成的最大值 假设数组元素都为正整数
/**
* 有两种方式 -> 递推关系
* 选择第 n 个数
* f(n) = f(n-2) + arr[n]
* 不选择第 n 个数
* f(n) = f(n-1)
* 结束条件 n = 0 -> arr[0]
* n = 1 -> max(arr[0], arr[1])
*/
// 递归解法
function rec_opt(arr, n) {
if (n === 0) return arr[0]
else if (n === 1) return Math.max(arr[0], arr[1])
else {
let a = rec_opt(arr, n - 2) + arr[n]
let b = rec_opt(arr, n - 1)
return Math.max(a, b)
}
}
// 动态规划解法
function dp_opt(arr) {
let l = arr.length
let result = new Array(l).fill(0)
// 初始值
result[0] = arr[0]
result[1] = Math.max(arr[0], arr[1])
for (let i = 2; i < l; i++) {
// 条件判断 递推关系
let a = result[i - 2] + arr[i]
let b = result[i - 1]
result[i] = Math.max(a, b)
}
return result[l - 1]
}
let a = [1, 2, 4, 2, 3]
console.log('dp_opt(a)', dp_opt(a))
console.log('dp_opt(a)', rec_opt(a, a.length - 1))
// 长度为 n 的数组中是否存在可以组成 m 的数 假设数组元素都为正整数
/**
* 有两种方式 -> 递推关系
* 选择第 n 个数
* subSet(arr, n, m) = subSet(arr, n - 1, m - arr[n])
* 不选择第 n 个数
* subSet(arr, n, m) = subSet(arr, n - 1, m)
* 结束条件 m = 0 -> true
* n = 0 -> arr[n] === m
*/
// 递归解法
function rec_subSet(arr, n, m) {
if (m === 0) return true
else if (n === 0) return arr[n] === m
else if (arr[n] > m) return rec_subSet(arr, n - 1, m)
else {
let a = rec_subSet(arr, n - 1, m - arr[n])
let b = rec_subSet(arr, n - 1, m)
return a || b
}
}
// 动态规划解法
function dp_subSet(arr, m) {
let l = arr.length
// 设置初始值 填空
let result = new Array(l).fill(0).map(item => new Array(m + 1).fill(false)).map(item => {
item[0] = true
return item
})
result[0][arr[0]] = true
for (let i = 1; i < l; i++) {
for (let j = 1; j < m + 1; j++) {
if (arr[i] > m) result[i][j] = result[i - 1][j]
else {
let a = result[i - 1][j - arr[i]]
let b = result[i - 1][j]
result[i][j] = a || b
}
}
}
return result[l - 1][m]
}
let b = [1, 2, 4, 2, 3]
console.log('dp_subSet', dp_subSet(b, 13))
console.log('rec_subSet', rec_subSet(b, b.length - 1, 13))
// 爬楼梯问题 每次只能走一步或者两步 到第N个台阶有多少种组合
/**
* -> 递推关系
* 选择第 n 个数
* f(n) = f(n-1) + f(n-2)
* 结束条件 n = 1 -> 1
* n = 2 -> 2
*/
// 递归解法
function rec_climbStairs(n) {
if (n === 1) return 1
if (n === 2) return 2
return rec_climbStairs(n - 1) + rec_climbStairs(n - 2)
}
// 动态规划解法
function dp_climbStairs(n) {
let arr = new Array(n + 1)
arr[0] = 1
arr[1] = 1
arr[2] = 2
for (let i = 3; i < n + 1; i++) {
arr[i] = arr[i - 1] + arr[i - 2]
}
return arr[n]
}
console.log('rec_climbStairs', rec_climbStairs(33))
console.log('dp_climbStairs', dp_climbStairs(33))
总结: