动态规划的常用状态转移方程总结
1.斐波那契数列
1. 斐波那契数列定义
斐波那契数列是一个经典的数学数列,其中每个数字是前两个数字的和。通常,斐波那契数列的前几个数字是:1, 1, 2, 3, 5, 8, 13, 21, ...
2. 动态规划方程
动态规划的转移方程为 dp[i] = dp[i-1] + dp[i-2]
,其中 dp[i]
表示第 i
个斐波那契数。
js
// 定义一个函数,接受斐波那契数列的序号作为参数
function fibonacci(n) {
// 初始化一个数组dp,用于存储斐波那契数列的值
const dp = [];
// 初始条件:第0个和第1个斐波那契数都为1
dp[0] = dp[1] = 1;
// 通过动态规划的转移方程计算每个斐波那契数
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回第n个斐波那契数
return dp[n];
}
// 测试例子
const n = 5;
const result = fibonacci(n);
console.log(`第${n}个斐波那契数为:${result}`);
//第5个斐波那契数为:8 数组[5] 1 1 2 3 5 8
fibonacci
函数接受一个参数n
,表示要计算第n
个斐波那契数。- 初始化一个数组
dp
,用于存储斐波那契数列的值。 - 初始条件设置
dp[0]
和dp[1]
均为 1,作为数列的起始。 - 通过循环计算每个斐波那契数,根据动态规划的转移方程
dp[i] = dp[i-1] + dp[i-2]
。 - 最后返回第
n
个斐波那契数。
2.爬楼梯问题
1. 爬楼梯问题定义
爬楼梯问题是一个经典的动态规划问题,其目标是计算爬到第 n
级楼梯的方法数,每次可以爬 1 或 2 级楼梯。动态规划的转移方程为 dp[i] = dp[i-1] + dp[i-2]
,其中 dp[i]
表示爬到第 i
级楼梯的方法数。
2.动态规划方程
dp[i] = dp[i-1] + dp[i-2]
,其中 dp[i]
表示爬到第 i
级楼梯的方法数。
js
// 定义一个函数,接受楼梯的级数作为参数
function climbStairs(n) {
// 初始化一个数组dp,用于存储到达每一级楼梯的方法数
const dp = [];
// 初始条件:爬到第0级和第1级楼梯的方法数都为1
dp[0] = dp[1] = 1;
// 通过动态规划的转移方程计算每一级楼梯的方法数
for (let i = 2; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
// 返回爬到第n级楼梯的方法数
return dp[n];
}
// 测试例子
const n = 4;
const result = climbStairs(n);
console.log(`爬到第${n}级楼梯的方法数为:${result}`);
//爬到第4级楼梯的方法数为:5
当 n=4 时,输出结果是 5,表示爬到第 4 级楼梯有 5 种不同的方式。这包括:
1 步 + 1 步 + 1 步 + 1 步
2 步 + 1 步 + 1 步
1 步 + 2 步 + 1 步
1 步 + 1 步 + 2 步
2 步 + 2 步
climbStairs
函数接受一个参数n
,表示楼梯的级数。- 初始化一个数组
dp
,用于存储到达每一级楼梯的方法数。 - 初始条件设置
dp[0]
和dp[1]
均为 1,作为楼梯的起始条件。 - 通过循环计算每一级楼梯的方法数,根据动态规划的转移方程
dp[i] = dp[i-1] + dp[i-2]
。 - 最后返回爬到第
n
级楼梯的方法数。
3.背包问题
1. 背包问题定义
背包问题是一个经典的动态规划问题,目标是在给定背包容量下,选择一定数量的物品放入背包,使得放入背包的物品总价值最大。这里采用的是 0/1 背包问题,即每个物品只能选择放入一次或不放入。
2. 动态规划方程
动态规划的转移方程为 dp[i][j] = max(dp[i-1][j], dp[i-1][j-weight[i]] + value[i])
,其中 dp[i][j]
表示在前 i
个物品中选择总重量不超过 j
的最大价值,weight[i]
表示第 i
个物品的重量,value[i]
表示第 i
个物品的价值。
js
// 定义一个函数,接受物品的重量数组和价值数组以及背包的容量作为参数
function knapsackProblem(weights, values, capacity) {
const n = weights.length; // 物品的数量
// 初始化一个二维数组dp,表示在前i个物品中选择总重量不超过j的最大价值
const dp = new Array(n + 1).fill(0).map(() => new Array(capacity + 1).fill(0));
// 动态规划,计算每个子问题的最优解
for (let i = 1; i <= n; i++) {
for (let j = 1; j <= capacity; j++) {
// 当前物品的重量
const currentWeight = weights[i - 1];
// 当前物品的价值
const currentValue = values[i - 1];
// 如果当前物品的重量小于等于背包的容量
if (currentWeight <= j) {
// 选择当前物品或不选择当前物品,取较大的价值
dp[i][j] = Math.max(dp[i - 1][j], dp[i - 1][j - currentWeight] + currentValue);
} else {
// 如果当前物品的重量大于背包的容量,则不能选择当前物品
dp[i][j] = dp[i - 1][j];
}
}
}
// 返回在前n个物品中选择总重量不超过capacity的最大价值
return dp[n][capacity];
}
// 测试例子
const weights = [2, 1, 3];
const values = [4, 2, 3];
const capacity = 4;
const result = knapsackProblem(weights, values, capacity);
console.log(`在给定容量为${capacity}的背包中,能够获得的最大价值为:${result}`);
//在给定容量为4的背包中,能够获得的最大价值为:6
knapsackProblem
函数接受三个参数:物品的重量数组weights
、价值数组values
,以及背包的容量capacity
。- 初始化一个二维数组
dp
,用于存储在前i
个物品中选择总重量不超过j
的最大价值。 - 通过嵌套循环计算每个子问题的最优解,根据动态规划的转移方程更新
dp
数组。 - 最后返回在前
n
个物品中选择总重量不超过capacity
的最大价值。
4.最长递增子序列
1. 最长递增子序列定义
最长递增子序列是指在一个数组中找到一个子序列,使得这个子序列中的元素是按照升序排列的,并且在所有符合条件的子序列中它是最长的。
2. 动态规划方程
动态规划的转移方程为 dp[i] = max(dp[j] + 1, dp[i])
,其中 dp[i]
表示以第 i
个元素结尾的最长递增子序列的长度,j
为 0
到 i-1
的索引,且 nums[i] > nums[j]
。
js
// 定义一个函数,接受数组作为参数
function longestIncreasingSubsequence(nums) {
const n = nums.length; // 数组的长度
// 初始化一个数组dp,表示以第i个元素结尾的最长递增子序列的长度
const dp = new Array(n).fill(1);
// 动态规划,计算每个子问题的最优解
for (let i = 1; i < n; i++) {
for (let j = 0; j < i; j++) {
// 如果当前元素大于前面某个元素,则更新最长递增子序列的长度
if (nums[i] > nums[j]) {
dp[i] = Math.max(dp[j] + 1, dp[i]);
}
}
}
// 返回最长递增子序列的最大长度
return Math.max(...dp);
}
// 测试例子
const nums = [10, 22, 9, 33, 21, 50, 41, 60, 80];
const result = longestIncreasingSubsequence(nums);
console.log(`最长递增子序列的长度为:${result}`);
//最长递增子序列的长度为:6
longestIncreasingSubsequence
函数接受一个数组nums
作为参数。- 初始化一个数组
dp
,用于存储以每个元素结尾的最长递增子序列的长度,初始值为1。 - 动态规划通过两层循环计算每个子问题的最优解。
- 如果当前元素
nums[i]
大于前面某个元素nums[j]
,则更新最长递增子序列的长度dp[i]
为当前的最大值。 - 最后返回最长递增子序列的最大长度,使用
Math.max(...dp)
获取数组中的最大值。
5.最大子数组和
1. 最大子数组和定义
最大子数组和问题是一个经典的动态规划问题,其目标是找到一个具有最大和的连续子数组。
2. 动态规划方程
动态规划的转移方程为 dp[i] = max(nums[i], nums[i] + dp[i-1])
,其中 dp[i]
表示以第 i
个元素结尾的最大子数组和。
js
// 定义一个函数,接受数组作为参数
function maxSubArray(nums) {
const n = nums.length; // 数组的长度
// 初始化一个数组dp,表示以第i个元素结尾的最大子数组和
const dp = new Array(n);
// 初始条件:第0个元素结尾的最大子数组和即为第0个元素本身
dp[0] = nums[0];
// 动态规划,计算每个子问题的最优解
for (let i = 1; i < n; i++) {
// 当前元素单独构成子数组,或者与前面的子数组连接
dp[i] = Math.max(nums[i], nums[i] + dp[i - 1]);
}
// 返回以每个元素结尾的最大子数组和中的最大值
return Math.max(...dp);
}
// 测试例子
const nums = [-2, 1, -3, 4, -1, 2, 1, -5, 4];
const result = maxSubArray(nums);
console.log(`最大子数组和为:${result}`);
//最大子数组和为:6
maxSubArray
函数接受一个数组nums
作为参数。- 初始化一个数组
dp
,用于存储以每个元素结尾的最大子数组和。 - 初始条件设置
dp[0]
为第0个元素本身。 - 动态规划通过循环计算每个子问题的最优解,即以当前元素结尾的最大子数组和。
- 转移方程
dp[i] = max(nums[i], nums[i] + dp[i-1])
表示当前元素单独构成子数组,或者与前面的子数组连接,取两者中的较大值。 - 最后返回以每个元素结尾的最大子数组和中的最大值,使用
Math.max(...dp)
获取数组中的最大值。
6.最长公共子序列
1. 最长公共子序列定义
最长公共子序列是指在两个字符串中找到一个最长的子序列,使得它在两个字符串中都出现,但不要求连续。
2. 动态规划方程
动态规划的转移方程为:
- 如果
str1[i]
等于str2[j]
,则dp[i][j] = dp[i-1][j-1] + 1
; - 否则,
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
,其中dp[i][j]
表示str1
的前i
个字符和str2
的前j
个字符的最长公共子序列的长度。
js
// 定义一个函数,接受两个字符串作为参数
function longestCommonSubsequence(str1, str2) {
const m = str1.length; // 字符串1的长度
const n = str2.length; // 字符串2的长度
// 初始化一个二维数组dp,表示str1的前i个字符和str2的前j个字符的最长公共子序列的长度
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
// 动态规划,计算每个子问题的最优解
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// 如果当前字符相等,则取左上方的值加1
if (str1[i - 1] === str2[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]);
}
}
}
// 返回str1的前m个字符和str2的前n个字符的最长公共子序列的长度
return dp[m][n];
}
// 测试例子
const str1 = "abcde";
const str2 = "ace";
const result = longestCommonSubsequence(str1, str2);
console.log(`最长公共子序列的长度为:${result}`);
//最长公共子序列的长度为:3
longestCommonSubsequence
函数接受两个字符串str1
和str2
作为参数。- 初始化一个二维数组
dp
,用于存储str1的前i个字符和str2的前j个字符的最长公共子序列的长度。 - 动态规划通过两层循环计算每个子问题的最优解。
- 转移方程中,如果当前字符相等,则取左上方的值加1;如果当前字符不相等,则取左方和上方的最大值。
- 最后返回
str1
的前m
个字符和str2
的前n
个字符的最长公共子序列的长度,即dp[m][n]
。
7.编辑距离
1. 编辑距离定义
编辑距离是衡量两个字符串相似程度的一种指标,表示将一个字符串转换成另一个字符串所需的最少操作次数。典型的操作包括插入一个字符、删除一个字符、替换一个字符。
2. 动态规划方程
动态规划的转移方程为:
- 如果
word1[i]
等于word2[j]
,则dp[i][j] = dp[i-1][j-1]
; - 否则,
dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
,其中dp[i][j]
表示将word1
的前i
个字符转换为word2
的前j
个字符所需的最少操作次数。
js
// 定义一个函数,接受两个单词作为参数
function minDistance(word1, word2) {
const m = word1.length; // 单词1的长度
const n = word2.length; // 单词2的长度
// 初始化一个二维数组dp,表示将word1的前i个字符转换为word2的前j个字符所需的最少操作次数
const dp = new Array(m + 1).fill(0).map(() => new Array(n + 1).fill(0));
// 初始化边界条件
for (let i = 0; i <= m; i++) {
dp[i][0] = i;
}
for (let j = 0; j <= n; j++) {
dp[0][j] = j;
}
// 动态规划,计算每个子问题的最优解
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
// 如果当前字符相等,则取左上方的值
if (word1[i - 1] === word2[j - 1]) {
dp[i][j] = dp[i - 1][j - 1];
} else {
// 如果当前字符不相等,则取左方、上方和左上方的最小值加1
dp[i][j] = Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]) + 1;
}
}
}
// 返回将word1的前m个字符转换为word2的前n个字符所需的最少操作次数
return dp[m][n];
}
// 测试例子
const word1 = "horse";
const word2 = "ros";
const result = minDistance(word1, word2);
console.log(`将"${word1}"转换为"${word2}"所需的最少操作次数为:${result}`);
//将"horse"转换为"ros"所需的最少操作次数为:3
minDistance
函数接受两个单词word1
和word2
作为参数。- 初始化一个二维数组
dp
,用于表示将word1
的前i
个字符转换为word2
的前j
个字符所需的最少操作次数。 - 初始化边界条件,即当一个字符串为空时,转换所需的操作次数为字符串的长度。
- 动态规划通过两层循环计算每个子问题的最优解。
- 转移方程中,如果当前字符相等,则取左上方的值;如果当前字符不相等,则取左方、上方和左上方的最小值加1。
- 最后返回将
word1
的前m
个字符转换为word2
的前n
个字符所需的最少操作次数,即dp[m][n]
。
8.打家劫舍
1. 打家劫舍问题定义
打家劫舍问题是一个典型的动态规划问题,目标是在一排房屋中选择一些房屋进行偷窃,但不能偷相邻的房屋,求能够获得的最大金额。
2. 动态规划方程
动态规划的转移方程为 dp[i] = max(dp[i-1], dp[i-2] + nums[i])
,其中 dp[i]
表示前 i
个房屋能够获得的最大金额,nums[i]
表示第 i
个房屋中的金额。
js
// 定义一个函数,接受一个包含每个房屋金额的数组作为参数
function rob(nums) {
const n = nums.length; // 房屋的数量
if (n === 0) {
return 0; // 没有房屋,则返回0
} else if (n === 1) {
return nums[0]; // 只有一个房屋,则返回该房屋的金额
}
// 初始化一个数组dp,表示前i个房屋能够获得的最大金额
const dp = new Array(n);
// 前两个房屋的最大金额为较大的那个
dp[0] = nums[0];
dp[1] = Math.max(nums[0], nums[1]);
// 动态规划,计算每个子问题的最优解
for (let i = 2; i < n; i++) {
// 在偷第i个房屋和不偷第i个房屋之间取较大值
dp[i] = Math.max(dp[i - 1], dp[i - 2] + nums[i]);
}
// 返回前n个房屋能够获得的最大金额
return dp[n - 1];
}
// 测试例子
const nums = [2, 7, 9, 3, 1];
const result = rob(nums);
console.log(`在给定房屋中能够获得的最大金额为:${result}`);
//在给定房屋中能够获得的最大金额为:12
rob
函数接受一个包含每个房屋金额的数组nums
作为参数。- 根据房屋的数量
n
分情况处理:如果没有房屋,则返回0;如果只有一个房屋,则返回该房屋的金额。 - 初始化一个数组
dp
,表示前i
个房屋能够获得的最大金额。 - 前两个房屋的最大金额为较大的那个,即
dp[0] = nums[0]
和dp[1] = Math.max(nums[0], nums[1])
。 - 动态规划通过循环计算每个子问题的最优解,转移方程为
dp[i] = max(dp[i-1], dp[i-2] + nums[i])
,表示在偷第i
个房屋和不偷第i
个房屋之间取较大值。 - 最后返回前
n
个房屋能够获得的最大金额,即dp[n - 1]
。
9.最大正方形
1. 最大正方形定义
最大正方形是指在一个二维矩阵中,由'1'组成的最大正方形的边长。
2. 动态规划方程
动态规划的转移方程为:
- 如果
matrix[i][j]
等于 1,则dp[i][j] = min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1]) + 1
; - 否则,
dp[i][j] = 0
,其中dp[i][j]
表示以matrix[i][j]
为右下角的最大正方形的边长。
js
// 定义一个函数,接受一个包含0和1的二维矩阵作为参数
function maximalSquare(matrix) {
const m = matrix.length; // 矩阵的行数
const n = matrix[0].length; // 矩阵的列数
// 初始化一个二维数组dp,表示以matrix[i][j]为右下角的最大正方形的边长
const dp = new Array(m).fill(0).map(() => new Array(n).fill(0));
// 初始化结果,表示最大正方形的边长
let maxSquare = 0;
// 动态规划,计算每个子问题的最优解
for (let i = 0; i < m; i++) {
for (let j = 0; j < n; j++) {
// 如果当前元素为1,则更新dp[i][j]为左、上、左上三个方向的最小值加1
if (matrix[i][j] === '1') {
dp[i][j] = Math.min(
i > 0 ? dp[i - 1][j] : 0,
j > 0 ? dp[i][j - 1] : 0,
i > 0 && j > 0 ? dp[i - 1][j - 1] : 0
) + 1;
// 更新最大正方形的边长
maxSquare = Math.max(maxSquare, dp[i][j]);
}
}
}
// 返回最大正方形的边长的平方
return maxSquare * maxSquare;
}
// 测试例子
const matrix = [
['1', '0', '1', '0', '0'],
['1', '0', '1', '1', '1'],
['1', '1', '1', '1', '1'],
['1', '0', '0', '1', '0']
];
const result = maximalSquare(matrix);
console.log(`最大正方形的面积为:${result}`);
//最大正方形的面积为:4
maximalSquare
函数接受一个包含0和1的二维矩阵matrix
作为参数。- 初始化一个二维数组
dp
,用于存储以matrix[i][j]
为右下角的最大正方形的边长。 - 初始化结果
maxSquare
,表示最大正方形的边长。 - 动态规划通过两层循环计算每个子问题的最优解。
- 转移方程中,如果当前元素为1,则更新
dp[i][j]
为左、上、左上三个方向的最小值加1;否则,dp[i][j]
为0。 - 在动态规划过程中,不断更新最大正方形的边长。
- 最后返回最大正方形的边长的平方,即
maxSquare * maxSquare
。