将区间覆盖要求转化为前缀和的不等式,利用差分约束系统求最长路得到最小元素数。包含首行格式自适应解析处理。
OJ: roj
题目 ID: 3194
难度:提高
标签:图论最短路贪心
创建: 2026-10-09 23:25
更新: 2026-10-09 23:25
目录
形式化题目
给定
数据范围与约定:
- 时间限制:
- 空间限制:
输出格式: 输出一个整数表示结果。
正解
思路
本题是典型的差分约束系统。我们可以利用前缀和将区间的要求转化为不等式。
设
同时,前缀和数组
处的元素最多只能被选择 1 次,即 处的元素不能被选择负数次,即
通过上述不等式,我们将代数关系转换为图论中的最长路模型:对于不等式
坐标偏移与输入适配:
- 由于
最小可能为 0,对应的 可能会出现 ,这不利于数组索引。因此将所有坐标统一向右偏移处理:令 , 。这样所有操作的节点都在 之间,规避越界风险。 - 真实数据的第一行与题面存在出入。代码中利用读入的总 token 数量
来判定首行是一个整数还是两个整数,实现安全自适应解析。
求出出现的最小节点
代码
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;
}复杂度
- 时间复杂度:建图复杂度为
,其中 为值域范围。由于不存在正环且本题的隐式图结构偏向线性,SPFA 求最长路在此图上运行极快,时间复杂度近似为 ( 为较小常数),能够稳定通过 级别的数据。 - 空间复杂度:需要存储图的边集和距离数组。点数与边数均在
以内,空间消耗为 ,远低于 的限制。
总结
1. 算法与数据结构差异(ref 异源核对)
- 正解 (
main.cpp):前缀和差分约束 + SPFA 最长路(【图论】)。 - 参考对照组 (
ref.cpp):按右端点升序排序 + 从右往左补点,使用 BIT 统计已选点数(【贪心】,)。 - 两者【无共享数据结构与推导】(图论的 dist 数组 vs 贪心的 BIT 区间求和),能够提供极强的异源交叉验证能力。
2. 证据层升级与对拍规模
- 本地
data/中的数据文件(inputN.txt等)解压后与官方归档完全一致,证据强度为最高级别的【与官方 .out 比对】。 - 异源跨源对拍进行了 857 组并且达到 0 分歧:
- 300 组【穷举真值】(坐标
、 ,枚举 个 集合,这是最高强度的验证方式)。 - 7 组结构性数据。
- 400 组中规模随机(
,坐标 )。 - 150 组值域上界数据(坐标
)。
- 300 组【穷举真值】(坐标
3. 原专家实现 9/10 FAIL —— 根因是输入格式
- 官方真实数据的首行是
X n(即坐标上界与区间数 共两个 token),而题面只提到“第一行包含整数 ”。 - 原代码直接将首 token 当作
读入,错把 当成了 (例如读到 ),导致 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. 题面过严的两处缺陷
实际上出现了 1333/17593 条记录(占比 ),但这违背了题面所写的 。 - 实测中
,数据中【从不出现 】。导致「坐标 偏移」在真实数据上【不承重】,去掉偏移仍能 满分通过。但在针对 构造的输入上会直接 SIGSEGV。
6. 官方数据的自洽性
- 首 token
全部成立。 - 所有坐标均
。 - 数据中
实际包含的区间条数。 这三个特性保障了数据整体自洽,完全可行。
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 五项
- 题目翻译是否准确:是
- 题目描述是否清晰:否(输入格式未说明首行的坐标上界,数据边界描述也比实际数据要严苛)
- 题目格式是否符合规范:是
- 测试数据是否正确:是(数据自洽且与官方归档一致)
- 题解代码是否通过:是(通过与官方
.out比对,以及 857 组异源对拍 0 分歧验证)