二分图

定义

对于一张无向图 G,若所有点可以分为两个点集 AB,且 AB 的内部没有连边,那么我们称 G 可以划分为一张二分图。

  • 二分图的划分不唯一,也不一定联通,也不一定有环

存在的充要条件

若无向图 G 是二分图,那么 G 没有奇环。

若无向图 G 没有奇环,那么 G 是二分图。

判定

染色法。

枚举没有访问的点 x 开始 dfs,遍历点时用 01 两种颜色交替给节点染色。若出现一个点 x 的邻接点 y 染色过且 colx=coly,则出现奇环,不是二分图。

所有点染色成功,则是一张二分图。

代码:

inline bool dfs(int x, int Col) {
	col[x] = Col;
	
	for(auto u : g[x]) {
		if(col[x] == col[u]) return false;
		
		if(! col[u]) return dfs(u, -Col);
	}
	
	return true;
}

例题

UVA10004 Bicoloring

模板题。

P1525 [NOIP2010 提高组] 关押罪犯

将监狱看成二分图的左右两部。

由于题目要求最大影响力最小,不难想到二分答案。又因为二分图有其判定算法,所以可以很好的结合二分。

二分当前的答案,将影响力大于当前二分值的关系建边(此处值得仔细思考),二分图判定即可。

代码:

inline bool dfs(int x, int Col) {
	col[x] = Col;
	
	for(auto u : g[x]) {
		if(col[x] == col[u]) return false;
		
		else if(! col[u] && dfs(u, -Col) == false) return false;
	}
	
	return true;
}

inline bool check(int x) {
	for(int i = 1 ; i <= n ; ++ i)
		col[i] = 0, g[i].clear();
	
	for(int i = 1 ; i <= m ; ++ i)
		if(w[i] > x) g[u[i]].pb(v[i]), g[v[i]].pb(u[i]);
	
	for(int i = 1 ; i <= n ; ++ i)
		if(! col[i])
			if(! dfs(i, 1)) return false;
	
	return true;
}

匹配

定义

对于一张二分图 G,一个匹配指的是一条边。

最大匹配

定义

选取的最大边集 EE 中任意两个边不共点,则称 EG 的一组最大匹配。

性质

  • 不一定唯一

  • 任意一条边一定在不同点集内

  • 有一些点和边必选,有一些可选

求取:匈牙利算法(Hungarian Algorithm)

1.枚举左部的点 x 从未访问的点出发 dfs

2.若到达右部的点 y,且 y 尚未匹配(mchy=0),则 yx 匹配,终止 dfs

3.若点 y 已经匹配(mchy0),则从 mchy 继续 dfs

4.重复执行 2,3,直到所有路径搜索完毕证明最大匹配无法增加

5.时间复杂度 O(nm)

代码:

inline bool hungary(int x) {
	for(auto u : g[x]) {
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || hungary(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	}
	
	return false;
}

注意建的是单向边。

建模特征

不共点,边最多。

例题

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

模板题。

P10937 車的放置

好题。

难点在于将行、列看成二分图的左部与右部节点,把车看作一条边。

代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 205;
int n, m, t, x, y, mch[N];
bool vis[N], ok[N][N];
vector<int> g[N];

inline bool hungary(int x) {
	for(auto u : g[x])
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || hungary(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	
	return false;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m >> t;
	for(int i = 1 ; i <= t ; ++ i) {
		cin >> x >> y;
		
		ok[x][y] = true;
	}
	
	for(int i = 1 ; i <= n ; ++ i)
		for(int j = 1 ; j <= m ; ++ j)
			if(! ok[i][j]) g[i].pb(j);
	
	int ans = 0;
	
	for(int i = 1 ; i <= n ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(hungary(i)) ++ ans;
	}
	
	cout << ans;
	
	return 0;
}

P2055 [ZJOI2009] 假期的宿舍

一定要注意的是只有在校生有床,回家的人不需要床

代码:

for(int i = 1 ; i <= n ; ++ i) {
	for(int j = 1 ; j <= n ; ++ j) {
		cin >> x;
		
		if(x && iss[j]) g[i].pb(j);
		else if(iss[i] && ! ish[i] && i == j) g[i].pb(i);
	}
}

for(int i = 1 ; i <= n ; ++ i)
	if((iss[i] && ! ish[i]) || ! iss[i]) ++ res;

int ans = 0;

for(int i = 1 ; i <= n ; ++ i) {
	memset(vis, false, sizeof vis);
	
	if(((iss[i] && ! ish[i]) || ! iss[i]) && hungary(i)) ++ ans;
}

if(ans >= res) cout << "^_^\n";
else cout << "T_T\n";

P2756 飞行员配对方案问题

注意 x 是右部的点,而 mchx 存的是左部的点。

分辨清楚自己将什么当作左部什么当作右部。

代码:

for(int i = 1 ; i <= n ; ++ i) {
	if(ok[mch[i]] || ! mch[i]) continue;

	ok[mch[i]] = true;

	cout << mch[i] << ' ' << i + m << '\n';
}

P2319 [HNOI2006] 超级英雄

一个不同寻常的建模方法。

将要求的答案作为一个点集去进行最大匹配,以后建模时要注意考虑到这一点。

代码:

#include <bits/stdc++.h>
#define int long long
#define pb push_back
using namespace std;

const int N = 1e3 + 5;
int n, m, u, v, mch[N], Ans[N];
bool vis[N];
vector<int> g[N];
inline bool dfs(int x) {
	for(auto u : g[x]) {
		if(! vis[u]) {
			vis[u] = true;
			
			if(! mch[u] || dfs(mch[u])) {
				mch[u] = x;
				
				return true;
			}
		}
	}
	
	return false;
}

signed main() {
	ios_base :: sync_with_stdio(NULL);
	cin.tie(nullptr);
	cout.tie(nullptr);
	
	cin >> n >> m;
	for(int i = 1 ; i <= m ; ++ i) {
		cin >> u >> v;
		
		++ u, ++ v;
		
		g[i].pb(u), g[i].pb(v);
	}
	
	int ans = 0;
	
	for(int i = 1 ; i <= m ; ++ i) {
		memset(vis, false, sizeof vis);
		
		if(dfs(i)) ++ ans;
		else break;
	}
	
	cout << ans << '\n';
	
	for(int i = 1 ; i <= n ; ++ i)
		Ans[mch[i]] = i;
	
	for(int i = 1 ; i <= ans ; ++ i)
		cout << Ans[i] - 1 << '\n'; 
	
	return 0;
}

最小点覆盖

定义

对于一张二分图 G,选取最少的点覆盖所有的边,选出的点的数量即为最小点覆盖大小。

性质

  • G 中任意一条边至少有一个端点在点集内

  • 点集选取不一定唯一

  • 可能存在一条边的两个端点都在点集内

  • 最小点覆盖大小 = 最大匹配大小

对于性质 4 的证明

假设最小点覆盖大小为 S,最大匹配大小为 E

由于 E 条边不共点,那么覆盖 E 条边至少需要 E 个点 SE

在点集内每个点都可以组成一个匹配,共 S 个,得到大小为 S 的匹配 SE

取交集,得:S=E

证毕。

建模特征

“至少二选一”

例题

UVA1194 Machine Schedule

1.每个人物的两个模式至少有一个需要重启得到

2.重启次数最少,等价于选取模式最少

3.用最少的模式覆盖所有的任务 + 至少二选一 —— 最小点覆盖

4.注意工作模式可以为 0 的任务不参与建模

代码:

for(int i = 1 ; i <= n ; ++ i)
	mch[i] = 0, g[i].clear();

cin >> n;

if(! n) return 0;

cin >> m >> k;

for(int i = 1 ; i <= k ; ++ i) {
	cin >> u >> u >> v;

	if(u && v) g[u].pb(v);
}

int ans = 0;

for(int i = 1 ; i < n ; ++ i) {
	memset(vis, false, sizeof vis);

	if(dfs(i)) ++ ans;
}

cout << ans << '\n';

P6062 [USACO05JAN] Muddy Fields G

题意:不能盖住草地,需要盖住所有泥地,允许重叠。

1.木板长度要尽可能的少,长度就要尽可能的长(反证)

2.木板分为横向和纵向,长度取决于连续泥地的数量

3.一块泥地需要被横向或(而且)纵向的木板盖住,至少二选一

4.把行和列视为二分图的左部和右部,每一块泥地视为边

5.预处理横向和纵向的泥地连通块,并编号

6.连通块之间建边即可跑最小点覆盖即可

代码:

tot = 1;

for(int i = 1 ; i <= n ; ++ i)
	for(int j = 1 ; j <= m + 1 ; ++ j)
		if(c[i][j] == '*') id[i][j][0] = tot; 
		else ++ tot;


int p = tot - 1;

for(int j = 1 ; j <= m ; ++ j)
	for(int i = 1 ; i <= n + 1 ; ++ i)
		if(c[i][j] == '*') id[i][j][1] = tot; 
		else ++ tot;

for(int i = 1 ; i <= n ; ++ i)
	for(int j = 1 ; j <= m ; ++ j)
		if(c[i][j] == '*') g[id[i][j][0]].pb(id[i][j][1]);

int ans = 0;

for(int i = 1 ; i <= p ; ++ i) {
	memset(vis, false, sizeof vis);

	if(dfs(i)) ++ ans;
}
posted @   end_switch  阅读(8)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· NetPad:一个.NET开源、跨平台的C#编辑器
· PowerShell开发游戏 · 打蜜蜂
· 凌晨三点救火实录:Java内存泄漏的七个神坑,你至少踩过三个!
点击右上角即可分享
微信分享提示