买卖股票的最佳时机 III 图解题解
这道题到底在问什么
- 输入
- prices=[3,3,5,0,0,3,1,4,2]
- 输出
- 6
最优解:为什么这么做
一句话答案:LeetCode 123 买卖股票的最佳时机 III:最多两笔交易,用 buy1、sell1、buy2、sell2 四个状态串成一条链、每天各更一遍,答案取 sell2。时间 O(n)、空间 O(1)。
买卖股票 III 比前两版多了哪条硬限制
给数组 prices,prices[i] 是第 i 天股价,最多完成两笔交易、任何时刻最多持一股,求最大总利润。它比只买卖一次的 121、无限次的 122 多了「最多两笔」这条卡子。题面 prices=[3,3,5,0,0,3,1,4,2] 答案 6:0 买、3 卖赚 3,再 1 买、4 卖赚 3。
为什么把两笔的买卖点全枚举会算爆
要挑出满足 买1<卖1<买2<卖2 的四个时点,全枚举是 O(n⁴)(大 O 记号,描述规模变大时操作数怎么涨)级别,n 稍大就算不完,还有大量重复子问题。把算过的结果存下来复用,就是动态规划(把『到今天为止、四个里程碑各自能达到的最好利润』记下来直接用)。
buy1、sell1、buy2、sell2 这四个状态各盯着什么
不用枚举哪天买卖,只盯四个里程碑此刻能拿到的最好利润。buy1 是第一次买入后的账面利润,买要掏钱所以是负数(所以 buy1 越接近 0、买得越便宜越好,别被负号唬住),就是到今天最低价取负。sell1 是在 buy1 上今天按价 p 卖出后的最大利润。buy2 是拿第一次卖完的钱再买后的最大利润。sell2 是第二次卖出后的最大利润,就是答案。四态一环扣一环,每个都建在前一个已算好的最优上顺推。
四条转移怎么写,为什么同一轮能用刚算好的前一个
每个状态每天都在两条路里取大:保持昨天的值,或今天就把这一步做掉。写成式子是 buy1=max(buy1,-p)、sell1=max(sell1,buy1+p)、buy2=max(buy2,sell1-p)、sell2=max(sell2,buy2+p),依次是买一、卖一、再买二、卖二。
更新次序不能乱:同一天先更 buy1,sell1 就用刚更的 buy1,buy2 用刚更的 sell1,sell2 用刚更的 buy2。每个状态只依赖前一个的当前值,整张表压成四个变量滚着用,这叫滚动(只留当前值、就地覆盖),空间 O(1)。
拿题面这九天的股价亲手滚一遍四态
prices=[3,3,5,0,0,3,1,4,2],初值 buy1=buy2=负无穷、sell1=sell2=0。第 0 天价 3:buy1=-3、buy2=-3(buy2 此刻只是占位、还没真第二次买)、sell1=sell2=0。第 2 天价 5:sell1=max(0,-3+5)=2、sell2=2。第 3 天价 0:buy1=0、buy2=max(-3,2-0)=2。第 5 天价 3:sell1=3、sell2=max(2,2+3)=5。第 7 天价 4:sell1=4、sell2=max(5,2+4)=6。其余各天四态不变。最后 sell2=6,正是题面答案。
buy 初值写成 0,利润为什么会凭空虚高
扫一遍 prices、每天四次取大,时间 O(n);只用四个变量滚动,空间 O(1)。buy1、buy2 初值写成 0 是最常见的错,那等于没花钱就凭空持股、利润偏高,得设成负无穷;sell1、sell2 才初值 0。顺序也不能乱,sell1 挪到 buy1 前面就会读到没更新的旧 buy1,链当场断。
▶ 动画逐步走查(共 21 步)——想跟着动画一帧帧对照就展开
- 3思路一句话:拆成四个状态 买1→卖1→买2→卖2,一环扣一环,每天各更新一遍。下面一步步演给你看。
- 4顶行是每天股价(固定)。下面四行 buy1 / sell1 / buy2 / sell2,待逐天填——它们就是两笔交易的四个里程碑。
- 5第 0 天开局:第一次买入花掉 3 元 → buy1=-3;当天买当天卖利润为 0 → sell1=sell2=0;第二次买入又垫一笔 → buy2=-3。
- 6落子四态:buy1=-3,sell1=0,buy2=-3,sell2=0。
- 7第 1 天(价 3):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 8取较大的:buy1=-3(保持),sell1=0(保持),buy2=-3(保持),sell2=0(保持)。
- 9第 2 天(价 5):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 10取较大的:buy1=-3(保持),sell1=2(今天第一次卖出更划算),buy2=-3(保持),sell2=2(今天第二次卖出更划算)。
- 11第 3 天(价 0):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 12取较大的:buy1=0(今天第一次买入更划算),sell1=2(保持),buy2=2(今天第二次买入更划算),sell2=2(保持)。
- 13第 4 天(价 0):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 14取较大的:buy1=0(保持),sell1=2(保持),buy2=2(保持),sell2=2(保持)。
- 15第 5 天(价 3):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 16取较大的:buy1=0(保持),sell1=3(今天第一次卖出更划算),buy2=2(保持),sell2=5(今天第二次卖出更划算)。
- 17第 6 天(价 1):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 18取较大的:buy1=0(保持),sell1=3(保持),buy2=2(保持),sell2=5(保持)。
- 19第 7 天(价 4):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 20取较大的:buy1=0(保持),sell1=4(今天第一次卖出更划算),buy2=2(保持),sell2=6(今天第二次卖出更划算)。
- 21第 8 天(价 2):四态各看两条路——保持昨天 或 今天做这一步动作。注意 sell1 依赖刚算出的 buy1、buy2 依赖刚算出的 sell1,串成一条链。
- 22取较大的:buy1=0(保持),sell1=4(保持),buy2=2(保持),sell2=6(保持)。
- 23最右的 sell2[8]=6 就是最大利润——两笔交易全部落袋,手里不留股。
⚠️ 容易写错的地方
✗ 错:四态更新顺序乱写
✓ 对:必须 buy1→sell1→buy2→sell2
后一步要用同一天刚算好的前一步
✗ 错:buy1 初值写 0
✓ 对:buy1 起手 −prices[0]
第一次买入已垫掉买入价
✗ 错:答案取 buy2/sell1
✓ 对:答案是 sell2[n-1]
两笔都做完且空仓才是最优终态
完整代码(Python / C++ / Java)
Python
def maxProfit(prices):
buy1 = buy2 = float("-inf")
sell1 = sell2 = 0
for p in prices:
buy1 = max(buy1, -p)
sell1 = max(sell1, buy1 + p)
buy2 = max(buy2, sell1 - p)
sell2 = max(sell2, buy2 + p)
return sell2C++
int maxProfit(vector<int>& prices){
long buy1 = LONG_MIN, buy2 = LONG_MIN;
long sell1 = 0, sell2 = 0;
for(int p : prices){
buy1 = max(buy1, (long)-p);
sell1 = max(sell1, buy1 + p);
buy2 = max(buy2, sell1 - p);
sell2 = max(sell2, buy2 + p);
}
return sell2;
}Java
int maxProfit(int[] prices){
int buy1 = Integer.MIN_VALUE, buy2 = Integer.MIN_VALUE;
int sell1 = 0, sell2 = 0;
for(int p : prices){
buy1 = Math.max(buy1, -p);
sell1 = Math.max(sell1, buy1 + p);
buy2 = Math.max(buy2, sell1 - p);
sell2 = Math.max(sell2, buy2 + p);
}
return sell2;
}复杂度
时间
O(n)
一遍线性递推
空间
O(1)
四个状态变量滚动
看不够?换成动画再走一遍
上面的推演每一步都对应一帧动画。点开交互动画版,能一步步看着 买卖股票的最佳时机 III 的数据怎么变、指针怎么走,还能切 Python / Java / C++ 跟着练。
面试官可能追问
买卖股票这一系列(121、122、123、188)到底怎么串起来的?+
它们共用「买入-卖出」的状态思路,只在交易次数上层层加码。121 只能买卖一次,一个 sell1 就够;122 不限次数,一进一出反复累加;123 卡死最多两笔,就用 buy1、sell1、buy2、sell2 四个状态串一条链;188 是「最多 k 笔」的通用版,把这四个状态推广成 2k 个(k 个买、k 个卖)用循环滚。会了 123,188 只是把两组状态扩成 k 组。
为什么 buy1、buy2 要设成负无穷,而 sell1、sell2 设成 0?+
sell1、sell2 表示「已经卖出、手里不持股」的利润,什么都不做时利润天然是 0,所以初值 0 合理。buy1、buy2 表示「已经买入、手里持着股」,可开局第一天之前根本没买过,这个状态压根不存在;设成负无穷,是让它在 max 里永远竞争不过真实的买入价,直到某天真被 -p 顶上来才生效。若误设成 0,等于宣称「没花一分钱就持股」,后面所有依赖它的利润都会被凭空垫高。
同一轮里 sell1 用的是刚更新的 buy1,会不会把同一天买了又卖也算进去出错?+
会算进去,但不会出错。同一天买又卖,sell1 那条路是 buy1+p,而刚更新的 buy1 可能正好是 -p,加起来是 0,等于这笔没做、利润为 0,不会抬高答案。它带来的好处是:两笔交易可以紧挨着同一天完成(第一笔卖出的当天就买第二笔),这种边界被自动兜住,不用额外写判断。所以顺序更新不是漏洞,是特意让链条把这些贴边情形一并覆盖。
想听吴师兄把这道题讲给你听?
文字版和动画都随便看。开通图解算法年卡,可以听吴师兄把 买卖股票的最佳时机 III 一步步讲透(全站已上线 905 份讲透,持续扩充), 卡住的地方还有 AI 私教小欧就着动画帮你拆到懂。
本平台为独立第三方培训机构,与华为技术有限公司无任何关联;课程的服务内容与权益以购买协议为准,学习效果因个人情况而异。「华为 OD」「华为可信」等仅为对岗位与考试方向的客观描述,相关商标归各自权利人所有。