「一本通 4.6 例 1」营业额统计

GitHub跳转原题关系图返回列表

使用平衡树(如 std::set)维护已出现的营业额,对于每天的营业额查询前驱和后继求最小差值,结合 Python 的 bisect 也可快速实现。

OJ: roj

题目 ID: 1565

难度:提高

标签:平衡树贪心

创建: 2026-10-09 08:56

更新: 2026-10-09 08:56

形式化题目

给定一个长度为 nn 的数列 a1,a2,…,ana_1, a_2, \dots, a_n。对于 i≥2i \ge 2,定义 fi=min⁡1≤j<i∣ai−aj∣f_i = \min_{1 \le j < i} |a_i - a_j|。特别地,f1=a1f_1 = a_1。求 ∑i=1nfi\sum_{i=1}^n f_i 的值。

数据范围:1≤n<2151 \le n < 2^{15},ai≤106a_i \le 10^6。题面称结果不超过 2312^{31}。

正解

思路

这道题需要我们在动态添加元素的过程中,快速求出一个新元素与集合中已有元素的最小差值。最小差值一定在与该元素值最接近的两个已有元素中产生,即在有序集合中该元素的前驱(小于等于它的最大元素)和后继(大于等于它的最小元素)中产生。

因此,我们可以维护一个动态有序的结构:

  • 在 C++ 中,可以直接使用 std::set。对于新元素 aia_i,使用 lower_bound 找到大于等于 aia_i 的第一个元素,即它的后继;将该迭代器前移一位,即可得到它的前驱。两者与 aia_i 差值的绝对值的最小值即为 fif_i。最后将 aia_i 插入 set 中。
  • 在 Python 中,可以使用 bisect 模块在有序列表 list 中查找插入位置,然后用 list.insert 插入。虽然列表插入最坏复杂度为 O(n)\mathcal{O}(n),但对于 n≤32767n \le 32767 的规模可以豁免通过。

注意:f1=a1f_1 = a_1 是题面明确规定的,需要特殊处理。由于数据中可能存在相同的营业额,差值为 0。公式 fi=∣ai−aj∣f_i = |a_i - a_j| 对负数同样天然成立。

代码

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-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()
cpp
/**
 * 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 的复杂度均为 O(log⁡n)\mathcal{O}(\log n),共 nn 次操作,总时间复杂度为 O(nlog⁡n)\mathcal{O}(n \log n)。
    • Python:使用 bisect 查找为 O(log⁡n)\mathcal{O}(\log n),但 list.insert 为 O(n)\mathcal{O}(n),整体时间复杂度为 O(n2)\mathcal{O}(n^2)。由于 n≤32767n \le 32767,底层内存移动依然能通过。
  • 空间复杂度:需要 O(n)\mathcal{O}(n) 的空间来存储出现的营业额。

总结

  1. 真实可取值与 int 安全性(T9 纪律):题面给的 2312^{31} 上界略超 int(231=21474836482^{31} = 2147483648,超出了 INT_MAX = 2147483647),但该上界是松的——真实可取值有更紧的上界。由 n≤32767,∣ai∣≤106n \le 32767, |a_i| \le 10^6,推导最大 ∑fi≤9,499,939\sum f_i \le 9,499,939(约 9.5×1069.5\times10^6)。这比 INT_MAX 有近 226 倍余量,因此 int 实际上是安全的。使用 long long 属规范/防御性选择,不是正确性要求。真实数据中最大答案仅为 5,743,365。

  2. 数据缺陷(负值问题):题面声称「接下来的 nn 行每行有一个正整数」,但实际数据中 p8 包含了 11,240 个负值,范围达 [−32736,−17][-32736, -17](首个负值在第 209 行)。好在 fi=∣ai−aj∣f_i = |a_i - a_j| 对于负值同样天然成立,此缺陷不影响答案正确性,但与题面矛盾,影响输入合法性判断。

  3. 数据缺陷(CRLF 换行):实测 10 个数据点的 .out 全部为 CRLF,这导致 check_bytes.py 严格校验时永远失败(且本题非多解题,无 SPJ 兜底)。

  4. lower_bound 与 upper_bound 的数学等价:将 C++ 的 lower_bound 替换为 upper_bound 测试时发现 0 失败,且不是数据盲区,而是真正的数学等价。upper_bound 给出第一个 >ai> a_i 的元素,其前驱恰好是 ≤ai\le a_i 的最大元素。当 aia_i 出现重复时,其前驱分支依然能给出差值 0,与 lower_bound 完全一致。穷举 n≤5n \le 5,字母表 1..41..4 的 1364 组验证确认 0 分歧。

  5. 异源 O(n2)\mathcal{O}(n^2) 暴力全等:使用不依赖 set/bisect 的纯暴力 O(n2)\mathcal{O}(n^2) 扫描重算,C++ 版和 Python 版(耗时 3m23s)的输出在所有 10 点上与正解 10/10 全等。

  6. 反事实探针与探针鉴别力:

    反事实 失败点数 最小复现
    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 → int 0/10 ⚠ 数据无法证伪(见前述分析)
    lower_bound → upper_bound 0/10 ⚠ 数学等价

    特别说明:

    • predonly 只算严格前驱在重复值上会算错,这是因为题面允许相等情况(遇到重复值时 fif_i 应为 0)。
    • p9 无鉴别力:由于 p9 数据仅有 2 个不同值(32765 次重复,几乎全是 999999),导致「只看前一元素」这种严重错误的算法在 p9 上依然侥幸通过。
  7. 自动化验证完备性:

    • 无硬编码 (V2):探针注入哨兵边界 32770, 2e9,输出答案与 10 点正确答案交集为空。
    • 异源对拍 (V6):批次 1(大量重复/稀疏/边界)和批次 2(负值/混合符号)对拍测试中,main.cpp 和 main.py 均为 0 分歧。
    • 闭区间探测 (V8):闭区间(前驱含等 + 后继含等)失败集为空。而错误的公式(只算前驱/只算后继等)失败集非空,证实探针确实在工作。
    • 不变量 (V4):in 首行等于后续行数,全为整数无空行;out 恰 1 行,结尾非负整数,out 无多余空白。
    • UBSan (V9):实际数据运行时无 UB,对照组注入 INT_MIN 取反确实触发了报错,证明探针和数据均健康。
  8. 数据规模:实际测试数据 n∈[10,32767]n \in [10, 32767],数据中不存在 n=1n=1 的边界点;max⁡ai=999999≤106\max a_i = 999999 \le 10^6。

  9. T9 五项:无强制类型转换;int 实际不会溢出;不涉及整除/取模运算、浮点格式化及字典序排序。