跳到主要内容C/C++ 动态规划入门:多状态 DP 实战(打家劫舍与股票买卖) | 极客日志C++算法
C/C++ 动态规划入门:多状态 DP 实战(打家劫舍与股票买卖)
深入解析 C/C++ 多状态动态规划,涵盖打家劫舍、粉刷房子及股票买卖系列问题。通过状态定义、转移方程推导及边界处理,详解线性 DP 中的互斥选择、环形数组策略及股票交易中的冷冻期、手续费与交易次数限制。结合具体代码示例,提供从基础到进阶的实战思路与优化技巧。
leon0 浏览 C/C++ 动态规划入门:多状态 DP 实战
引言
动态规划是算法中的核心难点之一。在掌握了基础的一维 DP 和路径问题后,本章将深入探讨多状态 DP。我们将通过经典的「打家劫舍」系列和「股票买卖」系列问题,由浅入深地学习如何定义多个状态、推导转移方程,并处理复杂的边界条件。
打家劫舍系列
1. 按摩师 (House Robber I)
题目描述:
一个专业的小偷,计划偷窃沿街的房屋。每间房内都藏有一定的现金,但是相邻的房屋装有相互连通的防盗系统,如果两间相邻的房屋在同一晚上被小偷闯入,系统会自动报警。给定一个代表每个房屋存放金额的非负整数数组,计算在不触动警报装置的情况下,能够偷窃到的最高金额。
思路分析:
这是一个典型的线性 DP 问题。对于每个位置 i,我们面临两种选择:偷或不偷。
- 若偷
i,则不能偷 i-1。
- 若不偷
i,则最大收益取决于 i-1 的状态。
我们需要维护两个状态数组:
f[i]:选择第 i 个位置时的最大价值。
g[i]:不选择第 i 个位置时的最大价值。
状态转移方程:
f[i] = g[i-1] + nums[i] (选了当前,前一个必须不选)
g[i] = max(f[i-1], g[i-1]) (没选当前,前一个选或不选取最大值)
代码实现:
class Solution {
public:
int massage(vector<int>& nums) {
int n = nums.size();
if (n == 0) return 0;
vector<int> f(n, 0);
vector<int> g(n, 0);
f[0] = nums[0];
g[] = ;
( i = ; i < n; i++) {
f[i] = g[i - ] + nums[i];
g[i] = (f[i - ], g[i - ]);
}
(f[n - ], g[n - ]);
}
};
0
0
for
int
1
1
max
1
1
return
max
1
1
*注:实际开发中可使用虚拟节点简化初始化逻辑,避免单独处理 n=0 或索引越界的情况。
2. 打家劫舍 II (House Robber II)
题目描述:
房屋围成一圈,第一个和最后一个房屋相邻。
思路分析:
由于首尾相连,不能同时偷第一家和最后一家。我们可以将其拆分为两种情况讨论:
- 偷第一家:则不能偷最后一家。范围是
[0, n-2]。
- 不偷第一家:可以偷最后一家。范围是
[1, n-1]。
最终结果是这两种情况的最大值。
class Solution {
public:
int rob(vector<int>& nums) {
int n = nums.size();
if (n == 1) return nums[0];
int res1 = robRange(nums, 0, n - 2);
int res2 = robRange(nums, 1, n - 1);
return max(res1, res2);
}
private:
int robRange(const vector<int>& nums, int start, int end) {
if (start > end) return 0;
int n = nums.size();
vector<int> f(n, 0);
vector<int> g(n, 0);
f[start] = nums[start];
g[start] = 0;
for (int i = start + 1; i <= end; i++) {
f[i] = g[i - 1] + nums[i];
g[i] = max(f[i - 1], g[i - 1]);
}
return max(f[end], g[end]);
}
};
3. 删除并获得点数 (Delete and Earn)
题目描述:
你可以选择任意数字 x,获得 x * count(x) 的分数,但必须删除所有 x-1 和 x+1。
思路分析:
这本质上是一个打家劫舍问题的变体。如果我们选择了数值 x,就不能选择 x-1 和 x+1。我们可以先统计每个数值的总和,构建一个新的数组,然后在这个新数组上应用打家劫舍的逻辑。
class Solution {
public:
int deleteAndEarn(vector<int>& nums) {
if (nums.empty()) return 0;
int maxVal = 0;
for (int x : nums) maxVal = max(maxVal, x);
vector<int> arr(maxVal + 1, 0);
for (int x : nums) arr[x] += x;
int n = arr.size();
vector<int> f(n, 0);
vector<int> g(n, 0);
f[0] = arr[0];
g[0] = 0;
for (int i = 1; i < n; i++) {
f[i] = g[i - 1] + arr[i];
g[i] = max(f[i - 1], g[i - 1]);
}
return max(f[n - 1], g[n - 1]);
}
};
4. 粉刷房子 (Paint House)
题目描述:
有 n 栋房子,每栋房子可以用 k 种颜色粉刷,不同颜色的成本不同。要求相邻房子颜色不同,求最小成本。
思路分析:
这是一个二维 DP 问题。状态不仅包含房子下标 i,还包含当前选择的颜色 j。
dp[i][j]:粉刷到第 i 栋房子且使用颜色 j 的最小花费。
- 转移时,需从上一栋房子的其他颜色中选择最小值。
class Solution {
public:
int minCost(vector<vector<int>>& costs) {
int n = costs.size();
if (n == 0) return 0;
vector<vector<int>> dp(n, vector<int>(3));
dp[0][0] = costs[0][0];
dp[0][1] = costs[0][1];
dp[0][2] = costs[0][2];
for (int i = 1; i < n; i++) {
dp[i][0] = costs[i][0] + min(dp[i - 1][1], dp[i - 1][2]);
dp[i][1] = costs[i][1] + min(dp[i - 1][0], dp[i - 1][2]);
dp[i][2] = costs[i][2] + min(dp[i - 1][0], dp[i - 1][1]);
}
return min({dp[n - 1][0], dp[n - 1][1], dp[n - 1][2]});
}
};
股票买卖系列
股票问题是多状态 DP 的经典应用场景,通常涉及买入、卖出、冷冻期、手续费等约束。
1. 含冷冻期 (Best Time to Buy and Sell Stock with Cooldown)
dp[i][0]:第 i 天持有股票的最大利润。
dp[i][1]:第 i 天不持有股票,处于冷冻期的最大利润。
dp[i][2]:第 i 天不持有股票,不处于冷冻期的最大利润。
dp[i][0] = max(dp[i-1][0], dp[i-1][2] - price[i]) (保持持有 或 从非冷冻期买入)
dp[i][1] = dp[i-1][0] + price[i] (今天卖出,进入冷冻期)
dp[i][2] = max(dp[i-1][1], dp[i-1][2]) (保持非冷冻期 或 从冷冻期恢复)
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0;
vector<vector<int>> dp(n, vector<int>(3));
dp[0][0] = -prices[0];
dp[0][1] = 0;
dp[0][2] = 0;
for (int i = 1; i < n; i++) {
dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
dp[i][1] = dp[i - 1][0] + prices[i];
dp[i][2] = max(dp[i - 1][1], dp[i - 1][2]);
}
return max(dp[n - 1][1], dp[n - 1][2]);
}
};
2. 含手续费 (Best Time to Buy and Sell Stock with Transaction Fee)
思路:
每次交易收取固定费用 fee。可以在买入或卖出时扣除,这里选择在卖出时扣除。
f[i]:第 i 天持有股票的最大利润。
g[i]:第 i 天不持有股票的最大利润。
f[i] = max(f[i-1], g[i-1] - price[i])
g[i] = max(g[i-1], f[i-1] + price[i] - fee)
class Solution {
public:
int maxProfit(vector<int>& prices, int fee) {
int n = prices.size();
vector<int> f(n), g(n);
f[0] = -prices[0];
g[0] = 0;
for (int i = 1; i < n; i++) {
f[i] = max(f[i - 1], g[i - 1] - prices[i]);
g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee);
}
return g[n - 1];
}
};
3. 最多两笔交易 (Best Time to Buy and Sell Stock III)
思路:
限制交易次数为 2 次。需要增加一个维度记录交易次数。
f[i][j]:第 i 天完成 j 次交易后,处于持有状态的最大利润。
g[i][j]:第 i 天完成 j 次交易后,处于不持有状态的最大利润。
注意: 交易次数 j 在卖出时增加。初始化为负无穷以表示非法状态。
class Solution {
public:
int maxProfit(vector<int>& prices) {
int n = prices.size();
if (n == 0) return 0;
const int INF = 0x3F3F3F3F;
vector<vector<int>> f(n + 1, vector<int>(3, -INF));
vector<vector<int>> g(n + 1, vector<int>(3, -INF));
g[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= 2; j++) {
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i - 1]);
g[i][j] = g[i - 1][j];
if (j >= 1) {
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i - 1]);
}
}
}
int res = 0;
for (int j = 0; j <= 2; j++) {
res = max(res, g[n][j]);
}
return res;
}
};
4. 最多 k 笔交易 (Best Time to Buy and Sell Stock IV)
思路:
这是第三题的通用版本,只需将交易次数限制从 2 改为 k。
class Solution {
public:
int maxProfit(int k, vector<int>& prices) {
int n = prices.size();
if (n == 0 || k == 0) return 0;
const int INF = 0x3F3F3F3F;
vector<vector<int>> f(n + 1, vector<int>(k + 1, -INF));
vector<vector<int>> g(n + 1, vector<int>(k + 1, -INF));
g[0][0] = 0;
for (int i = 1; i <= n; i++) {
for (int j = 0; j <= k; j++) {
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i - 1]);
g[i][j] = g[i - 1][j];
if (j >= 1) {
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i - 1]);
}
}
}
int res = 0;
for (int j = 0; j <= k; j++) {
res = max(res, g[n][j]);
}
return res;
}
};
总结
通过以上案例,我们可以总结出多状态 DP 的核心要点:
- 状态拆分:当单一状态无法表达决策结果时(如选/不选、买/卖),引入多维数组或并行数组。
- 互斥关系:如打家劫舍中相邻不可选,股票中买入前需卖出,明确状态间的依赖与互斥。
- 边界处理:注意初始化(如负无穷表示不可达)、数组越界及空输入情况。
- 降维优化:当状态过多时,可尝试将部分状态分离成独立数组,降低空间复杂度或提升可读性。
掌握这些模式后,面对复杂的 DP 问题,关键在于准确拆解状态,理清转移逻辑。
相关免费在线工具
- 加密/解密文本
使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online
- Gemini 图片去水印
基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
- Markdown转HTML
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
- HTML转Markdown
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online