JavaScript中的动态规划(Dynamic Programming,简称DP)是一种通过把原问题分解为相对简单的子问题的方式来求解复杂问题的方法。它主要致力于将“合适”的问题拆分成更小的子目标,并通过建立状态转移方程、缓存并复用以往结果以及按顺序从小往大算这三个步骤来解决问题。以下是对js动态规划算法的详细解析:
一、动态规划的基本概念
- 状态转移方程:动态规划的核心是找到一个能够描述问题状态转移的数学方程,即状态转移方程。这个方程描述了如何从较小的子问题的解推导出较大问题的解。
- 缓存并复用以往结果:为了避免重复计算,动态规划会将已经计算过的子问题的解存储起来,以便在后续的计算中直接引用。这通常通过一个数组或对象来实现,称为DP表。
- 按顺序从小往大算:动态规划通常按照某种顺序(如从小到大的子问题规模)来计算子问题的解,并最终得到原问题的解。
二、动态规划的应用示例
- 斐波那契数列
斐波那契数列是一个经典的动态规划问题。数列中的每个数字是前两个数字之和,通常以0和1开始。使用动态规划可以避免直接递归方法中的大量重复计算。
JavaScript代码示例:
function fibonacci(n, memo = []) {
// 初始化记忆数组
memo[0] = 0;
memo[1] = 1;
// 如果已经计算过该值,直接从记忆数组返回
if (memo[n] !== undefined) {
return memo[n];
}
// 递归计算斐波那契数,同时利用记忆化存储结果
memo[n] = fibonacci(n - 1, memo) + fibonacci(n - 2, memo);
return memo[n];
}
// 示例
console.log(fibonacci(10)); // 输出第10个斐波那契数
在这个例子中,memo数组用于存储已经计算过的斐波那契数,从而避免了重复计算。
- 01背包问题
01背包问题是另一个经典的动态规划问题。它描述了一个背包可以装载的最大重量为W,有N件物品,每件物品有一个重量和一个价值。要求选择若干件物品装入背包,使得背包中物品的总价值最大,同时不超过背包的最大重量。
JavaScript代码示例:
const w = [1, 4, 3]; // 物品重量
const value = [1500, 3000, 2000]; // 物品的价值
const m = 4; // 背包容量
const n = 3; // 物品的个数
// 二维数组v[i][j]表示在前i个物品中能够装入容量为j的背包中的最大价值
let v = new Array(n + 1).fill(0).map(() => new Array(m + 1).fill(0));
// 遍历物品和背包容量
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= m; j++) {
if (w[i - 1] > j) {
v[i][j] = v[i - 1][j];
} else {
v[i][j] = Math.max(v[i - 1][j], value[i - 1] + v[i - 1][j - w[i - 1]]);
}
}
}
console.log(v[n][m]); // 输出最大价值
核心:Math.max(v[i-1][j], value[i - 1] + v[i - 1][j - w[i - 1]])
在这个例子中,二维数组v用于存储子问题的解。通过遍历物品和背包容量,可以逐步计算出在前i个物品中能够装入容量为j的背包中的最大价值。
放了那些商品?【待思考】
function knapsack(weights, values, maxWeight) {
const n = weights.length;
// 创建一个二维数组dp,dp[i][w]表示前i个物品在重量不超过w的情况下的最大价值
const dp = Array.from({ length: n + 1 }, () => Array(maxWeight + 1).fill(0));
// 记录选择的物品
const selectedItems = Array.from({ length: n + 1 }, () => Array(maxWeight + 1).fill(false));
// 动态规划填表
for (let i = 1; i <= n; i++) {
for (let w = 0; w <= maxWeight; w++) {
if (weights[i - 1] <= w) {
if (dp[i - 1][w] + values[i - 1] > dp[i - 1][w]) {
dp[i][w] = dp[i - 1][w] + values[i - 1];
selectedItems[i][w] = true;
} else {
dp[i][w] = dp[i - 1][w];
}
} else {
dp[i][w] = dp[i - 1][w];
}
}
}
// 最大价值
const maxValue = dp[n][maxWeight];
// 追溯选择的物品
const selected = [];
let currentWeight = maxWeight;
for (let i = n; i > 0; i--) {
if (selectedItems[i][currentWeight] && dp[i][currentWeight] !== dp[i - 1][currentWeight]) {
selected.push(i - 1); // 物品索引从0开始,需要减1
currentWeight -= weights[i - 1];
}
}
return {
maxValue: maxValue,
selectedItems: selected
};
}
// 示例
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const maxWeight = 5;
const result = knapsack(weights, values, maxWeight);
console.log(`最大价值: ${result.maxValue}`);
console.log(`选择的物品索引: ${result.selectedItems}`);
console.log(`选择的物品: ${result.selectedItems.map(index => `物品${index + 1}`).join(', ')}`);
三、动态规划的优点和局限性
-
优点:
- 能够高效地解决具有重叠子问题的问题。
- 通过缓存和复用以往结果,避免了大量的重复计算。
-
局限性:
- 只适用于具有最优子结构和重叠子问题的问题。
- 对于某些问题,可能需要大量的空间来存储子问题的解(即DP表)。
四、总结
JavaScript中的动态规划是一种强大的算法设计范式,适用于解决具有重叠子问题的问题。通过建立状态转移方程、缓存并复用以往结果以及按顺序从小往大算这三个步骤,可以高效地求解复杂问题。然而,动态规划也有一定的局限性,只适用于具有最优子结构和重叠子问题的问题。在实际应用中,需要根据问题的特点选择合适的算法设计范式来求解。