二分图的匹配问题()

链接1
链接2
链接3

二分图

把一个无向图的点分为两个集合
图中的每条边的两个端点分别属于两个集合
所以,二分图中没有奇数个数点的环

判定

用染色法dfs或bfs
从一个未染色的点出发
枚举它的边
如果他的下一个点颜色与他相同
则该图不是二分图
如果下一个点为未染色
则搜索它

二分图的匹配

选定二分图中的一些边
如果这些边的两个端点都没有重复
(每个点在一个匹配中只出现一次)
则称它们为二分图的匹配

最大匹配

边数最多的匹配

完美匹配

图中每个点都为匹配点

匈牙利算法

增广路

从一个非匹配点开始
依次经过:
非匹配边 匹配边 非匹配边 匹配边……非匹配边
终点也为非匹配点
则这条路为增广路
将增广路中的边取反可以将二分图的匹配变大
没有增广路的匹配就是最大匹配

算法实现

通过不断找增广路来寻找最大匹配

code(dfs)

一个二分图
左边集合是1~n
右边集合是1+n~2n

bool dfs(int u){
	for(int i=head[u];i;i=last[i]){
		if(vis[to[i]])continue;
		vis[to[i]]=1;
		if(!match[to[i]] || dfs(match[to[i]])){//如果下一个点未匹配,那么直接找到增广路,否则跳到下一个点的对应匹配点(也就是两个两个点跳)
			match[to[i]]=u;
			return true;
		}
	}
	return false;
}
void solve(){
	for(int i=1;i<=n;i++){//只用枚举一个集合的点
		memset(vis,0,sizeof(vis));//vis数组防止死循环
			if(dfs(i))
				ans++;//记录最大匹配数
	}
   return;
}

邻接矩阵O(\(n^3\))
邻接表O(\(n^2\)m)

最小点覆盖

选取一些点
与这些点直接连接的点是覆盖点
(它们自己也是覆盖点)
选取最少的点覆盖全图就是最小点覆盖
最小点覆盖=最大匹配

KM算法

链接1
链接2

KM算法
寻找二分图最大权完美匹配

流程

首先左集合的点有一个期望值(顶标)
为它到右集合的点的边的最大值
右集合的期望值为0

接着对每个左集合点进行操作
首先寻找在期望范围内的右点(边的权值=左期望+右期望)
枚举所有可能的边
进行类似于匈牙利算法的操作(操作时只使用在期望范围内的边)
尝试寻找增广路

如果没找到
那么只能降低期望
当然降低最少的期望
使得左集合中的一些点多出一些可选边
同时为了让原来的边依然可选
应该将左集合的点降低最少期望
右集合的点增加最少期望
由于没有找到增广路
所以路径上左点比右点多一
等同于总期望也降低了最少期望

如果还没有找到
继续重复操作(降低期望)
直到找到增广路

因为是完美匹配
每个点肯定都能找到增广路

code(DFS)假的O(\(n^3\))真的O(\(n^4\))

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=510;
ll read(){
	ll x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9'){
		x=(x<<1)+(x<<3)+(c^48);
		c=getchar();
	}
	return x*f;
}
ll n,m;
ll slack[N],match[N],mp[N][N],va[N],vb[N],la[N],lb[N],ans;
bool dfs(int x){
	va[x]=1;
	for(int y=1;y<=n;y++){
		if(vb[y])continue;
		ll delta=la[x]+lb[y]-mp[x][y];
		if(!delta){
			vb[y]=1;
			if(!match[y] || dfs(match[y])){
				match[y]=x;
				return true;
			}
		}
		else slack[y]=min(slack[y],cha);
	}
	return false;
}
void KM(){
	for(int i=1;i<=n;i++){
		la[i]=mp[i][1];
		for(int j=2;j<=n;j++)
			la[i]=max(la[i],mp[i][j]);
	}//左集合的顶标初始值
	for(int i=1;i<=n;i++){
		memset(slack,127,sizeof(slack));//记录没法被选的点离被选的最小距离
		while(1){
			memset(va,0,sizeof(va));
			memset(vb,0,sizeof(vb));//标记
			if(dfs(i))break;
			ll minn=INT_MAX;
			for(int i=1;i<=n;i++)if(!vb[i])minn=min(minn,slack[i]);
			for(int i=1;i<=n;i++)if(va[i])la[i]-=minn;
			for(int i=1;i<=n;i++)if(vb[i])lb[i]+=minn;//降低期望(顶标)	
		}	
	}
	for(int i=1;i<=n;i++)ans+=mp[match[i]][i];
	return;
}
int main(){
	memset(mp,128,sizeof(mp));
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		ll t1=read(),t2=read(),t3=read();
		mp[t1][t2]=t3;//记录边
	}
	KM();
	printf("%lld\n",ans);
	for(int i=1;i<=n;i++)printf("%d ",match[i]);
	return 0;
}

code(BFS)

首先为什么要优化
因为dfs的KM每次找不到完美匹配后就要重置vis
并从头开始搜
但要是能顺着上次继续搜,从新加入的边开始
就可以优化

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=510,inf=-200000000;
ll read(){
	ll x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9'){
		x=(x<<1)+(x<<3)+(c^48);
		c=getchar();
	}
	return x*f;
}
ll n,m;
ll slack[N],match[N],mp[N][N],va[N],vb[N],la[N],lb[N],ans,pre[N],match2[N];
void aug(ll u){
	while(u){
		ll t=match2[pre[u]];
		match2[pre[u]]=u;//match2[i]=j表示左点i目前匹配右点j
		match[u]=pre[u];//match[i]=j表示右点i目前匹配左点j
		u=t;//画个图可以明白
	}
	return;
}//这是一个更新,因为bfs没法回溯,所以用pre[i]=j记录右边的i来自于左边的j
void bfs(ll u){
	queue<ll>q;
	memset(slack,127,sizeof(slack));
	memset(va,0,sizeof(va));
	memset(vb,0,sizeof(vb));
	q.push(u);
	while(1){
		while(!q.empty()){
			ll x=q.front();q.pop();
			va[x]=1;
			for(ll y=1;y<=n;y++){
				if(vb[y] || mp[x][y]==inf)continue;//没有路mp[x][y]==inf
				ll delta=la[x]+lb[y]-mp[x][y];
				if(delta<slack[y]){
					pre[y]=x;//不管有没有这条路先记录路径,万一后面加边后y会进入队列
					slack[y]=delta;
					if(!delta){
						vb[y]=1;
						if(!match[y]){
							aug(y);
							return;
						}
						else q.push(match[y]);
					}
				}		
			}
		}
		ll minn=INT_MAX;
		for(ll i=1;i<=n;i++)if(!vb[i])minn=min(minn,slack[i]);
		for(ll i=1;i<=n;i++)if(va[i])la[i]-=minn;
		for(ll i=1;i<=n;i++)if(vb[i])lb[i]+=minn;	else slack[i]-=minn;//这里直接更新slack,因为后面判断新搜的点就是用slack=0
		for(ll i=1;i<=n;i++){
			if(!vb[i] && slack[i]==0){
				vb[i]=1;
				if(!match[i]){
					aug(i);
					return;
				}
				else q.push(match[i]);//把新边可以到的新点加入队列
			}
		}
	}
	return;
}
void KM(){
	for(ll i=1;i<=n;i++){
		la[i]=mp[i][1];
		for(ll j=2;j<=n;j++)
			la[i]=max(la[i],mp[i][j]);
	}
	for(ll i=1;i<=n;i++)bfs(i);
	for(ll i=1;i<=n;i++)ans+=mp[match[i]][i];
	return;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			mp[i][j]=inf;
	for(ll i=1;i<=m;i++){
		ll t1=read(),t2=read(),t3=read();
		mp[t1][t2]=t3;
	}
	KM();
	printf("%lld\n",ans);
	for(ll i=1;i<=n;i++)printf("%lld ",match[i]);
	return 0;
}
————————

链接1
链接2
链接3

二分图

把一个无向图的点分为两个集合
图中的每条边的两个端点分别属于两个集合
所以,二分图中没有奇数个数点的环

判定

用染色法dfs或bfs
从一个未染色的点出发
枚举它的边
如果他的下一个点颜色与他相同
则该图不是二分图
如果下一个点为未染色
则搜索它

二分图的匹配

选定二分图中的一些边
如果这些边的两个端点都没有重复
(每个点在一个匹配中只出现一次)
则称它们为二分图的匹配

最大匹配

边数最多的匹配

完美匹配

图中每个点都为匹配点

匈牙利算法

增广路

从一个非匹配点开始
依次经过:
非匹配边 匹配边 非匹配边 匹配边……非匹配边
终点也为非匹配点
则这条路为增广路
将增广路中的边取反可以将二分图的匹配变大
没有增广路的匹配就是最大匹配

算法实现

通过不断找增广路来寻找最大匹配

code(dfs)

一个二分图
左边集合是1~n
右边集合是1+n~2n

bool dfs(int u){
	for(int i=head[u];i;i=last[i]){
		if(vis[to[i]])continue;
		vis[to[i]]=1;
		if(!match[to[i]] || dfs(match[to[i]])){//如果下一个点未匹配,那么直接找到增广路,否则跳到下一个点的对应匹配点(也就是两个两个点跳)
			match[to[i]]=u;
			return true;
		}
	}
	return false;
}
void solve(){
	for(int i=1;i<=n;i++){//只用枚举一个集合的点
		memset(vis,0,sizeof(vis));//vis数组防止死循环
			if(dfs(i))
				ans++;//记录最大匹配数
	}
   return;
}

邻接矩阵O(\(n^3\))
邻接表O(\(n^2\)m)

最小点覆盖

选取一些点
与这些点直接连接的点是覆盖点
(它们自己也是覆盖点)
选取最少的点覆盖全图就是最小点覆盖
最小点覆盖=最大匹配

KM算法

链接1
链接2

KM算法
寻找二分图最大权完美匹配

流程

首先左集合的点有一个期望值(顶标)
为它到右集合的点的边的最大值
右集合的期望值为0

接着对每个左集合点进行操作
首先寻找在期望范围内的右点(边的权值=左期望+右期望)
枚举所有可能的边
进行类似于匈牙利算法的操作(操作时只使用在期望范围内的边)
尝试寻找增广路

如果没找到
那么只能降低期望
当然降低最少的期望
使得左集合中的一些点多出一些可选边
同时为了让原来的边依然可选
应该将左集合的点降低最少期望
右集合的点增加最少期望
由于没有找到增广路
所以路径上左点比右点多一
等同于总期望也降低了最少期望

如果还没有找到
继续重复操作(降低期望)
直到找到增广路

因为是完美匹配
每个点肯定都能找到增广路

code(DFS)假的O(\(n^3\))真的O(\(n^4\))

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const int N=510;
ll read(){
	ll x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9'){
		x=(x<<1)+(x<<3)+(c^48);
		c=getchar();
	}
	return x*f;
}
ll n,m;
ll slack[N],match[N],mp[N][N],va[N],vb[N],la[N],lb[N],ans;
bool dfs(int x){
	va[x]=1;
	for(int y=1;y<=n;y++){
		if(vb[y])continue;
		ll delta=la[x]+lb[y]-mp[x][y];
		if(!delta){
			vb[y]=1;
			if(!match[y] || dfs(match[y])){
				match[y]=x;
				return true;
			}
		}
		else slack[y]=min(slack[y],cha);
	}
	return false;
}
void KM(){
	for(int i=1;i<=n;i++){
		la[i]=mp[i][1];
		for(int j=2;j<=n;j++)
			la[i]=max(la[i],mp[i][j]);
	}//左集合的顶标初始值
	for(int i=1;i<=n;i++){
		memset(slack,127,sizeof(slack));//记录没法被选的点离被选的最小距离
		while(1){
			memset(va,0,sizeof(va));
			memset(vb,0,sizeof(vb));//标记
			if(dfs(i))break;
			ll minn=INT_MAX;
			for(int i=1;i<=n;i++)if(!vb[i])minn=min(minn,slack[i]);
			for(int i=1;i<=n;i++)if(va[i])la[i]-=minn;
			for(int i=1;i<=n;i++)if(vb[i])lb[i]+=minn;//降低期望(顶标)	
		}	
	}
	for(int i=1;i<=n;i++)ans+=mp[match[i]][i];
	return;
}
int main(){
	memset(mp,128,sizeof(mp));
	n=read(),m=read();
	for(int i=1;i<=m;i++){
		ll t1=read(),t2=read(),t3=read();
		mp[t1][t2]=t3;//记录边
	}
	KM();
	printf("%lld\n",ans);
	for(int i=1;i<=n;i++)printf("%d ",match[i]);
	return 0;
}

code(BFS)

首先为什么要优化
因为dfs的KM每次找不到完美匹配后就要重置vis
并从头开始搜
但要是能顺着上次继续搜,从新加入的边开始
就可以优化

#include<bits/stdc++.h>
#define ll long long
using namespace std;
const ll N=510,inf=-200000000;
ll read(){
	ll x=0,f=1;char c=getchar();
	while(c>'9' || c<'0'){
		if(c=='-')f=-1;
		c=getchar();
	}
	while(c>='0' && c<='9'){
		x=(x<<1)+(x<<3)+(c^48);
		c=getchar();
	}
	return x*f;
}
ll n,m;
ll slack[N],match[N],mp[N][N],va[N],vb[N],la[N],lb[N],ans,pre[N],match2[N];
void aug(ll u){
	while(u){
		ll t=match2[pre[u]];
		match2[pre[u]]=u;//match2[i]=j表示左点i目前匹配右点j
		match[u]=pre[u];//match[i]=j表示右点i目前匹配左点j
		u=t;//画个图可以明白
	}
	return;
}//这是一个更新,因为bfs没法回溯,所以用pre[i]=j记录右边的i来自于左边的j
void bfs(ll u){
	queue<ll>q;
	memset(slack,127,sizeof(slack));
	memset(va,0,sizeof(va));
	memset(vb,0,sizeof(vb));
	q.push(u);
	while(1){
		while(!q.empty()){
			ll x=q.front();q.pop();
			va[x]=1;
			for(ll y=1;y<=n;y++){
				if(vb[y] || mp[x][y]==inf)continue;//没有路mp[x][y]==inf
				ll delta=la[x]+lb[y]-mp[x][y];
				if(delta<slack[y]){
					pre[y]=x;//不管有没有这条路先记录路径,万一后面加边后y会进入队列
					slack[y]=delta;
					if(!delta){
						vb[y]=1;
						if(!match[y]){
							aug(y);
							return;
						}
						else q.push(match[y]);
					}
				}		
			}
		}
		ll minn=INT_MAX;
		for(ll i=1;i<=n;i++)if(!vb[i])minn=min(minn,slack[i]);
		for(ll i=1;i<=n;i++)if(va[i])la[i]-=minn;
		for(ll i=1;i<=n;i++)if(vb[i])lb[i]+=minn;	else slack[i]-=minn;//这里直接更新slack,因为后面判断新搜的点就是用slack=0
		for(ll i=1;i<=n;i++){
			if(!vb[i] && slack[i]==0){
				vb[i]=1;
				if(!match[i]){
					aug(i);
					return;
				}
				else q.push(match[i]);//把新边可以到的新点加入队列
			}
		}
	}
	return;
}
void KM(){
	for(ll i=1;i<=n;i++){
		la[i]=mp[i][1];
		for(ll j=2;j<=n;j++)
			la[i]=max(la[i],mp[i][j]);
	}
	for(ll i=1;i<=n;i++)bfs(i);
	for(ll i=1;i<=n;i++)ans+=mp[match[i]][i];
	return;
}
int main(){
	n=read(),m=read();
	for(int i=1;i<=n;i++)
		for(int j=1;j<=n;j++)
			mp[i][j]=inf;
	for(ll i=1;i<=m;i++){
		ll t1=read(),t2=read(),t3=read();
		mp[t1][t2]=t3;
	}
	KM();
	printf("%lld\n",ans);
	for(ll i=1;i<=n;i++)printf("%lld ",match[i]);
	return 0;
}