151 · 买卖股票的最佳时机 III(序列dp)

链接:LintCode 炼码 - 更高效的学习体验!

买卖股票的最佳时机 II

题解:买卖股票的最佳时机 III

总共有5个阶段:

第1阶段,什么没有买卖过,无股票票
第2阶段,买过一次,有股票
第3阶段,卖过一次,无股票
第4阶段,买过二次,有股票
第5节点,卖过两次,无股票

dpij 前i个位置,在第j个节点,获得最大利润是什么

如果当前没有股票,

dpij = dpi-1j

dpij = max(dpij, dpi-1j-1 + pricesi-1 - pricesi-2); 前i-1天,前一个阶段有股票 + 今天卖出去(注意j需要 > 1为,因为j=0的状态没有意义,不存在第0个阶段)

如果当前有股票

dpij = dpi-1j-1 今天买入股票

dpij = max(dpij, dpi-1\[j + pricesi-1 - pricesi-2); 前i-1天有股票,需要累加上当天的股票收益

cpp 复制代码
class Solution {
public:
    /**
     * @param prices: Given an integer array
     * @return: Maximum profit
     */
    int maxProfit(vector<int>& prices) {
        // write your code here
        int len = prices.size();
        if (len <= 0) {
            return 0;
        }
        vector<vector<int>> dp(prices.size() + 1, vector<int>(6));

        for (int i = 1; i <= 5; ++i) {
            dp[0][i] = INT_MIN;
        }
        dp[0][1] = 0;
        // 第1阶段,什么没有买卖过,无股票票
        // 第2阶段,买过一次,有股票
        // 第3阶段,卖过一次,无股票
        // 第4阶段,买过二次,有股票
        // 第5节点,卖过两次,无股票
        for (int i = 1; i <= len; ++i) {
            for (int j = 1; j <= 5; j += 2) {
                dp[i][j] = dp[i - 1][j];
                /*j == 1的情况下,没有进行初始化,没有第0个阶段*/
                if (i >= 2 && j > 1 && dp[i - 1][j - 1] != INT_MIN) {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + prices[i - 1] -
                                                 prices[i - 2]);
                }
            }
            for (int j = 2; j <= 5; j += 2) {
                dp[i][j] = dp[i - 1][j - 1];
                if (i >= 2 && dp[i - 1][j] != INT_MIN) {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j] + prices[i - 1] -
                                                 prices[i - 2]);
                }
            }
        }
        int result = 0;
        for (int i = 1; i <= 5; i += 2) {
            result = max(result, dp[len][i]);
        }
        return result;
    }
};

LintCode 炼码 - 更高效的学习体验!

https://www.jiuzhang.com/problems/info/393

限制k次,需要n=2*k+1

cpp 复制代码
class Solution {
public:
    /**
     * @param k: An integer
     * @param prices: An integer array
     * @return: Maximum profit
     */
    int maxProfit(int k, vector<int>& prices) {
        // write your code here
        int len = prices.size();
        if (len <= 0) {
            return 0;
        }
        if (len / 2 < k) {
            int result = 0;
            for (int i = 1; i < len; ++i) {
                if (prices[i] > prices[i - 1]) {
                    result += prices[i] - prices[i - 1];
                }
            }
            return result;
        }
        int n = 2 * k + 1;
        vector<vector<int>> dp(prices.size() + 1, vector<int>(n + 1));

        for (int i = 1; i <= n; ++i) {
            dp[0][i] = INT_MIN;
        }
        dp[0][1] = 0;
        // 第1阶段,什么没有买卖过,无股票票
        // 第2阶段,买过一次,有股票
        // 第3阶段,卖过一次,无股票
        // 第4阶段,买过二次,有股票
        // 第5节点,卖过两次,无股票
        for (int i = 1; i <= len; ++i) {
            for (int j = 1; j <= n; j += 2) {
                dp[i][j] = dp[i - 1][j];
                /*j == 1的情况下,没有进行初始化,没有第0个阶段*/
                if (i >= 2 && j > 1 && dp[i - 1][j - 1] != INT_MIN) {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j - 1] + prices[i - 1] -
                                                 prices[i - 2]);
                }
            }
            for (int j = 2; j <= n; j += 2) {
                dp[i][j] = dp[i - 1][j - 1];
                if (i >= 2 && dp[i - 1][j] != INT_MIN) {
                    dp[i][j] = max(dp[i][j], dp[i - 1][j] + prices[i - 1] -
                                                 prices[i - 2]);
                }
            }
        }
        int result = 0;
        for (int i = 1; i <= n; i += 2) {
            result = max(result, dp[len][i]);
        }
        return result;
    }
};

状态机 DP:股票买卖(最多 2 次交易)

这段代码把"最多两次交易"建模成一个5 个状态的自动机 ,dp[i][j] 表示前 i 天处于状态 j 时的最大利润。

一、5 个状态的含义

状态 j 含义 奇偶
1 未持有股票,还没开始第 1 次交易 奇(未持有)
2 持有股票,正在进行第 1 次交易 偶(持有)
3 未持有股票,已完成第 1 次交易 奇(未持有)
4 持有股票,正在进行第 2 次交易 偶(持有)
5 未持有股票,已完成第 2 次交易 奇(未持有)

状态转移是一条链:

text

复制代码
1 → 2 → 3 → 4 → 5
未持有  持有  未持有  持有  未持有
  • 偶数状态(2、4):手里有股票

  • 奇数状态(1、3、5):手里没股票

  • 每次"买入"是 奇数 → 偶数,每次"卖出"是 偶数 → 奇数

二、转移方程

未持有状态(j = 1, 3, 5)

java

复制代码
dp[i][j] = dp[i - 1][j];                         // 继续不持有
if (j > 1 && i > 1 && dp[i-1][j-1] != MIN_VALUE) {
    dp[i][j] = max(dp[i][j], dp[i-1][j-1] + prices[i-1] - prices[i-2]);  // 今天卖出
}
  • 继续不持有:昨天也不持有,今天什么都不做。

  • 今天卖出 :昨天持有(状态 j-1),今天卖掉,赚取今天的差价 prices[i-1] - prices[i-2]。

持有状态(j = 2, 4)

java

复制代码
dp[i][j] = dp[i - 1][j - 1];                     // 今天买入
if (i > 1 && dp[i-1][j] != MIN_VALUE) {
    dp[i][j] = max(dp[i][j], dp[i-1][j] + prices[i-1] - prices[i-2]);  // 继续持有
}
  • 今天买入 :昨天未持有(状态 j-1),今天买入,利润不变。

  • 继续持有 :昨天持有,今天继续持有,累积今天的差价。

三、关键技巧:为什么用"差价"而不是"价格"

通常的写法是:

java

复制代码
// 买入:减去买入价格
dp[i][2] = dp[i-1][1] - prices[i-1];
// 卖出:加上卖出价格
dp[i][3] = dp[i-1][2] + prices[i-1];

但这个解法用的是每日差价累积:

text

复制代码
持有期间:dp[i][j] += prices[i-1] - prices[i-2]   // 每天累积涨跌
卖出当天:dp[i][j] += prices[i-1] - prices[i-2]   // 再加一次最后一天的差价

为什么等价?举个例子

假设第 2 天买入(价格 10),第 5 天卖出(价格 18):

差价写法:

天数 动作 累积利润
第 2 天 买入,进入状态 2 dp[2][2] = dp[1][1] = 0
第 3 天 继续持有 dp[3][2] = 0 + (p3 - p2)
第 4 天 继续持有 dp[4][2] = (p3 - p2) + (p4 - p3) = p4 - p2
第 5 天 卖出,进入状态 3 dp[5][3] = dp[4][2] + (p5 - p4) = p5 - p2

最终 dp[5][3] = p5 - p2 = 18 - 10 = 8 ✅

标准写法 :dp[5][3] = dp[4][2] + p5 = (dp[1][1] - p2) + p5 = p5 - p2 = 8 ✅

两者结果完全相同。差价写法的好处是不需要存储负的买入价格,所有状态都是"已实现的利润",更直观。

四、初始化

java

复制代码
for (int k = 1; k <= 5; k++) {
    dp[0][k] = Integer.MIN_VALUE;   // 第 0 天,除了状态 1,其他都不可达
}
dp[0][1] = 0;                        // 第 0 天未持有,利润 0
  • dp[0][1] = 0:还没开始交易,利润为 0。

  • 其他状态设为 MIN_VALUE:表示"第 0 天不可能持有股票或完成交易"。

转移时用 dp[i-1][j-1] != MIN_VALUE 检查,防止从不可达状态转移。

五、最终答案

java

复制代码
int res = 0;
for (int j = 1; j <= 5; j += 2) {   // 状态 1, 3, 5(未持有)
    res = Math.max(res, dp[n][j]);
}

最后一天必须未持有股票(手里有股票不算完成交易),所以在状态 1、3、5 中取最大值。

  • 状态 1:一次都没交易,利润 0

  • 状态 3:完成 1 次交易

  • 状态 5:完成 2 次交易

取最大就是"最多 2 次交易"的最优解。

六、举例走一遍

text

复制代码
prices = [3, 3, 5, 0, 0, 3, 1, 4]

最优策略:第 4 天(价格 0)买入,第 6 天(价格 3)卖出,赚 3;第 7 天(价格 1)买入,第 8 天(价格 4)卖出,赚 3。总利润 6。

DP 表最终 dp[8][5] = 6,dp[8][3] 也可能等于 3(只做第一次交易),dp[8][1] = 0。取最大值 6 ✅

七、这个解法的优缺点

优点 缺点
状态机清晰,5 个状态一目了然 状态数随交易次数线性增长,k 次交易需要 2k+1 个状态
差价写法不需要存负值,所有状态都是"已实现利润" 差价写法对初学者不太直观
可以直接推广到"最多 k 次交易"(LeetCode 188) 需要处理 MIN_VALUE 防止溢出

八、推广到 k 次交易

把 5 换成 2k+1,状态含义不变:

text

复制代码
1: 未持有,0 次交易
2: 持有,第 1 次进行中
3: 未持有,1 次完成
4: 持有,第 2 次进行中
...
2k+1: 未持有,k 次完成

转移逻辑完全一样,只是循环范围变大。这就是 LeetCode 188「买卖股票的最佳时机 IV」的标准解法。

九、一句话总结

dp[i][j] 表示前 i 天处于状态 j 的最大利润,状态机是"未持有 → 持有 → 未持有 → 持有 → 未持有",每次买入/卖出在奇偶状态间切换,用每日差价累积利润,最后在未持有状态中取最大值。

相关推荐
xunberg1 小时前
【生命科学 Agent】推理平台(三)
算法·程序员·架构
庖丁解牛1 小时前
十进制转二进制:凑数法(填1法)
算法
行者全栈架构师1 小时前
【共创稿事节】双引擎智能相册:HarmonyOS 7(API 26)文搜图 × 图像超分端侧 AI 联动实战
前端·算法·架构
圣保罗的大教堂1 小时前
leetcode 3644. 排序排列 中等
leetcode
小蒋观天下1 小时前
【无标题】红外补光近距过曝、远距欠曝:导致车牌AI识别失效调试
人工智能·深度学习·数码相机·算法·机器学习·ai大模型
吞下星星的少年·-·3 小时前
牛客周赛 119B题(贪心)
算法·贪心
NEFU_Jyj3 小时前
牛客周赛R166(max型瓶颈最短路,前后缀DP合并)
算法·深度优先
Tisfy3 小时前
LeetCode 3644.排序排列:不对则与(脑筋急转弯)
数据结构·算法·leetcode·题解·位运算
文丑颜不良啊4 小时前
LeetCode576~600题目解析
java·leetcode