断点只可能落在 2n 个区间端点处;交错区间对 l_i<l_j<r_i<r_j 使 r_i 与 l_j 二选一,答案 = 2n − 该二分图的最大匹配。
OJ: luogu
题目 ID: P9726
难度:提高+/省选-
标签:二分图匹配匈牙利算法König 定理bitset 优化构造
创建: 2026-10-02 15:23
更新: 2026-10-07 12:15
形式化题目
有
初始时长度为
注意下标从
暴力解法
思路
题意本身就是一个可以照做的过程:
它的价值有两点:一是作为对拍基准,二是能让我们放心地相信后面推出来的公式。做法没有任何取巧,但在
代码
/**
* 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-02 15:23
* update_at: 2026-10-03 14:28
*/
// brute.cpp:小数据暴力解,用来帮助理解题意并辅助对拍。
//
// 题意完全照做:枚举 n 个操作的 n! 种执行顺序,每种顺序都真的在数组上
// 从后往前覆盖赋值,然后统计 a_0..a_{2n} 中相邻不同的位置数,取最大值。
// 只适用于 n <= 8 左右,但对拍时它是绝对正确的基准。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 20;
int n;
int lft[maxn], rgt[maxn];
int a[2 * maxn + 5]; // 序列 a_0..a_{2n}
int ord[maxn]; // 当前枚举到的操作顺序
void read_data() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> lft[i] >> rgt[i];
}
}
// 按 ord 给出的顺序执行操作,返回相邻不同的位置数。
int count_breaks() {
for (int p = 0; p <= 2 * n; p++) {
a[p] = 0;
}
for (int t = 0; t < n; t++) {
int id = ord[t];
for (int p = lft[id]; p < rgt[id]; p++) {
a[p] = id + 1; // 赋值 i(用 i+1 表示,0 留给初始值)
}
}
int cnt = 0;
for (int i = 0; i < 2 * n; i++) {
if (a[i] != a[i + 1]) {
cnt++;
}
}
return cnt;
}
void solve() {
for (int i = 0; i < n; i++) {
ord[i] = i;
}
int best = 0;
do {
best = max(best, count_breaks());
} while (next_permutation(ord, ord + n));
cout << best << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_data();
solve();
return 0;
}复杂度
时间复杂度
瓶颈
瓶颈显然在于"枚举顺序"这件事本身。但真正值得反思的是:我们真的有
数组的值只由"每个位置最后被谁覆盖"决定。所有区间都覆盖
正解
思路
第一步:把"顺序"翻译成"端点的二选一"。
我们只关心每个端点
- 若
:最终 ;而 等于"在 之后(最后一次)写 的那个区间", 被 覆盖需要 。所以
- 若
: (因为 覆盖 ),而 由最后写 的那个区间决定, 被 覆盖同样需要 。所以
两类条件的形状完全一样:"端点
特别地,位置
第二步:找出真正的冲突——交错区间对。
每次从
-
:两个区间不相交,先后顺序互不影响,端点 都能成为断点。 -
:区间 被区间 包含。把 排在前面即可让 与 都成为断点,也无冲突。 -
:两个区间交错。这时: - 若
要成为断点,就需要区间 排在 之后(因为只有 覆盖 ); - 若
要成为断点,就需要区间 排在 之后(因为只有 覆盖 )。
两者不能同时成立。于是得到唯一的冲突:
- 若
第三步:建二分图,用 König 定理算答案。
把
为什么图上只要有边,冲突就一定来自交错对? 若存在区间
同时覆盖 与 ( 且 ),且 ,那么 与 、 一起恰好给出 ,也就是 本身构成一个交错对, 与 之间本来就有边。所以不必再为"被同一个区间覆盖"单独设约束。
那么答案上界是什么?设最终断点集合为
它的反面是
第四步:上界可以达到(构造顺序)。
取一个大小恰为
引理:
无环。 反设存在环,取其中长度最小的一个
。对每条弧 ,取一个见证 满足 。由 立刻得到 即环上相邻的两个区间必然相交。取环上
最大的那个区间。( 两两不同,所以它唯一。)
- 环上前驱
与 相交,且 ,所以两者嵌套。若 ,那就是 ,可弧 的见证必须落在开区间 内,而 、 说明 的两个端点都在这个开区间之外,矛盾。所以只能 ,即 。 - 对后继
同理:若 ,则 (最大性)给出 ;若 ,则 ,弧 的见证落在 内却必须是 的端点,也矛盾。所以 。 于是
, 覆盖了 内含的一切,特别地覆盖了见证 ,所以弧 存在。把它接进原环就得到一个长度 的环; 时它直接退化为 的矛盾。无论哪种都与"长度最小"矛盾,故 无环。
取
注意这一步只用到"存在性":代码只需要算
,不需要真的输出执行顺序,所以引理只用来说明答案刻度没有缺口。
综合第三、四步:
其中
样例验证。 样例的区间是
实现要点。 unsigned long long,寻找"还没访问过的出边"这一步就变成 adj[u][w] & ~visR[w] 一次跳过一个字,用 __builtin_ctzll 取最低位。这样最坏复杂度
内存也要小心(题面特意警告了 16 MiB):压位邻接矩阵是 unsigned long long,
代码
/**
* 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-02 15:23
* update_at: 2026-10-03 14:28
*/
// P9726 [EC Final 2022] Magic
//
// n 个区间 [l_i, r_i)(赋值 i 到位置 l_i..r_i-1),2n 个端点互不相同。
// 任意顺序执行每个区间一次,最大化 a_0..a_{2n} 中相邻不同的位置数。
//
// 模型:位置 i 成为“断点”当且仅当 a_i != a_{i+1}。
// 所有区间都覆盖 l_i,但都不覆盖 l_i-1;位置 r_i-1 被 i 覆盖,r_i 不被 i 覆盖。
// 因此断点只可能出现在 2n 个端点处,且每个端点 s 是否成为断点
// 等价于“把写着 s 的那个区间放到最后”这一件事是否被安排成真。
//
// 关键观察:对相交且不包含的两个区间 l_i < l_j < r_i < r_j,
// 它们不可能同时“在最后写自己的断点”:谁先执行,谁的那一侧端点就被对方覆盖。
// 于是只有 l_i < l_j < r_i < r_j 这样的“交错对”才会产生二选一冲突:
// 选 r_i 当断点 <=> 区间 j 必须排在 i 前面
// 选 l_j 当断点 <=> 区间 i 必须排在 j 前面
// 两者不能同时成立,也就是二分图中“r_i 与 l_j 不能同时被选”。
//
// 把 2n 个端点分成两类:左部点 = 每个区间的 r_i,右部点 = 每个区间的 l_j;
// 若 l_i < l_j < r_i < r_j 则在 r_i 与 l_j 之间连边。
// 那么“可同时当选断点的端点集合”正是这张二分图的一个独立集,
// 答案是 2n - 最大匹配(König 定理:最大独立集 = 总点数 - 最大匹配)。
// 上界方向:设最终断点集合为 B,则 {r_i : r_i 不在 B} ∪ {l_j : l_j 不在 B}
// 是一个大小 2n-|B| 的独立集,故 |B| <= 2n - 最大匹配。
// 可达方向:取最大独立集 S,对每个 s ∈ S 与包含它的区间连有向边
// (“被覆盖者必须在覆盖者之后执行”),这张图无环,
// 任一拓扑序都能让 S 中所有端点成为断点。
//
// 复杂度:建图 O(n^2),匈牙利算法 O(n^3/64)(邻接矩阵用 64 位压位),
// 空间 O(n^2/64)。
#include <bits/stdc++.h>
using namespace std;
const int maxn = 5005;
const int maxw = (maxn + 63) / 64;
int n;
int words; // 邻接矩阵按 64 位分成的“字”数
int lft[maxn], rgt[maxn]; // 第 i 个区间覆盖位置 [lft_i, rgt_i - 1]
unsigned long long adj[maxn][maxw]; // 二分图:左部点 i(r_i)-> 右部点 j(l_j)
int matchR[maxn]; // 右部点匹配到的左部点,-1 表示未匹配
int matchL[maxn]; // 左部点匹配到的右部点,-1 表示未匹配
unsigned long long visR[maxw]; // 本轮匈牙利中右部点的访问标记
// 匈牙利算法:从左侧点 u 出发找增广路。
// 右部点的访问标记按位存储,未访问的候选点 = adj[u] & ~visR,一次能跳过一整字。
bool dfs(int u) {
for (int w = 0; w < words; w++) {
unsigned long long cur = adj[u][w] & ~visR[w];
while (cur) {
int b = __builtin_ctzll(cur); // 取出最低位的 1
cur &= cur - 1; // 抹掉这一位
visR[w] |= 1ULL << b;
int v = (w << 6) + b;
if (matchR[v] == -1 || dfs(matchR[v])) {
matchR[v] = u;
matchL[u] = v;
return true;
}
}
}
return false;
}
void read_data() {
cin >> n;
for (int i = 0; i < n; i++) {
cin >> lft[i] >> rgt[i];
}
}
void solve() {
words = (n + 63) / 64;
// 建图:l_i < l_j < r_i < r_j 时连边 i -> j
for (int i = 0; i < n; i++) {
for (int j = 0; j < n; j++) {
if (i == j) {
continue;
}
if (lft[i] < lft[j] && lft[j] < rgt[i] && rgt[i] < rgt[j]) {
adj[i][j >> 6] |= 1ULL << (j & 63);
}
}
}
for (int i = 0; i < n; i++) {
matchR[i] = -1;
matchL[i] = -1;
}
int matching = 0;
for (int i = 0; i < n; i++) {
if (matchL[i] != -1) {
continue;
}
memset(visR, 0, sizeof(unsigned long long) * words);
if (dfs(i)) {
matching++;
}
}
cout << 2 * n - matching << "\n";
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
read_data();
solve();
return 0;
}复杂度
设
- 建图:两层循环
。 - 匈牙利算法:每个未匹配左侧点跑一次增广路,每次找增广路的代价是
,共 轮,最坏 ;实测 的最坏数据约 s。 - 空间:压位邻接矩阵
,其余数组 ,总计约 MiB。
总结
- 断点只在端点处。 每个区间都覆盖自己的
却不覆盖 ,覆盖 却不覆盖 ,所以只有 个端点可能成为断点,问题被压缩成"选哪些端点"。 - 端点成为断点的条件统一。 “
是断点” "某个覆盖 的区间排在 所属区间之后",这让"选端点"和"排顺序"互相翻译。 - 冲突只有交错对。 对
分类讨论,只有 会强制 与 二选一;其余两类(相离、包含)没有任何约束。 - **答案
= 2n - $ 最大匹配。** 把 $r_i 放左部、放右部、交错对连边,可选的断点集合正是这张二分图的独立集,“上界 = 独立集 ≤ ”,而按覆盖关系建出的先后约束图无环,任一拓扑序都能达到 (König 定理的直接应用)。 - 实现要压位。 稠密图下朴素的匈牙利太慢,用
unsigned long long压位邻接矩阵 +__builtin_ctzll枚举出边,先把空间压进 16 MiB,再把时间压到。