二分图(Bipartite Graph)

二分图(Bipartite Graph)

二分图的判定

理论:如果某个图为二分图,那么它至少有两个顶点,且其所有回路的长度均为偶数(偶环)。任何无回路的的图均是二分图。

方法:染色法——》用两种颜色,对所有顶点逐个染色,且相邻顶点染不同的颜色,如果发现相邻顶点染了同一种颜色(即存在奇环),就认为此图不为二分图。

时间复杂度: O ( N ) O(N) O(N)

板子
bfs

//存图
struct Edge{
	int to,ne,w;
}e[M<<1];
int head[N],cnt;
void add(int x,int y,int z){
	e[++cnt].w=z;
	e[cnt].to=y;
	e[cnt].ne=head[x];
	head[x]=cnt;
}
//数据结构
int color[N];
bool bfs(int mid)
{
	queue<int> q;
	for(int i=1;i<=n;++i) color[i]=0;
	for(int i=1;i<=n;++i){
		if(color[i]==0){
			color[i]=1;
			q.push(i);
			while(q.size()){
				int u=q.front();
				q.pop();
				for(int j=head[u];j;j=e[j].ne){
					if(e[j].w<=mid) continue;
					if(color[e[j].to]==color[u]) return false; //相邻点被染成同样的颜色,不是二分图 
					if(color[e[j].to]==0) { //未染色的,染成相反颜色
						color[e[j].to]=-color[u];
						q.push(e[j].to);
					}
				}
			}
		}
	}
	return true;
} 

dfs

#include <bits/stdc++.h>
using namespace std;

const int N=1e3; //点数 
int n,m;

//数据结构 
vector<int> g[N]; //存图
int color[N]; //标记点被染成什么颜色 

//核心 
bool dfs(int u,int c){ //点,颜色 
	color[u]=c;
	for(auto v:g[u]){
		if(color[v]==c) return false; //相邻点被染成同样的颜色,不是二分图 
		if(color[v]==0 && !dfs(v,-c)) return false; //如果v还未染色,dfs将v染成相反的颜色 
	}
	return true; 
}

void solve(){
	for(int i=1;i<=n;++i){
		if(!color[i] && !dfs(i,1)) printf("NOT Bipartite Graph\n");
	}
	printf("A Bipartite Graph\n");
}

int main(){
	cin>>n>>m; //点数,边数 
	memset(color,0,sizeof color);
	int u,v;
	for(int i=1;i<=m;++i) {
		cin>>u>>v;
		g[u].push_back(v);
		g[v].push_back(u);
	}
	solve();
	return 0;
}

例题:
洛谷 P1525 [NOIP2010 提高组] 关押罪犯

题意:有2的集合,n个人,m对矛盾关系,每对关系分别涉及到x,y两人,矛盾值为w。当x,y被分到一个集合里,会产生w的矛盾,集合的影响力定义为集合中产生的矛盾的最大值。问如何分配n个人,使得集合的影响力最小。

思路:二分答案+二分图判断。

#include <bits/stdc++.h>
using namespace std;

#define int long long
const int N=2e4+4;
const int M=1e5+5;
int n,m;
struct Edge{
	int to,ne,w;
}e[M<<1];
int head[N],cnt;
void add(int x,int y,int z){
	e[++cnt].w=z;
	e[cnt].to=y;
	e[cnt].ne=head[x];
	head[x]=cnt;
}
int l,r;

int color[N];
bool bfs(int mid) //bfs判断二分图
{
	queue<int> q;
	for(int i=1;i<=n;++i) color[i]=0;
	for(int i=1;i<=n;++i){
		if(color[i]==0){
			color[i]=1;
			q.push(i);
			while(q.size()){
				int u=q.front();
				q.pop();
				for(int j=head[u];j;j=e[j].ne){
					if(e[j].w<=mid) continue;
					if(color[e[j].to]==color[u]) return false;
					if(color[e[j].to]==0) {
						color[e[j].to]=-color[u];
						q.push(e[j].to);
					}
				}
			}
		}
	}
	return true;
} 

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	int u,v,w;
	for(int i=1;i<=m;++i){
		cin>>u>>v>>w;
		add(u,v,w);
		add(v,u,w);
		r=max(r,w);
	}
	while(l<=r){
		int mid=(l+r)/2;
		if(bfs(mid)) r=mid-1;
		else l=mid+1;
	} 
	if(bfs(0)) cout<<0<<'\n';
	else{
		l++;
		while(bfs(l) && l) l--;
		l++;
		cout<<l<<'\n';
	}
}

另一种做法:并查集

#include <bits/stdc++.h>
using namespace std;

const int N=2e4+10;
const int M=1e5+5;
int n,m;
struct Edge{
	int u,v,w;
}e[M];

bool cmp(Edge x,Edge y){
	return x.w>y.w;
}

int fa[N];

int get(int x){
	if(fa[x]==0) return fa[x]=x;
	return fa[x]==x?x:fa[x]=get(fa[x]);
}

void merge(int x,int y){
	fa[get(y)]=get(x);
}

bool check(int x,int y){
	if(get(x)==get(y)) return true;
	return false;
}

unordered_map<int,int> mp;

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	cin>>n>>m;
	for(int i=1;i<=m;++i){
		cin>>e[i].u>>e[i].v>>e[i].w;
	}
	sort(e+1,e+1+m,cmp);
	for(int i=1;i<=m+1;++i){
		if(check(e[i].u,e[i].v)) {
			cout<<e[i].w<<'\n';
			break; 
		}
		if(mp[e[i].u]==0) mp[e[i].u]=e[i].v;
		else merge(mp[e[i].u],e[i].v);
		if(mp[e[i].v]==0) mp[e[i].v]=e[i].u;
		else merge(mp[e[i].v],e[i].u);
	}
}
二分图最大匹配

二分图中的匹配是一组边的选择方式,使得一个端点不会对应两条边。最大匹配是最大边数的匹配。在最大匹配中,如果添加了任何边,则不再是匹配。给定的二部图可以有多个最大匹配。

这有什么用?

现实世界中有许多问题可以作为二分匹配来解决。例如,考虑以下问题:

有m个职位申请者和n个职位。每个申请人都有他/她感兴趣的工作子集。每个职位空缺只能接受一个应聘者,一个职位应聘者只能被指定一个职位。找一份工作分配给申请者,以便尽可能多的申请者得到工作。参考

基本理论

1.在二分图中,最小顶点覆盖和最大匹配在数值上相等。

2.增广路径
在这里插入图片描述

匈牙利算法

参考

裸题:
HDU-2963 过山车
题意:有n个女孩,m个男孩,k对男女关系x,y,表示女孩x愿意跟男孩y做partner,求最大组合数。

#include <bits/stdc++.h>
using namespace std;

#define int long long
int k,n,m;
vector<int> g[550]; //v[i]:标记Ai可以连B集合的哪些点 

//数据结构 
bool vis[550]; //每次标记B集合被占有的点 
int match[550]; //标记B集合的点匹配的点
//主要函数 
bool dfs(int u){
	for(auto v:g[u]) { //A集合中的u能连到B集合中的v 
		if(vis[v]==0) { //v点还没有被占有 
			vis[v]=1;
			if(match[v]==0 || dfs(match[v])) { //如果B集合的v点没有被占用或者占用v的可以换其他点占用,那么v点可以被u点占用 
				match[v]=u;
				return true;			
			}	
		}
	}
	return false;
} 

signed main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	while(cin>>k){
		if(k==0) break;
		cin>>n>>m;
		memset(match,0,sizeof match);
		memset(vis,0,sizeof vis);
		for(int i=0;i<=500;++i) g[i].clear();
		int u,v;
		while(k--){
			cin>>u>>v;
			g[u].push_back(v);
		}
		
		//主函数部分 
		int ans=0;
		for(int i=1;i<=n;++i){ //遍历A集合 
			for(int j=0;j<=m;++j) vis[j]=0; //初始化B集合点的占用情况 
			if(dfs(i)) ans++;
		}
		cout<<ans<<'\n'; 
	}
}

好题:Magic Potion
题意:有n个英雄,可以杀若干个怪兽,每个英雄只能杀一次,至多有k个英雄可以有两次杀怪兽的机会,问最多可以杀多少只怪兽。
解法:有限制匹配个数的二分图最大匹配。
代码:

#include <bits/stdc++.h>
using namespace std;

#define int long long
#define pb push_back
int k,n,m;
vector<int> g[1003]; //v[i]:标记Ai可以连B集合的哪些点 

//数据结构 
bool vis[1003]; //每次标记B集合被占有的点 
int match[1003]; //标记B集合的点匹配的点
//主要函数 
bool dfs(int u){
	for(auto v:g[u]) { //A集合中的u能连到B集合中的v 
		if(vis[v]==0) { //v点还没有被占有 
			vis[v]=1;
			if(match[v]==0 || dfs(match[v])) { //如果B集合的v点没有被占用或者占用v的可以换其他点占用,那么v点可以被u点占用 
				match[v]=u;
				return true;			
			}	
		}
	}
	return false;
} 

signed main(){
	ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
	cin>>n>>m>>k;
	int t,tt;
	for(int i=1;i<=n;++i){
		cin>>t;
		for(int j=1;j<=t;++j){
			cin>>tt;
			g[i].pb(tt);
			g[i+n].pb(tt);
		}
	}
	int ans=0,ans1=0;
	for(int i=1;i<=2*n;++i){ //遍历A集合 
		for(int j=0;j<=m;++j) vis[j]=0; //初始化B集合点的占用情况 
		if(dfs(i)) {
			if(i<=n) ans++;
			else ans1++;
		}
	}
	cout<<ans+min(ans1,k)<<'\n';
}
HK(hopcroft-karp) 算法

参考

优化点:每次选多条长度相同的增广路径:
在这里插入图片描述

复杂度: O ( N M ) O(\sqrt N M) O(N M)

裸题:洛谷 P3386 【模板】二分图最大匹配

板子:

#include <bits/stdc++.h>
using namespace std;

//存图
const int N=505; //左集合点数 
const int M=505; //右集合点数 
int bgraph[N][M];

//数据结构
const int INF=0x3f3f3f3f;
int cx[N]; //cx[i]表示左集合i顶点所匹配的右集合的顶点序号
int cy[M]; //cy[i]表示右集合i顶点所匹配的左集合的顶点序号
int nx,ny; //左集合点数,右集合点数
int dx[N]; //dx[i]:记录到左集合i点的距离 
int dy[M]; //dy[i]:记录到右集合i点的距离
int dis; //记录增广路的长度
bool vis[M]; //记录右集合点是否被标记,用法同匈牙利算法

bool bfs(){
	queue<int> q;
	dis=INF;
	memset(dx,-1,sizeof dx);
	memset(dy,-1,sizeof dy);
	for(int i=1;i<=nx;++i){
		if(cx[i]==-1){
			q.push(i);
			dx[i]=0;
		}
	}
	while(q.size()){
		int u=q.front();
		q.pop();
        //该路径长度大于dis,等待下一次BFS扩充
        //dis是增广路径的长度,所以dis一定是一个奇数
		if(dx[u]>dis) break;
		//取右侧节点
		for(int v=1;v<=ny;++v){
			if(bgraph[u][v]&&dy[v]==-1){
				dy[v]=dx[u]+1;
                //v是未匹配点,停止延伸(查找)
                //得到本次BFS的最大遍历层次
				if(cy[v]==-1) dis=dy[v];
				else { //v是匹配点,继续延伸
					dx[cy[v]]=dy[v]+1;
					q.push(cy[v]);
				}
			}
		} 
	}
	return dis!=INF; //若dis为INF说明右集合没有未匹配点,也就是没有增广路径了
} 

bool dfs(int u){
	for(int v=1;v<=ny;++v) {
		if(!vis[v] && bgraph[u][v] && dy[v]==dx[u]+1){
			vis[v]=1;
			if(cy[v]!=-1 && dy[v]==dis) continue;
			if(cy[v]==-1 || dfs(cy[v])) {
				cy[v]=u;
				cx[u]=v;
				return true;
			}
		}
	}
	return false;
}
 
int HK() {
	int ans=0;
	memset(cx,-1,sizeof cx);
	memset(cy,-1,sizeof cy);
	while(bfs()){ //找到了增广路 
		memset(vis,0,sizeof vis);
		for(int i=1;i<=nx;++i){
			if(cx[i]==-1 && dfs(i)) ans++;
		}
	} 
	return ans;
}

int main(){
	ios::sync_with_stdio(0);
	cin.tie(0);
	cout.tie(0);
	int e;
	cin>>nx>>ny>>e;
	int u,v;
	while(e--){
		cin>>u>>v;
		bgraph[u][v]=1;
	}
	cout<<HK()<<'\n';
} 
  • 30
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值