动态规划
动态规划是一种使用空间来换时间的一种典型的算法和思想,具有广泛的使用范围,不过动态规划经常用来解决一些最值问题,这些最值问题存在以下特点:
- 具有可降解规模的重复子结构;
- 子结构之间存在递推关系,一般体现为数列的递推公式的样子;回顾数列问题,递推公式、前 N 项和,在算法中分别称为状态转移方程、前缀和。
- 无后效性,前面的决策不会对后续的决策产生影响;
动态规划一般可以将时间复杂度大于 O(nk)的算法,优化为时间复杂度为 O(nk)的算法,代价是需要使用额外空间复杂度 O(nk),其中 k 是该动态规划问题的阶数,阶数越高对应的原本的问题的复杂度就越高,而常见的动态规划问题往往是一阶和二阶的,它们可以使用一个或者多个简单的数组序列或者表格作为 dp 数组,以此优化时间复杂度大于 O(n)或 O(n2)的问题。
动态规划解题步骤
- 定义状态数组:确定状态表示,选择合适的数据结构,确定状态维度
- 初始化状态:设置初始值,处理边界条件,考虑特殊情况
- 状态转移方程:分析状态之间的关系,确定转移条件,考虑所有可能的情况
- 优化空间复杂度:使用滚动数组,状态压缩,降维处理
javascript
function dynamicProgramming(n) {
// 定义状态数组
let dp = new Array(n).fill(0);
// 初始化初始状态和边界条件
dp[0] = initial_value; // 根据具体问题确定
// 根据状态转移方程计算每个状态
for (let i = 1; i <= n; i++) {
dp[i] = dpTransition(dp, i);
}
// 返回最终结果
return getResult(dp);
}
// 状态转移函数,根据具体问题定义
function dpTransition(dp, i) {
return some_function_of(dp, i); // 根据具体问题确定
// 示例转移方程
// dp[i] = min(dp[i-1] + cost1, dp[i-2] + cost2);
}
function getResult(dp) {
return dp[dp.length - 1]
}
自底向上和自顶向下
动态规划有两种表现方法,一种是 dp 数组法,一种是记忆化搜索 dfs 函数,以递归的形式出现;两者在一定程度上是等价的。
使用 dp
数组意味着自底向上的生成顺序,使用 dfs
递归函数是使用自顶向下的生成顺序,这两种方式的方向不同。但是,他们的维度相同(即 dp 数组的维度 index 等价于 dfs 函数的参数),转移方程也类似,二者几乎是等价的。对于 dp 的维度 index 或者是 dfs 的参数,必须是纯数字或者是字符串,字符串的情况下,dp 数组需要使用 hash 表,dfs 函数正常使用 string 类型的参数。
一般地,dfs 函数可以实现的情况,用 dp 数组也可以实现;dfs 函数胜在,如果题目中的生成顺序是自顶向下的,那么使用 dfs 会显得代码简单。
// 斐波那契
dp[i] = dp[i-1] + dp[i-2];
dfs(i) = dfs(i-1) + dfs(i-2);
经典问题示例
1. 斐波那契数列
javascript
// 递归解法 O(2^n)
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n-1) + fibonacci(n-2);
}
// 动态规划解法 O(n)
function fibonacciDP(n) {
if (n <= 1) return n;
const dp = [0, 1];
for (let i = 2; i <= n; i++) {
dp[i] = dp[i-1] + dp[i-2];
}
return dp[n];
}
// 空间优化 O(1)
function fibonacciOptimized(n) {
if (n <= 1) return n;
let prev = 0, curr = 1;
for (let i = 2; i <= n; i++) {
[prev, curr] = [curr, prev + curr];
}
return curr;
}
2. 最长公共子序列
javascript
function longestCommonSubsequence(text1, text2) {
const m = text1.length;
const n = text2.length;
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
if (text1[i-1] === text2[j-1]) {
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];
}
3. 背包问题
javascript
// 0/1背包问题
function knapsack(weights, values, W) {
const n = weights.length;
const dp = Array.from({ length: n + 1 }, () => Array(W + 1).fill(0));
for (let i = 1; i <= n; i++) {
for (let w = 1; w <= W; w++) {
if (weights[i-1] <= w) {
dp[i][w] = Math.max(
dp[i-1][w],
dp[i-1][w-weights[i-1]] + values[i-1]
);
} else {
dp[i][w] = dp[i-1][w];
}
}
}
return dp[n][W];
}
// 完全背包问题
function completeKnapsack(weights, values, W) {
const n = weights.length;
const dp = Array(W + 1).fill(0);
for (let i = 0; i < n; i++) {
for (let w = weights[i]; w <= W; w++) {
dp[w] = Math.max(dp[w], dp[w-weights[i]] + values[i]);
}
}
return dp[W];
}
4. 股票买卖问题
javascript
// 买卖股票的最佳时机(一次交易)
function maxProfit(prices) {
let minPrice = Infinity;
let maxProfit = 0;
for (let price of prices) {
minPrice = Math.min(minPrice, price);
maxProfit = Math.max(maxProfit, price - minPrice);
}
return maxProfit;
}
// 买卖股票的最佳时机(多次交易)
function maxProfitMultiple(prices) {
let profit = 0;
for (let i = 1; i < prices.length; i++) {
if (prices[i] > prices[i-1]) {
profit += prices[i] - prices[i-1];
}
}
return profit;
}
动态规划优化技巧
1. 状态压缩
javascript
// 状态压缩示例:斐波那契数列
function fibonacciCompressed(n) {
if (n <= 1) return n;
let prev = 0, curr = 1;
for (let i = 2; i <= n; i++) {
[prev, curr] = [curr, prev + curr];
}
return curr;
}
2. 滚动数组
javascript
// 滚动数组示例:背包问题
function knapsackRolling(weights, values, W) {
const n = weights.length;
const dp = Array(W + 1).fill(0);
for (let i = 0; i < n; i++) {
for (let w = W; w >= weights[i]; w--) {
dp[w] = Math.max(dp[w], dp[w-weights[i]] + values[i]);
}
}
return dp[W];
}
3. 记忆化搜索
javascript
// 记忆化搜索示例:斐波那契数列
function fibonacciMemo(n) {
const memo = new Map();
function helper(n) {
if (n <= 1) return n;
if (memo.has(n)) return memo.get(n);
const result = helper(n-1) + helper(n-2);
memo.set(n, result);
return result;
}
return helper(n);
}
常见问题类型
- 线性DP,最长递增子序列,最大子数组和,编辑距离
- 区间DP,矩阵链乘法,石子合并,回文子串
- 树形DP,二叉树最大路径和,树的最大独立集,树的最小支配集
- 状态压缩DP,旅行商问题,数位DP,轮廓线DP