动态规划(Dynamic Programming,简称DP)
是一种常用的解决优化问题的算法思想,它通常用于求解具有重叠子问题和最优子结构性质的问题。
动态规划算法常用于解决具有最优子结构性质的问题,例如最长递增子序列、背包问题、最短路径问题等。
下面是关于动态规划的一些重要知识点:
1. 重叠子问题(Overlapping Subproblems)
动态规划算法的核心思想之一是重叠子问题。在解决一个问题的过程中,如果问题的求解过程中涉及到了重复计算相同的子问题,则称这个问题具有重叠子问题的性质。动态规划算法通过将子问题的解存储起来,避免了重复计算,从而提高了算法的效率。
2. 最优子结构(Optimal Substructure)
动态规划算法的另一个核心思想是最优子结构。一个问题具有最优子结构的性质,意味着问题的最优解可以通过其子问题的最优解来构造。换句话说,如果一个问题的最优解可以由其子问题的最优解推导出来,则称该问题具有最优子结构性质。
3. 状态(State)
在动态规划算法中,状态是问题的解空间中的一个点,通常用一个或多个变量来表示。状态可以是问题的某个属性的取值,也可以是问题的子结构。动态规划算法通过定义状态来描述问题的性质,并根据状态之间的转移关系来构建状态转移方程。
4. 状态转移方程(State Transition Equation)
状态转移方程描述了问题中状态之间的转移关系,即从一个状态转移到另一个状态的过程。动态规划算法通过定义状态转移方程来描述问题的递推关系,并根据状态转移方程来计算问题的最优解。
5. 初始状态(Base Case)
初始状态是动态规划算法中的起始点,通常是问题的最小规模的解。初始状态的定义是问题求解的基础,是动态规划算法的一个重要组成部分。
6. 计算顺序(Computational Order)
计算顺序指的是动态规划算法中计算状态之间转移关系的顺序。通常情况下,计算顺序是按照状态之间的依赖关系从小到大进行计算的,确保每个状态的计算依赖于之前的状态。
7. 解的求解(Solution Reconstruction)
解的求解是动态规划算法的最后一步,通常是根据计算得到的状态和状态转移方程来构造问题的最优解。解的求解过程是动态规划算法的关键,通常需要根据问题的具体性质来确定解的构造方法。
动态规划算法通常分为以下几个步骤:
-
定义状态:首先明确定义问题的状态,找出问题中变化的部分,并将其转化为状态变量。
-
状态转移方程:根据问题的要求,确定状态之间的转移关系。这个转移关系通常通过状态之间的递推关系来描述,即找出当前状态与之前状态之间的关系。
-
初始状态:确定初始状态,即问题的最小规模的解,通常是状态转移方程中的边界条件。
-
计算顺序:确定状态之间的计算顺序,通常是按照状态转移方程中的依赖关系从小到大进行计算。
-
解的求解:根据问题的定义,得到最终的解。
示例
下面是一个简单的例子,演示了如何使用动态规划算法
题目:
假设有一个背包,它能容纳的重量为 W。现在有 n 个物品,每个物品的重量分别为 weights[i],价值为 values[i]。我们希望从这些物品中挑选一些放入背包中,使得放入的物品总重量不超过背包容量,并且总价值最大。这是一个经典的背包问题,可以使用动态规划来解决。
1. 定义状态
我们将问题的状态定义为背包的剩余容量和可选物品的索引。即状态可以用 (remainingCapacity, currentIndex) 表示,其中 remainingCapacity 表示背包的剩余容量,currentIndex 表示当前可选的物品索引。
2. 状态转移方程
我们可以定义状态转移方程来描述问题的递推关系。设 dp[i][j] 表示考虑前 i 个物品,在背包容量为 j 时,能够获得的最大总价值。则状态转移方程为:
dp[i][j] = max(dp[i-1][j], dp[i-1][j-weights[i]] + values[i])
其中,dp[i-1][j] 表示不选择第 i 个物品时的最大总价值,dp[i-1][j-weights[i]] + values[i] 表示选择第 i 个物品时的最大总价值。
3. 初始状态
初始状态为 dp[0][j] = 0(表示没有物品可选时的最大总价值),dp[i][0] = 0(表示背包容量为 0 时的最大总价值为 0)。
4. 计算顺序
我们可以按照状态转移方程中的依赖关系从小到大进行计算。即先计算出 dp[0][j] 和 dp[i][0],然后依次计算出 dp[1][j]、dp[2][j],直到计算出 dp[n][W]。
5. 解的求解
根据计算得到的 dp 数组,我们可以从中找出最终的解。在本问题中,我们需要找出 dp[n][W],即考虑前 n 个物品,背包容量为 W 时的最大总价值。
/**
* 01背包问题
*
* @param weights 物品重量数组
* @param values 物品价值数组
* @param W 背包容量
* @returns 返回最大价值
*/
function knapsack(weights, values, W) {
const n = weights.length;
// 创建一个二维数组dp,用于存储中间结果
const dp = Array.from({ length: n + 1 }, () => Array(W + 1).fill(0));
// 遍历每个物品
for (let i = 1; i <= n; i++) {
// 遍历每个背包容量
for (let j = 1; j <= W; j++) {
// 如果当前物品的重量小于等于当前背包容量
if (weights[i - 1] <= j) {
// 更新dp[i][j]的值为当前背包容量下,选择当前物品和不选择当前物品中的较大值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - weights[i - 1]] + values[i - 1]);
} else {
// 如果当前物品的重量大于当前背包容量,则无法选择当前物品,dp[i][j]的值与dp[i-1][j]相同
dp[i][j] = dp[i - 1][j];
}
}
}
// 返回dp[n][W],即背包容量为W时,能够装载的物品的最大价值
return dp[n][W];
}
const weights = [2, 3, 4, 5];
const values = [3, 4, 5, 6];
const W = 8;
console.log('背包能获得的最大总价值为:', knapsack(weights, values, W));
代码题目:(不断添加)
求解斐波那契数列
/**
* 计算斐波那契数列的第n项
*
* @param n 第n项,n为大于等于0的整数
* @returns 返回斐波那契数列的第n项
*/
function fibonacci(n) {
// 如果 n 小于等于 1,则直接返回 n
if (n <= 1) {
return n;
}
// 创建一个长度为 n+1 的数组 dp,用于存储斐波那契数列的值
let dp = new Array(n + 1);
// 初始化 dp 数组的前两个元素
dp[0] = 0;
dp[1] = 1;
// 从第三个元素开始,计算斐波那契数列的值
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回斐波那契数列的第 n 个值
return dp[n];
}
console.log('斐波那契数列的第 10 项为:', fibonacci(10)); // 输出:55