动态规划问题通过直接思考并编程解决问题是很难的,而且十分费时间,所以我整理了一份动态规划递推公式大全,可用于直接查阅并直接套用进行编程,同时附加上完整代码,文章将分为一维和多维进行总结。
1.斐波那契数列/爬楼梯
递推公式:dp[i] = dp[i-1] + dp[i-2]
初始化:
dp[0] = 1(地面算1种方式)
dp[1] = 1(爬1阶只有1种方式)
2.打家劫舍系列
基础版(无环)
递推公式:dp[i] = max(dp[i-1], dp[i-2] + nums[i])
初始化:
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
环形版
拆分为两个子问题:
不抢第一间:robRange(nums, 1, n-1)
不抢最后一间:robRange(nums, 0, n-2)
初始化:与基础版相同,但处理不同区间。
代码(递推公式有点特殊,需要示例代码才能说清楚):
int rob(vector& nums) {
int n = nums.size();
if (n == 0) return 0;
if (n == 1) return nums[0];
return max(robRange(nums, 0, n-2), robRange(nums, 1, n-1));
}
int robRange(vector& nums, int start, int end) {
if (start > end) return 0;
int prev1 = 0, prev2 = 0;
for (int i = start; i <= end; i++) {
int curr = max(prev1, prev2 + nums[i]);
prev2 = prev1;
prev1 = curr;
}
return prev1;
}
树形版
递推公式:
后序遍历返回 [抢当前节点的收益, 不抢的收益]
抢当前节点:rob_val = node->val + left[1] + right[1]
不抢当前节点:not_rob_val = max(left[0], left[1]) + max(right[0], right[1])
初始化:空节点返回 {0, 0}
代码(递推公式有点特殊,需要示例代码才能说清楚):
struct TreeNode {
int val;
TreeNode *left;
TreeNode *right;
TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}
};
pair dfs(TreeNode* node) {
if (!node) return {0, 0};
auto left = dfs(node->left);
auto right = dfs(node->right);
int rob_val = node->val + left.second + right.second;
int not_rob_val = max(left.first, left.second) + max(right.first, right.second);
return {rob_val, not_rob_val};
}
int rob(TreeNode* root) {
auto res = dfs(root);
return max(res.first, res.second);
}
3.最长递增子序列(LIS)
递推公式:dp[i] = max(dp[j] + 1)(对所有 j < i 且 nums[j] < nums[i])
初始化:dp 数组全初始化为 1
完整示例代码:
#include
#include
#include
using namespace std;
int lengthOfLIS(vector& nums) {
if (nums.empty()) return 0;
int n = nums.size();
// 初始化 dp 数组,每个元素的最长递增子序列初始为 1
vector dp(n, 1);
// 动态规划计算 dp 数组
for (int i = 1; i < n; ++i) {
for (int j = 0; j < i; ++j) {
if (nums[i] > nums[j]) {
dp[i] = max(dp[i], dp[j] + 1);
}
}
}
// 最长递增子序列的长度是 dp 数组中的最大值
return *max_element(dp.begin(), dp.end());
}
4.最大子数组和
递推公式:dp[i] = max(nums[i], dp[i-1] + nums[i])
初始化:dp[0] = nums[0]
完整示例代码:
#include
#include
#include
using namespace std;
int maxSubArray(vector& nums) {
if (nums.empty()) return 0;
int n = nums.size();
// 初始化 dp 数组,dp[i]表示以 nums[i] 结尾的最大子数组和
vector dp(n);
dp[0] = nums[0]; // 初始化 dp[0] 为 nums[0]
// 动态规划计算 dp 数组
for (int i = 1; i < n; ++i) {
dp[i] = max(nums[i], dp[i-1] + nums[i]);
}
// 返回 dp 数组中的最大值,即为最大子数组和
return *max_element(dp.begin(), dp.end());
}
5.零钱兑换(Coin Change)
变体1:最小硬币数
递推公式:dp[i] = min(dp[i], dp[i - coin] + 1) # 对所有硬币面额 coin <= i
初始化:dp[0] = 0,其他为正无穷。
说明:用最少数量的硬币凑出金额 amount,外层遍历金额,内层遍历硬币(顺序无关)。
完整示例代码:
#include
#include
#include
using namespace std;
int coinChange(vector& coins, int amount) {
// 初始化 dp 数组,dp[i] 表示凑成金额 i 的最小硬币数
vector dp(amount + 1, amount + 1); // 用一个大数表示正无穷
dp[0] = 0; // 需要 0 个硬币来凑成 0 元
// 外层遍历金额
for (int i = 1; i <= amount; ++i) {
// 内层遍历硬币
for (int coin : coins) {
if (i - coin >= 0) {
dp[i] = min(dp[i], dp[i - coin] + 1); // 更新 dp[i]
}
}
}
// 如果 dp[amount] 仍然是正无穷,说明无法凑成 amount
return dp[amount] > amount ? -1 : dp[amount];
}
int main() {
vector coins = {1, 2, 5}; // 可用的硬币面额
int amount = 11; // 要凑成的金额
cout << "Minimum coins required: " << coinChange(coins, amount) << endl;
return 0;
}
变体2:组合数
递推公式:dp[i] += dp[i - coin] # 对所有硬币面额 coin <= i
初始化:dp[0] = 1(空组合)。
说明:计算凑出金额 amount 的硬币组合数(顺序不同视为相同),外层遍历硬币 coin,内层遍历金额 i(避免重复计数顺序)。
完整示例代码:
#include
#include
#include
using namespace std;
int coinChangeCombinations(vector& coins, int amount) {
// 初始化 dp 数组,dp[i] 表示凑成金额 i 的硬币组合数
vector dp(amount + 1, 0);
dp[0] = 1; // 需要 1 种方式凑成 0 元(空组合)
// 外层遍历硬币
for (int coin : coins) {
// 内层遍历金额,从 coin 到 amount,避免重复计数顺序
for (int i = coin; i <= amount; ++i) {
dp[i] += dp[i - coin]; // 更新 dp[i]
}
}
// 返回凑成金额 amount 的硬币组合数
return dp[amount];
}
int main() {
vector coins = {1, 2, 5}; // 可用的硬币面额
int amount = 5; // 要凑成的金额
cout << "Number of combinations: " << coinChangeCombinations(coins, amount) << endl;
return 0;
}
变体3:排列数
递推公式:dp[i] += dp[i - coin] # 对所有硬币面额 coin <= i
初始化:dp[0] = 1(空组合)。
说明:计算凑出金额 amount 的硬币排列数(顺序不同视为不同方案),外层遍历金额 i,内层遍历硬币 coin(允许不同顺序组合)。
完整示例代码:
#include
#include
#include
using namespace std;
int coinChangePermutations(vector& coins, int amount) {
// 初始化 dp 数组,dp[i] 表示凑成金额 i 的硬币排列数
vector dp(amount + 1, 0);
dp[0] = 1; // 需要 1 种方式凑成 0 元(空组合)
// 外层遍历金额 i
for (int i = 1; i <= amount; ++i) {
// 内层遍历硬币,允许不同顺序组合
for (int coin : coins) {
if (i - coin >= 0) {
dp[i] += dp[i - coin]; // 更新 dp[i],加入当前硬币的排列数
}
}
}
// 返回凑成金额 amount 的硬币排列数
return dp[amount];
}
int main() {
vector coins = {1, 2, 5}; // 可用的硬币面额
int amount = 5; // 要凑成的金额
cout << "Number of permutations: " << coinChangePermutations(coins, amount) << endl;
return 0;
}
1. 背包问题
0-1背包(物品不可重复,只有一件)
// 初始化
vector> dp(n+1, vector(capacity+1, 0));
// 递推公式
if (j >= w[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j - w[i-1]] + v[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
// 返回值
return dp[n][capacity];
完整示例代码:
#include
#include
#include
using namespace std;
int knapsack_2d(vector& weights, vector& values, int capacity) {
int n = weights.size();
vector> dp(n + 1, vector(capacity + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= capacity; j++) {
if (j >= weights[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i-1][j - weights[i-1]] + values[i-1]);
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][capacity];
}
int main() {
vector weights = {2, 3, 4, 5}; // 物品重量
vector values = {3, 4, 5, 6}; // 物品价值
int capacity = 8; // 背包容量
cout << "Maximum value (2D DP): "
<< knapsack_2d(weights, values, capacity) << endl;
// 输出: 10 (选物品1和物品3,重量3+5=8,价值4+6=10)
return 0;
}
完全背包(每种物品数量无限)
// 初始化(同0-1背包)
vector> dp(n+1, vector(capacity+1, 0));
// 递推公式
if (j >= weights[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i][j - weights[i-1]] + values[i-1]); // 关键区别
} else {
dp[i][j] = dp[i-1][j];
}
// 返回值
return dp[n][capacity];
完整示例代码:
#include
#include
#include
using namespace std;
int unboundedKnapsack_2d(vector& weights, vector& values, int capacity) {
int n = weights.size();
vector> dp(n + 1, vector(capacity + 1, 0));
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= capacity; j++) {
if (j >= weights[i-1]) {
dp[i][j] = max(dp[i-1][j], dp[i][j - weights[i-1]] + values[i-1]); // 关键区别
} else {
dp[i][j] = dp[i-1][j];
}
}
}
return dp[n][capacity];
}
多重背包(每种物品有一定数量)
// 初始化
vector dp(capacity+1, 0);
// 递推公式
dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i]);
// 返回值
return dp[capacity];
完整示例代码:
#include
#include
#include
using namespace std;
int multiKnapsack_plain(vector& weights, vector& values, vector& counts, int capacity) {
vector dp(capacity + 1, 0);
for (int i = 0; i < weights.size(); i++) {
for (int j = capacity; j >= weights[i]; j--) { // 逆序
for (int k = 1; k <= counts[i] && k * weights[i] <= j; k++) {
dp[j] = max(dp[j], dp[j - k * weights[i]] + k * values[i]);
}
}
}
return dp[capacity];
}
int main() {
vector weights = {1, 2, 3}; // 物品重量
vector values = {6, 10, 12}; // 物品价值
vector counts = {2, 3, 2}; // 物品数量限制
int capacity = 5; // 背包容量
cout << "Maximum value (plain): "
<< multiKnapsack_plain(weights, values, counts, capacity) << endl;
// 输出: 22 (选2个物品0和1个物品1: 2*1 + 1*2 ≤5, 2*6 + 1*10=22)
return 0;
}
2. 股票交易系列
问题类型:在股票买卖限制下获取最大利润
基础版(一次交易)
// 初始化
vector> dp(n, vector(2, 0));
dp[0][1] = -prices[0];
// 递推公式
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], -prices[i])
// 返回值
return dp[n-1][0];
完整示例代码:
int maxProfit_dp(vector& prices) {
int n = prices.size();
if (n == 0) return 0;
// dp[i][0]: 第i天不持有股票的最大利润
// dp[i][1]: 第i天持有股票的最大利润
vector> dp(n, vector(2, 0));
dp[0][1] = -prices[0]; // 第一天买入
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]); // 卖出或保持不持有
dp[i][1] = max(dp[i-1][1], -prices[i]); // 买入或保持持有(只能买一次)
}
return dp[n-1][0]; // 最后一天不持有股票
}
无限次交易
// 递推公式
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i])
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i])
// 初始化(同基础版)
// 返回值
return dp[n-1][0];
完整示例代码:
#include
#include
using namespace std;
int maxProfit(vector& prices) {
int n = prices.size();
if (n == 0) return 0;
// dp[i][0]: 第i天不持股;dp[i][1]: 第i天持股
vector> dp(n, vector(2));
// 初始化第一天
dp[0][0] = 0; // 不持股收益为0
dp[0][1] = -prices[0]; // 持股收益为负,因为买入花钱了
for (int i = 1; i < n; ++i) {
// 今天不持股 = max(昨天不持股, 昨天持股今天卖出)
dp[i][0] = max(dp[i-1][0], dp[i-1][1] + prices[i]);
// 今天持股 = max(昨天持股, 昨天不持股今天买入)
dp[i][1] = max(dp[i-1][1], dp[i-1][0] - prices[i]);
}
// 最后一天不持股时的利润最大
return dp[n-1][0];
}
限k次交易(属于难题,不用太管它)
一个完整的示例代码
#include
#include
#include
using namespace std;
int maxProfit(int k, vector& prices) {
int n = prices.size();
if (n == 0 || k == 0) return 0;
// 如果交易次数大于n/2,则相当于无限次交易
if (k >= n / 2) {
int profit = 0;
for (int i = 1; i < n; ++i)
if (prices[i] > prices[i - 1])
profit += prices[i] - prices[i - 1];
return profit;
}
// 定义三维DP数组:天数 × 最大交易次数 + 1 × 持股状态(0/1)
vector>> dp(n, vector>(k + 1, vector(2, 0)));
// 初始化:第0天持股状态
for (int j = 0; j <= k; ++j) {
dp[0][j][0] = 0;
dp[0][j][1] = -prices[0]; // 花钱买入
}
for (int i = 1; i < n; ++i) {
for (int j = 1; j <= k; ++j) {
// 不持股:昨天就不持股 or 昨天持股今天卖出
dp[i][j][0] = max(dp[i - 1][j][0], dp[i - 1][j][1] + prices[i]);
// 持股:昨天就持股 or 昨天没持股今天买入(消耗一次交易)
dp[i][j][1] = max(dp[i - 1][j][1], dp[i - 1][j - 1][0] - prices[i]);
}
}
return dp[n - 1][k][0]; // 最后一天不能持股
}
3. 双序列问题
问题类型:处理两个序列的匹配/比较问题
最长公共子序列(LCS)
// 初始化
vector> dp(m+1, vector(n+1, 0));
// 递推公式
if(s1[i-1] == s2[j-1])
dp[i][j] = dp[i-1][j-1] + 1
else
dp[i][j] = max(dp[i-1][j], dp[i][j-1])
// 返回值
return dp[m][n];
一个完整的示例代码:
#include
#include
#include
using namespace std;
int longestCommonSubsequence(string text1, string text2) {
int m = text1.length(), n = text2.length();
// dp[i][j] 表示 text1[0..i-1] 和 text2[0..j-1] 的 LCS 长度
vector> dp(m + 1, vector(n + 1, 0));
// 遍历所有字符
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (text1[i - 1] == text2[j - 1]) {
// 字符相同,LCS长度加1
dp[i][j] = dp[i - 1][j - 1] + 1;
} else {
// 字符不同,取前一个状态的最大值
dp[i][j] = max(dp[i - 1][j], dp[i][j - 1]);
}
}
}
return dp[m][n];
}
编辑距离
// 初始化
vector> dp(m+1, vector(n+1, 0));
for(int i=1; i<=m; i++) dp[i][0] = i;
for(int j=1; j<=n; j++) dp[0][j] = j;
// 递推公式
if(s1[i-1] == s2[j-1])
dp[i][j] = dp[i-1][j-1]
else
dp[i][j] = 1 + min(dp[i-1][j], dp[i][j-1], dp[i-1][j-1])
// 返回值
return dp[m][n];
一个完整的示例代码:
#include
#include
#include
using namespace std;
int minDistance(string word1, string word2) {
int m = word1.length(), n = word2.length();
// 创建DP表,表示 word1[0..i-1] 转为 word2[0..j-1] 的最少编辑操作
vector> dp(m + 1, vector(n + 1, 0));
// 初始化边界条件
for (int i = 0; i <= m; ++i) dp[i][0] = i; // 删除所有字符
for (int j = 0; j <= n; ++j) dp[0][j] = j; // 插入所有字符
// 填表
for (int i = 1; i <= m; ++i) {
for (int 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] = 1 + min({
dp[i - 1][j], // 删除
dp[i][j - 1], // 插入
dp[i - 1][j - 1] // 替换
});
}
}
}
return dp[m][n];
}
4. 矩阵路径问题
问题类型:在矩阵中寻找最优路径
最小路径和
// 初始化
vector> dp(m, vector(n, 0));
dp[0][0] = grid[0][0];
for(int i=1; i
一个完整的示例代码:
#include
#include
using namespace std;
int minPathSum(vector>& grid) {
int m = grid.size();
int n = grid[0].size();
// dp[i][j] 表示从 (0,0) 到 (i,j) 的最小路径和
vector> dp(m, vector(n, 0));
// 初始化起点
dp[0][0] = grid[0][0];
// 初始化第一列(只能从上往下走)
for (int i = 1; i < m; ++i)
dp[i][0] = dp[i - 1][0] + grid[i][0];
// 初始化第一行(只能从左往右走)
for (int j = 1; j < n; ++j)
dp[0][j] = dp[0][j - 1] + grid[0][j];
// 填充整个DP表
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
// 取从上方或左方来的最小路径和
dp[i][j] = grid[i][j] + min(dp[i - 1][j], dp[i][j - 1]);
}
}
// 返回终点的最小路径和
return dp[m - 1][n - 1];
}
不同路径总数(带障碍物)
// 初始化
vector> dp(m, vector(n, 0));
dp[0][0] = (grid[0][0] == 0) ? 1 : 0;
// 递推公式
if(grid[i][j] == 1)
dp[i][j] = 0
else
dp[i][j] = dp[i-1][j] + dp[i][j-1]
// 返回值
return dp[m-1][n-1];
一个完整的示例代码:
#include
using namespace std;
int uniquePathsWithObstacles(vector>& obstacleGrid) {
int m = obstacleGrid.size();
int n = obstacleGrid[0].size();
vector> dp(m, vector(n, 0));
// 初始化起点
if (obstacleGrid[0][0] == 1)
return 0;
dp[0][0] = 1;
// 初始化第一列
for (int i = 1; i < m; ++i)
dp[i][0] = (obstacleGrid[i][0] == 0 && dp[i - 1][0] == 1) ? 1 : 0;
// 初始化第一行
for (int j = 1; j < n; ++j)
dp[0][j] = (obstacleGrid[0][j] == 0 && dp[0][j - 1] == 1) ? 1 : 0;
// 填DP表
for (int i = 1; i < m; ++i) {
for (int j = 1; j < n; ++j) {
if (obstacleGrid[i][j] == 0) {
dp[i][j] = dp[i - 1][j] + dp[i][j - 1];
} else {
dp[i][j] = 0; // 当前为障碍物
}
}
}
return dp[m - 1][n - 1];
}
5. 其他经典问题
正则表达式匹配
// 初始化
vector> dp(m+1, vector(n+1, false));
dp[0][0] = true;
// 递推公式
if(p[j-1] == '*')
dp[i][j] = dp[i][j-2] || (s[i-1] == p[j-2] || p[j-2] == '.') && dp[i-1][j]
else
dp[i][j] = (s[i-1] == p[j-1] || p[j-1] == '.') && dp[i-1][j-1]
// 返回值
return dp[m][n];
完整示例代码:
#include
#include
using namespace std;
bool isMatch(string s, string p) {
int m = s.size();
int n = p.size();
// dp[i][j] 表示 s[0..i-1] 与 p[0..j-1] 是否匹配
vector> dp(m + 1, vector(n + 1, false));
dp[0][0] = true; // 空串与空串匹配
// 初始化:s 是空串时,p 能否匹配空串
for (int j = 2; j <= n; ++j) {
if (p[j - 1] == '*')
dp[0][j] = dp[0][j - 2];
}
// 填表
for (int i = 1; i <= m; ++i) {
for (int j = 1; j <= n; ++j) {
if (p[j - 1] == '*') {
// 匹配 0 次 p[j-2]
dp[i][j] = dp[i][j - 2];
// 匹配 >=1 次 p[j-2]
if (s[i - 1] == p[j - 2] || p[j - 2] == '.') {
dp[i][j] |= dp[i - 1][j];
}
} else {
// 直接匹配或通配符 '.'
if (s[i - 1] == p[j - 1] || p[j - 1] == '.') {
dp[i][j] = dp[i - 1][j - 1];
}
}
}
}
return dp[m][n];
}
石子游戏(博弈论)
说明:给定一个整数数组 piles
,表示一排石子堆。两个玩家轮流从两端取一堆石子,取到的石子数量会累加为分数。假设两人都采用最优策略,判断先手是否一定能赢。
// 初始化
vector> dp(n, vector(n, 0));
for(int i=0; i 0;
完整代码示例:
#include
#include
#include
using namespace std;
bool stoneGame(vector& piles) {
int n = piles.size();
// dp[i][j] 表示从 piles[i..j] 区间中,当前玩家可以获得的最大净胜分
vector> dp(n, vector(n, 0));
// 初始化:只有一个石子时,当前玩家只能拿这个石子
for (int i = 0; i < n; ++i) {
dp[i][i] = piles[i];
}
// 枚举区间长度
for (int len = 2; len <= n; ++len) {
for (int i = 0; i + len - 1 < n; ++i) {
int j = i + len - 1;
// 当前玩家选择左或右,减去对方的最优选择(因为轮到对方)
dp[i][j] = max(piles[i] - dp[i + 1][j],
piles[j] - dp[i][j - 1]);
}
}
// 如果最终净胜分 > 0,则先手必胜
return dp[0][n - 1] > 0;
}
石子游戏(区间DP):
说明:给你一个石子堆数组 stones
,每次可以选择两个相邻的石子堆合并成一个新堆,代价为这两堆石子的数量之和。合并后的新石子堆仍可以与相邻的堆继续合并。最终只剩一堆石子,求最小总合并代价。
// 初始化
vector> dp(n, vector(n, 0));
vector prefix(n + 1, 0); // 前缀和,方便计算区间总和
for (int i = 0; i < n; ++i) {
prefix[i + 1] = prefix[i] + stones[i];
}
// 递推公式
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + prefix[j + 1] - prefix[i]);
// 返回值
return dp[0][n - 1];
一个完整的示例代码:
#include
#include
using namespace std;
int stoneGame(vector& stones) {
int n = stones.size();
// 前缀和数组
vector prefix(n + 1, 0);
for (int i = 0; i < n; ++i)
prefix[i + 1] = prefix[i] + stones[i];
// 区间 DP 表
vector> dp(n, vector(n, 0));
// 区间长度从2开始枚举
for (int len = 2; len <= n; ++len) {
for (int i = 0; i + len - 1 < n; ++i) {
int j = i + len - 1;
dp[i][j] = INT_MAX;
for (int k = i; k < j; ++k) {
dp[i][j] = min(dp[i][j], dp[i][k] + dp[k + 1][j] + prefix[j + 1] - prefix[i]);
}
}
}
return dp[0][n - 1];
}