对每个卖出时刻只需知道它左边出现过的最低价,扫一遍滚动维护前缀最小值,把 O(n²) 的两两枚举压成 O(n)。
OJ: roj
题目 ID: 1417
难度:入门
标签:枚举前缀最小值入门python
创建: 2026-10-29 19:40
更新: 2026-10-29 20:19
形式化题目
给定长度为
其中
下标的大小关系
以样例 5 3 1 3 4 3 为例,取
暴力解法
思路
把题面要求直接翻译成两层循环。题目问的是「哪一对买入卖出时刻最划算」,
所以按卖出时刻分组:外层枚举卖出时刻
选这个视角而不是「外层买入、内层卖出」,是因为题目求的是
倒过来以买入时刻分组,同样能归并成「右边的最大值」,只是要倒着扫一遍。 两种写法对称,复杂度也一样;本文按从左往右的扫描讲,是因为它与输入顺序一致,代码更直白。
这个写法的正确性不需要额外论证:它枚举了全部合法下标对,一个不漏。
用样例 5 3 1 3 4 3 把每一行重扫左侧的结果列出来:
| 卖出 |
价格 |
重扫的左侧 |
本行最大 |
|---|---|---|---|
| 1 | 3 | 0 | |
| 2 | 1 | 0,1 | |
| 3 | 3 | 0,1,2 | |
| 4 | 4 | 0,1,2,3 | |
| 5 | 3 | 0,1,2,3,4 |
表中每一行是一次外层循环,第二列是这一行重扫左侧时的卖出价, 第四列是这一行内层循环跑出来的最大值。五行取最大得到 3,与样例输出一致。
表格还暴露了一件事:第 4 行取到最优时,左侧重扫出的最小值是 1, 也就是整个序列到此刻为止的最低价格。换句话,每一行真正被用上的只有左侧的最小值。
代码
/**
* 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-29 19:35
* update_at: 2026-10-29 20:19
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
// 按「每个卖出时刻各挑一个买入时刻」的视角写:外层枚举卖出时刻 j,
// 内层把它左边所有 i < j 重新扫一遍,逐个算 R[j] - R[i] 取本行最大。
// 每个 j 都从头重扫左侧,共 O(n^2) 次比较,只适合 n <= 5000 的小数据。
#include <cstdio>
typedef long long ll;
const int MAXN = 5001; // 暴力只处理 n <= 5000 的小规模数据
ll n;
ll r[MAXN]; // r[i]:第 i 个时刻的价格
int main() {
if (scanf("%lld", &n) != 1) return 0;
if (n > MAXN - 1) { // 超出暴力适用范围,直接退出,避免越界
fprintf(stderr, "brute.cpp 仅支持 n <= %d\n", MAXN - 1);
return 0;
}
for (ll i = 0; i < n; i++) {
scanf("%lld", &r[i]);
}
ll ans = -2000000000LL; // 答案可能为负,初值要比 -1e9 更小
// 外层固定卖出时刻 j,内层把左侧的买入时刻 i 全部重扫一遍
for (ll j = 1; j < n; j++) {
for (ll i = 0; i < j; i++) {
ll profit = r[j] - r[i];
if (profit > ans) ans = profit;
}
}
printf("%lld\n", ans);
return 0;
}复杂度
- 时间:
,比较次数为 。 - 空间:
,需要把整个价格序列存进数组,两层循环才能随机访问。
瓶颈
瓶颈不是「枚举」这个动作,而是左侧元素被反复重扫。
扫到
从表格里也能直接看到这层浪费:第 1 行重扫 1 个元素,第 2 行重扫 2 个,……,第 5 行重扫 5 个, 而五行真正有用的信息其实只有「左侧最小值」这一个数。
如果能把「左侧集合」压缩成一个可以增量更新的量,重复扫描就没有必要了。
正解
关键观察
对固定的卖出时刻
于是把答案改写成一个只依赖「左侧最小值」的形式:
式子里
而这个最小值可以增量维护:扫到
思路
用一个变量 min_price 记住「到目前为止见过的最低价」,扫描过程只有三个动作:
- 读入当前价格
; - 用
更新答案; - 用
更新 min_price。
第 2 步和第 3 步的顺序不能交换。若先更新 min_price 再算利润,-1 就永远算不出来。
把「先算利润」写在前面的效果是:min_price 里只装着
用样例 1 走一遍,观察两个变量的变化:
进入循环时的 min_price |
本次利润 |
更新后的 ans |
更新后的 min_price |
||
|---|---|---|---|---|---|
| 1 | 3 | 5 | 3 | ||
| 2 | 1 | 3 | 1 | ||
| 3 | 3 | 1 | 1 | ||
| 4 | 4 | 1 | 1 | ||
| 5 | 3 | 1 | 1 |
表中第 min_price 降到 1 之后再没变过,后面三次卖出都拿它当买入价。
最终 ans = 3,出现在
正确性。 要证明的是:算法算出的
- 对每个
, 一定在某个具体的下标 上取到。 - 于是
就是一个合法的买入卖出对,按定义不超过 。 - 每个
都不超过 ,取最大后 。
- 设题面的最优对是
,则 。 是 上的最小值,而 就在这个范围里,所以 。 - 两边同取相反数得
,加上 得 。 - 算法在
这一轮算出的正是左端,所以 。
两个方向合起来得
两个方向缺一不可:只证
代码
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-10-29 19:35
# update_at: 2026-10-29 19:35
import sys
NEG_INF = -10**18 # 答案可能为负(价格一路下跌),初值取得足够小
def solve() -> None:
"""一遍扫描维护见过的最低价,对每个卖出时刻算出它能拿到的最大利润。"""
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data)
min_price = next(data) # 第 0 个时刻只能当买入点,先把它记为最低价
ans = NEG_INF
for _ in range(n - 1):
price = next(data)
# 先算利润、再更新最低价,保证买入时刻严格早于卖出时刻
if price - min_price > ans:
ans = price - min_price
if price < min_price:
min_price = price
print(ans)
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-29 19:35
* update_at: 2026-10-29 19:35
*/
// main.cpp:ROJ 1417 最大利润(AOJ ALDS1_1_D)。
// 从左往右扫一遍,维护「扫到当前位置之前出现过的最低价格」min_price。
// 对每个卖出时刻 j,它能拿到的最大利润就是 R[j] - min_price,用它更新答案。
// 价格只被用一次,不必落地成数组:读到就更新 min_price,额外空间 O(1)。
#include <cstdio>
typedef long long ll;
const ll NEG_INF = -2000000000LL; // 答案可能为负(价格一路下跌),初值要比 -1e9 更小
ll n;
ll min_price; // min(R[0..j-1]):扫到当前时刻之前出现过的最低价格
ll ans; // 目前见过的最好利润
void solve() {
if (scanf("%lld", &n) != 1) return; // 单组输入:第一行 n
ll price;
scanf("%lld", &price);
min_price = price; // 第 0 个时刻只能当买入点,先把它记为最低价
ans = NEG_INF;
for (ll j = 1; j < n; j++) {
scanf("%lld", &price);
// 先算利润、再更新最低价,保证买入时刻严格早于卖出时刻
ll profit = price - min_price;
if (profit > ans) ans = profit;
if (price < min_price) min_price = price;
}
printf("%lld\n", ans);
}
int main() {
solve();
return 0;
}复杂度
- 时间:
。每个价格只被读一次,参与一次减法、两次比较,共 次操作。 - 空间:算法本身只用
n、min_price、ans、price几个标量,额外空间,价格读到就处理,不必存成数组。
C++ 版的 scanf 逐个读取,确实只有常数个变量;Python 版按本仓库的读入惯例用
sys.stdin.buffer.read().split() 一次性取出全部数字,输入缓冲本身占
对比暴力层的
总结
本题的解法只有一步:把「枚举所有买入卖出对」换成「对每个卖出时刻,只关心它左边的最低价」。
这一步成立靠两个条件,都值得记住:
- 目标是
,它对 单调递减,所以固定 后最优 唯一确定,就是左侧最小值所在的位置。 - 左侧集合每次只增长一个元素,所以这个最小值能增量维护,不需要回头重扫。
实现上唯一容易写错的是顺序:必须先用旧的最小值算利润,再把当前价格并入最小值。
写成反序时程序不会报错,样例 1 甚至照样输出 3,但样例 2 会输出 0 而不是 -1,
一路下跌的数据全都会错。这类「顺序决定约束是否成立」的细节,用一个能暴露负答案的样例就能测出来。