
🔥承渊政道: 个人主页
❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》
✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介:

在算法学习的过程中,动态规划(Dynamic Programming,简称DP)几乎是每个学习者都会遇到的一道"分水岭".它不像排序或搜索那样直观,却在解决复杂问题时展现出极强的威力.尤其是在面对最优化问题、计数问题以及具有重叠子问题结构的场景时,动态规划往往能够将看似复杂的问题转化为一系列有规律的子问题,从而高效求解.对于初学者来说,动态规划最大的难点并不在于代码实现,而在于如何建立"状态"的概念以及如何设计状态之间的转移关系.很多人在刚接触DP时,常常会感到无从下手:不知道如何定义dp数组,不清楚状态转移方程如何推导,也难以判断一个问题是否适合用动态规划来解决.而在这些困难中,"多状态DP"又是一个进一步提升难度的重要阶段,它要求我们在建模时考虑多个维度的信息,使问题更加贴近实际,但也更具挑战性.本文从基础入手,逐步引导读者理解什么是多状态动态规划,以及它与单状态DP的区别.我们将通过典型例题,详细拆解状态设计思路、状态转移过程以及边界条件的处理方法,帮助你建立起清晰的解题框架.在学习过程中,你不仅能够掌握如何"写出"一个动态规划解法,更重要的是学会如何"想到"动态规划------如何从问题特征中识别出DP的适用性,如何将复杂问题拆解为可递推的子结构,以及如何优化空间和时间复杂度.通过对经典题型的深入分析,你将逐渐形成对多状态DP的直觉和经验.如果你已经掌握了基础的动态规划知识,但在面对稍微复杂一些的问题时仍感到吃力,那么这篇文章将帮助你跨过这一关键门槛.接下来,让我们一起从简单入门,逐步深入多状态DP的核心思想与实战技巧.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!
目录
- 1.简单多状态dp问题背景介绍
- 2.按摩师(OJ题)
- 3.打家劫舍||(OJ题)
- 4.删除并获得点数(OJ题)
- 5.粉刷房子(OJ题)
- 6.买卖股票的最佳时机含冷冻期(OJ题)
- 7.买卖股票的最佳时机含手续费(OJ题)
- 8.买卖股票的最佳时机|||(OJ题)
- 9.买卖股票的最佳时机IV(OJ题)
1.简单多状态dp问题背景介绍
在正式学习"多状态动态规划"之前,有必要先了解这类问题通常出现的背景,以及为什么我们需要引入"多状态"这一概念.在基础的动态规划问题中,我们往往只需要用一个维度来描述状态,例如 dp[i] 表示"前i个元素的最优解"或"到达第i个位置的方案数".这类问题结构相对简单,状态转移也比较直观,比如经典的斐波那契数列、爬楼梯问题等,都属于单状态DP的范畴.然而,随着问题复杂度的提升,单一维度的状态往往不足以完整描述问题的'当前情况".例如,在一些场景中,我们不仅需要知道"走到了第几个位置",还需要知道"当前处于什么状态"(如是否持有某种资源、是否满足某种条件、是否已经执行过某个操作等).这时,如果仍然只用一个维度来建模,就会丢失关键信息,导致无法正确进行状态转移.这类问题的典型特征是:每一个阶段的决策,不仅依赖于位置或数量,还依赖于额外的状态信息 .因此,我们需要引入"多状态"的概念,将状态扩展为二维甚至多维,例如 dp[i][j]、dp[i][j][k] 等,用来同时刻画多个维度的信息.因此,多状态动态规划的本质,就是在单状态DP的基础上,引入更多维度来完整描述问题,使得每一步的决策都有充分的信息支撑,从而能够建立正确的状态转移关系.
好的,我帮你整理成一个更清晰、有层次、适合写在文章中的背景介绍👇
1.为什么会出现多状态 DP?
在动态规划的学习过程中,我们通常是从"单状态DP"入门的.例如:
dp[i]:表示前i个元素的最优解dp[i]:表示到达第i个位置的方法数
这类问题的共同特点是:状态只依赖一个维度(通常是"位置"或"数量") ,问题结构简单、转移清晰.
但是,在实际问题中,我们很快会遇到这样的情况:
👉 仅用一个状态,无法完整描述当前局面
这时,如果强行用 dp[i] 表示,就会出现:
- 信息丢失
- 无法转移
- 或逻辑混乱
于是,就自然引出了------多状态 DP
2.多状态问题的核心特征
一个问题需要使用多状态DP,通常具备以下几个特征:
1️⃣决策依赖"多个因素"
不仅依赖"当前位置",还依赖额外信息,例如:
- 是否持有某个物品
- 是否已经执行某个操作
- 当前处于哪种状态(开/关、选/不选)
👉本质:状态不止一个维度
2️⃣单一状态无法表达完整信息
如果只用 dp[i],会出现:
- 不同情况被混在一起
- 无法区分路径
- 状态转移不成立
例如:到第 i 天赚的钱是多少?
如果不知道:
- 当前是否持有股票
👉 就无法决定"卖还是不卖"
3️⃣状态之间存在"切换关系"
多状态问题往往包含:
- 状态 A → 状态 B
- 状态 B → 状态 A
- 状态保持不变
👉 也就是说:状态不仅是记录,还会发生变化
3.什么是"多状态"?
多状态的本质就是:
👉 在原有"位置维度"基础上,引入额外维度描述状态
常见形式:
1️⃣ 二维状态
dp[i][j]
含义:
- i:阶段(时间/位置)
- j:状态(某种条件)
2️⃣三维及以上状态
dp[i][j][k]
用于更复杂场景,例如:
- 限制操作次数
- 多种资源约束
4.常见的"状态维度类型"
在多状态DP 中,"状态"通常来自以下几类:
1️⃣ 二选一状态(最常见)
例如:
- 是否持有股票(0 / 1)
- 是否选择当前元素
- 是否使用某种能力
👉 特点:简单但非常关键
2️⃣次数型状态
例如:
- 已经交易了多少次
- 使用技能的次数
👉 常见形式:
dp[i][k]
3️⃣资源/条件状态
例如:
- 剩余容量(背包问题)
- 剩余能量
- 当前金币数
4️⃣方向/阶段状态
例如:
- 当前是上升还是下降
- 是否在冷却期(股票冷冻期问题)
5.本质总结(非常重要)
多状态 DP 的核心可以总结为一句话:
✅ 当一个维度不足以描述问题时,就增加状态维度
或者更直白一点:
👉 "多状态"不是技巧,而是对问题信息的补充表达
6.一个直观理解(帮助建立感觉)
可以把 DP 想象成:
👤"一个人在做决策"
-
单状态 DP:只知道"你走到第几步"
-
多状态 DP:不仅知道步数,还知道:
- 你手里有没有东西
- 你有没有用过技能
- 你现在处于什么状态
👉 信息越完整,决策才越正确
2.按摩师(OJ题)

算法思路:解法(动态规划):
1.状态表示:
对于简单的线性 dp,我们可以用经验 + 题目要求来定义状态表示:
i. 以某个位置为结尾...
ii. 以某个位置为起点...
这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:
dp[i] 表示:选择到 i 位置时,此时的最长预约时长.
但是我们这个题在 i 位置的时候,会面临选择或者不选择两种抉择,所依赖的状态需要细分:
f[i]表示:选择到i位置时,nums[i]必选,此时的最长预约时长;g[i]表示:选择到i位置时,nums[i]不选,此时的最长预约时长.
2.状态转移方程:
因为状态表示定义了两个,因此我们的状态转移方程也要分析两个:
-
对于
f[i]:如果
nums[i]必选,那么我们仅需知道i - 1位置在不选 的情况下的最长预约时长,然后加上nums[i]即可,因此:
f[i] = g[i - 1] + nums[i] -
对于
g[i]:如果
nums[i]不选,那么i - 1位置上选或者不选都可以 .因此,我们需要知道i - 1位置上选或者不选两种情况下的最长时长,因此:
g[i] = max(f[i - 1], g[i - 1])
3.初始化:
这道题的初始化比较简单,因此无需加辅助节点,仅需初始化:
f[0] = nums[0],g[0] = 0 即可.
4.填表顺序
根据状态转移方程得:从左往右,两个表一起填.
5.返回值
根据状态表示,应该返回 max(f[n - 1], g[n - 1]).

核心代码
cpp
class Solution {
public:
//按摩师问题:不能接受连续的预约,求最长总预约时长
int massage(vector<int>& nums) {
//1.创建一个 dp 表
//2.初始化
//3.填表
//4.返回值
// 获取数组长度
int n = nums.size();
// 边界条件:空数组,直接返回0
if(n == 0) return 0;
//定义两个动态规划数组:
//f[i]:选择第i个预约,此时的最长总时长
//g[i]:不选择第i个预约,此时的最长总时长
vector<int> f(n);
//拷贝初始化g数组,让g和f大小一致
auto g = f;
//初始化:第一个位置
//选第一个元素,时长就是nums[0]
f[0] = nums[0];
//不选第一个元素,时长为0(数组初始化默认0,可省略)
//g[0] = 0;
//从第二个元素开始,从左往右填表
for(int i = 1; i < n; i++)
{
//状态转移1:选第i个元素 → 前一个必须不选
f[i] = g[i - 1] + nums[i];
//状态转移2:不选第i个元素 → 前一个可选/可不选,取最大值
g[i] = max(f[i - 1], g[i - 1]);
}
//最终结果:最后一个位置选或不选的最大值
return max(f[n - 1], g[n - 1]);
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
//按摩师问题:不能接受连续预约,返回最长总预约时长
int massage(vector<int>& nums) {
// 获取数组长度
int n = nums.size();
// 边界条件:空数组,直接返回0
if(n == 0) return 0;
// 定义两个动态规划数组
// f[i]:选择第i个预约,此时的最长总时长
// g[i]:不选择第i个预约,此时的最长总时长
vector<int> f(n);
// 初始化g数组,大小与f一致
auto g = f;
// 初始化第一个位置
f[0] = nums[0]; // 选第一个元素,时长为nums[0]
// g[0] = 0; 数组默认初始化0,可省略
// 从左往右遍历填表
for(int i = 1; i < n; i++)
{
// 选第i个元素 → 前一个必须不选
f[i] = g[i - 1] + nums[i];
// 不选第i个元素 → 前一个选/不选取最大值
g[i] = max(f[i - 1], g[i - 1]);
}
// 最终结果:最后一个位置两种情况的最大值
return max(f[n - 1], g[n - 1]);
}
};
void testMassage(vector<int>& nums, const string& caseName) {
Solution sol;
int res = sol.massage(nums);
cout << "测试用例:" << caseName << endl;
cout << "输入数组:";
for (int num : nums) {
cout << num << " ";
}
cout << "\n最长预约时长:" << res << "\n-------------------------" << endl;
}
int main() {
//测试用例1:空数组
vector<int> case1;
testMassage(case1, "空数组");
//测试用例2:单个元素
vector<int> case2 = {1};
testMassage(case2, "单个元素");
//测试用例3:两个元素
vector<int> case3 = {2, 1};
testMassage(case3, "两个元素");
//测试用例4:经典场景 [3,2,7,10] → 最优解 3+10=13
vector<int> case4 = {3,2,7,10};
testMassage(case4, "经典场景1");
//测试用例5:经典场景 [2,7,9,3,1] → 最优解 2+9+1=12
vector<int> case5 = {2,7,9,3,1};
testMassage(case5, "经典场景2");
return 0;
}

3.打家劫舍||(OJ题)

算法思路:解法(动态规划)
这一个问题是打家劫舍I问题的变形.
上一个问题是一个单排的模式,这一个问题是一个环形的模式,也就是首尾是相连的.但是我们可以将环形问题转化为两个单排问题:
a. 偷第一个房屋时的最大金额(x),此时不能偷最后一个房子,因此就是偷 ([0, n - 2]) 区间的房子;
b. 不偷第一个房屋时的最大金额(y),此时可以偷最后一个房子,因此就是偷([1, n - 1])区间的房子;
两种情况下的最大值,就是最终的结果.
因此,问题就转化成求两次单排结果的最大值.

核心代码
cpp
class Solution
{
public:
//打家劫舍II:环形房屋(首尾相连,不能同时偷)
int rob(vector<int>& nums){
int n = nums.size();
// 边界:只有1间房,直接偷
if(n == 1) return nums[0];
//核心:环形问题拆分为两个单排问题,取最大值
//情况1:偷第一个房屋 → 不能偷最后一个 → 计算区间 [2, n-2] + nums[0]
//情况2:不偷第一个房屋 → 可以偷最后一个 → 计算区间 [1, n-1]
return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
}
//单排房屋打家劫舍通用解法:计算区间 [left, right] 的最大金额
//f[i]:必选i位置,最大金额
//g[i]:不选i位置,最大金额
int rob1(vector<int>& nums, int left, int right){
//区间无效(无房屋可偷),返回0
if(left > right) return 0;
//1.创建dp表
int n = nums.size();
vector<int> f(n); //必选当前位置
auto g = f; //不选当前位置(大小和f一致)
//2.初始化:区间起始位置left
f[left] = nums[left];
//3.填表:从left+1遍历到right
for(int i = left + 1; i <= right; i++)
{
//选i:前一个必须不选 → g[i-1] + 当前金额
f[i] = g[i - 1] + nums[i];
//不选i:前一个可选/可不选 → 取最大值
g[i] = max(f[i - 1], g[i - 1]);
}
//4.返回结果:区间最后一个位置,选/不选的最大值
return max(f[right], g[right]);
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
int rob(vector<int>& nums){
int n = nums.size();
// 边界:只有1间房,直接偷
if(n == 1) return nums[0];
//核心:环形问题拆分为两个单排问题,取最大值
//情况1:偷第一个房屋 → 不能偷最后一个 → 计算区间 [2, n-2] + nums[0]
//情况2:不偷第一个房屋 → 可以偷最后一个 → 计算区间 [1, n-1]
return max(nums[0] + rob1(nums, 2, n - 2), rob1(nums, 1, n - 1));
}
//单排房屋打家劫舍通用解法:计算区间 [left, right] 的最大金额
//f[i]:必选i位置,最大金额
//g[i]:不选i位置,最大金额
int rob1(vector<int>& nums, int left, int right){
//区间无效(无房屋可偷),返回0
if(left > right) return 0;
//1.创建dp表
int n = nums.size();
vector<int> f(n); //必选当前位置
auto g = f; //不选当前位置(大小和f一致)
//2.初始化:区间起始位置left
f[left] = nums[left];
//3.填表:从left+1遍历到right
for(int i = left + 1; i <= right; i++)
{
//选i:前一个必须不选 → g[i-1] + 当前金额
f[i] = g[i - 1] + nums[i];
//不选i:前一个可选/可不选 → 取最大值
g[i] = max(f[i - 1], g[i - 1]);
}
//4.返回结果:区间最后一个位置,选/不选的最大值
return max(f[right], g[right]);
}
};
void testRob(vector<int>& nums, const string& caseName) {
Solution sol;
int res = sol.rob(nums);
cout << "测试用例:" << caseName << endl;
cout << "房屋金额:";
for (int num : nums) {
cout << num << " ";
}
cout << "\n最大可偷金额:" << res << "\n-------------------------" << endl;
}
int main() {
//用例1:只有1间房屋(边界)
vector<int> case1 = {5};
testRob(case1, "1间房屋");
//用例2:2间房屋(首尾相连,只能偷一个)
vector<int> case2 = {2, 3};
testRob(case2, "2间房屋");
//用例3:经典环形 [2,3,2] → 不能偷首尾,结果3
vector<int> case3 = {2,3,2};
testRob(case3, "经典场景1 [2,3,2]");
//用例4:经典环形 [1,2,3,1] → 偷1+3=4
vector<int> case4 = {1,2,3,1};
testRob(case4, "经典场景2 [1,2,3,1]");
//用例5:全相同金额 [5,5,5]
vector<int> case5 = {5,5,5};
testRob(case5, "全相同金额");
return 0;
}

4.删除并获得点数(OJ题)

算法思路:解法(动态规划):
其实这道题依旧是打家劫舍I问题的变型.
我们注意到题目描述,选择(x)数字的时候,(x - 1)与(x + 1)是不能被选择的.像不像打家劫舍问题中,选择 (i)位置的金额之后,就不能选择(i - 1)位置以及(i + 1)位置的金额呢~
因此,我们可以创建一个大小为10001(根据题目的数据范围)的hash数组,将nums数组中每一个元素(x),累加到 hash 数组下标为(x)的位置处,然后在hash数组上来一次打家劫舍即可.

核心代码
cpp
class Solution {
public:
//删除并获得点数:选择数字x后,删除x-1和x+1,求最大获得点数
//核心转化:等价于【打家劫舍】问题(不能选相邻数字)
int deleteAndEarn(vector<int>& nums) {
//题目规定nums中元素范围是 [1, 10000],定义数组大小为10001
const int N = 10001;
//1.预处理:统计每个数字对应的总点数
// arr[x] 表示选择数字x能获得的总点数(所有x相加)
int arr[N] = { 0 };
for(auto x : nums)
arr[x] += x; // 累加相同数字的总点数
//2.在 arr 数组上,做一次【打家劫舍】动态规划
//状态定义:
//f[i]:选择 i 这个数字,能获得的最大点数
//g[i]:不选择 i 这个数字,能获得的最大点数
vector<int> f(N);
auto g = f; //初始化g数组,大小与f一致
//3. 填表:从1遍历到10000
for(int i = 1; i < N; i++)
{
//选i:则不能选i-1 → 最大点数 = 不选i-1的最大值 + 当前数字总点数
f[i] = g[i - 1] + arr[i];
//不选i:则i-1可选可不选 → 取两者最大值
g[i] = max(f[i - 1], g[i - 1]);
}
//4.返回结果:最后一个位置(10000)选或不选的最大值
return max(f[N - 1], g[N - 1]);
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
//删除并获得点数:选择数字x后,删除x-1和x+1,求最大获得点数
//核心转化:等价于【打家劫舍】问题(不能选相邻数字)
int deleteAndEarn(vector<int>& nums) {
//题目规定nums中元素范围是 [1, 10000],定义数组大小为10001
const int N = 10001;
//1.预处理:统计每个数字对应的总点数
//arr[x] 表示选择数字x能获得的总点数(所有x相加)
int arr[N] = { 0 };
for(auto x : nums)
arr[x] += x; // 累加相同数字的总点数
//2.在 arr 数组上,做一次【打家劫舍】动态规划
//状态定义:
//f[i]:选择 i 这个数字,能获得的最大点数
//g[i]:不选择 i 这个数字,能获得的最大点数
vector<int> f(N);
auto g = f; //初始化g数组,大小与f一致
//3.填表:从1遍历到10000
for(int i = 1; i < N; i++)
{
//选i:则不能选i-1 → 最大点数 = 不选i-1的最大值 + 当前数字总点数
f[i] = g[i - 1] + arr[i];
//不选i:则i-1可选可不选 → 取两者最大值
g[i] = max(f[i - 1], g[i - 1]);
}
//4.返回结果:最后一个位置(10000)选或不选的最大值
return max(f[N - 1], g[N - 1]);
}
};
void testDeleteAndEarn(vector<int>& nums, const string& caseName) {
Solution sol;
int res = sol.deleteAndEarn(nums);
cout << "测试用例:" << caseName << endl;
cout << "输入数组:";
for (int num : nums) {
cout << num << " ";
}
cout << "\n最大获得点数:" << res << "\n-------------------------" << endl;
}
int main() {
//用例1:空数组(边界)
vector<int> case1;
testDeleteAndEarn(case1, "空数组");
//用例2:单个元素
vector<int> case2 = {5};
testDeleteAndEarn(case2, "单个元素");
//用例3:经典用例 [3,4,2] → 选3+2=5 或 选4 → 最大4
vector<int> case3 = {3,4,2};
testDeleteAndEarn(case3, "经典场景1 [3,4,2]");
//用例4:经典用例 [2,2,3,3,3,4] → 选3个3=9
vector<int> case4 = {2,2,3,3,3,4};
testDeleteAndEarn(case4, "经典场景2 [2,2,3,3,3,4]");
//用例5:复杂场景 [1,1,1,2,4,5] → 1+1+1 +4=7
vector<int> case5 = {1,1,1,2,4,5};
testDeleteAndEarn(case5, "复杂场景 [1,1,1,2,4,5]");
return 0;
}

5.粉刷房子(OJ题)

算法思路:解法(动态规划):
1.状态表示:
对于线性 dp,我们可以用经验 + 题目要求来定义状态表示:
i. 以某个位置为结尾...
ii. 以某个位置为起点...
这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:
但是我们这个题在 i 位置的时候,会面临红蓝绿三种抉择,所依赖的状态需要细分:
dp[i][0]表示:粉刷到i位置的时候,最后一个位置粉刷上红色,此时的最小花费;dp[i][1]表示:粉刷到i位置的时候,最后一个位置粉刷上蓝色,此时的最小花费;dp[i][2]表示:粉刷到i位置的时候,最后一个位置粉刷上绿色,此时的最小花费.
2.状态转移方程:
因为状态表示定义了三个,因此我们的状态转移方程也要分析三个:
对于 dp[i][0]:
- 如果第
i个位置粉刷上红色,那么i-1位置上可以是蓝色或者绿色.因此我们需要知道粉刷到i-1位置上的时候,粉刷上蓝色或者绿色的最小花费,然后加上i位置的花费即可.于是状态转移方程为:
dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
同理,我们可以推导出另外两个状态转移方程为:
dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
dp[i][2] = min(dp[i - 1][0], dp[i - 1][1]) + costs[i - 1][2];
3.初始化:
可以在最前面加上一个辅助结点,帮助我们初始化.使用这种技巧要注意两个点:
i. 辅助结点里面的值要保证后续填表是正确的;
ii.下标的映射关系.
在本题中,添加一个节点,并且初始化为0即可.
4.填表顺序
根据状态转移方程得从左往右,三个表一起填.
5.返回值
根据状态表示,应该返回最后一个位置粉刷上三种颜色情况下的最小值,因此需要返回:
min(dp[n][0], min(dp[n][1], dp[n][2]))。

核心代码
cpp
class Solution {
public:
//粉刷房子问题:
//有n个房子,3种颜色,相邻房子颜色不能相同
//costs[i][j]:第i个房子刷第j种颜色的花费,求最小总花费
int minCost(vector<vector<int>>& costs) {
//房子的总数
int n = costs.size();
//1.创建dp表
//dp[i][j]:表示【第i个房子】刷成【第j种颜色】时,前i个房子的最小总花费
//j=0/1/2 代表三种不同颜色
//dp[0][...] 为初始状态(0个房子,花费为0)
vector<vector<int>> dp(n + 1, vector<int>(3));
//2.填表:从第1个房子遍历到第n个房子
for (int i = 1; i <= n; i++) {
//第i个房子刷颜色0 → 第i-1个房子只能刷颜色1或2,取最小值 + 当前花费
dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
//第i个房子刷颜色1 → 第i-1个房子只能刷颜色0或2,取最小值 + 当前花费
dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
//第i个房子刷颜色2 → 第i-1个房子只能刷颜色0或1,取最小值 + 当前花费
dp[i][2] = min(dp[i - 1][1], dp[i - 1][0]) + costs[i - 1][2];
}
//3.返回结果:最后一个房子(第n个)刷三种颜色的最小花费
return min(dp[n][0], min(dp[n][1], dp[n][2]));
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution {
public:
// 粉刷房子问题:
// 有n个房子,3种颜色,相邻房子颜色不能相同
// costs[i][j]:第i个房子刷第j种颜色的花费,求最小总花费
int minCost(vector<vector<int>>& costs) {
// 房子的总数
int n = costs.size();
// 1. 创建dp表
// dp[i][j]:表示【第i个房子】刷成【第j种颜色】时,前i个房子的最小总花费
// j=0/1/2 代表三种不同颜色
// dp[0][...] 为初始状态(0个房子,花费为0)
vector<vector<int>> dp(n + 1, vector<int>(3));
// 2. 填表:从第1个房子遍历到第n个房子
for (int i = 1; i <= n; i++) {
// 第i个房子刷颜色0 → 第i-1个房子只能刷颜色1或2,取最小值 + 当前花费
dp[i][0] = min(dp[i - 1][1], dp[i - 1][2]) + costs[i - 1][0];
// 第i个房子刷颜色1 → 第i-1个房子只能刷颜色0或2,取最小值 + 当前花费
dp[i][1] = min(dp[i - 1][0], dp[i - 1][2]) + costs[i - 1][1];
// 第i个房子刷颜色2 → 第i-1个房子只能刷颜色0或1,取最小值 + 当前花费
dp[i][2] = min(dp[i - 1][1], dp[i - 1][0]) + costs[i - 1][2];
}
// 3. 返回结果:最后一个房子(第n个)刷三种颜色的最小花费
return min(dp[n][0], min(dp[n][1], dp[n][2]));
}
};
void testMinCost(vector<vector<int>>& costs, const string& caseName) {
Solution sol;
int res = sol.minCost(costs);
cout << "测试用例:" << caseName << endl;
cout << "房子粉刷花费:" << endl;
for (auto& row : costs) {
for (int num : row) {
cout << num << " ";
}
cout << endl;
}
cout << "最小总花费:" << res << "\n-------------------------" << endl;
}
int main() {
// 用例1:空数组(无房子,边界)
vector<vector<int>> case1;
testMinCost(case1, "空数组(无房子)");
// 用例2:单个房子
vector<vector<int>> case2 = {{17, 2, 17}};
testMinCost(case2, "单个房子");
// 用例3:官方经典用例 → 最小花费 2+6=8
vector<vector<int>> case3 = {{17,2,17},{6,33,1}};
testMinCost(case3, "经典场景1");
// 用例4:三个房子测试用例
vector<vector<int>> case4 = {{7,6,2},{3,9,1},{4,5,3}};
testMinCost(case4, "三个房子场景");
return 0;
}

6.买卖股票的最佳时机含冷冻期(OJ题)

算法思路:解法(动态规划):
1.状态表示:
对于线性dp,我们可以用经验 + 题目要求来定义状态表示:
i. 以某个位置为结尾...
ii. 以某个位置为起点...
这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:
由于有买入可交易冷冻期三个状态,因此我们可以选择用三个数组,其中:
dp[i][0]表示:第i天结束后,处于买入状态,此时的最大利润;dp[i][1]表示:第i天结束后,处于可交易状态,此时的最大利润;dp[i][2]表示:第i天结束后,处于冷冻期状态,此时的最大利润.
2.状态转移方程:
我们要谨记规则:
i. 处于买入状态的时候,我们现在有股票,此时不能买股票,只能继续持有股票,或者卖出股票;
ii. 处于卖出状态的时候:
-
如果在冷冻期,不能买入;
-
如果不在冷冻期,才能买入.
-
对于
dp[i][0],我们有两种情况能到达这个状态:i. 在
i - 1天持有股票,此时最大收益应该和i - 1天的保持一致:dp[i - 1][0];ii. 在
i天买入股票,那我们应该选择i - 1天不在冷冻期的时候买入,由于买入需要花钱,所以此时最大收益为:dp[i - 1][1] - prices[i];两种情况应取最大值,因此:
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]). -
对于
dp[i][1],我们有两种情况能到达这个状态:i. 在
i - 1天的时候,已经处于冷冻期,然后啥也不干到第i天,此时对应的状态为:dp[i - 1][2];ii. 在
i - 1天的时候,手上没有股票,也不在冷冻期,但是依旧啥也不干到第i天,此时对应的状态为dp[i - 1][1];两种情况应取最大值,因此:
dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]). -
对于
dp[i][2],我们只有一种情况能到达这个状态:i. 在
i - 1天的时候,卖出股票.因此对应的状态转移为:
dp[i][2] = dp[i - 1][0] + prices[i].
3.初始化:
三种状态都会用到前一个位置的值,因此需要初始化每一行的第一个位置:
dp[0][0]:此时要想处于买入状态,必须把第一天的股票买了,因此dp[0][0] = -prices[0];dp[0][1]:啥也不用干即可,因此dp[0][1] = 0;dp[0][2]:手上没有股票,买一下卖一下就处于冷冻期,此时收益为0,因此dp[0][2] = 0.
4.填表顺序:
根据状态表示,我们要三个表一起填,每一个表从左往右.
5.返回值:
应该返回卖出状态下的最大值,因此应该返回 max(dp[n - 1][1], dp[n - 1][2]).

核心代码
cpp
class Solution
{
public:
//条件:卖出股票后,无法在次日买入股票(冷冻期1天),可多次买卖
int maxProfit(vector<int>& prices)
{
//1.创建 dp 表
//2.初始化
//3.填表
//4.返回结果
int n = prices.size();
//定义二维dp数组:dp[i][j] 表示第i天,处于状态j时的最大利润
//三种状态:
//j=0:第i天**持有股票**
//j=1:第i天**不持有股票,且处于冷冻期**(当天刚卖出)
//j=2:第i天**不持有股票,且不处于冷冻期**(可以买入)
vector<vector<int>> dp(n, vector<int>(3));
//初始化:第0天(第一天)的状态
//第0天持有股票:只能是当天买入,利润为 -股票价格
dp[0][0] = -prices[0];
//第0天不持有且冷冻/非冷冻:初始利润为0(数组默认初始化,可省略)
//dp[0][1] = 0;
//dp[0][2] = 0;
//从第1天开始,依次填表
for(int i = 1; i < n; i++)
{
//状态0:第i天持有股票
//两种情况:① 前一天已经持有;② 前一天不持有且非冷冻期,今天买入
dp[i][0] = max(dp[i - 1][0], dp[i - 1][2] - prices[i]);
//状态1:第i天处于冷冻期
//两种情况:① 前一天就是冷冻期;② 前一天不持有非冷冻,今天保持
dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]);
//状态2:第i天不持有非冷冻期
//唯一来源:前一天持有股票,今天卖出
dp[i][2] = dp[i - 1][0] + prices[i];
}
//最终结果:最后一天一定是不持有股票的状态(持有股票无利润)
//取【冷冻期】和【非冷冻期】的最大值
return max(dp[n - 1][1], dp[n - 1][2]);
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 条件:卖出股票后,无法在次日买入股票(冷冻期1天),可多次买卖
int maxProfit(vector<int>& prices)
{
//1.创建 dp 表
//2.初始化
//3.填表
//4.返回结果
int n = prices.size();
//空数组直接返回0
if(n == 0) return 0;
//定义二维dp数组:dp[i][j] 表示第i天,处于状态j时的最大利润
//三种状态:
//j=0:第i天**持有股票**
//j=1:第i天**不持有股票,且不处于冷冻期**(可以买入)
//j=2:第i天**不持有股票,且处于冷冻期**(当天刚卖出)
vector<vector<int>> dp(n, vector<int>(3));
//初始化:第0天(第一天)的状态
//第0天持有股票:只能是当天买入,利润为 -股票价格
dp[0][0] = -prices[0];
//第0天不持有非冷冻期、冷冻期:利润为0
dp[0][1] = 0;
dp[0][2] = 0;
//从第1天开始,依次填表
for(int i = 1; i < n; i++)
{
//状态0:第i天持有股票
//两种情况:① 前一天已经持有;② 前一天不持有且非冷冻期,今天买入
dp[i][0] = max(dp[i - 1][0], dp[i - 1][1] - prices[i]);
//状态1:第i天不持有非冷冻期
//两种情况:① 前一天就是非冷冻期;② 前一天冷冻期,今天解冻
dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]);
//状态2:第i天处于冷冻期
//唯一来源:前一天持有股票,今天卖出
dp[i][2] = dp[i - 1][0] + prices[i];
}
//最终结果:最后一天一定是不持有股票的状态,取两种状态最大值
return max(dp[n - 1][1], dp[n - 1][2]);
}
};
void testMaxProfit(vector<int>& prices, const string& caseName) {
Solution sol;
int res = sol.maxProfit(prices);
cout << "测试用例:" << caseName << endl;
cout << "股票价格数组:";
for (int num : prices) {
cout << num << " ";
}
cout << "\n最大利润:" << res << "\n-------------------------" << endl;
}
int main() {
//用例1:空数组(边界)
vector<int> case1;
testMaxProfit(case1, "空数组");
//用例2:单个交易日(无法买卖)
vector<int> case2 = {5};
testMaxProfit(case2, "单个交易日");
//用例3:两个交易日
vector<int> case3 = {1,5};
testMaxProfit(case3, "两个交易日");
//用例4:官方经典用例 [1,2,3,0,2] → 最大利润3
vector<int> case4 = {1,2,3,0,2};
testMaxProfit(case4, "官方经典场景");
//用例5:递减数组(无利润)
vector<int> case5 = {5,4,3,2,1};
testMaxProfit(case5, "价格递减");
return 0;
}

7.买卖股票的最佳时机含手续费(OJ题)

算法思路:解法(动态规划):
1.状态表示:
对于线性dp,我们可以用经验 + 题目要求来定义状态表示:
i. 以某个位置为结尾...
ii. 以某个位置为起点...
这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:
由于有买入可交易两个状态,因此我们可以选择用两个数组,其中:
f[i]表示:第i天结束后,处于买入状态,此时的最大利润;g[i]表示:第i天结束后,处于卖出状态,此时的最大利润.
2.状态转移方程:
我们选择在卖出的时候,支付这个手续费,那么在买入的时候,就不用再考虑手续费的问题.
-
对于
f[i],我们有两种情况能到达这个状态:i. 在
i - 1天持有股票,第i天啥也不干.此时最大收益为f[i - 1];ii. 在
i - 1天的时候没有股票,在第i天买入股票.此时最大收益为g[i - 1] - prices[i];两种情况下应该取最大值,因此:
f[i] = max(f[i - 1], g[i - 1] - prices[i]). -
对于
g[i],我们也有两种情况能够到达这个状态:i. 在
i - 1天持有股票,但是在第i天将股票卖出.此时最大收益为:f[i - 1] + prices[i] - fee,记得手续费;ii. 在
i - 1天没有股票,然后第i天啥也不干.此时最大收益为:g[i - 1];两种情况下应该取最大值,因此:
g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee).
3.初始化:
由于需要用到前面的状态,因此需要初始化第一个位置.
- 对于
f[0],此时处于买入状态,因此f[0] = -prices[0]; - 对于
g[0],此时处于没有股票状态,啥也不干即可获得最大收益,因此g[0] = 0.
4.填表顺序:
毫无疑问是从左往右,但是两个表需要一起填.
5.返回值:
应该返回卖出状态下,最后一天的最大值收益:g[n - 1].

核心代码
cpp
class Solution
{
public:
//规则:可多次买卖,每完成一笔交易(卖出)需支付一次手续费,求最大利润
int maxProfit(vector<int>& prices, int fee)
{
//动态规划四步走
//1.创建 dp 表
//2.初始化
//3.填表
//4.返回结果
int n = prices.size();
//边界处理:空数组直接返回0
if(n == 0) return 0;
//定义两个dp数组:
//f[i]:第i天**持有股票**时的最大利润
//g[i]:第i天**不持有股票**时的最大利润
vector<int> f(n);
auto g = f; //初始化g数组,大小与f一致
//初始化:第0天(第一天)
//持有股票:只能当天买入,利润 = -股票价格
f[0] = -prices[0];
//不持有股票:无操作,利润为0(数组默认初始化,可省略)
//g[0] = 0;
//从第1天开始遍历填表
for(int i = 1; i < n; i++)
{
//状态转移:第i天持有股票
//情况1:前一天已经持有,保持不变 → f[i-1]
//情况2:前一天不持有,今天买入 → g[i-1] - prices[i]
//取两种情况的最大值
f[i] = max(f[i - 1], g[i - 1] - prices[i]);
//状态转移:第i天不持有股票
//情况1:前一天已经不持有,保持不变 → g[i-1]
//情况2:前一天持有,今天卖出 → f[i-1] + 股价 - 手续费(扣除交易费)
//取两种情况的最大值
g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee);
}
//最终结果:最后一天不持有股票的利润最大(持有股票无法兑现利润)
return g[n - 1];
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 规则:可多次买卖,每卖出一次股票支付一次手续费,求最大利润
int maxProfit(vector<int>& prices, int fee)
{
// 动态规划四步走
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = prices.size();
// 边界处理:空数组直接返回0
if(n == 0) return 0;
// 定义两个dp数组:
// f[i]:第i天**持有股票**时的最大利润
// g[i]:第i天**不持有股票**时的最大利润
vector<int> f(n);
auto g = f; // 初始化g数组,大小与f一致
// 初始化:第0天(第一天)
// 持有股票:只能当天买入,利润 = -股票价格
f[0] = -prices[0];
// 不持有股票:无操作,利润为0
g[0] = 0;
// 从第1天开始遍历填表
for(int i = 1; i < n; i++)
{
// 第i天持有股票:前一天已持有 / 前一天不持有今日买入
f[i] = max(f[i - 1], g[i - 1] - prices[i]);
// 第i天不持有股票:前一天不持有 / 前一天持有今日卖出(扣手续费)
g[i] = max(g[i - 1], f[i - 1] + prices[i] - fee);
}
// 最终结果:最后一天不持有股票的利润最大
return g[n - 1];
}
};
void testMaxProfit(vector<int>& prices, int fee, const string& caseName) {
Solution sol;
int res = sol.maxProfit(prices, fee);
cout << "测试用例:" << caseName << endl;
cout << "股票价格数组:";
for (int num : prices) {
cout << num << " ";
}
cout << "\n手续费:" << fee << "\n最大利润:" << res << "\n-------------------------" << endl;
}
int main() {
// 用例1:空数组(边界)
vector<int> case1;
testMaxProfit(case1, 2, "空数组");
// 用例2:单个交易日(无法买卖)
vector<int> case2 = {5};
testMaxProfit(case2, 2, "单个交易日");
// 用例3:官方经典用例 [1,3,2,8,4,9] 手续费2 → 最大利润8
vector<int> case3 = {1,3,2,8,4,9};
testMaxProfit(case3, 2, "官方经典场景");
// 用例4:价格递减(无利润)
vector<int> case4 = {7,6,5,4,3};
testMaxProfit(case4, 1, "价格递减");
// 用例5:价格递增
vector<int> case5 = {1,2,3,4,5};
testMaxProfit(case5, 1, "价格递增");
return 0;
}

8.买卖股票的最佳时机|||(OJ题)

算法思路:解法(动态规划):
1.状态表示:
对于线性dp,我们可以用经验 + 题目要求来定义状态表示:
i. 以某个位置为结尾...
ii. 以某个位置为起点...
这里我们选择比较常用的方式,以某个位置为结尾,结合题目要求,定义一个状态表示:
由于有买入可交易两个状态,因此我们可以选择用两个数组.但是这道题里面还有交易次数的限制,因此我们还需要再加上一维,用来表示交易次数.其中:
f[i][j]表示:第i天结束后,完成了j次交易,处于买入状态,此时的最大利润;g[i][j]表示:第i天结束后,完成了j次交易,处于卖出状态,此时的最大利润.
2.状态转移方程:
对于 f[i][j],我们有两种情况到这个状态:
i. 在 i - 1 天的时候,交易了 j 次,处于买入状态,第 i 天啥也不干即可.此时最大利润为:f[i - 1][j];
ii. 在 i - 1 天的时候,交易了 j 次,处于卖出状态,第 i 天的时候把股票买了.此时的最大利润为:g[i - 1][j] - prices[i].
综上,我们要的是最大利润,因此取两者的最大值:
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]).
对于 g[i][j],我们也有两种情况可以到达这个状态:
i. 在 i - 1 天的时候,交易了 j 次,处于卖出状态,第 i 天啥也不干即可.此时的最大利润为:g[i - 1][j];
ii. 在 i - 1 天的时候,交易了 j - 1 次,处于买入状态,第 i 天把股票卖了,然后就完成了 j 笔交易.此时的最大利润为:f[i - 1][j - 1] + prices[i].但这个状态不一定存在,要先判断一下.
综上,我们要的是最大利润,因此状态转移方程为:
g[i][j] = g[i - 1][j];
if(j >= 1) g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
3.初始化:
由于需要用到 i = 0 时的状态,因此我们初始化第一行即可.
- 当处于第
0天的时候,只能处于买入过一次的状态,此时的收益为-prices[0],因此f[0][0] = -prices[0]. - 为了取
max的时候,一些不存在的状态起不到干扰的作用,我们统统将它们初始化为-INF(用INT_MIN在计算过程中会有溢出的风险,这里INF折半取0x3f3f3f3f,足够小即可).
4.填表顺序:
从上往下填每一行,每一行从左往右,两个表一起填.
5.返回值:
返回处于卖出状态的最大值,但是我们也不知道是交易了几次,因此返回 g 表最后一行的最大值.

核心代码
cpp
class Solution
{
public:
//定义无穷大,用于初始化非法状态(表示该情况不可能出现)
const int INF = 0x3f3f3f3f;
// 规则:最多只能完成 **两笔** 交易(买+卖算一笔),求最大利润
int maxProfit(vector<int>& prices)
{
//动态规划四步走
//1.创建 dp 表
//2.初始化
//3.填表
//4.返回结果
int n = prices.size();
//边界处理:空数组直接返回0
if(n == 0) return 0;
//状态定义:
//f[i][j]:第i天 **持有股票**,且恰好完成 j 笔交易时的最大利润
//g[i][j]:第i天 **不持有股票**,且恰好完成 j 笔交易时的最大利润
//j 的取值:0/1/2 → 代表完成0次、1次、2次交易
vector<vector<int>> f(n, vector<int>(3, -INF));
auto g = f; //g数组和f数组初始化规则一致
//初始化:第0天(第一天)的初始状态
//第0天持有股票,0次交易:当天买入,利润为 -股价
f[0][0] = -prices[0];
//第0天不持有股票,0次交易:无操作,利润为0
g[0][0] = 0;
//第0天无法完成1/2次交易,保持初始的非法状态(-INF)
//从第1天开始遍历每一天
for(int i = 1; i < n; i++)
{
//遍历交易次数:0次、1次、2次
for(int j = 0; j < 3; j++)
{
//状态转移:第i天持有股票 f[i][j]
//情况1:前一天已经持有股票,保持不变 → f[i-1][j]
//情况2:前一天不持有股票,今天买入 → g[i-1][j] - prices[i]
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);
//状态转移:第i天不持有股票 g[i][j]
//情况1:前一天已经不持有股票,保持不变 → g[i-1][j]
g[i][j] = g[i - 1][j];
//情况2:前一天持有股票,今天卖出(交易次数+1)
//仅当 j >= 1 时,才能从 j-1 次交易转移而来
if(j >= 1)
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
}
}
//最终结果:最后一天不持有股票的所有状态的最大值
//因为持有股票无法兑现利润,所以只看 g 数组
int ret = 0;
for(int j = 0; j < 3; j++)
ret = max(ret, g[n - 1][j]);
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 定义无穷大,初始化非法状态(表示该情况无法实现)
const int INF = 0x3f3f3f3f;
int maxProfit(vector<int>& prices)
{
// 动态规划四步走
// 1. 创建 dp 表
// 2. 初始化
// 3. 填表
// 4. 返回结果
int n = prices.size();
// 边界处理:空数组直接返回0
if(n == 0) return 0;
// 状态定义:
// f[i][j]:第i天 持有股票,完成j笔交易的最大利润
// g[i][j]:第i天 不持有股票,完成j笔交易的最大利润
// j=0/1/2:代表完成0次、1次、2次交易
vector<vector<int>> f(n, vector<int>(3, -INF));
auto g = f;
// 初始化:第0天(第一天)初始状态
f[0][0] = -prices[0]; // 第0天买入股票,0笔交易
g[0][0] = 0; // 第0天不操作,0笔交易
// 遍历每一天
for(int i = 1; i < n; i++)
{
// 遍历所有交易次数
for(int j = 0; j < 3; j++)
{
// 持有股票的状态转移
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);
// 不持有股票的状态转移
g[i][j] = g[i - 1][j];
// 卖出股票,交易次数+1
if(j >= 1)
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
}
}
// 最终结果:最后一天不持有股票的最大利润
int ret = 0;
for(int j = 0; j < 3; j++)
ret = max(ret, g[n - 1][j]);
return ret;
}
};
void testMaxProfit(vector<int>& prices, const string& caseName) {
Solution sol;
int res = sol.maxProfit(prices);
cout << "测试用例:" << caseName << endl;
cout << "股票价格数组:";
for (int num : prices) {
cout << num << " ";
}
cout << "\n最大利润:" << res << "\n-------------------------" << endl;
}
int main() {
// 用例1:空数组(边界)
vector<int> case1;
testMaxProfit(case1, "空数组");
// 用例2:单个交易日(无法交易)
vector<int> case2 = {5};
testMaxProfit(case2, "单个交易日");
// 用例3:官方经典用例 [3,3,5,0,0,3,1,4] → 最大利润6
vector<int> case3 = {3,3,5,0,0,3,1,4};
testMaxProfit(case3, "官方经典场景");
// 用例4:价格递减(无利润)
vector<int> case4 = {7,6,4,3,1};
testMaxProfit(case4, "价格递减");
// 用例5:价格递增(一笔交易即可)
vector<int> case5 = {1,2,3,4,5};
testMaxProfit(case5, "价格递增");
// 用例6:两笔交易最优场景 → 最大利润13
vector<int> case6 = {1,2,4,2,5,7,2,4,9};
testMaxProfit(case6, "两笔交易最优");
return 0;
}

9.买卖股票的最佳时机IV(OJ题)

算法思路:解法(动态规划):
1.状态表示:
为了更加清晰的区分买入和卖出,我们换成有股票和无股票两个状态.
f[i][j]表示:第i天结束后,完成了j笔交易,此时处于有股票状态的最大收益;g[i][j]表示:第i天结束后,完成了j笔交易,此时处于无股票状态的最大收益.
2.状态转移方程:
对于 f[i][j],我们也有两种情况能在第 i 天结束之后,完成 j 笔交易,此时手里有股票的状态:
i. 在 i - 1 天的时候,手里有股票,并且交易了 j 次.在第 i 天的时候,啥也不干.此时的收益为 f[i - 1][j];
ii. 在 i - 1 天的时候,手里没有股票,并且交易了 j 次.在第 i 天的时候,买了股票.那么 i 天结束之后,我们就有股票了.此时的收益为 g[i - 1][j] - prices[i];
上述两种情况,我们需要的是最大值,因此 f 的状态转移方程为:
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i])
对于 g[i][j],我们有下面两种情况能在第 i 天结束之后,完成 j 笔交易,此时手里没有股票的状态:
i. 在 i - 1 天的时候,手里没有股票,并且交易了 j 次.在第 i 天的时候,啥也不干.此时的收益为 g[i - 1][j];
ii. 在 i - 1 天的时候,手里有股票,并且交易了 j - 1 次.在第 i 天的时候,把股票卖了.那么 i 天结束之后,我们就交易了 j 次.此时的收益为 f[i - 1][j - 1] + prices[i];
上述两种情况,我们需要的是最大值,因此 g 的状态转移方程为:
g[i][j] = max(g[i - 1][j], f[i - 1][j - 1] + prices[i])
如果画⼀个图的话,它们之间交易关系如下:

3.初始化:
由于需要用到 i = 0 时的状态,因此我们初始化第一行即可.
- 当处于第
0天的时候,只能处于买入过一次的状态,此时的收益为-prices[0],因此f[0][0] = -prices[0]. - 为了取
max的时候,一些不存在的状态起不到干扰的作用,我们统统将它们初始化为-INF(用INT_MIN在计算过程中会有溢出的风险,这里INF折半取0x3f3f3f3f,足够小即可).
4.填表顺序:
从上往下填每一行,每一行从左往右,两个表一起填.
5.返回值:
返回处于卖出状态的最大值,但是我们也不知道是交易了几次,因此返回 g 表最后一行的最大值.
优化点:
我们的交易次数是不会超过整个天数的一半的,因此我们可以先把 k 处理一下,优化一下问题的规模:k = min(k, n / 2)

核心代码
cpp
class Solution
{
public:
// 规则:最多可以完成 k 笔交易(买入+卖出=1笔),求最大利润
int maxProfit(int k, vector<int>& prices)
{
//定义无穷大常量,用于初始化【非法/不可能】的状态
const int INF = 0x3f3f3f3f;
int n = prices.size();
//核心优化:一次交易至少需要 2 天(买+卖),因此最多只能完成 n/2 笔有效交易
//如果 k 超过这个上限,等价于无限次交易,直接缩小 k 避免空间浪费
k = min(k, n / 2);
//边界处理:没有股票价格,直接返回0
if(n == 0) return 0;
//状态定义
//f[i][j]:第 i 天【持有股票】,且恰好完成 j 笔交易时的最大利润
//g[i][j]:第 i 天【不持有股票】,且恰好完成 j 笔交易时的最大利润
//初始化所有状态为 -INF(表示初始状态不可达)
vector<vector<int>> f(n, vector<int>(k + 1, -INF));
auto g = f; // g 数组与 f 数组初始化规则一致
//初始化:第 0 天(第一天)的初始状态
f[0][0] = -prices[0]; //第0天买入股票,完成0笔交易,利润为 -股价
g[0][0] = 0; //第0天不操作,不持有股票,0笔交易,利润为0
//填表:遍历每一天
for(int i = 1; i < n; i++)
{
//遍历所有交易次数(0 ~ k 笔)
for(int j = 0; j <= k; j++)
{
//状态转移:第 i 天持有股票
//情况1:前一天已经持有股票,保持不变 → f[i-1][j]
//情况2:前一天不持有股票,今天买入 → g[i-1][j] - prices[i]
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);
//状态转移:第 i 天不持有股票(默认:前一天不持有,保持不变)
g[i][j] = g[i - 1][j];
//如果 j ≥ 1,说明可以通过【卖出股票】完成第 j 笔交易
//情况2:前一天持有股票,今天卖出 → f[i-1][j-1] + prices[i](交易次数+1)
if(j >= 1)
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
}
}
//返回结果
//最终利润一定是【不持有股票】的状态,遍历 0~k 笔交易取最大值
int ret = 0;
for(int j = 0; j <= k; j++)
ret = max(ret, g[n - 1][j]);
return ret;
}
};
完整测试代码
cpp
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
class Solution
{
public:
// 规则:最多可以完成 k 笔交易(买入+卖出=1笔),求最大利润
int maxProfit(int k, vector<int>& prices)
{
// 定义无穷大常量,用于初始化【非法/不可能】的状态
const int INF = 0x3f3f3f3f;
int n = prices.size();
// 核心优化:一次交易至少需要 2 天(买+卖),因此最多只能完成 n/2 笔有效交易
// 如果 k 超过这个上限,等价于无限次交易,直接缩小 k 避免空间浪费
k = min(k, n / 2);
// 边界处理:没有股票价格,直接返回0
if(n == 0) return 0;
//状态定义
// f[i][j]:第 i 天【持有股票】,且恰好完成 j 笔交易时的最大利润
// g[i][j]:第 i 天【不持有股票】,且恰好完成 j 笔交易时的最大利润
// 初始化所有状态为 -INF(表示初始状态不可达)
vector<vector<int>> f(n, vector<int>(k + 1, -INF));
auto g = f; // g 数组与 f 数组初始化规则一致
//初始化:第 0 天(第一天)的初始状态
f[0][0] = -prices[0]; // 第0天买入股票,完成0笔交易,利润为 -股价
g[0][0] = 0; // 第0天不操作,不持有股票,0笔交易,利润为0
//填表:遍历每一天
for(int i = 1; i < n; i++)
{
// 遍历所有交易次数(0 ~ k 笔)
for(int j = 0; j <= k; j++)
{
// 状态转移:第 i 天持有股票
// 情况1:前一天已经持有股票,保持不变 → f[i-1][j]
// 情况2:前一天不持有股票,今天买入 → g[i-1][j] - prices[i]
f[i][j] = max(f[i - 1][j], g[i - 1][j] - prices[i]);
// 状态转移:第 i 天不持有股票(默认:前一天不持有,保持不变)
g[i][j] = g[i - 1][j];
// 如果 j ≥ 1,说明可以通过【卖出股票】完成第 j 笔交易
// 情况2:前一天持有股票,今天卖出 → f[i-1][j-1] + prices[i](交易次数+1)
if(j >= 1)
g[i][j] = max(g[i][j], f[i - 1][j - 1] + prices[i]);
}
}
//返回结果
// 最终利润一定是【不持有股票】的状态,遍历 0~k 笔交易取最大值
int ret = 0;
for(int j = 0; j <= k; j++)
ret = max(ret, g[n - 1][j]);
return ret;
}
};
void testMaxProfit(int k, vector<int>& prices, const string& caseName) {
Solution sol;
int res = sol.maxProfit(k, prices);
cout << "测试用例:" << caseName << endl;
cout << "最大交易次数 k:" << k << endl;
cout << "股票价格数组:";
for (int num : prices) {
cout << num << " ";
}
cout << "\n最大利润:" << res << "\n-------------------------" << endl;
}
int main() {
// 用例1:空数组(边界)
vector<int> case1;
testMaxProfit(2, case1, "空数组");
// 用例2:单个交易日(无法交易)
vector<int> case2 = {5};
testMaxProfit(2, case2, "单个交易日");
// 用例3:k=0(禁止任何交易)
vector<int> case3 = {1,2,3,4};
testMaxProfit(0, case3, "k=0 无法交易");
// 用例4:官方经典用例 k=2, [3,2,6,5,0,3] → 最大利润7
vector<int> case4 = {3,2,6,5,0,3};
testMaxProfit(2, case4, "官方经典场景1");
// 用例5:官方用例 k=2, [1,2,3,4,5] → 最大利润4
vector<int> case5 = {1,2,3,4,5};
testMaxProfit(2, case5, "官方经典场景2");
// 用例6:价格递减(无利润)
vector<int> case6 = {7,6,5,4,3};
testMaxProfit(2, case6, "价格递减");
// 用例7:k极大(超过n/2,等价无限次交易)
vector<int> case7 = {2,4,1,5,7,2,4};
testMaxProfit(100, case7, "k极大(无限次交易)");
return 0;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!
敬请期待下一篇文章内容:【动态规划算法】(子数组系列问题建模与解题思路精讲)
每日心灵鸡汤:"有些东西必须摧毁,才能迎来新生."
我很喜欢一个词叫"不破不立".有些东西必须摧毁,必须放弃,才能迎来新生.不管是消耗你的人,还是让你恐惧和焦虑的事,又或是脆弱敏感的你,必须打破他们,才能迎来新生.审视自己,审视过去,复盘总结,摧毁重塑,涅槃重生.虽然过程会很痛苦,但也是我们蜕变的必经之路.当我们越是恐惧什么的时候,越是要勇敢面对,因为勇敢的人,才能迎来更好的生活.大浪淘沙,去伪存真.破而后立,否极泰来.
