跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C/C++ 动态规划入门:多状态 DP 实战(打家劫舍与股票买卖)

C/C++ 动态规划核心在于状态定义与转移。本文聚焦多状态 DP,从打家劫舍的选/不选模型,延伸至粉刷房子的多颜色互斥,再到股票交易的买入/卖出/冷冻期/手续费/交易次数限制。通过拆解状态为多个数组或二维表,利用状态机思维推导方程。内容包含边界处理技巧、虚拟节点应用及空间优化思路,辅以完整 C++ 代码示例,帮助读者掌握复杂 DP 问题的通用解法。

念念不忘发布于 2026/3/30更新于 2026/6/1023 浏览
C/C++ 动态规划入门:多状态 DP 实战(打家劫舍与股票买卖)

C/C++ 动态规划入门:多状态 DP 实战

绪论

本章是动态规划的进阶篇。从基础的一维 DP、二维路径问题,我们逐步深入到多状态 DP。本章将结合前面的基础,由浅入深地讲解多状态问题的建模思路,涵盖经典的'打家劫舍'系列到复杂的'股票买卖'系列。过程中会不断总结规律,帮助大家建立通用的解题框架。

提示:若对基础 DP 尚不熟悉,建议先复习一维 DP 和路径 DP 相关章节。


打家劫舍系列

遇到相邻元素不能同时选择的场景时,往往可以联想到打家劫舍模型。这类问题通常使用两个 DP 表来分别存储不同状态下的最优解。

  • f[i]:选择第 i 个位置时的最大价值
  • g[i]:不选择第 i 个位置时的最大价值

1. 按摩师(LeetCode 面试题)

题目描述: 给定一个整数数组 nums,表示每个预约的时长。不能连续选择相邻的预约,求最长预约总时长。

分析: 这是一个典型的线性 DP 问题。对于每个位置,我们有两种选择:选或不选。

  1. 状态定义:

    • dp[i][0] (或 f[i]):选择第 i 个预约时的最大时长。
    • dp[i][1] (或 g[i]):不选择第 i 个预约时的最大时长。
  2. 状态转移:

    • 如果选择第 i 个,则前一个必须不选:f[i] = g[i-1] + nums[i]
    • 如果不选第 i 个,则前一个可选可不选,取最大值:g[i] = max(f[i-1], g[i-1])
  3. 初始化:

    • 处理边界情况,当 n=0 时直接返回 0。
    • f[0] = nums[0], g[0] = 0
  4. 返回值:

    • max(f[n-1], g[n-1])

代码实现:

class Solution {
public:
    int massage(vector<int>& nums) {
        int n = nums.size();
        if (n == 0) return 0;
        
        vector<int> f(n, ); 
        ; 
        
        f[] = nums[];
         ( i = ; i < n; i++) {
            f[i] = g[i - ] + nums[i];
            g[i] = (f[i - ], g[i - ]);
        }
         (f[n - ], g[n - ]);
    }
};
0
// 选择 i 位置
vector<int> g(n, 0)
// 不选择 i 位置
0
0
for
int
1
1
max
1
1
return
max
1
1

2. 打家劫舍 II(环形数组)

题目描述: 房屋围成一圈,首尾相连,不能同时偷窃第一间和最后一间。

分析: 由于首尾互斥,可以将问题拆分为两种情况:

  1. 偷第一家,不偷最后一家(范围 [0, n-2])
  2. 不偷第一家,可偷最后一家(范围 [1, n-1]) 取这两种情况的最大值即可。

代码实现:

class Solution {
public:
    int rob(vector<int>& nums) {
        int n = nums.size();
        if (n == 1) return nums[0];
        return max(robRange(nums, 0, n - 2), robRange(nums, 1, n - 1));
    }

private:
    int robRange(const vector<int>& nums, int start, int end) {
        int n = nums.size();
        vector<int> f(n, 0);
        vector<int> g(n, 0);
        f[start] = nums[start];
        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. 删除并获得点数

题目描述: 选择一个数 x,获得 x 分,但必须删除所有 x-1 和 x+1。

分析: 这本质上也是打家劫舍的变种。如果我们选择了数值 i,就不能选择 i-1 和 i+1。我们可以先统计每个数值出现的总得分,将其转化为一个数组,然后在这个新数组上运行打家劫舍逻辑。

代码实现:

class Solution {
public:
    int deleteAndEarn(vector<int>& nums) {
        const int N = 10010;
        int arr[N] = {};
        int maxVal = 0;
        
        // 统计每个数的总价值
        for (int x : nums) {
            arr[x] += x;
            maxVal = max(maxVal, x);
        }
        
        vector<int> f(maxVal + 1, 0);
        vector<int> g(maxVal + 1, 0);
        
        for (int i = 1; i <= maxVal; i++) {
            f[i] = g[i - 1] + arr[i];
            g[i] = max(f[i - 1], g[i - 1]);
        }
        return max(f[maxVal], g[maxVal]);
    }
};

4. 粉刷房子

题目描述: 有 n 栋房子,每栋房子可以用 k 种颜色粉刷,费用不同。相邻房子颜色不能相同,求最小花费。

分析: 这是多状态 DP 的典型应用。状态不仅包含下标,还包含当前选择的颜色。

  • dp[i][j]:粉刷到第 i 栋房子,且第 i 栋房子使用第 j 种颜色的最小花费。
  • 状态转移时,需遍历上一栋房子的其他颜色,取最小值。

代码实现:

class Solution {
public:
    int minCost(vector<vector<int>>& costs) {
        int n = costs.size();
        // dp[i][j] 表示第 i 栋房子刷第 j 种颜色的最小花费
        vector<vector<int>> dp(n + 1, vector<int>(3, 0));
        
        for (int i = 1; i <= n; 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];
        }
        return min({dp[n][0], dp[n][1], dp[n][2]});
    }
};

阶段总结

通过上述几道题,我们可以总结出多状态 DP 的核心思想:

  1. 状态拆分:当单一状态无法表达约束时,引入多个 DP 表或增加维度。
  2. 状态互斥:如打家劫舍中的选/不选,粉刷房子中的颜色互斥。
  3. 状态转化:如删除点数问题,通过预处理将复杂约束转化为标准模型。

接下来我们将进入更复杂的股票交易系列,涉及冷冻期、手续费及交易次数限制。


股票买卖系列

1. 含冷冻期

题目描述: 卖出股票后,第二天无法买入(冷冻期)。无限次交易。

分析: 需要维护三个状态:

  • dp[i][0]:持有股票(买入状态)
  • dp[i][1]:不持有股票,处于可交易状态
  • dp[i][2]:不持有股票,处于冷冻期

状态转移:

  • dp[i][0] = max(dp[i-1][0], dp[i-1][1] - price[i]) (保持持有 或 从前一天可交易买入)
  • dp[i][1] = max(dp[i-1][1], dp[i-1][2]) (保持空闲 或 从冷冻期解冻)
  • dp[i][2] = dp[i-1][0] + price[i] (今天卖出,昨天必须是持有状态)

代码实现:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        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][1] - prices[i]);
            dp[i][1] = max(dp[i - 1][1], dp[i - 1][2]);
            dp[i][2] = dp[i - 1][0] + prices[i];
        }
        return max(dp[n - 1][1], dp[n - 1][2]);
    }
};

2. 含手续费

题目描述: 每次交易收取固定手续费,无限次交易。

分析: 简化为两个状态:持有 (f) 和 不持有 (g)。

  • 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. 最多两笔交易 (III)

题目描述: 最多完成两笔交易。

分析: 状态空间扩大。我们需要记录交易次数 k。

  • f[i][j]:第 i 天,完成了 j 次交易,处于买入状态的最大利润。
  • g[i][j]:第 i 天,完成了 j 次交易,处于卖出状态的最大利润。

注意:交易次数在卖出时才增加。初始化时需将无效状态设为负无穷。

代码实现:

class Solution {
public:
    int maxProfit(vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> f(n + 1, vector<int>(3, -0x3F3F3F3F));
        vector<vector<int>> g(n + 1, vector<int>(3, -0x3F3F3F3F));
        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 >= 0) {
                    g[i][j] = max(g[i - 1][j], f[i - 1][j - 1] + prices[i - 1]);
                }
            }
        }
        
        int res = INT_MIN;
        for (int j = 0; j <= 2; j++) {
            res = max(res, g[n][j]);
        }
        return res;
    }
};

4. 任意 k 笔交易 (IV)

题目描述: 最多完成 k 笔交易。

分析: 逻辑与 III 完全一致,只需将交易次数上限改为 k。

代码实现:

class Solution {
public:
    int maxProfit(int k, vector<int>& prices) {
        int n = prices.size();
        vector<vector<int>> f(n + 1, vector<int>(k + 1, -0x3F3F3F3F));
        vector<vector<int>> g(n + 1, vector<int>(k + 1, -0x3F3F3F3F));
        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 >= 0) {
                    g[i][j] = max(g[i - 1][j], f[i - 1][j - 1] + prices[i - 1]);
                }
            }
        }
        
        int res = INT_MIN;
        for (int j = 0; j <= k; j++) {
            res = max(res, g[n][j]);
        }
        return res;
    }
};

最终总结

通过以上 8 道由浅入深的多状态 DP 题目,我们可以归纳出解决此类问题的通用方法论:

  1. 识别互斥性:打家劫舍类问题核心在于相邻互斥,通常拆分为'选/不选'两个状态。
  2. 多维状态定义:当存在多种约束(如颜色、天数、交易次数)时,需在状态中显式体现。例如 dp[i][j] 中 j 代表交易次数或颜色索引。
  3. 降维技巧:当状态超过三维难以维护时,可将'买入'和'卖出'状态拆分为两个二维数组,从而降低复杂度。
  4. 状态机思维:股票问题本质是状态机的流转。明确每个状态的来源和去向,列出转移方程即可。
  5. 边界处理:虚拟节点或负无穷初始化能有效避免越界和非法状态干扰。

掌握这些核心思路,面对复杂的多状态 DP 问题时,便能抽丝剥茧,找到最优解法。

目录

  1. C/C++ 动态规划入门:多状态 DP 实战
  2. 绪论
  3. 打家劫舍系列
  4. 1. 按摩师(LeetCode 面试题)
  5. 2. 打家劫舍 II(环形数组)
  6. 3. 删除并获得点数
  7. 4. 粉刷房子
  8. 阶段总结
  9. 股票买卖系列
  10. 1. 含冷冻期
  11. 2. 含手续费
  12. 3. 最多两笔交易 (III)
  13. 4. 任意 k 笔交易 (IV)
  14. 最终总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • C++ 二叉搜索树基础实现:增删查改详解
  • WebResearcher 迭代式深度研究智能体架构与使用指南
  • SD-XL 1.0 Refiner 图像优化工具使用指南
  • 基于 LLama-Factory 的游戏 NPC 对话系统重构实践
  • Rust 重构 Android 蓝牙协议栈:从 C++ 到安全高效之路
  • Python 实现自动化办公:文件、文档与邮件操作指南
  • 动态规划示例:统计字符串中 shy 子序列数量
  • 低空无人机 AI 算法详解:覆盖公安、消防、水利等 11 大应用场景
  • C++ 标准库 string 类详解:接口、原理与模拟实现
  • DAMO-YOLO 工业视觉部署:深色模式与异步渲染实战
  • DAMO-YOLO 目标检测部署:深色模式与异步渲染的工业 Web 方案
  • 基于 Django 构建 Python 监控系统
  • AI 生成 UI 工具评测与工程化实践指南
  • 前端新手必备:10 款 VS Code 插件提升开发效率与配置指南
  • 基于 Windows 搭建闲鱼 AI 自动回复系统教程
  • 大模型分布式训练与高效调参实战
  • 具身智能年度报告:4 万次真机评测揭示模型真实能力,榜首成功率仅 51%
  • 从零搭建可落地 AI Agent:开发全流程与实战
  • 基于 SpringBoot+Vue+MyBatis 的企业级 Web 药店管理系统
  • 通义万相 2.1 在 AIGC 中的应用与部署实践

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如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