二分图

Posted by TH911 on January 26, 2025

「理论上到现在应该已经学习完了所有 NOIP 考纲内的知识点,如果还有不会的赶紧学习。」

突然发现自己从来没有学过二分图。想起来去年本来想学,但是看了一下好像没怎么考,学别的去了。

二分图基础

定义

对于一张图,如果其点集可以划分为两个部分,且图上的每一条边的两个端点都不在同一个部分中,则称这个图为「二分图」。

形式化地,设图 $G=(V,E)$,若 $V=X\cup Y,X\cap Y=\varnothing$,则对于 $\forall (u,v)\in E$ 有 $u\in X,v\in Y$,则 $G$ 为二分图。

常见的二分图有:树、偶环、网格。

如果已知 $G=(V,E),V=X\cap Y,X\cap Y=\varnothing$,则二分图 $G$ 也可记作 $G=(X,Y,E)$。称 $X,Y$ 分别为 $G$ 的左部、右部。

性质

  1. 二分图可以仅用两种颜色对所有节点染色,且没有边连接颜色相同的点。
  2. 二分图中不存在奇环。
  3. 二分图的各连通块互不影响。

性质 $1$ 是显然的,且性质 $1$ 与二分图的定义等价。

对于性质 $2$,考虑奇环不满足性质 $1$。

判定

常用的判定方法有:

  1. 可以仅用两种颜色对所有节点染色,且没有边连接颜色相同的点的图为二分图。
  2. 不存在奇环的图为二分图。

判定方法 $1$ 与定义等价。

对于判定方法 $2$,考虑到原图要么是树,要么是含有偶环的图。前者是二分图,而后者也是,因为将偶环缩为一个点不影响其是否为二分图,则等价于一棵树。

实际应用中,就常常通过在图上染色的方式来判定二分图——间隔染色,如果当前节点 $x$ 应该染的颜色 $\textit{color}$ 与之前染的颜色 $\textit{vis}_x$ 不同,则不为二分图。

SP3377 BUGLIFE - A Bug’s Life

虫子还有同性恋。

给定 $n$ 个点,$m$ 条边,求能否将所有节点划分为两个部分,使得所有边连接的节点都处于不同部分,多测。如果可以,输出 No suspicious bugs found!,否则输出 Suspicious bugs found!

显然是二分图判定问题。注意要对所有连通块进行 DFS 判断。时间复杂度 $\mathcal O(n+m)$。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
//#include<bits/stdc++.h>
#include<algorithm>
#include<iostream>
#include<cstring>
#include<iomanip>
#include<cstdio>
#include<string>
#include<vector>
#include<cmath>
#include<ctime>
#include<deque>
#include<queue>
#include<stack>
#include<list>
using namespace std;
constexpr const int N=2000,M=1e6;
int n;
vector<int>g[N+1];
int vis[N+1];
void dfs(int x,int fx,int color,bool &ans){
	if(vis[x]){
		if(vis[x]!=color){
			ans=true;
		}
		return;
	}
	vis[x]=color;
	for(int i:g[x]){
		if(i==fx){
			continue;
		}
		dfs(i,x,color^3,ans);
		if(ans){
			return;
		}
	}
}
int main(){
	/*freopen("test.in","r",stdin);
	freopen("test.out","w",stdout);*/
	
	ios::sync_with_stdio(false);
	cin.tie(0);cout.tie(0);
	
	int T;
	cin>>T;
	for(int i=1;i<=T;i++){
		int m;
		cin>>n>>m;
		for(int i=1;i<=n;i++){
			g[i].resize(0);
		}
		while(m--){
			int u,v;
			cin>>u>>v;
			g[u].push_back(v);
			g[v].push_back(u);
		}
		bool ans=false;
		memset(vis,0,sizeof(vis));
		for(int i=1;i<=n;i++){
			if(!vis[i]){
				dfs(i,0,1,ans);
			}
		}
		cout<<"Scenario #"<<i<<":\n";
		if(ans){
			cout<<"Suspicious bugs found!\n";
		}else{
			cout<<"No suspicious bugs found!\n";
		}
	}
	
	cout.flush();
	
	/*fclose(stdin);
	fclose(stdout);*/
	return 0;
}

二分图最大匹配

图的匹配

无向图 $G=(V,E)$ 的匹配为边集 $M\subseteq E$,且 $M$ 中的边没有共顶点。即从图中选择若干条没有共顶点的边。

而最大匹配,即边数最多的匹配。

匈牙利算法

二分图算法怎么那么多名字。也可以叫 Kuhn 算法,实际上,Kuhn 算法是匈牙利算法的一部分。

对于一个匹配,引入概念:

  • 交错路(alternating path):匹配边、非匹配边交错形成的路径。
  • 增广路(augmenting path):两端为非匹配点的交错路。

考虑一条增广路,对其进行增广操作,即将增广路上的匹配边与非匹配边交换,则可以增加 $1$ 条匹配边

考虑不断进行增广操作,则不能增广的时候为最大匹配。

因此可以枚举左部点 $u$,令其和与其相连的右部点 $v$ 配对(边 $(u,v)$ 存在):

  • 如果 $v$ 已经配对了,就尝试让与之配对的 $u’$ 换一个点 $v’$ 配对:
    • 如果找到了合法 $v’$,那么 $u,v$ 配对,可以进行增广操作。
    • 找不到,则 $v$ 不能与 $u$ 配对。
  • 如果 $v$ 没有配对,自然最好,$u,v$ 配对。

时间复杂度:$\mathcal O(nm)$,$n$ 为左部点数(总点数),$m$ 为边数。

luogu P3386 【模板】二分图最大匹配

给定一个二分图,其左部点的个数为 $n$,右部点的个数为 $m$,边数为 $e$,求其最大匹配的边数。

左部点从 $1$ 至 $n$ 编号,右部点从 $1$ 至 $m$ 编号。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=500,M=500; int n,m; vectorg[N+1]; bool used[M+1]; int match[M+1]; bool dfs(int x){ for(int i=0;i<g[x].size();i++){ int v=g[x][i]; if(used[v]){ continue; } used[v]=true; if(!match[v]||dfs(match[v])){ match[v]=x; return true; } } return false; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int e; cin>>n>>m>>e; while(e--){ int u,v; cin>>u>>v; g[u].push_back(v); } int ans=0; for(int i=1;i<=n;i++){ memset(used,0,sizeof(used)); ans+=dfs(i); } cout<<ans<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details>
关于最大匹配的实际取值

匈牙利算法看起来好像无法得出最大匹配实际包含哪些边。但其实是可以知道的。也许就只有我有这种愚蠢疑问了。

$u$ 与节点 $u$ 配对的节点 $v$ 组成的边 $(u,v)$ 即为一条匹配边。

也可以写一下 UOJ#78. 二分图最大匹配,需要输出具体的边。

参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=500,M=500; int n,m; vectorg[N+1]; bool used[M+1]; int match[M+1],reMatch[N+1]; bool dfs(int x){ for(int i=0;i<g[x].size();i++){ int v=g[x][i]; if(used[v]){ continue; } used[v]=true; if(!match[v]||dfs(match[v])){ match[v]=x; reMatch[x]=v; return true; } } return false; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int e; cin>>n>>m>>e; while(e--){ int u,v; cin>>u>>v; g[u].push_back(v); } int ans=0; for(int i=1;i<=n;i++){ memset(used,0,sizeof(used)); ans+=dfs(i); } cout<<ans<<'\n'; for(int i=1;i<=n;i++){ cout<<reMatch[i]<<' '; } cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> </details> ## Dinic 算法 > [luogu P3386 【模板】二分图最大匹配](https://www.luogu.com.cn/problem/P3386) > > 给定一个二分图,其左部点的个数为 $n$,右部点的个数为 $m$,边数为 $e$,求其最大匹配的边数。 > > 左部点从 $1$ 至 $n$ 编号,右部点从 $1$ 至 $m$ 编号。 也许应当叫 Hopcroft-Karp 算法,Dinic 算法本身是网络流算法,Hopcroft-Karp 算法是 Dinic 算法的特殊形式。 考虑建立一个超级源点 $s$ 和超级汇点 $t$。$s$ 向左部连边,右部向 $t$ 连边,每条边流量均为 $1$,则最大流即最大匹配。 时间复杂度 $\mathcal O\left(m\sqrt n\right)$。
参考代码 ```cpp //#include<bits/stdc++.h> #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace std; constexpr const int N=500*2+2,M=5e4+N; constexpr const int inf=0x3f3f3f3f; struct graph{ struct edge{ int v,r,w; }a[(M<<1)+1+1]; int size=1,h[N+1]; inline void create(int u,int v,int w){ a[++size]={v,h[u],w}; h[u]=size; } edge& operator [](int x){ return a[x]; } }g; int n,s,t,cur[N+1],dis[N+1]; bool bfs(){ memset(dis,0x3f,sizeof(dis)); for(int i=1;i<=n;i++){ cur[i]=g.h[i]; } queueq; q.push(s); dis[s]=0; while(q.size()){ int x=q.front();q.pop(); for(int i=g.h[x];i;i=g[i].r){ auto [v,r,w]=g[i]; if(w>0&&dis[v]==inf){ q.push(v); dis[v]=dis[x]+1; if(v==t){ return true; } } } } return false; } int dfs(int x,int Min){ if(x==t){ return Min; } int ans=0; for(int i=cur[x];i&&Min;i=g[i].r){ cur[x]=i; auto [v,r,w]=g[i]; if(w&&dis[v]==dis[x]+1){ int pl=dfs(v,min(Min,w)); if(!pl){ dis[v]=inf; } g[i].w-=pl; g[i^1].w+=pl; ans+=pl; Min-=pl; } } return ans; } int Dinic(){ int ans=0; while(bfs()){ ans+=dfs(s,inf); } return ans; } int main(){ /*freopen("test.in","r",stdin); freopen("test.out","w",stdout);*/ ios::sync_with_stdio(false); cin.tie(0);cout.tie(0); int m,e; cin>>n>>m>>e; while(e--){ int u,v; cin>>u>>v; g.create(u,v+n,1); g.create(v+n,u,0); } s=n+m+1; t=n+m+2; for(int i=1;i<=n;i++){ g.create(s,i,1); g.create(i,s,0); } for(int i=1;i<=m;i++){ g.create(i+n,t,1); g.create(t,i+n,0); } n=t; cout<<Dinic()<<'\n'; cout.flush(); /*fclose(stdin); fclose(stdout);*/ return 0; } ``` </details> ## 二分图最小点覆盖 即在一张无向图中选择最少的点,满足每条边至少有一个端点被选。
Kőnig 定理

二分图中,最小点覆盖中的顶点数量等于最大匹配中的边数量。

由 Kőnig 定理,直接求解二分图最大匹配即可。 设二分图 $G=(X,Y,E)$ 的最大匹配 $M$,未匹配的左部点集 $U$,从 $U$ 出发沿交错路到达点集为 $Z$,则 $(X\setminus Z)\cup(Y\cap Z)$ 为最小点覆盖。 ![](/img/2025/11/002.svg) ## 二分图最大独立集 在一张无向图中选择最多的顶点,满足两两之间互不相邻。 在图 $G=(V,E)$ 中,点覆盖 $C$ 的补集 $V\setminus C$ 为独立集。 因此可以利用二分图最大匹配求解二分图最大独立集。 # 二分图最大权匹配 即满足匹配边的权值和最大的匹配。