以第 i-W-1 天的 DP 快照为前驱,把买卖转移拆成滑动窗口最大值,单调队列摊还 O(1),总复杂度 O(T·MaxP)。
OJ: roj
题目 ID: 1605
难度:提高
标签:动态规划单调队列python一本通
创建: 2026-09-30 21:16
更新: 2026-10-07 13:50
形式化题目
已知
- 某一天的买入或卖出均算一次交易,两次交易之间至少间隔
天(第 天交易后, 天不能交易); - 任何时刻持仓不超过
; - 初始资金无限、持股 0——资金无限意味着买入总能成交,因此求的是净增金额的最大值(记账以 0 为初始现金,允许现金为负)。
求
每天只允许至多一个动作(不交易 / 买一批 / 卖一批)是不失一般性的:同日往返必亏(
样例
| 天 | 1 | 2 | 3 | 4 | 5 |
|---|---|---|---|---|---|
| 动作 | 买 1 股 @ |
买 1 股 @ |
不交易 | 卖 1 股 @ |
卖 1 股 @ |
| 持股 | 1 | 2 | 2 | 1 | 0 |
| 现金 |
第 3 天不交易使第 4、5 天连续交易仍然合法(
正解
思路
朴素 DP 与瓶颈。 设
第一项是当天不交易;后两项分别是从前驱状态买入、卖出。间隔
若对每个
拆项:窗口里被最大的量与
对固定的第
样例的 DP 全程(
| 第 |
关键转移 | |||
|---|---|---|---|---|
| 0(初始) | 0 | |||
| 1 | 0 | 买: |
||
| 2 | 0 | 买: |
||
| 3 | 0 | 买 @ |
||
| 4 | 卖: |
|||
| 5 | 卖: |
观察表中两点:
实现上
代码
与正文符号的对应:dp,前驱 base(环形历史 hist),base[idx] + price * idx;transfer_buy / transfer_sell 分别是买入、卖出两个单调队列转移,NEG = -10**18 充当 solve 读入后逐天滚动,最后 max(dp) 出答案。
Python 版:
#!/usr/bin/env python3
# Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
# rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
# rainboy的学习导航网站: https://idx.roj.ac.cn
# create_at: 2026-09-30 21:22
# update_at: 2026-09-30 21:22
import sys
from array import array
from collections import deque
NEG = -10**18 # 不可达哨兵:真实钱数下界约 -2000×2000×2000 = -8e9,远离此值
def transfer_buy(base: array, price: int, limit: int, max_p: int, dp: array) -> None:
"""买入转移:对每个 j,从窗口 j' ∈ [j-limit, j-1] 里取 base[j']-price*(j-j') 的最大值写进 dp。"""
q: deque[int] = deque() # 单调队列存下标,按 base[j']+price*j' 单调递减,队头即窗口最大
for j in range(1, max_p + 1):
nj = j - 1 # 窗口右端新进来的持股数
v = base[nj] + price * nj
while q and base[q[-1]] + price * q[-1] <= v:
q.pop()
q.append(nj)
out = j - limit - 1 # 刚滑出窗口左端的持股数;它是队里最小下标,若在则必在队头
if out >= 0 and q[0] == out:
q.popleft()
best = q[0]
cand = base[best] + price * (best - j) # = base[best] - price*(j-best)
if cand > dp[j]:
dp[j] = cand
def transfer_sell(base: array, price: int, limit: int, max_p: int, dp: array) -> None:
"""卖出转移:对每个 j,从窗口 j' ∈ [j+1, j+limit] 里取 base[j']+price*(j'-j) 的最大值写进 dp。"""
q: deque[int] = deque()
for j in range(max_p - 1, -1, -1): # 窗口整体左移,从大到小枚举 j
while q and q[-1] > j + limit: # 右端(下标最大者)先滑出窗口
q.pop()
nj = j + 1 # 窗口左端新进来的持股数
v = base[nj] + price * nj
while q and base[q[0]] + price * q[0] <= v: # 队头被更晚过期的更大值压制,弹出
q.popleft()
q.appendleft(nj)
best = q[-1] # 队里按值单调递增,队尾即窗口最大
cand = base[best] + price * (best - j) # = base[best] + price*(best-j)
if cand > dp[j]:
dp[j] = cand
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
days, max_p, W = next(data), next(data), next(data)
AP = [0] * days # 每天买入价
BP = [0] * days # 每天卖出价
AS = [0] * days # 每天一次买入的股数上限
BS = [0] * days # 每天一次卖出的股数上限
for i in range(days):
AP[i], BP[i], AS[i], BS[i] = next(data), next(data), next(data), next(data)
# 环形历史快照:第 d 天结束时的持股状态;第 i 天交易只回看 d = i-W-1 的快照
ring = W + 2
day0 = array("q", [0] + [NEG] * max_p) # 开盘前:0 股 0 钱,唯一可达状态
hist: list[array | None] = [None] * ring
hist[0] = day0
dp = day0 # 截至昨天的状态;每天先原样继承(当天不交易)
for day in range(1, days + 1):
prev_day = day - W - 1 # 两次交易至少隔 W 天:上次交易最晚在 prev_day
base = day0 if prev_day < 0 else hist[prev_day % ring]
dp = array("q", dp) # 继承「昨天也不交易」
if AS[day - 1]: # 买入:多持有 1..AS 股,每股花 AP
transfer_buy(base, AP[day - 1], AS[day - 1], max_p, dp)
if BS[day - 1]: # 卖出:少持有 1..BS 股,每股收 BP
transfer_sell(base, BP[day - 1], BS[day - 1], max_p, dp)
hist[day % ring] = dp
print(max(dp)) # 结束时持股任意(不交易就是 0 股),取钱数最大
if __name__ == "__main__":
solve()C++ 版(同一算法):
/**
* Author by Rainboy blog: https://rainboylv.com github: https://github.com/rainboylvx
* rbook: -> https://rbook.roj.ac.cn https://rbook2.roj.ac.cn
* rainboy的学习导航网站: https://idx.roj.ac.cn
* create_at: 2026-10-05 12:15
* update_at: 2026-10-05 12:15
*
* 股票交易(SCOI 2010 / 一本通 5.5 练习 4)
* DP:f[i][j] = 第 i 天结束时持有 j 股的最大现金。
* 第 i 天交易的前驱固定为第 p = max(i-W-1, 0) 天结束时的状态(间隔 W 天)。
* 买入/卖出转移各是一个随 j 平移的固定长度窗口最值,拆项后用单调队列摊还 O(1),
* 总复杂度 O(T * MaxP)。
*/
#include <cstdio>
#include <algorithm>
using namespace std;
typedef long long ll;
const int MAXN = 2005;
const ll NEG = -1000000000000000000LL; // 不可达哨兵:真实钱数下界约 -8e9,远离此值
int T, MaxP, W;
ll AP[MAXN], BP[MAXN]; // 第 i 天的买入价 / 卖出价
ll AS[MAXN], BS[MAXN]; // 第 i 天一次买入 / 卖出的股数上限
ll f[MAXN][MAXN]; // f[i][j]:第 i 天结束时持有 j 股的最大现金
int que[MAXN]; // 单调队列,存持股数下标
int qh, qt; // 队头 / 队尾(半开区间 [qh, qt))
// 买入转移:对每个 j,从窗口 j' ∈ [j-AS, j-1] 里取 f[p][j'] - AP*(j-j') 的最大值写进 f[i][j]
// 窗口里被比较的量 g(j') = f[p][j'] + AP*j' 与 j 无关,j 增大时窗口整体右移一格
void transfer_buy(int i, int p) {
ll price = AP[i];
ll limit = AS[i];
qh = 0, qt = 0;
for (int j = 1; j <= MaxP; j++) {
int nj = j - 1; // 窗口右端新进来的持股数
ll v = f[p][nj] + price * nj;
while (qt > qh && f[p][que[qt - 1]] + price * que[qt - 1] <= v)
qt--; // 值不大于新值的下标从队尾弹出,队列按 g 递减,队头即窗口最大
que[qt++] = nj;
int out = j - (int)limit - 1; // 刚滑出窗口左端的持股数;若还在队里必在队头
if (out >= 0 && que[qh] == out)
qh++;
int best = que[qh];
ll cand = f[p][best] + price * (best - j); // = f[p][best] - price*(j-best)
if (cand > f[i][j])
f[i][j] = cand;
}
}
// 卖出转移:对每个 j,从窗口 j' ∈ [j+1, j+BS] 里取 f[p][j'] + BP*(j'-j) 的最大值写进 f[i][j]
// j 从大到小枚举,窗口整体左移;队列下标递减、g 值递减,队头即窗口最大
void transfer_sell(int i, int p) {
ll price = BP[i];
ll limit = BS[i];
qh = 0, qt = 0;
for (int j = MaxP - 1; j >= 0; j--) {
while (qt > qh && que[qh] > j + limit)
qh++; // 队头是队里最大下标,先滑出窗口右端
int nj = j + 1; // 窗口左端新进来的持股数
ll v = f[p][nj] + price * nj;
while (qt > qh && f[p][que[qt - 1]] + price * que[qt - 1] <= v)
qt--; // 队尾值不大于新值则弹出(它下标更大、更早过期,被完全压制)
que[qt++] = nj;
int best = que[qh];
ll cand = f[p][best] + price * (best - j); // = f[p][best] + price*(best-j)
if (cand > f[i][j])
f[i][j] = cand;
}
}
int main() {
scanf("%d %d %d", &T, &MaxP, &W);
for (int i = 1; i <= T; i++)
scanf("%lld %lld %lld %lld", &AP[i], &BP[i], &AS[i], &BS[i]);
// 第 0 天开盘前:0 股 0 钱,其余状态不可达
for (int j = 1; j <= MaxP; j++)
f[0][j] = NEG;
for (int i = 1; i <= T; i++) {
int p = i - W - 1; // 上次交易最晚发生在第 p 天
if (p < 0)
p = 0;
for (int j = 0; j <= MaxP; j++)
f[i][j] = f[i - 1][j]; // 当天不交易:原样继承昨天
if (AS[i] > 0)
transfer_buy(i, p);
if (BS[i] > 0)
transfer_sell(i, p);
}
// 结束时持股任意(不交易即 0 股),对...
ll ans = f[T][0];
for (int j = 1; j <= MaxP; j++)
if (f[T][j] > ans)
ans = f[T][j];
printf("%lld\n", ans);
return 0;
}复杂度
- 时间复杂度:
——单调队列里每个持股数至多入队、出队各一次,每天买卖各扫一遍 ;与标准 C++ 解法(单调队列优化 DP)同阶。 时约 次基本操作。 - 空间复杂度:
——环形保存 天快照( array('q')每项 8 字节),最坏约 32 MB;朴素的整表约 64 MB,环形已把它压掉。
总结
本题的关键是把间隔规则翻译成前驱下标:第