二分图学习笔记(施工中)
定义
对于一个无向图 \(G=(V,E)\),如果存在点集 \(A,B\),满足 \(A\neq\varnothing\),\(B\neq\varnothing\),\(A\cap B=\varnothing\),\(A\cup B=V\),且 \(\forall u,v\in A\) 或 \(u,v\in B\),都有 \((u,v)\notin E\),则称这个图是一个二分图,\(A\) 称为这个二分图的左部,\(B\) 称为右部。
一个二分图可以记作 \(G=(A,B,E)\),下文中使用这种记法时,默认 \(|A|\le|B|\)。
判定
一个图是二分图 \(\iff\) 图中没有奇环。
可以用 DFS 染色 或 扩展域并查集 判断。
例题
二分图最大匹配
定义
-
对于一个图 \(G=(V,E)\),如果一个边集 \(S\subseteq E\) 满足其中任意两条不同的边没有公共端点,则称 \(S\) 为图 \(G\) 的一个匹配。
-
对于匹配 \(S\),属于 \(S\) 的边叫做匹配边,不属于 \(S\) 的边叫做非匹配边。
-
如果点 \(u\) 是一条匹配边的端点,则称 \(u\) 为匹配点,否则是非匹配点。
匹配边的两端都是匹配点;非匹配边的两端要么都是匹配点,要么一个是匹配点,一个是非匹配点。
-
对于二分图 \(G=(A,B,E)\),如果它的一个匹配 \(S\) 满足 \(|S|=|A|\),则称 \(S\) 为 \(G\) 的一个完美匹配。
求解
二分图最大匹配问题可以用网络流求解。建立超级源点 \(S\) 和超级汇点 \(T\),原图中每条边从左部连向右部,容量为 \(1\),从 \(S\) 向每个左部点连一条容量为 \(1\) 的边,从每个右部点向 \(T\) 连一条容量为 \(1\) 的边,新图的最大流就是最大匹配,其中匹配边流量为 \(1\),非匹配边流量为 \(0\)。
在单位容量的网络中,使用 Dinic 的时间复杂度为 \(O(m\sqrt n)\)。
我太菜了,不会匈牙利算法。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1005, M = 2e5 + 5;
int n, m, e, t;
int hd[N], to[M], nxt[M], cap[M], tot = 1;
int dep[N], cur[N];
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
queue<int> q;
q.push(0);
dep[0] = 1;
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = hd[u]; i; i = nxt[i])
if(cap[i] && !dep[to[i]])
dep[to[i]] = dep[u] + 1, q.push(to[i]);
}
return dep[t];
}
int dfs(int u, int flow) {
if(u == t) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
if(dep[to[i]] == dep[u] + 1) {
int o = dfs(to[i], min(cap[i], flow));
flow -= o;
res += o;
cap[i] -= o;
cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) {
rep(i, 0, t) cur[i] = hd[i];
ans += dfs(0, INT_MAX);
}
return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> n >> m >> e;
t = n + m + 1;
rep(i, 1, e) {
int u, v; cin >> u >> v;
v += n;
add(u, v, 1), add(v, u, 0);
}
rep(i, 1, n) add(0, i, 1), add(i, 0, 0);
rep(i, 1, m) add(i + n, t, 1), add(t, i + n, 0);
cout << dinic() << endl;
return 0;
}
常见模型
最小路径覆盖
对于一个有向无环图 \(G=(V,E)\),如果存在一个简单路径的集合 \(S\),满足 \(V\) 中的每个点都恰好在 \(S\) 中的一条路径上,则称 \(S\) 为 \(G\) 的一个路径覆盖。求出所含路径条数最少的路径覆盖。
首先每个点单独构成一条路径肯定是一组路径覆盖。考虑通过图中的边合并这些路径使它变小。
将每个点 \(u\) 拆成两个点 \(u_{in}\) 和 \(u_{out}\),对于原图中的每一条边 \((u,v)\),在新图中连接 \((u_{out},v_{in})\)。在路径覆盖中,每个点最多只有 \(1\) 条入边和 \(1\) 条出边,这对应着新图中的每个点最多只被一条边选中,因此求出新图的最大匹配 \(P\),每选择一条边代表合并了一次路径,最小路径覆盖的大小即为 \(|V|-P\)。每条匹配边对应路径覆盖中的一条边,因此也容易构造出最小路径覆盖。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 205, M = 6005;
int n, m, t;
int to[M * 4], nxt[M * 4], cap[M * 4], hd[N * 2], tot = 1;
int cur[N * 2], dep[N * 2];
int fr[N];
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.push(0);
while(!q.empty()) {
int u = q.front();
cur[u] = hd[u];
q.pop();
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(cap[i] && !dep[v])
dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[t];
}
int dfs(int u, int flow) {
if(u == t) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(flow, cap[i]));
flow -= o;
res += o;
cap[i] -= o;
cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) ans += dfs(0, INT_MAX);
return ans;
}
int find(int x) {
for(int i = hd[x]; i; i = nxt[i])
if(!(i & 1) && !cap[i]) return to[i] - n;
return 0;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> n >> m;
t = n * 2 + 1;
rep(i, 1, n) {
add(0, i, 1), add(i, 0, 0);
add(i + n, t, 1), add(t, i + n, 0);
}
rep(i, 1, m) {
int u, v; cin >> u >> v;
add(u, v + n, 1), add(v + n, u, 0);
}
int ans = n - dinic();
rep(i, 1, n) fr[find(i)] = i;
rep(i, 1, n) if(!fr[i]) {
cout << i << ' ';
int o = find(i);
while(o) {
cout << o << ' ';
o = find(o);
}
cout << endl;
}
cout << ans << endl;
return 0;
}
最小可重路径覆盖
对于一个有向无环图 \(G=(V,E)\),如果存在一个简单路径的集合 \(S\),满足 \(V\) 中的每个点都至少在 \(S\) 中的一条路径上,则称 \(S\) 为 \(G\) 的一个可重路径覆盖。
做传递闭包后转化为不可重路径覆盖。
常用定理
König 定理
对于二分图 \(G=(V,E)\),设 \(P\) 为其最大匹配的大小,则有最小点覆盖 \(=P\)(点覆盖:一个点集 \(S\subseteq V\),满足 \(\forall(u,v)\in E\),都有 \(u\in S\lor v\in S\)。即每条边至少选择一个端点)
-
证明(含构造方案):
-
首先显然有最小点覆盖 \(\ge P\),因为每条匹配边至少有一个端点被选择。
-
考虑从右部每个非匹配点开始 DFS,从右往左只能走非匹配边,从左往右只能走匹配边。设 左部被 DFS 到的点 和 右部没被 DFS 到的点 构成的集合为 \(S\),\(S\) 就是一个最小点覆盖。
-
首先证明 \(|S|=P\):
-
首先注意到每次 DFS 中,除了起点,被 DFS 到的都是匹配点。
-
因为从左往右只能走匹配边,也就只能走到匹配点。而从右往左虽然只能走非匹配边,但是走不到非匹配点,因为如果走到了非匹配点,就说明发生了以下这种情况:(红色为匹配边)
-
显然这存在更大的匹配,与最大匹配矛盾,因此不可能从右部走到非匹配点。
-
而右部的非匹配点都作为起点被 DFS 过,没被 DFS 过的只能是匹配点。如果右部的一个匹配点没被 DFS 过,它就会被加入 \(S\)。右部的一个匹配点被 DFS 过,当且仅当其对应的左部匹配点也被 DFS 过,因此这个左部匹配点被加入 \(S\)。这说明对于每条匹配边,都有且仅有一个端点被加入了 \(\boldsymbol S\)。因此 \(|S|=P\)。
-
-
然后证明 \(S\) 是一个点覆盖:
- 上文已经证明匹配边肯定全被覆盖。一条非匹配边没被 \(S\) 覆盖到的充要条件是,左部端点没被 DFS 过 且 右部端点被 DFS 过。
- 如果一个右部点被 DFS 过,则所有与它相连的非匹配边都被 DFS 过,因此这个条件不可能满足,即不存在未被覆盖的边,所以 \(S\) 是一个点覆盖。
-
综上,\(S\) 是一个最小点覆盖。
-
推论:最大独立集 \(=|V|-P\)(独立集:一个点集 \(S\subseteq V\),满足 \(\forall u,v\in S\),都有 \((u,v)\notin E\)。即选择的点之间不能有边)
-
证明:
- 对于每个独立集 \(S\),其补集 \(\complement_VS\) 都是一个点覆盖。
- 所以最大独立集对应最小点覆盖,大小为 \(|V|-P\)。
最小边覆盖
对于二分图 \(G=(V,E)\),设 \(P\) 为其最大匹配的大小,则有最小边覆盖 \(=|V|-P\)(边覆盖:一个边集 \(S\subseteq E\),满足 \(\forall u\in V\),都 \(\exists v\in V,(u,v)\in E\)。即每个点至少选择一条出边)
-
证明:
- 一条边加入边覆盖中,可能覆盖了一个或两个未覆盖的点。
- 要使边覆盖最小,就要让覆盖了两个未覆盖点的边最多。
- 这种边就是匹配边,向边覆盖中加入每条匹配边,总共覆盖了 \(2P\) 个点。
- 剩下的点每个点对应一条边,这些边的数量为 \(|V|-2P\),边覆盖中的总边数就是 \(|V|-2P+P=|V|-P\)。
Hall 定理:
对于二分图 \(G=(A,B,E)\),定义函数 \(f(S)\) \((S\subseteq A)\) 表示与 \(S\) 中的点有连边的点的数量,则 \(G\) 存在完美匹配的充要条件是 \(\forall S\subseteq A,f(S)\ge|S|\)。
看着很对,证明不会。
例题:CF1009G Allowed Letters
有一个长为 \(n\) 的字符串 \(s\),只包含 \(\texttt a\dots\texttt f\) 共 \(6\) 种字符。你知道每种字符的出现次数,和每个位置可能出现哪些字符,你需要构造出满足条件且字典序最小的 \(s\)。
不难发现这是一个位置和字符之间的匹配问题,考虑建图网络流。
设字符 \(c\) 的出现次数为 \(\mathrm{cnt}_c\)。从源点 \(S\) 向每个位置 \(i\) 连容量为 \(1\) 的边,每个位置向这个位置可能出现的字符连容量为 \(1\) 的边,每个字符 \(c\) 向汇点 \(T\) 连容量为 \(\mathrm{cnt}_c\) 的边,跑最大流,如果能流满就有解,否则无解。
然而直接网络流难以找到字典序最小的匹配。网络流算法的流程很复杂,我们把握不住,所以尽量不要尝试改动网络流的板子,而是考虑其他方法。
最大 / 最小化字典序的问题都可以贪心。从前往后枚举位置 \(i\),尽量让 \(i\) 放更小的字符。因此我们需要解决的问题转化为:当位置 \(i\) 流向字符 \(c\) 时,剩下的点还能否流满。
注意到如果不算源点和汇点,这就判断二分图是否有完美多重匹配(多重匹配:每个点 \(u\) 最多被 \(l_u\) 条边覆盖,而非一般匹配的 \(1\) 条边)。当 \(A,B\) 中只有一个集合存在 \(u\) 满足 \(l_u\neq 1\) 时,我们可以通过把每个点拆成 \(l_u\) 个点转化成一般匹配。我们称位置对应的点集为“位置部”,字符对应的点集为“字符部”。
我们考虑使用 Hall 定理判定有没有完美匹配。
我们显然不能枚举位置部的所有子集,但是字符部只有 \(6\) 个本质不同的点,相同的点无论放多少个,对 \(f\) 值都只会产生一个点的影响,所以只需要枚举 \(6\) 种字符的所有子集,只有 \(2^6=64\) 种情况。
对于每个 \(i\),预处理出每个子集的后缀 \(f\) 值,即只考虑 \(i\) 到 \(n\) 这些位置时,每个子集的 \(f\) 值,然后贪心枚举即可。
设字符集为 \(\Sigma\),则时间复杂度为 \(O(n|\Sigma|2^{|\Sigma|})\)。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1e5 + 5;
int n, t, cnt[6], sum[64], f[N][64];
bool ava[N][6];
string s;
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> s; n = s.size();
for(auto c : s) cnt[c - 'a']++;
rep(i, 0, 63) rep(j, 0, 5) if(i >> j & 1) sum[i] += cnt[j];
cin >> t;
memset(ava, 1, sizeof ava);
rep(i, 1, t) {
int p; cin >> p >> s;
memset(ava[p], 0, sizeof ava[p]);
for(auto c : s) ava[p][c - 'a'] = 1;
}
per(i, n, 1) {
memcpy(f[i], f[i + 1], sizeof f[i]);
rep(j, 0, 63) rep(k, 0, 5)
if((j >> k & 1) && ava[i][k])
{ ++f[i][j]; break; }
}
s.clear();
rep(i, 1, n) {
bool flg = 0;
rep(j, 0, 5) if(ava[i][j]) {
bool fl = 1;
rep(k, 0, 63)
if(k >> j & 1) {
if(f[i + 1][k] < sum[k] - 1) { fl = 0; break; }
}
else if(f[i + 1][k] < sum[k]) { fl = 0; break; }
if(fl) {
flg = 1; s.push_back(j + 'a');
rep(k, 0, 63) if(k >> j & 1) --sum[k];
break;
}
}
if(!flg) cout << "Impossible\n", exit(0);
}
cout << s << endl;
return 0;
}
Dilworth 定理
对于一个有向无环图 \(G=(V,E)\),其最长反链的大小等于最小可重路径覆盖的大小。
反链:如果一个点集 \(S\subseteq V\) 满足 \(\forall u,v\in S,u\neq v\),不存在 \(u\) 到 \(v\) 的路径,则称 \(S\) 为图 \(G\) 的一个反链。
证明不会。
模板题:Luogu P4298 [CTSC2008] 祭祀
给定一个有向无环图 \(G=(V,E)\, (|V|=n,|E|=m)\),有三问:
- 求出其最长反链长度。
- 构造最长反链。
- 判断每个点是否可能在最长反链中。
\(n\le100,m\le1000\)。
第一问,根据 Dilworth 定理,做出传递闭包,做最小不可重路径覆盖即可。构造的二分图 \(G'=(V_{in},V_{out},E')\) 中的一条边 \((u_{out},v_{in})\in E\) 代表 \(u\) 能到达 \(v\)。
第二问,找出二分图的最大独立集 \(S\),所有满足 \(u_{in}\in S\land u_{out}\in S\) 的 \(u\) 构成的集合 \(L\) 就是最长反链。最大独立集是最小点覆盖的补集,先求出最小点覆盖再求补集即可,构造方案见上文 König 定理。
因为取出的每个 \(u\) 都满足 \((u_{in},u_{out})\) 都在同一个独立集中,所以这些点也构成独立集,也就是不存在边 \((u_{in},v_{out})\)(代表 \(u\) 能到达 \(v\)),满足反链的定义。
接下来证明这些点构成的反链是最长的:
设 \(P\) 是 \(G'\) 的最大匹配数,显然 \(P\le n\),\(|S|=|V_{in}|+|V_{out}|-P=2n-P\)。满足在 \(v_{in}\) 和 \(v_{out}\) 中有且仅有一个属于 \(S\) 的节点 \(v\) 最多只有 \(n-|L|\) 个,而每个 \(L\) 中的元素在 \(S\) 中对应两个元素,所以 \(|S|-2|L|\) 就是这样的 \(v\) 的数量,所以 \(|S|-2|L|\le n-|L|\),移项得到 \(|L|\ge|S|-n\) 即 \(|L|\ge n-P\),反链最长也只有 \(n-P\),所以 \(|L|=n-P\),\(L\) 是最长反链。
第三问,枚举每个点分别判断。考虑强制选中这个点,然后再求最长反链,如果最长反链不变,这个点就可以在最长反链中。具体地,如果选中一个点,那么所有和这个点有偏序关系(路径)的点都不能选,所以将这些点都删掉,对剩下的点求最长反链,如果只减少了 \(1\),这个点就合法。
时间复杂度:求传递闭包的时间复杂度为 \(O(n^3)\),求完传递闭包后的边数 \(m'=O(n^2)\),跑一次二分图匹配的时间复杂度是 \(O(m'\sqrt n)=O(n^{2.5})\),第三问要枚举每个点分别跑二分图匹配,所以总时间复杂度为 \(O(n^{3.5})\)。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105, M = 1005;
int n, m, c[N][N];
int to[N * N], nxt[N * N], cap[N * N], hd[N * 2], tot = 1;
int cur[N * 2], dep[N * 2], t;
bool del[N * 2], vis[N * 2];
string s1, s2;
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.emplace(0);
while(!q.empty()) {
int u = q.front();
q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(cap[i] && !del[v] && !dep[v]) {
dep[v] = dep[u] + 1;
q.push(v);
}
}
}
return dep[t];
}
int dfs(int u, int flow) {
if(u == t || !flow) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(!del[v] && dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
flow -= o;
res += o;
cap[i] -= o;
cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) ans += dfs(0, n);
return ans;
}
void recover() {
rep(i, 2, tot)
if(i & 1) cap[i] = 0;
else cap[i] = 1;
memset(del, 0, sizeof del);
}
void dfs(int u) {
vis[u] = 1;
for(int i = hd[u]; i; i = nxt[i])
if(!vis[to[i]] && !cap[i]) dfs(to[i]);
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> n >> m;
rep(i, 1, m) {
int u, v; cin >> u >> v;
c[u][v] = 1;
}
rep(k, 1, n) rep(i, 1, n) rep(j, 1, n)
c[i][j] |= c[i][k] & c[k][j];
t = n * 2 + 1;
rep(i, 1, n) rep(j, 1, n)
if(c[i][j]) add(i, j + n, 1), add(j + n, i, 0);
rep(i, 1, n)
add(0, i, 1), add(i, 0, 0), add(i + n, t, 1), add(t, i + n, 0);
int l = n - dinic();
cout << l << endl;
vis[0] = vis[t] = 1;
for(int i = hd[t]; i; i = nxt[i])
if(!cap[i]) dfs(to[i]);
rep(i, 1, n) s1.push_back(!vis[i] && vis[i + n] ? '1' : '0');
cout << s1 << endl;
rep(i, 1, n) {
recover();
del[i] = del[i + n] = 1;
int t = n - 1;
rep(j, 1, n) if(c[i][j] | c[j][i]) del[j] = del[j + n] = 1, --t;
s2.push_back(t - dinic() == l - 1 ? '1' : '0');
}
cout << s2 << endl;
return 0;
}
练习题
1. Luogu P2756 飞行员配对方案问题
二分图最大匹配板子。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105, M = 2e4 + 5;
int n, m, t;
int to[M], nxt[M], cap[M], hd[N], tot = 1;
int dep[N], cur[N];
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.push(0);
cur[0] = hd[0];
while(!q.empty()) {
int u = q.front();
q.pop();
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(!dep[v] && cap[i])
cur[v] = hd[v], dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[t];
}
int dfs(int u, int flow) {
if(u == t) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
int v = to[i];
cur[u] = i;
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
res += o;
flow -= o;
cap[i] -= o;
cap[i ^ 1] += o;
}
}
return res;
}
int dinic(int ans = 0) {
while(bfs()) ans += dfs(0, INT_MAX);
return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> m >> n;
t = n + 1;
int u, v; cin >> u >> v;
while(u != -1) {
add(u, v, 1);
add(v, u, 0);
cin >> u >> v;
}
rep(i, 1, m) add(0, i, 1), add(i, 0, 0);
rep(i, m + 1, n) add(i, t, 1), add(t, i, 0);
cout << dinic() << endl;
rep(u, 1, m) {
for(int i = hd[u]; i; i = nxt[i])
if(!(i & 1) && !cap[i]) cout << u << ' ' << to[i] << endl;
}
return 0;
}
2. Luogu P2763 试题库问题
二分图最大多重匹配板子。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 1005, K = 25;
int n, k, T;
int to[N * K * 2], nxt[N * K * 2], cap[N * K * 2], hd[N + K], tot = 1;
int dep[N + K], cur[N + K];
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.push(0);
while(!q.empty()) {
int u = q.front(); q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(!dep[v] && cap[i])
dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[T];
}
int dfs(int u, int flow) {
if(u == T || !flow) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
flow -= o, res += o;
cap[i] -= o, cap[i ^ 1] += o;
}
}
return res;
}
void dinic() {
while(bfs()) dfs(0, INT_MAX);
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> k >> n;
T = n + k + 1;
rep(i, 1, k) {
int x; cin >> x;
add(0, i, x), add(i, 0, 0);
}
rep(i, k + 1, k + n) {
int c; cin >> c;
while(c--) {
int x; cin >> x;
add(x, i, 1), add(i, x, 0);
}
add(i, T, 1), add(T, i, 0);
}
dinic();
rep(i, 1, k) {
cout << i << ':';
for(int j = hd[i]; j; j = nxt[j]) {
int v = to[j];
if(v && !cap[j]) cout << ' ' << v - k;
}
cout << endl;
}
return 0;
}
3. Luogu P3254 圆桌问题
也是二分图最大多重匹配板子。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int M = 155, N = 275;
int m, n, T, sum;
int hd[N + M], to[N * M * 2], nxt[N * M * 2], cap[N * M * 2], tot = 1;
int dep[N + M], cur[N + M];
void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
queue<int> q;
dep[0] = 1;
q.push(0);
while(!q.empty()) {
int u = q.front();
q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i])
if(cap[i] && !dep[to[i]])
dep[to[i]] = dep[u] + 1, q.push(to[i]);
}
return dep[T];
}
int dfs(int u, int flow) {
if(u == T || !flow) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(flow, cap[i]));
flow -= o, res += o;
cap[i] -= o, cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) ans += dfs(0, INT_MAX);
return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> m >> n;
T = m + n + 1;
rep(i, 1, m) {
int r; cin >> r; sum += r;
add(0, i, r), add(i, 0, 0);
rep(j, m + 1, m + n)
add(i, j, 1), add(j, i, 0);
}
rep(i, m + 1, m + n) {
int c; cin >> c;
add(i, T, c), add(T, i, 0);
}
if(dinic() == sum) cout << "1\n";
else cout << "0\n", exit(0);
rep(i, 1, m) {
for(int j = hd[i]; j; j = nxt[j])
if(to[j] && !cap[j]) cout << to[j] - m << ' ';
cout << endl;
}
return 0;
}
4. Luogu P2765 魔术球问题
构造图 \(G\),编号为 \(i\) 的球对应节点 \(i\),如果 \(i+j\) 是完全平方数且 \(i<j\),那么由 \(i\) 号点向 \(j\) 号点连一条有向边。问题转化为求最大的 \(k\),使得当 \(G\) 有 \(k\) 个节点(编号由 \(1\) 到 \(k\))时,最小路径覆盖 \(\le n\)。
从 \(1\) 开始枚举答案,依次加点最大流即可。二分答案也行,但是代码难度和时间复杂度都不如直接枚举。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 60;
int n, T;
int hd[N * N * 2], to[N * N * 16], nxt[N * N * 16], cap[N * N * 16], tot = 1;
int dep[N * N * 2], cur[N * N * 2];
bool vis[N * N];
#define ou(x) ((x) << 1)
#define in(x) ((x) << 1 | 1)
inline bool chk(int x) {
int o = sqrtl(x);
if(o * o == x) return 1;
return 0;
}
inline void add(int u, int v, int w) {
to[++tot] = v;
cap[tot] = w;
nxt[tot] = hd[u];
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[1] = 1;
queue<int> q;
q.push(1);
while(!q.empty()) {
int u = q.front();
q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(!dep[v] && cap[i])
dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[T];
}
int dfs(int u, int flow) {
if(u == T || !flow) return flow;
int res = 0;
for(int i = cur[u]; i && flow; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
flow -= o, res += o;
cap[i] -= o, cap[i ^ 1] += o;
}
}
return res;
}
int find(int u) {
for(int i = hd[ou(u)]; i; i = nxt[i])
if(to[i] > 1 && !cap[i]) return to[i] >> 1;
return 0;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> n;
int i = 1, ans = 0;
T = n * n * 2;
while(1) {
add(1, ou(i), 1), add(ou(i), 1, 0);
add(in(i), T, 1), add(T, in(i), 0);
rep(j, 1, i - 1) if(chk(i + j))
add(ou(j), in(i), 1), add(in(i), ou(j), 0);
while(bfs()) ans += dfs(1, INT_MAX);
if(i - ans > n) break;
++i;
}
--i;
cout << i << endl;
vis[0] = 1;
rep(j, 1, i) if(!vis[j]) {
int o = j;
do {
vis[o] = 1;
cout << o << ' ';
o = find(o);
} while(!vis[o]);
cout << endl;
}
return 0;
}
5. Luogu P2172 [国家集训队] 部落战争
相对上一题更容易想到,每个点向它能到达的点连边,即可转化为最小路径覆盖问题。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 55;
int n, m, r, c, T, cnt;
char s[N][N];
int hd[N * N * 2], to[N * N * 16], nxt[N * N * 16], cap[N * N * 16], tot = 1;
int dep[N * N * 2], cur[N * N * 2];
inline int id(int x, int y) {
return (x - 1) * m + y;
}
inline int ou(int x) {
return x << 1;
}
inline int in(int x) {
return x << 1 | 1;
}
void add(int u, int v, int w) {
to[++tot] = v;
nxt[tot] = hd[u];
cap[tot] = w;
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.push(0);
while(!q.empty()) {
int u = q.front();
q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(cap[i] && !dep[v])
dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[T];
}
int dfs(int u, int flow) {
if(u == T || !flow) return flow;
int res = 0;
for(int i = cur[u]; i; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
flow -= o, res += o;
cap[i] -= o, cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) ans += dfs(0, INT_MAX);
return ans;
}
signed main() {
cin >> n >> m >> r >> c;
const int dx[] = {r, r, c, c}, dy[] = {c, -c, r, -r};
T = n * m * 2 + 2;
rep(i, 1, n) scanf("%s", s[i] + 1);
rep(i, 1, n) rep(j, 1, m) if(s[i][j] == '.') {
++cnt;
int u = id(i, j);
add(0, ou(u), 1), add(ou(u), 0, 0);
add(in(u), T, 1), add(T, in(u), 0);
rep(k, 0, 3) {
int x = i + dx[k], y = j + dy[k];
if(x < 1 || y < 1 || x > n || y > m) continue;
int v = id(x, y);
add(ou(u), in(v), 1), add(in(v), ou(u), 0);
}
}
cout << cnt - dinic();
return 0;
}
6. Luogu P4311 士兵占领
将行列看做点,将点看做边,问题转化为选择最少的边,覆盖每个点至少一定次数。发现这个东西和二分图最小边覆盖很相似,套用上文对最小边覆盖的证明即可得出,答案是 \(\sum L+\sum C-F\),其中 \(L,C\) 与题面意义相同,\(F\) 是最大多重匹配。
点击查看代码
#include<bits/stdc++.h>
#define endl '\n'
#define rep(i, s, e) for(int i = s, i##E = e; i <= i##E; ++i)
#define per(i, s, e) for(int i = s, i##E = e; i >= i##E; --i)
#define F first
#define S second
// #define int ll
#define gmin(x, y) (x = min(x, y))
#define gmax(x, y) (x = max(x, y))
using namespace std;
typedef long long ll;
typedef unsigned long long ull;
typedef long double f128;
typedef pair<int, int> pii;
constexpr int N = 105;
int n, m, k, T, r[N], c[N];
bool ava[N][N];
int hd[N * 2], to[N * N * 2], nxt[N * N * 2], cap[N * N * 2], tot = 1;
int dep[N * 2], cur[N * 2];
void add(int u, int v, int w) {
to[++tot] = v;
nxt[tot] = hd[u];
cap[tot] = w;
hd[u] = tot;
}
bool bfs() {
memset(dep, 0, sizeof dep);
dep[0] = 1;
queue<int> q;
q.push(0);
while(!q.empty()) {
int u = q.front();
q.pop();
cur[u] = hd[u];
for(int i = hd[u]; i; i = nxt[i]) {
int v = to[i];
if(cap[i] && !dep[v])
dep[v] = dep[u] + 1, q.push(v);
}
}
return dep[T];
}
int dfs(int u, int flow) {
if(u == T || !flow) return flow;
int res = 0;
for(int i = cur[u]; i; i = nxt[i]) {
cur[u] = i;
int v = to[i];
if(dep[v] == dep[u] + 1) {
int o = dfs(v, min(cap[i], flow));
flow -= o, res += o;
cap[i] -= o, cap[i ^ 1] += o;
}
}
return res;
}
int dinic() {
int ans = 0;
while(bfs()) ans += dfs(0, INT_MAX);
return ans;
}
signed main() {
#ifdef ONLINE_JUDGE
ios::sync_with_stdio(0);
cin.tie(0), cout.tie(0);
#endif
cin >> n >> m >> k;
T = n + m + 1;
int sum = 0;
rep(i, 1, n) cin >> r[i], sum += r[i], add(0, i, r[i]), add(i, 0, 0);
rep(i, 1, m) cin >> c[i], sum += c[i], add(i + n, T, c[i]), add(T, i + n, 0);
rep(i, 1, k) {
int u, v; cin >> u >> v;
++r[u], ++c[v]; ava[u][v] = 1;
if(m < r[u]) cout << "JIONG!\n", exit(0);
if(n < c[v]) cout << "JIONG!\n", exit(0);
}
rep(i, 1, n) rep(j, 1, m) if(!ava[i][j])
add(i, j + n, 1), add(j + n, i, 0);
cout << sum - dinic() << endl;
return 0;
}