【HAOI2017】新型城市化,二分图最大匹配的必经与可行点边
题意
给定一张图的补图,保证原图可以被划分为不超过两个团。对于补图中的每条边,判断在原图中加入这条边后,最大团大小是否会增大。
数据范围: \(n \leq 100000, m \leq 150000\)
题解
容易发现,补图是一张二分图。
因此,原图中的最大团就对应补图中的最大独立集。
二分图最大独立集 \(=\) 点数 \(-\) 最大匹配数
只需判断每条边是否一定在最大匹配上。
先用最大流跑一遍二分图匹配,再在残量网络上跑一遍 tarjan 缩点。如果一条边被匹配,而且两个端点不在同一个强连通分量中,那么这条边是必须边。
两个端点不在一个强连通分量中,就是说没有一条从一个端点到另一个端点的增广路,也就是说这条边是无可替代的。
一些拓展
二分图最大匹配的可行边
指存在一个最大匹配包含这条边。
要么这条边已经在求出的匹配中,要么这条边两个端点在同一个强连通分量里。
二分图最大匹配的必经点
只需求出非必经点即可。
从 \(S\) 开始,走 \(cap=1\) 的边,走到所有 \(X\) 部的点都是答案;从 \(T\) 开始,走 \(cap=0\) 的边,走到所有 \(Y\) 部的点都是答案。
感性理解:首先一个不在匹配的点一定会被走到,这些点都是非必经点。一个最大匹配中的点如果是非必经点,一定可以被一个不在匹配中的点替代掉。从某个不在匹配中的 \(X\) 部点出发走非匹配边到 \(Y\) 部,如果能走匹配边回来,那么翻转所有匹配边和非匹配边,就能用不在匹配中的那个点代替掉匹配中的 \(X\) 部点。
二分图最大匹配的可行点
存在一条可行边或者必经边就是可行点。
#pragma GCC optimize("2,Ofast,inline")
#include<bits/stdc++.h>
#define fi first
#define se second
#define mp make_pair
#define pb push_back
#define LL long long
#define pii pair<int, int>
using namespace std;
const int N = 1e6 + 10;
template <typename T> T read(T &x) {
int f = 0;
register char c = getchar();
while (c > '9' || c < '0') f |= (c == '-'), c = getchar();
for (x = 0; c >= '0' && c <= '9'; c = getchar())
x = (x << 3) + (x << 1) + (c ^ 48);
if (f) x = -x;
return x;
}
int n, m, E, S, T, tim, sccn;
int u[N], v[N];
int fir[N], nex[N], arr[N], vis[N], cap[N], col[N];
int dis[N], cur[N];
int top, st[N], dfn[N], low[N], scc[N];
vector<pii> ans;
inline void Add_Edge(int x, int y, int c) {
nex[++E] = fir[x];
fir[x] = E; arr[E] = y; cap[E] = c;
nex[++E] = fir[y];
fir[y] = E; arr[E] = x; cap[E] = 0;
}
void dfs(int x) {
vis[x] = 1;
for (int i = fir[x]; i; i = nex[i]) {
if (vis[arr[i]]) continue;
col[arr[i]] = col[x] ^ 1;
dfs(arr[i]);
}
}
int bfs() {
queue<int> Q;
memset(dis, 0x3f, sizeof dis);
dis[S] = 0; Q.push(S);
while (!Q.empty()) {
int x = Q.front(); Q.pop();
for (int i = fir[x]; i; i = nex[i]) {
if (cap[i] && dis[arr[i]] > dis[x] + 1) {
dis[arr[i]] = dis[x] + 1;
Q.push(arr[i]);
}
}
}
return (dis[T] < 1e9);
}
int dinic(int x, int mf) {
if (!mf || x == T) return mf;
int ans = 0;
for (int &i = cur[x]; i; i = nex[i]) {
if (!cap[i] || dis[arr[i]] != dis[x] + 1) continue;
int del = dinic(arr[i], min(cap[i], mf));
cap[i] -= del; cap[i ^ 1] += del;
ans += del; mf -= del;
if (!mf) break;
}
return ans;
}
void tarjan(int x) {
dfn[x] = low[x] = ++tim;
st[++top] = x;
for (int i = fir[x]; i; i = nex[i]) {
if (!cap[i]) continue;
if (!dfn[arr[i]]) {
tarjan(arr[i]);
low[x] = min(low[x], low[arr[i]]);
}
else if (!scc[arr[i]]) {
low[x] = min(low[x], dfn[arr[i]]);
}
}
if (low[x] == dfn[x]) {
int y;
++sccn;
do {
y = st[top--];
scc[y] = sccn;
} while (x != y);
}
}
int main() {
read(n); read(m);
for (int i = 1; i <= m; ++i) {
int x, y;
read(x); read(y);
Add_Edge(x, y, 0);
u[i] = x; v[i] = y;
}
for (int i = 1; i <= n; ++i) {
if (!vis[i]) {
dfs(i);
}
}
memset(fir, 0, sizeof fir);
E = 1;
for (int i = 1; i <= m; ++i) {
if (col[u[i]]) swap(u[i], v[i]);
Add_Edge(u[i], v[i], 1);
}
S = n + 1; T = n + 2;
for (int i = 1; i <= n; ++i) {
if (!col[i]) Add_Edge(S, i, 1);
else Add_Edge(i, T, 1);
}
while (bfs()) {
for (int i = 1; i <= n + 2; ++i) {
cur[i] = fir[i];
}
dinic(S, n);
}
for (int i = 1; i <= n + 2; ++i) {
if (!scc[i]) tarjan(i);
}
for (int i = 1; i <= n; ++i) {
if (col[i]) continue;
for (int j = fir[i]; j; j = nex[j]) {
if (cap[j]) continue;
if (scc[i] != scc[arr[j]] && arr[j] != S && arr[j] != T) {
ans.pb(mp(i, arr[j]));
}
}
}
for (int i = 0; i < ans.size(); ++i) {
if (ans[i].fi > ans[i].se) {
swap(ans[i].fi, ans[i].se);
}
}
sort(ans.begin(), ans.end());
cout << ans.size() << endl;
for (int i = 0; i < ans.size(); ++i) {
printf("%d %d\n", ans[i].fi, ans[i].se);
}
return 0;
}