题单 · 习题解析

洛谷 P1803 凌乱的yyy / 线段覆盖

★★★ 顶格 n = 10⁶ 时 2ⁿ 暴力那个参照物没了 —— 而能跑顶格的参照物不必是暴力;外加「触发条件是两层的」:有端点重合 191/300,真被抓 3/300

原题:洛谷 P1803出自 第 19 章 贪心基础:排序型贪心 的题单题面本地存档:2026-08-29
⚠ 先自己写一遍,再往下看

这一页不是标准答案,是一条阶梯:把大多数人真实会写出的第一版摆出来, 看它错在哪、慢在哪,再一步一步改。所以它对「已经自己动手撞过一次墙」的人最有用。
还没写过的话,先去写 —— 写出来的哪怕是错的,也比直接看这一页收获大。

0题目原文(本地存了一份)

为什么要在这儿抄一份题面

原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。

转录自洛谷 P1803,日期见页头。两边不一致时信原站。

题目背景

快 noip 了,yyy 很紧张!

题目描述

现在各大 oj 上有 n 个比赛,每个比赛的开始、结束的时间点是知道的。

yyy 认为,参加越多的比赛,noip 就能考的越好(假的)。

所以,他想知道他最多能参加几个比赛。

由于 yyy 是蒟蒻,如果要参加一个比赛必须善始善终,而且不能同时参加 2 个及以上的比赛

输入格式

第一行是一个整数 n,接下来 n 行每行是 2 个整数 aᵢ, bᵢ (aᵢ < bᵢ),表示比赛开始、结束的时间。

输出格式

一个整数,最多参加的比赛数目。

数据规模与约定

  • 对于 20% 的数据,n ≤ 10
  • 对于 50% 的数据,n ≤ 10³
  • 对于 70% 的数据,n ≤ 10⁵
  • 对于 100% 的数据,1 ≤ n ≤ 10⁶0 ≤ aᵢ < bᵢ ≤ 10⁶

输入输出样例

输入

3
0 2
2 4
1 3

输出

2

三场比赛 [0,2][2,4][1,3],最多参加 2 场(选第一场和第二场)。 ★ 注意这组样例是特意造的[0,2][2,4] 端点正好重合 —— 第 ④ 步会说明它挡住了什么。

★ 这是第 19 章的第二道本章原题 —— 这一页讲「顶格」

第 19 章第 ⑨ 步起讲的就是它,而且算法证完了: 按右端点从早到晚排序、能选就选,交换论证(结束得越早,留给后面的时间越多)。 正文还用 2ⁿ 枚举子集对拍钉死了三种排法(第 ⑫ 步那张表)。

⇒ 这一页不重复那些。它讲的是正文没碰的那一半:题面写着 n ≤ 10⁶

而这一句话一改,最先塌掉的不是算法,是对拍 —— 因为 2ⁿ 那个参照物没了。

1正解:算法部分抄一遍就完了

p1803.cpp★ 这一版就能 AC
// P1803 凌乱的yyy / 线段覆盖 —— ★ 这一版就能 AC
//
// ★ 这是第 19 章的第二道例题,而正文已经把算法证完了(第 ⑨~⑫ 步):
// **按右端点从早到晚排序,能选就选**,交换论证两行。
// ⇒ 所以解析页不重复那些,它讲的是**顶格 n = 10⁶ 带来的三件事**。
//
// ⚠ 题面的两个约定,一个字都不能读漏:
// ① `a_i < b_i`(不存在「一瞬间」的比赛);
// ② **上一场结束的时刻可以正好是下一场开始的时刻** ⇒ 代码里写 `l >= last`,不是 `l > last`。
// ★ 这一条在顶格随机数据上**几乎抓不到**(页面第 ④ 步:坐标 ≤ 10⁶ 时 300 轮只抓 3 次,
// 坐标压到 100 以内就是 300 / 300)—— 正文那个生成器为此把坐标压到了 14。
//
// 读入 2 × 10⁶ 个整数。★ 页面第 ⑤ 步实测了四种读法:这道题**关同步的 cin 就够**
// (时限 3 秒,而它只要 0.2 秒上下)—— 「读入量大」和「要写快读」是两句话。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n); // (右端点, 左端点)
for (int i = 0; i < n; i++) {
int l, r;
cin >> l >> r;
a[i] = {r, l};
}
sort(a.begin(), a.end()); // 按右端点从早到晚
int cnt = 0, last = -1; // 坐标 ≥ 0,所以 -1 比任何左端点都小
for (auto& [r, l] : a)
if (l >= last) { cnt++; last = r; } // ★ 是 >=,端点重合不算冲突
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

O(n log n),顶格 n = 10⁶ 本机 0.12 秒(时限 3 秒)。

下面全是「顶格」带来的事。

2★★★ 顶格对拍:参照物不必是暴力

正文的对拍是这么搭的:随机造 n ≤ 10 的数据,拿 2ⁿ 枚举子集当标准答案。 它在这道题的顶格上一点用都没有 —— 2¹⁰⁰⁰⁰⁰⁰ 这个数写出来要三十万位。

★ 但「能跑顶格的参照物」不是只有暴力一种

换一个复杂度相同、而选法完全不同的算法就行。这道题现成就有一个:

按右端点排好之后,f[i] = max(f[i-1], f[k] + 1), 其中 k 是「右端点 ≤ 第 i 场左端点」的最后一场(二分找)。

这是「第 i 场选不选」的经典 DP,O(n log n)顶格跑得动。 它和贪心只共用「按右端点排序」这一步,选的逻辑一个字都不共享

p1803Dp.cpp第二个算法:DP + 二分(参照物)
// P1803 的**第二个算法**:动态规划 + 二分 —— 它不是用来交的,是用来当参照物的
//
// ★★★ 这一页的主线就在这份文件上:
// 题面 `n ≤ 10⁶`,而正文那个 `2ⁿ` 枚举子集的暴力只跑得动 `n ≤ 20` ——
// ⇒ **顶格对拍时,那个参照物没了。**
// 而「能跑顶格的参照物」**不必是暴力**:换一个复杂度相同、但选法完全不同的算法就行。
//
// 这一份的选法和贪心毫无关系:
// 按右端点排好之后,`f[i] = max(f[i-1], f[k] + 1)`,
// 其中 `k` 是「右端点 ≤ 第 i 场左端点」的最后一场(二分找)。
// —— 也就是「第 i 场选不选」的经典 DP(带权区间调度的无权版本)。
// ★ 它和贪心只共用「按右端点排序」这一步,**选的逻辑一个字都不共享**。
//
// 复杂度 O(n log n),顶格 10⁶ 跑得动 ⇒ 顶格对拍成立(页面第 ② 步)。
// ⇒ 和同一天那份 [P1223](/sol/p1223/) 凑成一对:
// 那儿的顶格参照物是「同一个算法换个类型」,这儿是「另一个算法」。**两次都不是暴力。**
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n); // (右端点, 左端点)
for (int i = 0; i < n; i++) { int l, r; cin >> l >> r; a[i] = {r, l}; }
sort(a.begin(), a.end());
vector<int> rs(n + 1, -1); // rs[i] = 前 i 场里第 i 场的右端点(1 下标)
for (int i = 1; i <= n; i++) rs[i] = a[i - 1].first;
vector<int> f(n + 1, 0);
for (int i = 1; i <= n; i++) {
int l = a[i - 1].second;
// 找最大的 k < i 使 rs[k] <= l —— rs[1..i-1] 是升序的
int k = (int)(upper_bound(rs.begin() + 1, rs.begin() + i, l) - rs.begin()) - 1;
f[i] = max(f[i - 1], f[k] + 1);
}
cout << f[n] << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

两级参照物,各管一段:

规模 参照物 实测
n ≤ 12(坐标 ≤ 14) 2ⁿ 枚举子集 + DP + 贪心 三方 360 组,不一致 0 组
n = 10⁶(题面顶格) DP(O(n log n),本机 0.22 秒) 贪心 ≡ DP
★★★ 和同一天那份 P1223 凑成一对

P1223 那一页也遇到了「顶格没有参照物」这件事, 而它的出路是同一个算法换一个类型int 版 vs long long 版)。 这一页的出路是另一个算法

⇒ 两次都不是暴力。所以那条老话要连主语一起说:

「对拍只能跑小数据」的主语从来不是对拍,是「你把暴力当成了唯一的参照物」。

★ 这也是第 11 章 P1908 那条经验的第三次现场。

3★ 排错了序值多少分:正文那张小表在顶格上要乘 39

p1803Left.cpp错法一:按左端点排
// P1803 错法二:按**左端点**排序(开始得早就先上)
//
// 正文第 ⑫ 步已经用小数据的表打过它了(三种排法:按左端点 3 场、按区间短 3 场、按右端点 4 场)。
// 这一页只补一件正文没做的事:**在题面顶格的规模上,它差多少**(页面第 ③ 步那张表)。
//
// ★ 直觉版的反驳:一场从头开到尾的比赛开始得最早,却把整天都占了。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n); // (左端点, 右端点)
for (int i = 0; i < n; i++) { int l, r; cin >> l >> r; a[i] = {l, r}; }
sort(a.begin(), a.end()); // ← 按左端点
int cnt = 0, last = -1;
for (auto& [l, r] : a)
if (l >= last) { cnt++; last = r; }
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

正文第 ⑫ 步那张表是在 n ≤ 10 上量的:正解 4 场、按左端点 3 场 —— 差 33%。 看着像「差一点」。 而在题面的规模上:

n(坐标 ≤ 10⁶) 10 1 000 100 000
和正解不同的轮数 131 / 200 200 / 200 20 / 20
正解场数 ÷ 它的场数 1.33 倍 6.56 倍 38.83 倍
★ 「一个错法值多少分」的主语是规模

n 越大,「先开始的那场」把后面挡掉得越狠 —— 一场从头开到尾的比赛开始得最早, 却把整天都占了,而 n 大的时候这种比赛必然存在。

⇒ 小数据上量出来的 33%,会让人以为「这个排法只是差一点」。 它在顶格上拿到的是正解的 1 / 39

4★★★ 那个 `>=`:抓获率是两层的,而顶格随机只剩 1.6%

题面写着「不能同时参加 2 个及以上的比赛」—— 上一场 14:00 结束、下一场 14:00 开始,算不算「同时」?题目说不算 (正文第 ⑩ 步专门把这句话钉死过)。所以代码里是 l >= last,不是 l > last

p1803Strict.cpp错法二:把 >= 写成 >
// P1803 错法一:把 `l >= last` 写成 `l > last`(端点重合当成冲突)
//
// 题面写着「参加一个比赛必须善始善终,而且不能同时参加 2 个及以上的比赛」——
// 上一场 14:00 结束、下一场 14:00 开始,算不算「同时」?**题目说不算。**
//
// ★★★ 而它的抓获率**是一条两层的曲线**(页面第 ④ 步实测,n 固定 1000):
// 坐标上限 14 100 10⁴ 10⁶(题面顶格)
// 被抓 300 300 113 **3** / 300
// 有端点重合 300 300 300 **191** / 300
// ⇒ 第一层是「输入里到底有没有端点重合」,第二层是「那一对得正好卡在贪心的路上」。
// 顶格那一档 191 轮里只有 3 轮被抓 —— **按第一层去推,会把抓获率高估 60 倍**。
// ★ 正文那个生成器(`itvGen.cpp`)把坐标压到 14 就是为了这件事,它的注释里写着
// 「坐标范围开到 1e9 …… 对拍就白跑了」—— 这一页把那句话量了出来:
// **不是白跑,是从 300 掉到 3(100 倍)**。
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n;
if (!(cin >> n)) return 0;
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) { int l, r; cin >> l >> r; a[i] = {r, l}; }
sort(a.begin(), a.end());
int cnt = 0, last = -1;
for (auto& [r, l] : a)
if (l > last) { cnt++; last = r; } // ← 这里
cout << cnt << "\n";
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

它多久现形一次? n 固定 1000,只拧坐标上限:

坐标上限 14 100 10 000 10⁶(题面顶格)
输入里存在端点重合 300 300 300 191 / 300
真的被抓 300 300 113 3 / 300
★★★ 触发条件是两层的 —— 按第一层去推会高估 60 倍

顶格那一档,191 轮的输入里真的有「某场的右端点 == 另一场的左端点」, 可只有 3 轮被抓。因为还差第二层:那一对得正好卡在贪心走的那条路上。

⇒ 191 ÷ 3 ≈ 60。「有触发条件」和「被抓」之间差了 60 倍。

⚠ 而那个 3 不是一个精确值:换一条随机流再跑 300 轮,抓到的是 2。 这一档的抓获率就在 1% 上下晃 —— 写「大约 1%」才对,写「3 次」是把一次采样当成了常数。

★ 这和同一天那份 P1223 是同一件事的第二次: 那儿是「有并列的输入 104 轮,被抓 56 轮」(差 1.9 倍),这儿差 60 倍。 ⇒ 第 13 章 P1162 那条「触发条件要量不要推」, 在这两页上各兑现了一次。

⚠ 顺带订正正文生成器里的一句话

正文那个生成器 itvGen.cpp 把坐标压到了 14,注释里写着:

如果坐标范围开到 1e9,随机出来的区间基本互不相干,选谁都一样,对拍就白跑了

方向是对的,「白跑」说重了。 实测(坐标 10⁶,就是题面顶格): 不是 0,是 3 / 300 —— 从 300 掉到 3,一百倍,但没有掉到零。

⇒ 这条差别是有用的:「精确的 0」和「掉了一百倍」要用不同的办法救 —— 前者只能改生成器的结构,后者加轮数也行(只是要多花一百倍)。

✓ 而官方样例把它挡住了 —— 这次样例是特意造的

样例只有三场:[0,2] [2,4] [1,3],而 [0,2][2,4] 端点正好重合

版本 样例输出 挡住了吗
正解 2 ——
>= 写成 > 1 挡住
按左端点排 2 放过

⇒ 和 P1223 正好相反:那道题的样例放过了最隐蔽的那个, 这道题的样例专门为最隐蔽的那个而造。 ★★ 所以「样例挡不挡得住」真的只能一个一个试 —— 它取决于出题人有没有想到, 而这件事你没法从题面推出来。

5读入 2 × 10⁶ 个整数:这一关不构成分数线

p1803Read.cpp四种读法跑同一个贪心
它自己造顶格数据,不用喂输入。四趟的答案必须一模一样。
// P1803 的读入:2 × 10⁶ 个整数,四种读法差多少
//
// 用法:./p1803Read [n] [csv] 默认 n = 1000000(题面顶格就是它)
// ★ 它自己造一份 P1803 形状的输入写进临时文件,再 freopen 回 stdin 读四遍 ——
// 不需要喂输入。四遍算出来的答案必须一模一样(csv 里的 same 钉这件事)。
//
// ⚠ 顺序不能换:关掉同步之后 cin 会自己预读一大块,之后再 freopen 换文件,
// cin 缓冲里剩的就是上一份文件的残渣 ⇒ 「同步开着」那一趟必须排在最前面
// (这条坑是第 45 章 read.cpp 踩过的,照抄它的顺序)。
//
// ★ 这一份想说的是一个**否定的结论**:这道题的读入**不构成分数线**。
// [第 6 章 P2367](/sol/p2367/) 要读两千万个数、时限 1 秒 ⇒ 连 scanf 都不够;
// 这道题读两百万个、时限 3 秒 ⇒ 默认 cin 也就零点几秒。
// **「读入量大」和「要写快读」是两句话,中间隔着时限这把尺子。**
#include <bits/stdc++.h>
#include <chrono>
using namespace std;
using namespace std::chrono;
static char path[64];
static const int MAXN = 1000006;
static int L[MAXN], R[MAXN];
static void makeData(int n) {
snprintf(path, sizeof(path), "/tmp/p1803-bench-%d.txt", (int)getpid());
FILE* f = fopen(path, "w");
if (!f) { fprintf(stderr, "写不出临时文件 %s\n", path); exit(1); }
mt19937 rng(20260829u);
fprintf(f, "%d\n", n);
for (int i = 0; i < n; i++) {
int a = (int)(rng() % 1000000u);
int b = a + 1 + (int)(rng() % (unsigned)(1000000 - a));
fprintf(f, "%d %d\n", a, b);
}
fclose(f);
}
static void reopenIn() {
if (!freopen(path, "r", stdin)) { fprintf(stderr, "读不回临时文件\n"); exit(1); }
}
/* 贪心那几行 —— 四趟共用,所以四趟之间的差别只可能来自读入 */
static int finish(int n) {
vector<pair<int, int>> a(n);
for (int i = 0; i < n; i++) a[i] = {R[i], L[i]};
sort(a.begin(), a.end());
int cnt = 0, last = -1;
for (auto& [r, l] : a) if (l >= last) { cnt++; last = r; }
return cnt;
}
static char ibuf[1 << 22];
static size_t ipos = 0, ilen = 0;
static inline int gc() {
if (ipos == ilen) { ilen = fread(ibuf, 1, sizeof(ibuf), stdin); ipos = 0; if (!ilen) return EOF; }
return ibuf[ipos++];
}
static inline int readIntFast() {
int c = gc(), x = 0;
while (c != EOF && (c < '0' || c > '9')) c = gc();
for (; c >= '0' && c <= '9'; c = gc()) x = x * 10 + (c - '0');
return x;
}
int main(int argc, char** argv) {
int n = argc > 1 ? atoi(argv[1]) : 1000000;
bool csv = (argc > 2 && string(argv[2]) == "csv") || (argc > 1 && string(argv[1]) == "csv");
if (argc > 1 && string(argv[1]) == "csv") n = 1000000;
n = max(1, min(1000000, n));
makeData(n);
long long ms[4]; int ans[4];
/* ① cin,同步开着(大多数人的第一反应)—— 必须排最前面 */
{
reopenIn();
auto t0 = steady_clock::now();
int m; cin >> m;
for (int i = 0; i < m; i++) cin >> L[i] >> R[i];
ans[0] = finish(m);
ms[0] = duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
/* ② cin + sync_with_stdio(false) */
{
reopenIn();
ios::sync_with_stdio(false);
cin.tie(nullptr);
auto t0 = steady_clock::now();
int m; cin >> m;
for (int i = 0; i < m; i++) cin >> L[i] >> R[i];
ans[1] = finish(m);
ms[1] = duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
/* ③ scanf */
{
reopenIn();
auto t0 = steady_clock::now();
int m = 0;
if (scanf("%d", &m) != 1) return 1;
for (int i = 0; i < m; i++) if (scanf("%d %d", &L[i], &R[i]) != 2) return 1;
ans[2] = finish(m);
ms[2] = duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
/* ④ 手写快读(fread 整块读进来自己拼数字) */
{
reopenIn();
ipos = ilen = 0;
auto t0 = steady_clock::now();
int m = readIntFast();
for (int i = 0; i < m; i++) { L[i] = readIntFast(); R[i] = readIntFast(); }
ans[3] = finish(m);
ms[3] = duration_cast<milliseconds>(steady_clock::now() - t0).count();
}
remove(path);
bool same = (ans[0] == ans[1] && ans[1] == ans[2] && ans[2] == ans[3]);
if (csv) {
printf("same,%d\n", same ? 1 : 0);
printf("ans,%d\n", ans[0]);
printf("ms,%lld,%lld,%lld,%lld\n", ms[0], ms[1], ms[2], ms[3]);
return 0;
}
const char* names[4] = {"cin(默认,同步开着)", "cin + sync_with_stdio(false)", "scanf", "手写快读(fread)"};
printf("n = %d,四种读法(含排序和贪心那几行,四趟完全一样):\n", n);
long long best = *min_element(ms, ms + 4);
for (int i = 0; i < 4; i++)
printf(" %-32s %6lld 毫秒 比最快的慢 %.1f 倍 答案 %d\n",
names[i], ms[i], best ? (double)ms[i] / best : 1.0, ans[i]);
printf("四趟答案一致:%s\n", same ? "是" : "**否(这就是 bug)**");
return 0;
}
点一下即可编辑
输出
点「运行 ▶」看结果

本机实测(顶格 n = 10⁶,输入 13.9 MB,时限 3 秒):

读法 毫秒 比最快的慢 3 秒时限
cin(默认,同步开着) 372 4.5 倍
cin + sync_with_stdio(false) 122 1.5 倍
scanf 146 1.8 倍
手写快读(fread 82 1.0 倍
★★ 「读入量大」和「要写快读」是两句话,中间隔着时限

四种读法的倍数第 6 章 P2367 那页几乎一样(默认 cin 慢 4~12 倍, 快读最快)—— 倍数是读法的属性,跨题基本不变。

变的是绝对时间,而它才是分数线:

要读几个数 时限 默认 cin 结论
P2367 2 × 10⁷ 1 秒 8.1 秒 scanf 都不够
这道题 2 × 10⁶ 3 秒 0.37 秒 四种全都够

⇒ 「要不要写快读」是一道三十秒的算术题:数的个数 × 每个数的耗时,和时限比一比。 这道题的答案是「不用」,而这同样是一个要算出来的结论,不是猜的。

6★ 顺手用一次判据:题面那句 `aᵢ < bᵢ` 是哪一类

第 12 章 P1226 那一页把题面里的约束分成三类 —— 情报 / 命门 / 噪声,而判据是同一个动作:造一档违反它的数据,看有没有任何一版的行为变了。

这道题的 aᵢ < bᵢ(不存在「一瞬间」的比赛):放开它,允许 l == r,300 轮 ——

结果
贪心和 DP 还一致吗 一致,0 轮不同
l > last 那个错法还是被抓吗 照样被抓,0 轮漏

⇒ 它是噪声。(⚠ 但这仍然要跑一遍才知道 —— 正文那个生成器造出 l == r 的区间, 而题面保证不会有,两边不一致却什么事也没有,这本身就得验一次。)

7度量程序和生成器

p1803Count.cpp度量程序
// P1803 的度量程序 —— 这一页除耗时以外的数字都出自这一份。
//
// `./p1803Count` 人看的版本
// `./p1803Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用
//
// 四段:
// ① ★ 三方一致:小数据上「贪心 / DP / 2ⁿ 枚举子集」三个算法逐组比
// —— 先确认 DP 那份真的对,它才有资格在顶格当参照物;
// ② ★★★ `l > last`(端点重合当冲突)的**值域曲线**:
// 照题面随机(坐标 ≤ 10⁶)是**精确的 0**,坐标压到几十才现形;
// 顺带数一数「输入里真的存在端点重合」的轮数 —— 两条线要能对上;
// ③ ★ 按左端点排在**题面顶格形状**上差多少(正文只量过 n ≤ 10 的小数据);
// ④ ★★ 题面那句 `a < b` 是情报 / 命门 / 噪声?判据是造一档**违反它**的数据,
// 看有没有任何一版的行为变了。
#include <bits/stdc++.h>
using namespace std;
static bool CSV = false;
static void row(const char* key, const vector<long long>& v) {
if (!CSV) return;
printf("%s", key);
for (long long x : v) printf(",%lld", x);
printf("\n");
}
struct Seg { int l, r; };
/** 正解:按右端点排,能选就选(端点重合不算冲突) */
static int greedy(vector<Seg> a, bool strict = false) {
sort(a.begin(), a.end(), [](const Seg& x, const Seg& y) { return x.r < y.r; });
int cnt = 0, last = -1;
for (auto& s : a) {
if (strict ? s.l > last : s.l >= last) { cnt++; last = s.r; }
}
return cnt;
}
/** 错法二:按左端点排 */
static int byLeft(vector<Seg> a) {
sort(a.begin(), a.end(), [](const Seg& x, const Seg& y) { return x.l < y.l; });
int cnt = 0, last = -1;
for (auto& s : a) if (s.l >= last) { cnt++; last = s.r; }
return cnt;
}
/** 第二个算法:按右端点排 + DP + 二分(和贪心只共用排序那一步) */
static int dp(vector<Seg> a) {
int n = a.size();
sort(a.begin(), a.end(), [](const Seg& x, const Seg& y) { return x.r < y.r; });
vector<int> rs(n + 1, -1), f(n + 1, 0);
for (int i = 1; i <= n; i++) rs[i] = a[i - 1].r;
for (int i = 1; i <= n; i++) {
int k = (int)(upper_bound(rs.begin() + 1, rs.begin() + i, a[i - 1].l) - rs.begin()) - 1;
f[i] = max(f[i - 1], f[k] + 1);
}
return f[n];
}
/** 2ⁿ 枚举子集(正文那个暴力的同一件事),n ≤ 20 */
static int brute(const vector<Seg>& a) {
int n = a.size(), best = 0;
for (int mask = 0; mask < (1 << n); mask++) {
vector<Seg> pick;
for (int i = 0; i < n; i++) if (mask >> i & 1) pick.push_back(a[i]);
sort(pick.begin(), pick.end(), [](const Seg& x, const Seg& y) { return x.r < y.r; });
bool ok = true;
for (size_t i = 1; i < pick.size(); i++) if (pick[i].l < pick[i - 1].r) { ok = false; break; }
if (ok) best = max(best, (int)pick.size());
}
return best;
}
/** 造一组:n 场,坐标 ∈ [0, hi],严格 a < b;allowEq 时允许 a == b(违反题面) */
static vector<Seg> gen(mt19937& rng, int n, int hi, bool allowEq = false) {
vector<Seg> a(n);
for (int i = 0; i < n; i++) {
int l = (int)(rng() % (unsigned)hi);
int r = allowEq ? l + (int)(rng() % (unsigned)(hi - l + 1))
: l + 1 + (int)(rng() % (unsigned)(hi - l));
a[i] = {l, r};
}
return a;
}
/** 输入里存在「某场的右端点 == 另一场的左端点」吗 */
static bool hasTouch(const vector<Seg>& a) {
unordered_set<int> ls;
for (auto& s : a) ls.insert(s.l);
for (auto& s : a) if (ls.count(s.r)) return true;
return false;
}
int main(int argc, char** argv) {
CSV = argc > 1 && string(argv[1]) == "csv";
/* ① 三方一致(贪心 / DP / 2ⁿ 暴力) */
{
mt19937 rng(20260829u);
int groups = 0, bad = 0;
for (int n = 1; n <= 12; n++)
for (int rep = 0; rep < 30; rep++, groups++) {
vector<Seg> a = gen(rng, n, 14);
int g = greedy(a), d = dp(a), b = brute(a);
if (!(g == d && d == b)) bad++;
}
if (!CSV) printf("① 三方一致:%d 组(n <= 12,坐标 <= 14),贪心/DP/2^n 暴力不一致 %d 组\n", groups, bad);
row("three", {groups, bad});
}
/* ② l > last 的值域曲线 */
{
const int HIS[] = {14, 100, 10000, 1000000};
vector<long long> out;
for (int hi : HIS) {
mt19937 rng(hi * 2654435761u + 17u);
int bad = 0, touch = 0;
for (int r = 0; r < 300; r++) {
vector<Seg> a = gen(rng, 1000, hi);
if (hasTouch(a)) touch++;
if (greedy(a, true) != greedy(a)) bad++;
}
out.push_back(bad); out.push_back(touch);
if (!CSV) printf("② n = 1000,坐标 <= %7d:`l > last` 错 %d / 300,输入里存在端点重合的 %d / 300\n",
hi, bad, touch);
}
row("strictHi", out);
}
/* ③ 按左端点排:题面顶格形状上差多少 */
{
const int NS[] = {10, 1000, 100000};
vector<long long> out;
for (int n : NS) {
mt19937 rng(n * 40503u + 23u);
int bad = 0; long long sumG = 0, sumL = 0;
int rounds = n >= 100000 ? 20 : 200;
for (int r = 0; r < rounds; r++) {
vector<Seg> a = gen(rng, n, 1000000);
int g = greedy(a), l = byLeft(a);
sumG += g; sumL += l;
if (g != l) bad++;
}
out.push_back(bad); out.push_back(rounds);
out.push_back((long long)((double)sumG / sumL * 100 + 0.5));
if (!CSV) printf("③ n = %6d(坐标 <= 10^6):按左端点排 %d / %d 轮和正解不同,平均场数比 %.2f 倍\n",
n, bad, rounds, (double)sumG / sumL);
}
row("left", out);
}
/* ④ 题面那句 `a < b`:情报 / 命门 / 噪声?造一档违反它的数据看行为变不变 */
{
mt19937 rng(777u);
int diffG = 0, diffD = 0, diffS = 0;
for (int r = 0; r < 300; r++) {
vector<Seg> a = gen(rng, 200, 60, true); // 允许 l == r
int g = greedy(a), d = dp(a), s = greedy(a, true);
if (g != d) diffG++; // 贪心和 DP 还一致吗
if (d != g) diffD++;
if (s == g) diffS++; // 那个错法在这一档还是被抓吗
}
if (!CSV) printf("④ 允许 l == r(违反题面)300 轮:贪心和 DP 不一致 %d 轮;`l > last` 那版**没**被抓的 %d 轮\n",
diffG, diffS);
row("eq", {diffG, diffD, diffS});
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果
p1803Gen.cpp数据生成器
// P1803 对拍生成器:`./p1803Gen <seed> [n] [坐标上限]`
// 默认 `n = 1000`、坐标 `≤ 10⁶` —— ★ **就是「照题面随机」的样子**(题面 `0 ≤ a < b ≤ 10⁶`)。
//
// ★ 默认档故意写成「照题面随机」,因为这一页要演示的正是它的盲区:
// `l >= last` 写成 `l > last` 那个 bug,在这一档上 300 轮只被抓 **3** 次;
// 把坐标上限压到 100,它当场 **300 / 300**(页面第 ④ 步那张表)。
// ⚠ 而「输入里有没有端点重合」这一档有 **191 / 300** —— 两个数差 60 倍,
// 所以**别拿第一层当抓获率**。
//
// ⚠ 严格按题面造 `a < b`(题面保证不存在「一瞬间」的比赛)——
// 页面第 ④ 步用「违反它」的一档做过判据:那句约束是**噪声**,四个版本行为一个没变。
#include <bits/stdc++.h>
using namespace std;
int main(int argc, char** argv) {
unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1;
int n = argc > 2 ? atoi(argv[2]) : 1000;
int hi = argc > 3 ? atoi(argv[3]) : 1000000;
n = max(1, min(1000000, n));
hi = max(1, min(1000000, hi));
mt19937 rng(seed * 2654435761u + 999u);
printf("%d\n", n);
for (int i = 0; i < n; i++) {
int a = (int)(rng() % (unsigned)hi); // a ∈ [0, hi-1]
int b = a + 1 + (int)(rng() % (unsigned)(hi - a)); // b ∈ (a, hi]
printf("%d %d\n", a, b);
}
return 0;
}
点一下即可编辑
输入(stdin)
输出
点「运行 ▶」看结果

8一页纸

关键的一步 右端点排序,能选就选(第 19 章正文证过,交换论证)
哪一版能 AC p1803.cpp;顶格 0.12 秒,时限 3 秒
这一页的主线 ★★★ 顶格对拍的参照物不必是暴力 —— 这道题用的是
另一个算法(DP + 二分,O(n log n));P1223 用的是同一算法换类型
最容易挂的一行 l >= last 写成 l > last;★ 而它在照题面随机上只被抓 3 / 300
抓获率的形状 ★★★ 两层:有端点重合 191 / 300,真被抓 3 / 300 —— 差 60 倍
排错序值多少分 正文小数据上差 33%,顶格差 ★ 38.83 倍(「值多少分」的主语是规模)
读入 不构成分数线:四种读法全过(默认 cin 0.37 秒 / 时限 3 秒)——
倍数和 P2367 一样,而绝对时间差十倍,分数线在绝对时间上
题面 aᵢ < bᵢ 噪声(放开它,两个版本的行为一个没变)
样例的表现 挡住了最隐蔽的那个(三场里就藏着一对端点重合)—— 和 P1223 相反