二分图及其应用

二分图及其应用

二分图(Bipartite Graph),又称二部图,是一个比较重要的考点,在竞赛中占了一定的比例。

定义

二分图的定义是:若一个图\(G=<V,E>\)的顶点集\(V\)可分割为两个互不相交的子集\(X\)(称为左部)和\(Y\)(称为右部),并且边集\(E\)中的每条边所关联的两个顶点分别属于\(X\)\(Y\)两个集合,则图\(G\)为二分图。

一个图\(G\)为二分图的充要条件是图中没有奇环(长度为奇数的环)。

二分图判定

由二分图的充要条件推出一个判定方法:染色法,具体算法如下:
任意选取一个点,染为黑色,然后DFS每个节点,将每次遍历到的节点染成与上个节点不同的颜色,上个节点为白色则该点为黑色,上个节点为黑色则该点为白色。若染色时产生矛盾,则此图不是二分图;若遍历了整个图都没有矛盾产生,则此图是二分图。

vector<int>g[N];//vector存图
int n,m,vis[N],col[N],ans=1;
//col[i]表示i的颜色,0为黑色,1为白色,-1为未染色
//搜索前,将col[]全部初始化为-1,col[根节点]设为0
bool dfs(int u)
{
	vis[u]=1;//如果原图不一定连通,则对每个连通块分别判定
	for(int i=0,v;i<g[u].size();i++)
	{
		v=g[u][i];
		if(col[u]==col[v])
			return false;
		if(col[v]==-1)
		{
			col[v]=!col[u];
			if(!dfs(v))
				return false;
		}
	}
	return true;
}
for(int i=1;i<=n;i++)
{
	memset(col,-1,sizeof(col)),col[i]=0;
	if(!vis[i])
		ans&=dfs(i);
}

二分图最大匹配

定义与性质

一个任何两条边都没有公共端点的边集称为为图的一组匹配(matching)或边独立集(edge independet set)。边数最多的匹配称为二分图的最大匹配
匹配了二分图中较小集合(\(X\),\(Y\)中小的一个)的匹配称为完备匹配完全匹配(complete matching),匹配了所有点的匹配称为完美匹配(perfect matching)。
属于匹配\(M\)的边称为匹配边,其余为非匹配边。匹配边的端点称为匹配点,其余为非匹配点

若一条路径\(P\)连接两个非匹配点,且匹配边和非匹配边在\(P\)上交错出现,那么称\(P\)\(M\)的一条增广路(augmenting path)。
显然,若我们将匹配\(M\)中的一条增广路\(P\)上的匹配边变为非匹配边,非匹配边变为匹配边(这个操作称为路径取反),可得一个更大的匹配\(M'\),且匹配数增加\(1\)

推论

二分图的一组匹配\(M\)是最大匹配,当且仅当图中不存在相对于\(M\)的增广路

算法

匈牙利算法

由这个推论可以得到一个“增广路算法”,即以DFS为框架,遍历图中的每个点并不断增广,直到找不到增广路为止。
该算法就是著名的匈牙利算法。现在来看它的具体过程:

  1. \(M=\varnothing\)。即所有边均为非匹配边。
  2. 寻找增广路\(P\)。给当前节点\(x\)寻找匹配点\(y\),回溯时进行路径取反,得到更大的匹配\(M'\)
  3. 重复第2步,直到找不到增广路(即发现所有节点都被搜索过了)为止。

对于左部点\(x\)那么如何寻找匹配点\(y\)
经过分析可发现,点y能为匹配点的两个条件是:

  1. \(y\)为非匹配点。
    此时\(x\)~\(y\)是一条增广路。
  2. \(y\)已与\(x'\)匹配,但又有\(y'\)能与\(x'\)匹配。
    此时\(x\)$y$\(x'\)~\(y'\)是一条增广路。

    另外在这种情况中,还可能出现我们找到的\(y'\)已经匹配的情况。

    这是一个子问题,我们对\(x'\)递归处理(这也是要采用DFS作为搜索框架的原因)

有了以上分析,我们就可以写代码了。
时间复杂度\(O(nm)\)
模板题代码:

#include<iostream>
#include<algorithm>
#include<string>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#define N 1005
#define M 50005
using namespace std;
struct edge{
	int nxt,to;
}e[M<<1];
int n,m,e0,tot,head[N],vis[N],match[N],ans;
void addedge(int x,int y)
{
	e[++tot]=(edge){head[x],y};
	head[x]=tot;
}
bool dfs(int x)
{
      //用标记数组vis判重,避免重复搜索
	for(int i=head[x],y;i;i=e[i].nxt)
		if(!vis[y=e[i].to])
		{
			vis[y]=1;
			if(!match[y]||dfs(match[y]))//y点未匹配,或从y点能找到增广路
			{
				match[y]=x;//路径取反
				return true;//成功找到一条增广路,返回1
			}
		}
	return false;//从x出发能搜索到的所有点都被搜索过了,找不到新的增广路,返回0
}
int main()
{
	ios::sync_with_stdio(0);
	cin>>n>>m>>e0;
	for(int i=1,u,v;i<=e0;i++)
		cin>>u>>v,addedge(u,v+n);
	for(int i=1;i<=n;i++)
	{
		memset(vis,0,sizeof(vis));
		if(dfs(i)) ans++;
	}
	cout<<ans<<endl;
	exit(0);
}

网络最大流

利用网络流的思想,我们可以做到时间复杂度\(O(m\sqrt n)\)
具体做法如下:
新建超级源点S与超级汇点T,从S向左部点连接有向边,从右部点向T连接有向边,网络中每条边的容量设为1。

然后使用Dinic算法求出最大流即为最大匹配数,流经过的点与边就是匹配点和匹配边。

另外,有一种名为Hopcroft-Karp的算法也能做到\(O(m\sqrt n)\)的时间复杂度。

posted @ 2020-07-25 23:04  LZShuing  阅读(1117)  评论(0编辑  收藏  举报