使用平衡树(如 std::set)维护已出现的营业额,对于每天的营业额查询前驱和后继求最小差值,结合 Python 的 bisect 也可快速实现。
OJ: roj
题目 ID: 1565
难度:提高
标签:平衡树贪心
创建: 2026-10-09 08:56
更新: 2026-10-09 08:56
形式化题目
给定一个长度为
数据范围:
正解
思路
这道题需要我们在动态添加元素的过程中,快速求出一个新元素与集合中已有元素的最小差值。最小差值一定在与该元素值最接近的两个已有元素中产生,即在有序集合中该元素的前驱(小于等于它的最大元素)和后继(大于等于它的最小元素)中产生。
因此,我们可以维护一个动态有序的结构:
- 在 C++ 中,可以直接使用
std::set。对于新元素,使用 lower_bound找到大于等于的第一个元素,即它的后继;将该迭代器前移一位,即可得到它的前驱。两者与 差值的绝对值的最小值即为 。最后将 插入 set中。 - 在 Python 中,可以使用
bisect模块在有序列表list中查找插入位置,然后用list.insert插入。虽然列表插入最坏复杂度为,但对于 的规模可以豁免通过。
注意:
代码
#!/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-09 08:34
# update_at: 2026-10-09 08:34
import sys
from bisect import bisect_left, insort
def solve() -> None:
"""逐天累加最小波动值:第 i 天取已出现营业额的有序序列中 a[i] 的前驱与后继,
f_i = min(|a[i]-前驱|, |a[i]-后继|);第 1 天按题面规定直接取 a[1]。"""
data = iter(map(int, sys.stdin.buffer.read().split()))
n = next(data, None)
if n is None:
return
values = [next(data, None) for _ in range(n)] # 数据缺行时补 None 作保护
first = values[0] if values else None
if first is None:
return
total = first # 第一天的最小波动值 = 第一天的营业额
appeared: list[int] = [first] # 已经出现过的营业额,始终保持升序
for value in values[1:]:
if value is None:
break
pos = bisect_left(appeared, value) # 第一个 >= value 的位置,即后继
best = appeared[pos] - value if pos < len(appeared) else None
if pos > 0:
diff = value - appeared[pos - 1] # 前驱:小于 value 的最大元素
best = diff if best is None else min(best, diff)
total += best
insort(appeared, value) # 保持升序;list 插入为 O(n),整体 O(n^2),但常数小可过
print(total)
if __name__ == "__main__":
solve()/**
* 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-09 08:34
* update_at: 2026-10-09 08:34
*/
#include <iostream>
#include <set>
using namespace std;
typedef long long ll;
const int MAXN = 32770; // n < 2^15 = 32768,再留一点余量
const int INF = 2000000000; // 候选差值的哨兵,只需大于 max|a_i - a_j|
int n;
int a[MAXN]; // 每天的营业额;|a_i| <= 10^6,用 int 足够
void read_input() {
if (!(cin >> n)) return;
for (int i = 1; i <= n; ++i) {
cin >> a[i];
}
}
// 用有序集合维护「已经出现过的营业额」,逐天累加最小波动值:
// 第 i 天 (i >= 2) 在集合里取 a[i] 的前驱与后继,f_i = 两者差值绝对值的较小者;
// 第 1 天的最小波动值就是当天的营业额(题面规定),直接累加 a[1]。
void solve() {
set<int> appeared; // 已经出现过的营业额,始终有序
ll total = 0; // 答案;题面称不超过 2^31,仍用 64 位防临界溢出
for (int i = 1; i <= n; ++i) {
if (i == 1) {
total += a[i];
appeared.insert(a[i]);
continue;
}
// lower_bound 给出第一个 >= a[i] 的元素,即后继
set<int>::iterator it = appeared.lower_bound(a[i]);
int min_diff = INF; // i >= 2 时集合非空,前驱/后继至少命中一个,下界一定被更新
if (it != appeared.end()) {
int diff = *it - a[i];
if (diff < 0) diff = -diff;
if (diff < min_diff) min_diff = diff;
}
if (it != appeared.begin()) {
set<int>::iterator pred_it = it;
--pred_it; // 前驱:小于 a[i] 的最大元素
int diff = a[i] - *pred_it;
if (diff < 0) diff = -diff;
if (diff < min_diff) min_diff = diff;
}
total += min_diff;
appeared.insert(a[i]);
}
cout << total << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_input();
solve();
return 0;
}复杂度
- 时间复杂度:
- C++:每次
set::insert和set::lower_bound的复杂度均为,共 次操作,总时间复杂度为 。 - Python:使用
bisect查找为,但 list.insert为,整体时间复杂度为 。由于 ,底层内存移动依然能通过。
- C++:每次
- 空间复杂度:需要
的空间来存储出现的营业额。
总结
-
真实可取值与
int安全性(T9纪律):题面给的上界略超 int(,超出了 INT_MAX = 2147483647),但该上界是松的——真实可取值有更紧的上界。由,推导最大 (约 )。这比 INT_MAX有近 226 倍余量,因此int实际上是安全的。使用long long属规范/防御性选择,不是正确性要求。真实数据中最大答案仅为 5,743,365。 -
数据缺陷(负值问题):题面声称「接下来的
行每行有一个正整数」,但实际数据中 p8包含了 11,240 个负值,范围达(首个负值在第 209 行)。好在 对于负值同样天然成立,此缺陷不影响答案正确性,但与题面矛盾,影响输入合法性判断。 -
数据缺陷(CRLF 换行):实测 10 个数据点的
.out全部为 CRLF,这导致check_bytes.py严格校验时永远失败(且本题非多解题,无 SPJ 兜底)。 -
lower_bound与upper_bound的数学等价:将 C++ 的lower_bound替换为upper_bound测试时发现 0 失败,且不是数据盲区,而是真正的数学等价。upper_bound给出第一个的元素,其前驱恰好是 的最大元素。当 出现重复时,其前驱分支依然能给出差值 0,与 lower_bound完全一致。穷举,字母表 的 1364 组验证确认 0 分歧。 -
异源
暴力全等:使用不依赖 set/bisect的纯暴力扫描重算,C++ 版和 Python 版(耗时 3m23s)的输出在所有 10 点上与正解 10/10 全等。 -
反事实探针与探针鉴别力:
反事实 失败点数 最小复现 base 0/10 — f1 = 0(去掉total += a[i])10/10 printf '1\n42\n'→0(应42)只算前驱(严格 <的前驱覆盖后继)10/10 printf '3\n10\n3\n10\n'→24(应17)只算后继 10/10 printf '2\n1\n100\n'→2000000100(哨兵泄漏;应199)f_i = |a_i - a_{i-1}|(只看前一元素)9/10 printf '3\n10\n1\n20\n'→38(应29);★p9无鉴别力long long→int0/10 ⚠ 数据无法证伪(见前述分析) lower_bound→upper_bound0/10 ⚠ 数学等价 特别说明:
predonly只算严格前驱在重复值上会算错,这是因为题面允许相等情况(遇到重复值时应为 0)。 p9无鉴别力:由于p9数据仅有 2 个不同值(32765 次重复,几乎全是 999999),导致「只看前一元素」这种严重错误的算法在p9上依然侥幸通过。
-
自动化验证完备性:
- 无硬编码 (
V2):探针注入哨兵边界32770, 2e9,输出答案与 10 点正确答案交集为空。 - 异源对拍 (
V6):批次 1(大量重复/稀疏/边界)和批次 2(负值/混合符号)对拍测试中,main.cpp和main.py均为 0 分歧。 - 闭区间探测 (
V8):闭区间(前驱含等 + 后继含等)失败集为空。而错误的公式(只算前驱/只算后继等)失败集非空,证实探针确实在工作。 - 不变量 (
V4):in首行等于后续行数,全为整数无空行;out恰 1 行,结尾非负整数,out无多余空白。 - UBSan (
V9):实际数据运行时无 UB,对照组注入INT_MIN取反确实触发了报错,证明探针和数据均健康。
- 无硬编码 (
-
数据规模:实际测试数据
,数据中不存在 的边界点; 。 -
T9五项:无强制类型转换;int实际不会溢出;不涉及整除/取模运算、浮点格式化及字典序排序。