「Intervals」 区间

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

将区间覆盖要求转化为前缀和的不等式,利用差分约束系统求最长路得到最小元素数。包含首行格式自适应解析处理。

OJ: roj

题目 ID: 3194

难度:提高

标签:图论最短路贪心

创建: 2026-10-09 23:25

更新: 2026-10-09 23:25

形式化题目

给定 nn 个区间 [ai,bi][a_i, b_i] 以及对应的整数 cic_i。 需要构造一个整数集合 ZZ,使得对于任意 1≤i≤n1 \le i \le n,集合 ZZ 中满足 ai≤x≤bia_i \le x \le b_i 的整数 xx 的个数至少为 cic_i 个。 求使得条件成立的集合 ZZ 中最少包含多少个整数。

数据范围与约定:

  • 1≤n≤500001 \le n \le 50000
  • 0≤ai,bi≤500000 \le a_i, b_i \le 50000
  • 1≤ci≤bi−ai+11 \le c_i \le b_i - a_i + 1
  • 时间限制:1000 ms1000 \text{ ms}
  • 空间限制:128 MB128 \text{ MB}

输出格式: 输出一个整数表示结果。

正解

思路

本题是典型的差分约束系统。我们可以利用前缀和将区间的要求转化为不等式。

设 SxS_x 表示集合 ZZ 中小于等于 xx 的元素的个数(即前缀和)。 对于区间 [ai,bi][a_i, b_i] 中至少有 cic_i 个数的要求,可以表示为:

Sbi−Sai−1≥ci  ⟹  Sbi≥Sai−1+ciS_{b_i} - S_{a_i - 1} \ge c_i \implies S_{b_i} \ge S_{a_i - 1} + c_i

同时,前缀和数组 SxS_x 本身还隐含了两个基本限制:

  1. xx 处的元素最多只能被选择 1 次,即 Sx−Sx−1≤1  ⟹  Sx−1≥Sx−1S_x - S_{x-1} \le 1 \implies S_{x-1} \ge S_x - 1
  2. xx 处的元素不能被选择负数次,即 Sx−Sx−1≥0  ⟹  Sx≥Sx−1+0S_x - S_{x-1} \ge 0 \implies S_x \ge S_{x-1} + 0

通过上述不等式,我们将代数关系转换为图论中的最长路模型:对于不等式 v≥u+wv \ge u + w,我们连一条从 uu 到 vv、边权为 ww 的有向边。然后求出给定起点到终点的最长路。

坐标偏移与输入适配:

  1. 由于 aia_i 最小可能为 0,对应的 ai−1a_i - 1 可能会出现 −1-1,这不利于数组索引。因此将所有坐标统一向右偏移处理:令 u=ai+1u = a_i + 1,v=bi+2v = b_i + 2。这样所有操作的节点都在 1…500021 \dots 50002 之间,规避越界风险。
  2. 真实数据的第一行与题面存在出入。代码中利用读入的总 token 数量 (cnt−1)%3(cnt-1)\%3 来判定首行是一个整数还是两个整数,实现安全自适应解析。

求出出现的最小节点 min_vmin\_v 和最大节点 max_vmax\_v,从 min_vmin\_v 开始运行 SPFA 算法求解最长路。最终 dist[max_v]dist[max\_v] 即为集合 ZZ 的最少元素数量。

代码

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 21:30
# update_at: 2026-10-09 23:08
#
# ROJ 3194《Intervals 区间》:区间选点,前缀和转差分约束 + SPFA 最长路。
#   S_b - S_{a-1} >= c   => S_b >= S_{a-1} + c
#   S_x - S_{x-1} <= 1   => S_{x-1} >= S_x - 1
#   S_x - S_{x-1} >= 0   => S_x >= S_{x-1} + 0
# 答案 = 最长路终点值 dist[max_val]。
# 坐标整体 +1 规避 a_i = 0 时 a_i-1 = -1 的下标问题。

import sys
from collections import deque

NEG = -10 ** 9


def solve() -> None:
    data = list(map(int, sys.stdin.buffer.read().split()))
    if not data:
        return

    # 首行有两种格式:题面「第一行 n」(1 个 token)与官方数据「上界 n」(2 个 token)。
    # 每个区间恒占 3 个 token,故由 token 总数的模 3 余数唯一判定(1+3n≡1、2+3n≡2)。
    n, head = (data[0], 1) if (len(data) - 1) % 3 == 0 else (data[1], 2)

    adj = [[] for _ in range(50005)]
    min_val, max_val = 50005, 0
    for i in range(n):
        a, b, c = data[head + 3 * i] + 1, data[head + 3 * i + 1] + 1, data[head + 3 * i + 2]
        adj[a - 1].append((b, c))
        min_val, max_val = min(min_val, a - 1), max(max_val, b)

    for i in range(min_val, max_val):
        adj[i].append((i + 1, 0))
        adj[i + 1].append((i, -1))

    dist = [NEG] * (max_val + 2)
    inq = [False] * (max_val + 2)
    dist[min_val] = 0
    q = deque([min_val])
    inq[min_val] = True
    while q:
        u = q.popleft()
        inq[u] = False
        du = dist[u]
        for v, w in adj[u]:
            if du + w > dist[v]:
                dist[v] = du + w
                if not inq[v]:
                    q.append(v)
                    inq[v] = True

    print(dist[max_val])


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 21:30
 * update_at: 2026-10-09 23:08
 */
// ROJ 3194《Intervals 区间》:区间选点,差分约束 + SPFA 最长路。
//
// 设 S_x = 集合 Z 中 <= x 的整数个数(前缀和),区间约束 [a,b] >= c 变成
//     S_b - S_{a-1} >= c   =>   S_b >= S_{a-1} + c
// 再加上前缀和自身的两条限制:
//     S_x - S_{x-1} <= 1   =>   S_{x-1} >= S_x - 1
//     S_x - S_{x-1} >= 0   =>   S_x >= S_{x-1} + 0
// 全部化成 v >= u + w 的形式后连有向边 u->v 权 w,跑最长路,答案 = dist[终点]。
//
// 坐标偏移:a_i 可为 0,a_i-1 会到 -1,所以整体 +1,令 u=a_i、v=b_i+1(原坐标)。
#include <iostream>
#include <vector>
#include <queue>

using namespace std;

typedef long long ll;

const int MAXV = 50005;    // 坐标节点上限(原坐标 0..50000 各占一个节点)
const int MAXT = 200005;   // 输入 token 上限:2 + 3*50000
const int NEG = -1000000000;

struct Edge {
    int v;
    int w;
};

vector<Edge> adj[MAXV];    // adj[u] = 从 u 出发的长路边
int token[MAXT];           // 全部输入 token(用于自动识别首行格式)
int dist_val[MAXV];        // 最长路距离
bool in_queue[MAXV];       // SPFA 入队标记

void solve() {
    int cnt = 0;
    while (cnt < MAXT && (cin >> token[cnt])) cnt++;
    if (cnt == 0) return;

    // 首行格式有两种:题面写的是「第一行 n」(1 个 token),
    // 官方数据实测首行是「上界 n」(2 个 token)。用 token 总数 3 同余自动判定:
    // 1 + 3n ≡ 1,2 + 3n ≡ 2 (mod 3),两者绝不相混。
    int n;
    int head;
    if ((cnt - 1) % 3 == 0) { n = token[0]; head = 1; }
    else { n = token[1]; head = 2; }

    int min_val = MAXV;
    int max_val = 0;

    for (int i = 0; i < n; i++) {
        int a = token[head + 3 * i] + 1;          // 坐标右移 1,等价于用 a_i-1 建点
        int b = token[head + 3 * i + 1] + 1;
        int c = token[head + 3 * i + 2];
        adj[a - 1].push_back({b, c});   // S_{b} >= S_{a-1} + c
        if (a - 1 < min_val) min_val = a - 1;
        if (b > max_val) max_val = b;
    }

    // 前缀和的相邻两条限制:S_{x} >= S_{x-1} + 0 与 S_{x-1} >= S_{x} - 1
    for (int i = min_val; i < max_val; i++) {
        adj[i].push_back({i + 1, 0});
        adj[i + 1].push_back({i, -1});
    }

    for (int i = 0; i <= max_val + 1; i++) {
        dist_val[i] = NEG;
        in_queue[i] = false;
    }

    // 起点是 min_val:它下面没有任何约束,取 dist = 0 即 S_{min_val} = 0
    queue<int> q;
    dist_val[min_val] = 0;
    q.push(min_val);
    in_queue[min_val] = true;

    while (!q.empty()) {
        int u = q.front();
        q.pop();
        in_queue[u] = false;

        for (int k = 0; k < (int)adj[u].size(); k++) {
            int v = adj[u][k].v;
            int w = adj[u][k].w;
            if (dist_val[u] + w > dist_val[v]) {
                dist_val[v] = dist_val[u] + w;
                if (!in_queue[v]) {
                    q.push(v);
                    in_queue[v] = true;
                }
            }
        }
    }

    cout << dist_val[max_val] << "\n";
}

int main() {
    ios_base::sync_with_stdio(false);
    cin.tie(NULL);
    solve();
    return 0;
}

复杂度

  • 时间复杂度:建图复杂度为 O(N+M)O(N + M),其中 MM 为值域范围。由于不存在正环且本题的隐式图结构偏向线性,SPFA 求最长路在此图上运行极快,时间复杂度近似为 O(k⋅E)O(k \cdot E)(kk 为较小常数),能够稳定通过 10510^5 级别的数据。
  • 空间复杂度:需要存储图的边集和距离数组。点数与边数均在 1.5×1051.5 \times 10^5 以内,空间消耗为 O(N+M)O(N + M),远低于 128 MB128 \text{ MB} 的限制。

总结

1. 算法与数据结构差异(ref 异源核对)

  • 正解 (main.cpp):前缀和差分约束 + SPFA 最长路(【图论】)。
  • 参考对照组 (ref.cpp):按右端点升序排序 + 从右往左补点,使用 BIT 统计已选点数(【贪心】,O(nlog⁡C)O(n \log C))。
  • 两者【无共享数据结构与推导】(图论的 dist 数组 vs 贪心的 BIT 区间求和),能够提供极强的异源交叉验证能力。

2. 证据层升级与对拍规模

  • 本地 data/ 中的数据文件(inputN.txt 等)解压后与官方归档完全一致,证据强度为最高级别的【与官方 .out 比对】。
  • 异源跨源对拍进行了 857 组并且达到 0 分歧:
    • 300 组【穷举真值】(坐标 ≤8\le 8、n≤4n \le 4,枚举 292^9 个 ZZ 集合,这是最高强度的验证方式)。
    • 7 组结构性数据。
    • 400 组中规模随机(n≤100n \le 100,坐标 ≤2000\le 2000)。
    • 150 组值域上界数据(坐标 ≤50000\le 50000)。

3. 原专家实现 9/10 FAIL —— 根因是输入格式

  • 官方真实数据的首行是 X n(即坐标上界 XX 与区间数 nn 共两个 token),而题面只提到“第一行包含整数 nn”。
  • 原代码直接将首 token 当作 nn 读入,错把 XX 当成了 nn(例如读到 248248),导致 2 点 TLE、7 点输出了错误极值。
  • 修复方案:【不改算法】,只加头部自适应判定。利用总 token 数 cnt 进行校验,1+3n ≡ 1 (mod 3) 和 2+3n ≡ 2 (mod 3) 两者绝不相等,从而做到唯一、安全的头部判定。

4. 闸门静默空转的真因

  • datafiles.find_inputs 只 glob 匹配 *.in 后缀,而本题的数据文件名居然是 input0..9.txt 和 output0..9.txt。这是【全仓 1468 题中唯一这种命名】,导致了闸门误报“data/ 为空(跳过)”。
  • worker 影子实验将数据改名为 *.in 后,修复版代码能够稳稳跑通 10/10,证明闸门本身的验证逻辑依然有效。

5. 题面过严的两处缺陷

  1. ci=0c_i = 0 实际上出现了 1333/17593 条记录(占比 7.6%7.6\%),但这违背了题面所写的 1≤ci1 \le c_i。
  2. 实测中 min⁡(ai)=1\min(a_i) = 1,数据中【从不出现 ai=0a_i = 0】。导致「坐标 +1+1 偏移」在真实数据上【不承重】,去掉偏移仍能 10/1010/10 满分通过。但在针对 ai=0a_i = 0 构造的输入上会直接 SIGSEGV。

6. 官方数据的自洽性

  • 首 token X≥max⁡(bi)X \ge \max(b_i) 全部成立。
  • 所有坐标均 ∈[1,X]\in [1, X]。
  • 数据中 n==n == 实际包含的区间条数。 这三个特性保障了数据整体自洽,完全可行。

7. V1 反事实表与样例承重

探针 真实数据结果
基线 main.cpp PASS=10 FAIL=0
基线 ref.cpp(贪心) PASS=10 FAIL=0
>=c 改成 >c PASS=0 FAIL=10(p0 TLE)
丢边 S_{x-1} >= S_x - 1 PASS=1 FAIL=9(p0 got=224 exp=246)
丢边 S_x >= S_{x-1} + 0 PASS=2 FAIL=8(p0 got=245 exp=246)
丢坐标 +1 偏移 PASS=10(数据无 a_i=0,不承重)—— 但 a_i=0 输入 SIGSEGV
源点 dist[min_val] 不置 0 PASS=0 FAIL=10(-999999755)
答案取 dist[max_val-1] PASS=7 FAIL=3
答案取 dist[max_val+1] PASS=0 FAIL=10(-1000000000)
★ 离散化 b 侧节点少 1(b 不 +1) PASS=0 FAIL=10(p0 TLE)
★ 离散化 b 侧节点多 1(b+2) PASS=1 FAIL=9(p0 got=243)
0 权边改 1 PASS=1 FAIL=9(p0 got=248)
★ 只认 2-token 头 PASS=10(真实数据【无法区分】) —— 但题面样例 got=8 exp=6 ⇒ 【样例才承重】
只认 1-token 头(=原专家实现) PASS=1 FAIL=9(2147483647)
★ 头部判定 (cnt-1)%3 改成 cnt%3 PASS=10(数据无法区分) —— 但样例 got=8 exp=6
ref 贪心改按左端点排序 PASS=1 FAIL=9(p0 got=247)

关键洞察:哪些证据能区分什么

  • 注意表中的「只认 2-token 头」与「头部判定错改」。因为真实数据中必然全是 2-token 头,这两个探针在真实数据上都拿到了 PASS=10,无法被区分出来!
  • 但如果在题面样例上测试,样例是 1-token 头,导致错误解析,分别输出了 got=8 而非期待的 6。
  • 结论:真实数据加上题面样例**【共同】**才能完全钉死头部的解析规则。这是“哪些证据能区分什么”的又一典型实例。

8. T9 五项

  1. 题目翻译是否准确:是
  2. 题目描述是否清晰:否(输入格式未说明首行的坐标上界,数据边界描述也比实际数据要严苛)
  3. 题目格式是否符合规范:是
  4. 测试数据是否正确:是(数据自洽且与官方归档一致)
  5. 题解代码是否通过:是(通过与官方 .out 比对,以及 857 组异源对拍 0 分歧验证)