二分图学习笔记

前言

什么是二分图?二分图,又称二部图,如下图,是一类结构特殊的图。它的顶点集可以划分为两个互不相交的子集,使得图中的每条边都连接这两个集合之间的一对点,而不会连接同一集合内部的点,简单讲就是一个图的节点被分为两个集合,每个集合只向另一个集合建边的图,这样的图有很多性质,本篇着重讲述二分图染色法判断,二分图最大匹配(匈牙利算法),二分图最大独立集

二分图

树、偶环、网格图等都是常见的二分图的例子

染色法二分图

首先染色法,顾名思义,我们把每个节点染上颜色,通过dfs或bfs,从一个节点开始将当前节点染成黑色,递归到第二个节点,将节点染成白色,再将第三个节点再染成黑色即可,如果递归过程中出现了该节点的下一个节点被上过颜色,且与当前节点要上的颜色相同,那就产生了奇数环(一个环中节点的个数为奇数),有奇环就不是二分图,这就是二分图判定

【参考代码】

// 只展示dfs做法
inline bool find(int u, int c){
	col[u] = c;
	for(register int j : g[u]){
		if(col[j] == c){
			return false;
		}
		else if(col[j] == 0 && !find(j, c ^ 2)){
			return false;
		}
	}
	return true;
}

P1525 [NOIP 2010 提高组] 关押罪犯

二分答案,染色法

思路

首先我们可以想到二分答案,二分取得最小的怨气。根据每个人之间的怨气,大于mid的就不能放在一个监狱里,就建边。如果最终的图是个二分图,证明可行,如果不是,则证明不可行。如此二分出最终答案即可

【参考代码】

#include<bits/stdc++.h>

using namespace std;

const int N = 1e6 + 10;
typedef long long LL;
typedef pair<int, int> PII;

int n, m;
int col[N];

vector<int> g[N];

struct Edges{
	int a, b, c;
}e[N];

inline bool find(int u, int c){
	col[u] = c;
	for(register int j : g[u]){
		if(col[j] == c){
			return false;
		}
		else if(col[j] == 0 && !find(j, c ^ 2)){
			return false;
		}
	}
	return true;
}

inline bool check(int mid){
	
	for(register int i = 1 ; i <= n ; i ++){
		g[i].clear();
		col[i] = 0;
	}
	
	for(register int i = 1 ; i <= m ; i ++){
		register int a = e[i].a, b = e[i].b, c = e[i].c;
		if(c <= mid) continue;
		g[a].push_back(b);
		g[b].push_back(a);
	}
	
	for(register int i = 1 ; i <= n ; i ++){
		if(!col[i] && !find(i, 1)) return false;
	}
	return true;
}

int main(){
	
	ios::sync_with_stdio(false);
	cin.tie(0);
	
	cin>>n>>m;
	
	for(register int i = 1 ; i <= m ; i ++){
		register int a, b, c;
		cin>>a>>b>>c;
		e[i] = {a, b, c};
	}
	
	int l = 0, r = 1e9;
	while(l < r){
		int mid = l + r >> 1;
		if(check(mid)) r = mid;
		else l = mid + 1;
	}
	
	cout<<r;
	
	return 0;
} 

二分图最大匹配

一个重要的小性质:最小点覆盖=最大匹配数(konig定理)

二分图最大匹配可以看做是男生女生配对,左边是男生,右边是女生,假设每个男生都有自己喜欢的女孩(可以不止一个),我们希望让每一个男孩子都找到喜欢的对象,也就是让匹配对数更多,如何处理呢?

匈牙利算法

增广路 :从一个未匹配的点出发,走交错路,到达了一个未匹配过的点,这条路叫做增广路。

为了求出最大匹配,算法依次枚举所有顶点,求出从它出发的一条增广路,每当找到一条增广路,就将路径上路径匹配状态都取反,这样就可以找到一个更大的匹配。因为增广路的长度总是奇数,所以在二分图中,它的端点必然分别位于左右两个部分。这说明,只需要考虑从左部出发的增广路即可

这是匈牙利算法的演示动图

动图

代码如下

bool vis[N];
int match[N];

inline bool find(int u){
	for(register int j : e[u]){
		if(!vis[j]){
			vis[j] = true;
			if(!match[j] || find(match[j])){
				match[j] = u;
				return true;
			}
		}
	}
	return false;
}

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

思路

模板题没什么说法,就是建边跑个二分图匹配就好了

【参考代码】

#include<bits/stdc++.h>
using namespace std;
const int N = 510;
int n1, n2, m;
vector<int> e[N];
bool st[N];
int match[N];
bool find(int u){
	for(int j : e[u]){
		if(!st[j]){
			st[j] = true;
			if(!match[j] || find(match[j])){
				match[j] = u;
				return true;
			}
		}
	}
	return false;
}
int main(){
	cin>>n1>>n2>>m;
	while(m --){
		int a, b;
		cin>>a>>b;
		e[a].push_back(b);
	}
	int ans = 0;
	for(int i = 1 ; i <= n1 ; i ++){
		memset(st, 0, sizeof st);
		if(find(i)) ans ++;
	}
	cout<<ans;
	return 0;
}

P2825 [HEOI2016/TJOI2016] 游戏 & 放置机器人

思路

如果这个棋盘上只有空地,每行列只能放一个炸弹,那么我们很容易想到将行列设为二分图的左右点集,每个行向可以放置列建边,不可放置的位置就是石头,软石头很好处理,只需在建边的时候判掉就行了,硬石头的处理就需要拆点,行和列分开处理,每当遇到硬石头就将行和列迭代一位,再将迭代的行和列处理,最后跑最大匹配数边的个数即可

【参考代码】

#include<bits/stdc++.h>

using namespace std;

const int N = 1010;
typedef long long LL;
typedef pair<int, int> PII;

int n, m, ntot, mtot;
char s[N][N];
PII last;
int row[N][N], col[N][N];
vector<int> e[N];

bool vis[N];
int match[N];

inline bool find(int u){
	for(register int j : e[u]){
		if(!vis[j]){
			vis[j] = true;
			if(!match[j] || find(match[j])){
				match[j] = u;
				return true;
			}
		}
	}
	return false;
}

int main(){
	
	cin>>n>>m;
	
	bool flag = true;
	for(register int i = 1 ; i <= n ; i ++){
		for(register int j = 1 ; j <= m ; j ++){
			cin>>s[i][j];
			if(s[i][j] == '#'){
				flag = true;
				continue;
			}
			if(s[i][j] == 'x') continue;
			if(!flag){
				row[i][j] = row[last.first][last.second];
				last = {i, j};
			}
			else if(flag){
				row[i][j] = row[last.first][last.second] + 1;
				last = {i, j};
				ntot ++;
				flag = false;
			}
		}
		flag = true;
	}
	
	flag = 1;
	last = {0, 0};
	
	for(register int i = 1 ; i <= m ; i ++){
		for(register int j = 1 ; j <= n ; j ++){
			if(s[j][i] == '#'){
				flag = 1;
				continue;
			}
			if(s[j][i] == 'x') continue;
			if(!flag){
				col[j][i] = col[last.second][last.first];
				last = {i, j};
			}
			else if(flag){
				col[j][i] = col[last.second][last.first] + 1;
				last = {i, j};
				mtot ++;
				flag = false;
			}
		}
		flag = true;
	}
	
	for(register int i = 1 ; i <= n ; i ++){
		for(register int j = 1 ; j <= m ; j ++){
			if(s[i][j] == '*'){
				register int a = row[i][j], b = col[i][j];
				e[a].push_back(b);
			}
		}
	}
	
	int ans = 0;
	for(register int i = 1 ; i <= ntot ; i ++){
		memset(vis, 0, sizeof vis);
		if(find(i)){
			ans ++;
		}
	}
	cout<<ans;
	
	return 0;
} 

二分图-最小点覆盖

最小点覆盖问题指的是,在一个图中,每条边两边的节点至少有一个被选

我们在这里只研究在二分图中的最小点覆盖,一般的最小点覆盖问题难度是NP级的,但是在二分图中,我们有一个非常神奇的定理:

Kőnig 定理

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

二分图-最大独立集

对于最大独立集,难度是NP级的,但二分图的最大独立集,也有定理和推论

定理 推论

图 \(G=(V,E)\) 中,点集 \(C\subseteq V\) 是点覆盖,当且仅当它的补集 \(V\setminus C\) 是独立集

图 \(G=(V,E)\) 中,最小点覆盖与最大独立集的大小之和等于顶点数目

贴点题

posted @ 2026-03-01 14:21  神烦doge  阅读(23)  评论(0)    收藏  举报