[CSP-X2023 山东] 赚钱 的题解
在解题之前提交题解的代码会导致封禁。
作者:
概述
小 A 要从编号 的城市出发,按编号从小到大依次经过共
个城市,第
个城市的纪念品价格为
。他必须在某个城市买入 1 个纪念品,并在编号更大的某个城市把它卖掉(不能在同一个城市买入后立刻卖出)。要求算出最多能赚多少钱,亏钱时答案可以是负数。
形式化地说:给定长为 的正整数序列
,求
数据范围:,
;其中
的数据满足
。
分析
核心观察
卖出城市 一旦确定,买入城市
的选择与前缀之外的数完全无关:在
这些城市中,买价格最低的那个一定最划算。于是
这是因为对任意合法的 都有
,右侧这种取法不会漏掉任何更优的方案。
要注意下标是严格小于:处理第 个城市时,用于买入的那段最小值只能由前
个城市构成,第
个城市自己不能参与,否则就变成了同一城市买入又卖出。
思路
直接枚举买入城市和卖出城市是 的,当
时约有
对,必然超时。
利用核心观察,可以把「前缀最小值」在扫描过程中增量维护。设 mn 表示当前城市之前出现过的最低价,按编号从小到大读入价格,对每个 依次做两件事:
- 用
更新答案,这一步对应「在第
个城市卖出,在之前的最低价处买入」;
- 再把
并入
mn,让后面的城市可以把它当作买入点。
两步的顺序不能颠倒:如果先并入、再算答案,就等价于允许在同一个城市买入又卖出,样例 2 会由 变成
。
此外,题目允许亏钱,答案可能为负,所以答案初值必须取一个极小值(如 ),不能写成
。
具体示例
以样例 1 的 为例,
mn 初值为无穷大、ans 初值为负无穷,逐项模拟:
计算前的 mn |
候选 |
更新后的 ans |
更新后的 mn |
||
|---|---|---|---|---|---|
| 无穷大 | 无效 | 负无穷 | |||
最终输出 ,对应在第 2 个城市花
元买入、在第 4 个城市花
元卖出。
再看样例 2 的 ,逐项算出的候选值依次是
,答案为
(第 2 个城市买入、第 3 个城市卖出,亏
元)。这个例子同时说明两点:答案可以为负,
ans 的初值不能是 ;并且这一步必须用「当前城市之前」的最小值,否则第二个城市会拿自己和自己作差,算出错误的
。
算法步骤
- 读入城市个数
。
- 令
mn为正无穷(取一个比任何价格都大的数,如),
ans为负无穷(取)。
依次读入每个价格
:
- 令
ans对取较大值(在第
个城市卖出);
- 令
mn对取较小值(第
个城市可以成为后面的买入点)。
- 令
输出
ans。
整个过程边读边算,不需要保存整个序列。
复杂度分析
- 时间复杂度:
。每个价格只做一次减法、两次比较,共
轮。
- 空间复杂度:
。只需要
mn、ans两个变量和当前读入的值,不保存整个序列。
时运算量约
次,任何语言的普通读入都能轻松通过。
实现注意事项
- 先算答案、后更新最小值。顺序写反就等价于允许同一城市买入又卖出,样例 2 会从
变成
。
- 初值要足够小。答案可能为负,
ans初值设成或
都是错的,应取
或更小;同理,正无穷一侧也要真的比所有价格大。
- 用 64 位整数。价格本身不超过
,但初值
远超 32 位
int的范围,统一用long long最稳妥。 - 第一个城市没有可以买入的前驱,它对应的候选值由负无穷兜住,不会影响答案。
- 输入规模只有
个数,用普通
cin/scanf即可;用cin时习惯性关掉同步(ios::sync_with_stdio(false); cin.tie(nullptr);)更快。
源代码
#include <cstdio>
#include <algorithm>
int main() {
int n;
if (scanf("%d", &n) != 1) return 0;
// 只能「先买后卖」,所以答案 = max_{j < i} (a_i - a_j)。
// 固定卖出城市 i,买入城市取 i 之前价格最低的那个即可,边扫边维护前缀最小值。
// 答案可能为负(全程降价),初值要给得足够小。
const long long INF = (1LL << 62);
long long mn = INF, ans = -INF;
for (int i = 0; i < n; ++i) {
long long x;
scanf("%lld", &x);
ans = std::max(ans, x - mn); // 在 i 卖出,在之前的最低点买入
mn = std::min(mn, x); // 把 i 也纳入「之前」
}
printf("%lld\n", ans);
return 0;
}
import sys
def solve(a):
"""O(n) 贪心:固定卖出城市,买入价取它之前的最小值。"""
INF = 1 << 62
mn, ans = INF, -INF # 答案可能为负,初值取极小
for x in a:
if x - mn > ans: # 先结算:在第 i 个城市卖出
ans = x - mn
if x < mn: # 再更新:第 i 个城市可以成为后面的买点
mn = x
return ans
def brute(a):
"""O(n^2) 暴力:枚举所有 j < i。只适合小数据。"""
return max(a[i] - a[j] for j in range(len(a)) for i in range(j + 1, len(a)))
def main():
data = list(map(int, sys.stdin.buffer.read().split()))
n = data[0]
a = data[1:1 + n]
print(solve(a))
if __name__ == "__main__":
main()
评论