【动态规划算法】(简单多状态dp问题入门与经典题型解析)


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

在算法学习的过程中,动态规划(Dynamic Programming,简称DP)几乎是每个学习者都会遇到的一道"分水岭".它不像排序或搜索那样直观,却在解决复杂问题时展现出极强的威力.尤其是在面对最优化问题、计数问题以及具有重叠子问题结构的场景时,动态规划往往能够将看似复杂的问题转化为一系列有规律的子问题,从而高效求解.对于初学者来说,动态规划最大的难点并不在于代码实现,而在于如何建立"状态"的概念以及如何设计状态之间的转移关系.很多人在刚接触DP时,常常会感到无从下手:不知道如何定义dp数组,不清楚状态转移方程如何推导,也难以判断一个问题是否适合用动态规划来解决.而在这些困难中,"多状态DP"又是一个进一步提升难度的重要阶段,它要求我们在建模时考虑多个维度的信息,使问题更加贴近实际,但也更具挑战性.本文从基础入手,逐步引导读者理解什么是多状态动态规划,以及它与单状态DP的区别.我们将通过典型例题,详细拆解状态设计思路、状态转移过程以及边界条件的处理方法,帮助你建立起清晰的解题框架.在学习过程中,你不仅能够掌握如何"写出"一个动态规划解法,更重要的是学会如何"想到"动态规划------如何从问题特征中识别出DP的适用性,如何将复杂问题拆解为可递推的子结构,以及如何优化空间和时间复杂度.通过对经典题型的深入分析,你将逐渐形成对多状态DP的直觉和经验.如果你已经掌握了基础的动态规划知识,但在面对稍微复杂一些的问题时仍感到吃力,那么这篇文章将帮助你跨过这一关键门槛.接下来,让我们一起从简单入门,逐步深入多状态DP的核心思想与实战技巧.废话不多说,下面跟着小编的节奏🎵一起去疯狂的学习吧!

目录

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;
}


🚀真正的勇者不是流泪的人,而是含泪奔跑的人!


敬请期待下一篇文章内容:【动态规划算法】(子数组系列问题建模与解题思路精讲)


每日心灵鸡汤:"有些东西必须摧毁,才能迎来新生."
我很喜欢一个词叫"不破不立".有些东西必须摧毁,必须放弃,才能迎来新生.不管是消耗你的人,还是让你恐惧和焦虑的事,又或是脆弱敏感的你,必须打破他们,才能迎来新生.审视自己,审视过去,复盘总结,摧毁重塑,涅槃重生.虽然过程会很痛苦,但也是我们蜕变的必经之路.当我们越是恐惧什么的时候,越是要勇敢面对,因为勇敢的人,才能迎来更好的生活.大浪淘沙,去伪存真.破而后立,否极泰来.

相关推荐
南境十里·墨染春水2 小时前
C++笔记——STL map
开发语言·c++·笔记
神の愛2 小时前
macOS--brewhome安装镜像
macos
fie88892 小时前
免疫优化算法在物流配送中心选址中的应用
算法·数学建模
阿洛学长2 小时前
OpenClaw零成本部署指南:Windows/Mac/Linux/阿里云搭建+两个免费大模型API配置攻略
linux·windows·macos
Aliex_git2 小时前
Nuxt 学习笔记(二)
前端·笔记·学习
南境十里·墨染春水2 小时前
C++笔记·-- STL unordered_map
开发语言·c++·笔记
勤劳的进取家2 小时前
SSH配置
运维·网络·学习
三品吉他手会点灯2 小时前
C语言学习笔记 - 17.C编程预备计算机专业知识 - 数据类型
c语言·笔记·学习
珹洺2 小时前
C++远程调用组件库JsonRpc(一)项目背景、核心概念与环境搭建
开发语言·c++·rpc