0题目原文(本地存了一份)
原题在洛谷上(页头有链接)。别人的网站不归我们管,打不开、改版、题号调整都可能发生。 所以每个解析页都把题面转录一份存在本地,跟着仓库一起进版本库。
转录自洛谷 P2782,日期见页头。两边不一致时信原站。
题目描述
有一条横贯东西的大河,河有笔直的南北两岸,岸上各有位置各不相同的 N 个城市。
北岸的每个城市有且仅有一个友好城市在南岸,而且不同城市的友好城市不相同。
每对友好城市都向政府申请在河上开辟一条直线航道连接两个城市,
但是由于河上雾太大,政府决定避免任意两条航道交叉,以避免事故。
编程帮助政府做出一些批准和拒绝申请的决定,
使得在保证任意两条航道不相交的情况下,被批准的申请尽量多。
输入格式
第一行,一个整数 N,表示城市数。
第二行到第 N+1 行,每行两个整数,分别表示南岸和北岸的一对友好城市的坐标。
输出格式
仅一行,输出一个整数,表示政府所能批准的最多申请数。
数据规模与约定
- 对于 50% 的数据,
1 ≤ N ≤ 5000,0 ≤ xᵢ ≤ 10000; - 对于 100% 的数据,
1 ≤ N ≤ 2 × 10⁵,0 ≤ xᵢ ≤ 10⁶。
输入输出样例
输入
7 22 4 2 6 10 3 15 12 9 8 17 17 4 2
输出
4
七对友好城市,最多能批准 4 条互不相交的航道。 ⚠ 这组样例两个错法都放过了 —— 第 ③ ④ 步各说一个原因。
题单给这道题写的是:「排序之后就是 LIS。难点在看出「排完序之后这题就是 LIS」, 这一步才是 DP 题的真正门槛。」
⇒ 这一页第 ① 步就把那一步说透;剩下的两步是这一轮那条曲线的收尾: 那个「一字之差」的坑在这道题上又是精确的 0,而且和 P1439 一样 是题面保证的(第 ④ 步,含自检)。
1★★★ 关键一步:两条航道什么时候不相交
一条航道就是一对坐标 (a, b)(a 是南岸、b 是北岸)。两条航道
(a₁, b₁) 和 (a₂, b₂) 画在河上:
南岸 ---a1--------a2---
\ / <- 交叉:a1 < a2 而 b1 > b2
\ /
北岸 -----b2----b1----
南岸 ---a1--------a2---
| | <- 不交叉:a1 < a2 而 b1 < b2
| |
北岸 ---b1--------b2---⇒ 不相交 ⟺ a 的大小关系和 b 的一致。
于是:把所有城市对按南岸坐标 a 排序,a 那一维就自动递增了 ——
剩下的条件只有「b 递增」。
⇒ 答案 = 排序后 b 序列的最长上升子序列长度。
// P2782 友好城市 —— ★ 这一版就能 AC//// 题意:大河南北两岸各有 N 个位置互不相同的城市,每对友好城市想连一条直线航道。// 要求任意两条航道**不相交**,问最多能批准几条。//// ★★★ 关键一步:**看出这题是 LIS** —— 题单里说「这一步才是 DP 题的真正门槛」。// 两条航道 `(a₁, b₁)` 和 `(a₂, b₂)`(a 是南岸坐标、b 是北岸坐标)不相交// ⟺ **a 和 b 的大小关系一致**(`a₁ < a₂` 且 `b₁ < b₂`,或者反过来)。// ⇒ 把所有城市对**按南岸坐标 a 排序**,问题就变成「在 b 这个序列里挑最长的上升子序列」。//// ⚠ 而这一步之所以成立,靠的是题面那句「**位置各不相同**」:// a 互不相同 ⇒ 排序之后顺序唯一;b 互不相同 ⇒ 严格上升和不下降是同一件事// (页面第 ④ 步:那个「一字之差」的坑在这道题上又是**精确的 0**,和// [P1439](/sol/p1439/) 一样是**题面保证**的)。//// 复杂度 `O(N log N)`(排序 + LIS)。题面 `N ≤ 2 × 10⁵`,`O(N²)` 过不去。
#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>> c(n); // (南岸, 北岸) for (auto& p : c) cin >> p.first >> p.second; sort(c.begin(), c.end()); // ★ 按南岸坐标排
vector<int> tails; for (auto& [a, b] : c) { auto it = lower_bound(tails.begin(), tails.end(), b); if (it == tails.end()) tails.push_back(b); else *it = b; } cout << tails.size() << "\n"; return 0;}点「运行 ▶」看结果
O(N log N)。⚠ 顶格 N = 2 × 10⁵ ⇒ O(N²) 要算 4 × 10¹⁰ 次,
而 O(N log N) 约 3.6 × 10⁶ 次 —— 差 11 111 倍。
2参照物:枚举所有子集,逐对验「不相交」
// P2782 的参照物:**枚举所有子集**,逐个检查「两两不相交」//// ★ 它把题面照抄一遍 —— 一点「排序 + LIS」的巧劲都不用,// 而这一页要验的恰恰是「看出它是 LIS」这一步对不对。// ⚠ 只跑得动 `N ≤ 18` 左右。
#include <bits/stdc++.h>using namespace std;
int main() { int n; if (!(cin >> n)) return 0; vector<pair<int, int>> c(n); for (auto& p : c) cin >> p.first >> p.second;
int best = 0; for (int mask = 0; mask < (1 << n); mask++) { vector<pair<int, int>> v; for (int i = 0; i < n; i++) if (mask >> i & 1) v.push_back(c[i]); bool ok = true; for (size_t i = 0; i < v.size() && ok; i++) for (size_t j = i + 1; j < v.size() && ok; j++) { // 两条航道相交 ⟺ a 的大小关系和 b 的相反 bool cross = (v[i].first < v[j].first) != (v[i].second < v[j].second); if (cross) ok = false; } if (ok) best = max(best, (int)v.size()); } cout << best << "\n"; return 0;}点「运行 ▶」看结果
它把上面那句「不相交 ⟺ 大小关系一致」直接拿来两两验,一点排序和 LIS 都不用—— 而这一页要验的恰恰是「看出它是 LIS」这一步对不对。
300 组(照题面,N ≤ 12) |
|
|---|---|
排序 + LIS vs 2ᴺ 枚举子集 |
不一致 0 组 |
3★ 忘了排序会怎样
// P2782 错法一:**忘了排序**,直接对输入顺序里的北岸坐标求 LIS//// 「按南岸坐标排序」这一步不是优化,是**把二维问题降成一维**的那一步:// 排完序之后,「南岸也递增」这个条件才自动满足,剩下的才只是「北岸递增」。//// ★ 不排序的话,输入顺序完全是随机的 —— 它算出来的东西和题目问的没有关系。// ⚠ 而它在**输入本来就按南岸有序**的数据上会蒙对 —— 页面第 ③ 步量了这件事。
#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>> c(n); for (auto& p : c) cin >> p.first >> p.second; // ← 少了 sort
vector<int> tails; for (auto& [a, b] : c) { auto it = lower_bound(tails.begin(), tails.end(), b); if (it == tails.end()) tails.push_back(b); else *it = b; } cout << tails.size() << "\n"; return 0;}点「运行 ▶」看结果
「按南岸坐标排序」不是优化,是把二维问题降成一维的那一步 —— 排完序之后「南岸也递增」才自动成立。
| 照题面随机 300 轮,答案不同 | 167 / 300 |
| 官方样例 | 也输出 4 ⇒ 放过 |
★ 样例放过它,是因为那七行数据碰巧让两种顺序算出同一个数 —— 又一次「样例只有一组」。
4★★★ 那个「一字之差」的坑:这一轮那条曲线的收尾
// P2782 「错法」:把 `lower_bound` 写成 `upper_bound`//// ★★★ 而它在这道题上**又是精确的 0** —— 和 [P1439](/sol/p1439/) 同一个原因:// 题面写着两岸城市的「位置**各不相同**」⇒ 排序后的北岸坐标序列**没有重复元素**,// 而[第 22 章第 ⑩ 步](/ch/22-lis/)那个判据说得很清楚:// **没有重复元素时,严格上升和不下降是同一件事。**//// ⇒ 这是这一轮那条曲线上的**第五个点**,也是第二个「**题面保证**的 0」。// ⚠ 而这个 0 同样配了自检(页面第 ④ 步):放开「位置各不相同」,它当场就分得出来。
#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>> c(n); for (auto& p : c) cin >> p.first >> p.second; sort(c.begin(), c.end());
vector<int> tails; for (auto& [a, b] : c) { auto it = upper_bound(tails.begin(), tails.end(), b); // ← 一字之差 if (it == tails.end()) tails.push_back(b); else *it = b; } cout << tails.size() << "\n"; return 0;}点「运行 ▶」看结果
| 300 组(照题面) | |
|---|---|
upper_bound 版 vs lower_bound 版 |
★ 不同 0 组 |
题面写着两岸城市「位置各不相同」⇒ 排序后的北岸坐标序列没有重复元素, 而第 22 章第 ⑩ 步那个判据说:没有重复元素时,严格上升和不下降是同一件事。
⇒ 加多少轮、换什么种子都不会变。这是这一轮第二个「题面保证的 0」 (第一个是 P1439)。
把题面那句「位置各不相同」放开(让北岸坐标出现重复),同一批代码再跑 300 轮:
upper_bound 和 lower_bound 不同的轮数 |
★ 288 / 300 |
⇒ 那个 0 不是空壳,是题面保证出来的。 ★ 和 P1439 完全一样的做法:「造一档违反题面的数据」这一个动作, 既给精确的 0 做了自检,也称出了那句约束的重量。
5★ 这一轮那条曲线,到这儿钉齐了
同一个坑(lower_bound 还是 upper_bound),这一章五道题量出五个点:
n / 值域 |
那个坑被抓 | |
|---|---|---|
| P1439 | 题面保证无重复 | ★ 精确的 0 |
| 这道题 | 题面保证无重复 | ★ 精确的 0 |
| B3637 | 0.005 | 2 / 300 |
| P1091 | 1.0 | 215 / 300 |
| P1020 | 2.0 | 几乎必抓 |
- P1439 / P2782:抓不到是题面保证的 —— 加轮数、换种子都没用, 而且这不是坏事:它意味着你可以放心用任意一种写法;
- B3637:抓不到是比值太小 —— 换个生成器(压值域)当场就能抓到;
- P1091 / P1020:题面的比值本身就大 —— 躲不掉,必须写对。
⇒ ★★★ 「这个 bug 对拍抓不抓得到」从来不是一句话能回答的, 要连着「哪道题、什么数据、什么比值」一起说 —— 而这一章正好给了同一个 bug 的五个不同答案。
6度量程序和生成器
// P2782 的度量程序 —— 这一页所有数字都出自这一份。//// `./p2782Count` 人看的版本// `./p2782Count csv` 一行一项,给 scripts/check-viz.mjs 写断言用//// 四段:// ① ★ 「按南岸排序 + 北岸 LIS」≡ `2ᴺ` 枚举子集逐对验「不相交」;// ② ★ 忘了排序会怎样(照题面随机的抓获率);// ③ ★★★ 那个「一字之差」的坑在这道题上是**精确的 0** —— 题面「位置各不相同」保证的;// ⚠ 而这个 0 **配了自检**:放开那句约束,它当场就分得出来;// ④ ★ `O(N²)` 在题面顶格上是什么概念。
#include <bits/stdc++.h>using namespace std;typedef long long ll;
static bool CSV = false;static void row(const char* key, const vector<ll>& v) { if (!CSV) return; printf("%s", key); for (ll x : v) printf(",%lld", x); printf("\n");}
typedef vector<pair<int, int>> Cities;
/** 正解:按南岸排序 + 北岸 LIS。loose 用 upper_bound;noSort 跳过排序 */static int solve(Cities c, bool loose = false, bool noSort = false) { if (!noSort) sort(c.begin(), c.end()); vector<int> t; for (auto& [a, b] : c) { auto it = loose ? upper_bound(t.begin(), t.end(), b) : lower_bound(t.begin(), t.end(), b); if (it == t.end()) t.push_back(b); else *it = b; } return (int)t.size();}/** 参照物:枚举所有子集,逐对验「不相交」 */static int brute(const Cities& c) { int n = c.size(), best = 0; for (int mask = 0; mask < (1 << n); mask++) { vector<pair<int, int>> v; for (int i = 0; i < n; i++) if (mask >> i & 1) v.push_back(c[i]); bool ok = true; for (size_t i = 0; i < v.size() && ok; i++) for (size_t j = i + 1; j < v.size() && ok; j++) if ((v[i].first < v[j].first) != (v[i].second < v[j].second)) ok = false; if (ok) best = max(best, (int)v.size()); } return best;}
static Cities genPerm(mt19937& rng, int n) { vector<int> a(n), b(n); iota(a.begin(), a.end(), 0); iota(b.begin(), b.end(), 0); shuffle(a.begin(), a.end(), rng); shuffle(b.begin(), b.end(), rng); Cities c(n); for (int i = 0; i < n; i++) c[i] = {a[i], b[i]}; return c;}static Cities genDup(mt19937& rng, int n) { vector<int> a(n); iota(a.begin(), a.end(), 0); shuffle(a.begin(), a.end(), rng); Cities c(n); for (int i = 0; i < n; i++) c[i] = {a[i], (int)(rng() % (unsigned)max(1, n / 2))}; return c;}
int main(int argc, char** argv) { CSV = argc > 1 && string(argv[1]) == "csv";
/* ①②③ 照题面随机 */ { mt19937 rng(20260829u); int groups = 0, bad = 0, noSort = 0, upper = 0; for (int rep = 0; rep < 300; rep++, groups++) { Cities c = genPerm(rng, (int)(rng() % 12) + 1); int ok = solve(c); if (brute(c) != ok) bad++; if (solve(c, false, true) != ok) noSort++; if (solve(c, true) != ok) upper++; } if (!CSV) printf("① %d 组(照题面:两岸坐标各自互不相同,N ≤ 12):" "排序 + LIS vs 2^N 枚举子集不一致 %d 组\n" "② 忘了排序:不同 %d 组\n" "③ 那个「一字之差」的坑(upper_bound):不同 %d 组 —— **精确的 0**\n", groups, bad, noSort, upper); row("perm", {groups, bad, noSort, upper}); }
/* ③ 自检:放开「位置各不相同」,upper_bound 当场就分得出来 */ { mt19937 rng(2782u); int rounds = 300, upper = 0; for (int r = 0; r < rounds; r++) { Cities c = genDup(rng, (int)(rng() % 12) + 4); // ← 违反题面:北岸会重复 if (solve(c, true) != solve(c)) upper++; } if (!CSV) printf("③ ⚠ 自检(放开「位置各不相同」,%d 轮):" "upper_bound 和 lower_bound 不同 %d 轮 ⇒ 上面那个 0 不是空壳\n", rounds, upper); row("selfcheck", {rounds, upper}); }
/* ④ O(N²) 在顶格上是什么概念 */ { ll n = 200000; if (!CSV) printf("④ 顶格 N = 2×10^5:O(N²) 要算 %lld 次(4×10^10 量级)," "而 O(N log N) 约 %lld 次 —— 差 %lld 倍\n", n * n, (ll)(n * 18), (ll)(n / 18)); row("scale", {n * n, (ll)(n * 18), (ll)(n / 18)}); } return 0;}点「运行 ▶」看结果
// P2782 对拍生成器:`./p2782Gen <seed> [N 上限] [是否允许坐标重复]`// 默认 `N ≤ 12`、**严格照题面:两岸坐标各自互不相同**。//// ★ 第三个参数(默认 0)是给页面第 ④ 步用的**违反题面**的一档 ——// 题面写着两岸城市「位置各不相同」,而这一档故意让北岸坐标出现重复。// ⇒ 它同时干两件事:给「精确的 0」做自检,顺便称一称那句约束的重量// (和同一轮 [P1439](/sol/p1439/) 是同一个动作)。//// ⚠ 默认 N 压在 12,因为参照物是 `2ᴺ` 枚举子集。
#include <bits/stdc++.h>using namespace std;
int main(int argc, char** argv) { unsigned seed = argc > 1 ? (unsigned)atoi(argv[1]) : 1; int nHi = argc > 2 ? atoi(argv[2]) : 12; int dup = argc > 3 ? atoi(argv[3]) : 0; nHi = max(1, min(200000, nHi));
mt19937 rng(seed * 2654435761u + 2782u); int n = (int)(rng() % (unsigned)nHi) + 1; printf("%d\n", n); if (!dup) { vector<int> a(n), b(n); iota(a.begin(), a.end(), 0); iota(b.begin(), b.end(), 0); shuffle(a.begin(), a.end(), rng); shuffle(b.begin(), b.end(), rng); for (int i = 0; i < n; i++) printf("%d %d\n", a[i], b[i]); } else { vector<int> a(n); iota(a.begin(), a.end(), 0); shuffle(a.begin(), a.end(), rng); for (int i = 0; i < n; i++) printf("%d %u\n", a[i], (unsigned)(rng() % (unsigned)max(1, n / 2))); // ← 北岸会重复 } return 0;}点「运行 ▶」看结果
★ 生成器的第三个参数就是那个「违反题面」的开关(默认 0 = 严格照题面,两岸坐标各自互不相同)。
7一页纸
| ★★★ 关键的一步 | 不相交 ⟺ 两岸的大小关系一致 ⇒ 按南岸排序后求北岸的 LIS |
| 哪一版能 AC | p2782.cpp,O(N log N)(顶格 O(N²) 要 4 × 10¹⁰ 次,差 11 111 倍) |
| 错法一 | 忘了排序 —— 那一步不是优化,是把二维降成一维(167 / 300 被抓,样例放过) |
| ★★★ 错法二 | upper_bound —— 精确的 0,题面「位置各不相同」保证的(第二个这样的 0) |
| ⚠⚠ 自检 | 放开那句约束,upper 和 lower 当场不同 288 / 300 ⇒ 那个 0 不是空壳 |
| ★★ 这一章的收尾 | 同一个坑五个点:两个题面保证的 0 / 比值 0.005 抓 2 次 / 1.0 抓 215 次 / 2.0 必抓 ⇒ 「对拍抓不抓得到」要连着「哪道题、什么比值」一起说 |
| 参照物 | 2ᴺ 枚举子集逐对验「不相交」—— 300 组不一致 0 组 |