把编号与幻灯片建成二分图,用匈牙利算法求完美匹配,再对每条匹配边删边判交错路,找出所有完美匹配公共的必须边。
OJ: roj
题目 ID: 3222
难度:提高
标签:图论二分图二分图最大匹配的必须边匈牙利算法
创建: 2026-10-02 03:02
更新: 2026-10-10 11:30
形式化题目
平面上有 none。
正解
思路
第一步:把"唯一确定"翻译成图论语言。 把编号
必须边有一个好刻画的等价定义:
理由很直接:若删掉后仍存在完美匹配,那它就是一条不含
第二步:朴素枚举为什么不行,必须边检测为什么快。 暴力做法是枚举全部
第三步:删掉一条匹配边后怎么判断还能不能完美匹配。 删掉匹配边
- 找得到:按交错路换边,得到避开
的新完美匹配, 不是必须边; - 找不到:每个完美匹配都必须含
,它是必须边,登记配对。
下面是样例 1(
digraph G {
rankdir = LR;
node [shape = circle, style = filled, fillcolor = lightblue];
1 2 3 4;
node [shape = box, style = filled, fillcolor = lightyellow];
A B C D;
1 -> A [label = " ", style = dashed];
1 -> D [label = " ", penwidth = 2];
2 -> B [label = " ", penwidth = 2];
2 -> C [label = " ", style = dashed];
2 -> D [label = " ", style = dashed];
3 -> B [label = " ", style = dashed];
3 -> C [label = " ", penwidth = 2];
3 -> D [label = " ", style = dashed];
4 -> B [label = " ", penwidth = 2];
{ rank = same; 1; 2; 3; 4; }
{ rank = same; A; B; C; D; }
}
以编号 1(实线配在 D)为例:删掉 (A,4)。再看一条虚线边为什么不用检验:比如
第四步:输出。 必须边两两不冲突:none。注意题意是输出所有能唯一确定的幻灯片,部分幻灯片确定不了不影响其余的输出(这也是本题最容易误解的地方)。
代码
/**
* 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-10 11:30
* update_at: 2026-10-10 11:30
*/
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
struct Rect { int x1, x2, y1, y2; };
Rect rects[30];
int match[30]; // 幻灯片 -> 当前占用它的编号
vector<int> adj[30]; // 编号 i 的邻接表(能落到的幻灯片)
bool augment(int i, bool seen[]) {
for (size_t t = 0; t < adj[i].size(); t++) {
int s = adj[i][t];
if (seen[s]) continue;
seen[s] = true;
if (match[s] == -1 || augment(match[s], seen)) {
match[s] = i;
return true;
}
}
return false;
}
// 删掉匹配边 (i, s) 后,编号 i 能否沿交错路绕回 s 重新落位
bool can_swap(int i, int s, int n) {
bool seen[30] = {false};
deque<int> q;
for (size_t t = 0; t < adj[i].size(); t++) {
int u = adj[i][t];
if (u != s) { seen[u] = true; q.push_back(u); }
}
while (!q.empty()) {
int t = q.front(); q.pop_front();
int j = match[t];
for (size_t t2 = 0; t2 < adj[j].size(); t2++) {
if (adj[j][t2] == s) return true;
int u = adj[j][t2];
if (!seen[u]) { seen[u] = true; q.push_back(u); }
}
}
return false;
}
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int heap_no = 0;
int n;
while (cin >> n && n != 0) {
heap_no++;
for (int i = 0; i < n; i++)
cin >> rects[i].x1 >> rects[i].x2 >> rects[i].y1 >> rects[i].y2;
// 编号 i 的坐标
for (int i = 0; i < n; i++) {
int x, y;
cin >> x >> y;
adj[i].clear();
for (int s = 0; s < n; s++) {
if (rects[s].x1 <= x && x <= rects[s].x2 &&
rects[s].y1 <= y && y <= rects[s].y2)
adj[i].push_back(s);
}
}
// 求一个完美匹配
for (int s = 0; s < n; s++) match[s] = -1;
for (int i = 0; i < n; i++) {
bool seen[30] = {false};
augment(i, seen);
}
int owner[30];
for (int s = 0; s < n; s++) owner[s] = -1;
bool perfect = true;
for (int s = 0; s < n; s++)
if (match[s] == -1) { perfect = false; break; }
if (perfect) {
// 编号 i 的当前匹配边是否必须
int owner_by_num[30];
for (int i = 0; i < n; i++) owner_by_num[i] = -1;
for (int s = 0; s < n; s++) owner_by_num[match[s]] = s;
for (int i = 0; i < n; i++) {
int s = owner_by_num[i];
if (!can_swap(i, s, n)) {
owner[s] = i; // 必须边:幻灯片 s 被编号 i 唯一确定
}
}
}
vector<string> parts;
for (int s = 0; s < n; s++) {
if (owner[s] != -1) {
string p = "(";
p += (char)('A' + s);
p += "," + to_string(owner[s] + 1) + ")";
parts.push_back(p);
}
}
cout << "Heap " << heap_no << "\n";
if (parts.empty()) cout << "none\n";
else {
for (size_t i = 0; i < parts.size(); i++) {
if (i) cout << " ";
cout << parts[i];
}
cout << "\n";
}
cout << "\n";
}
return 0;
}#!/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-02 03:02
# update_at: 2026-10-02 03:02
import sys
from collections import deque
Rect = tuple[int, int, int, int] # 一张幻灯片的边界 (x_min, x_max, y_min, y_max)
def slides_covering(rects: list[Rect], point: tuple[int, int]) -> list[int]:
"""返回包含该点的幻灯片下标:编号可能落在哪些幻灯片上(二分图中该编号的邻接表)。"""
x, y = point
return [i for i, (x1, x2, y1, y2) in enumerate(rects) if x1 <= x <= x2 and y1 <= y <= y2]
def augment(match: list[int], i: int, adj: list[list[int]], seen: list[bool]) -> bool:
"""匈牙利增广:编号 i 能否沿交错路找到一个空幻灯片,找到则更新 match。"""
for t in adj[i]:
if seen[t]:
continue
seen[t] = True
if match[t] == -1 or augment(match, match[t], adj, seen):
match[t] = i
return True
return False
def can_swap(i: int, s: int, adj: list[list[int]], match: list[int]) -> bool:
"""删掉匹配边 (i, s) 后,编号 i 能否沿交错路绕回 s 重新落位:能则 (i, s) 不是必须边。"""
seen = [False] * len(adj)
queue = deque(t for t in adj[i] if t != s) # 第一步不许再走被删的边
for t in queue:
seen[t] = True
while queue:
t = queue.popleft()
j = match[t] # 占用 t 的编号,沿匹配边走到它
if s in adj[j]:
return True # j 腾退到 s、i 顶替 t:交错环成立
for u in adj[j]:
if not seen[u]:
seen[u] = True
queue.append(u)
return False
def must_owner(adj: list[list[int]]) -> list[int | None]:
"""返回 owner[s] = 幻灯片 s 被唯一确定的编号,无法确定记为 None。
必须边 (i, s) 含义:删掉它后不存在完美匹配,于是它出现在每个完美匹配中,
各编号的必须边天然互不冲突,可以逐个登记后按幻灯片输出。
"""
n = len(adj)
match = [-1] * n # 幻灯片 -> 当前占用它的编号
for i in range(n):
augment(match, i, adj, [False] * n)
if -1 in match: # 完美匹配不存在,任何幻灯片都无法确定
return [None] * n
# 编号 i 只有当前匹配边可能成为必须边:其余边都被这份匹配本身避开
must: list[int | None] = [None] * n
for i in range(n):
s = match.index(i) # 编号 i 当前占用的幻灯片
if not can_swap(i, s, adj, match):
must[i] = s # 删掉后无法重排:每个完美匹配都含这条边
owner: list[int | None] = [None] * n
for i, s in enumerate(must):
if s is not None:
owner[s] = i
return owner
def solve() -> None:
data = iter(map(int, sys.stdin.buffer.read().split()))
out: list[str] = []
heap_no = 0
while (n := next(data)) != 0:
heap_no += 1
rects: list[Rect] = []
for _ in range(n):
rects.append((next(data), next(data), next(data), next(data)))
points = [(next(data), next(data)) for _ in range(n)] # 编号 1..n 的坐标
adj = [slides_covering(rects, p) for p in points]
owner = must_owner(adj)
# 只输出能唯一确定的幻灯片;一个都确定不了才输出 none
parts = [f"({chr(65 + s)},{owner[s] + 1})" for s in range(n) if owner[s] is not None]
out += [f"Heap {heap_no}", " ".join(parts) if parts else "none", ""]
print("\n".join(out))
if __name__ == "__main__":
solve()复杂度
设
- 建图与匈牙利求完美匹配:
; - 必须边判定:对
条匹配边各做一次 的交错路 BFS,合计 。
总复杂度
总结
- 合法分配 = 二分图完美匹配,“幻灯片编号唯一确定” = 配对边是必须边(出现在每个完美匹配中)。
- 必须边检测只需两步:先匈牙利求出一个完美匹配,再对每条匹配边删边跑一次交错路 BFS——能绕回则非必须,绕不回则必须。
- 两个易错点:① 非匹配边不可能是必须边,不必枚举全部
条边;② 输出的是"所有能唯一确定的幻灯片",部分确定时输出确定的部分,一个都确定不了才输出 none。
