二分图那套理论

二分图那套理论

一、定义

二分图是什么?节点由两个集合组成,且两个集合内部没有边的图。
换言之,存在一种方案,能将节点划分成满足以上性质的两个集合。

一个最常用的性质是不存在奇环,可以用来 \(\mathcal O(n + m)\) 地检验一张图是否是二分图。

二、二分图匹配

一个图的匹配是图边集的一个子集,满足每个点至多出现一次。

最大匹配就是不同顶点的个数最多的匹配。

二分图最大匹配:使用匈牙利算法或者网络流解决:

网络流做法:\(\mathcal O(m\sqrt{n})\),从源点向所有左部点连流量为\(1\)的边,所有右部点向汇点连流量为\(1\)的边,原图中的边依然保留,流量为\(1\),最终最大流就是最大匹配。

匈牙利算法:\(\mathcal O(nm)\),本质是不断是模拟最大流的过程,不断寻找增广路来扩大匹配数。

具体实现是依次考虑每一个左部点加入匹配,考虑它的每一个出边\(v\),如果\(v\)没有被匹配则直接匹配,否则考虑与\(v\)匹配的左部点能不能更换匹配对象,这个过程可以递归处理。

这里给出匈牙利算法的模板,网络流的就不用给了:

inline bool find(int x){
	for(int i=1;i<=m;++i){
		if(w[x][i]){
			if(vis[i]) continue;
			vis[i]=1;
			if(!match[i]||find(match[i])){
				match[i]=x;
				return true;
			}
		}
	}
	return false;
}
inline int hungary(){
	for(int i=1;i<=m;++i) match[i]=0;
	int ret=0;
	for(int i=1;i<=n;++i){
		for(int j=1;j<=m;++j) vis[j]=0;
		ret+=find(i);
	}
	return ret;
}

拓展:如果在最大匹配的基础上,要求匹配的字典序最小,该如何完成?

例:「NOI2009」变换序列

一个长为 \(n\) 的序列,每个位置上有两种填数的方案,请你寻找一种合法的填数方案使得填出的数是一个排列且字典序最小。\(n\le 10000\)

首先容易根据题目建出一个二分图,那么问题就转化为求字典序最小的完美匹配。考虑匈牙利算法的过程,当我们从前往后遍历左部点每个数的匹配时,我们总是优先考虑后加入的数,让右部点尽量的从匹配之间的数换成匹配该数。因此当我们要求字典序最小时,只需要从 \(n\) 枚举到 \(1\),每个位置的出边都从小到大枚举,这样就实现了优先让更靠前的位置选择更小的点进行匹配。

#include<bits/stdc++.h>
using namespace std;
const int N=1e4+10;
int ans[N],n,a[N],mat[N],vis[N],top;
vector<int> to[N];
inline bool find(int x){
	vis[x]=top;
	for(int v:to[x]){
		if(mat[v]==-1){mat[v]=x;return true;}
		else if(vis[mat[v]]!=top&&find(mat[v])){mat[v]=x;return true;}
	}
	return false;
}
int main(){
	scanf("%d",&n);
	for(int i=0;i<n;++i){
		scanf("%d",&a[i]);
		to[i].push_back((i-a[i]+n)%n);
		to[i].push_back((i+a[i])%n);
		if(to[i][0]>to[i][1]) swap(to[i][0],to[i][1]);
		mat[i]=-1;
	}
	for(int i=n-1;i>=0;--i){
		++top;
		if(!find(i)){puts("No Answer");exit(0);}
	}
	for(int i=0;i<n;++i) ans[mat[i]]=i;
	for(int i=0;i<n;++i){
		printf("%d",ans[i]);
		if(i<n-1) putchar(' ');
	}
	return 0;
}

二分图最大权匹配

费用流做法:没什么好说的,复杂度与值域相关(也可以用CS优化到弱多项式复杂度)

\(KM\)做法:可以在\(\mathcal O(n^3)\)时间内求出二分图的最大权完美匹配

为了用于求解二分图最大权匹配,先将两边点补到一样多,然后将没有的边改为权值为\(0\),然后就可以用\(KM\)算法了。

我们为每个顶点分配一个顶标\(l(x)\),满足每条边\((u,v)\)都满足\(w(u,v)\le l(u)+l(v)\)

同时我们定义相等子图为原图的一个子图,包含所有点但只包含了所有满足\(w(u,v)=l(u)+l(v)\)的边。

首先容易发现,对于任意一组可行顶标,如果其相等子图存在完美匹配,那么这个匹配就是二分图的最大权完美匹配,这是易于证明的。

于是问题变成了通过调整顶标使得相等子图存在完美匹配。

一开始,我们随意分配一组可行顶标,设左部点的顶标为\(lx\),右部点的顶标为\(ly\),一种容易想到的方法是对于左部点\(i\),令\(lx_i=\max_{j}w(i,j)\),同时将所有\(ly\)设为\(0\)

接下来,我们依次考虑每个未匹配的点,找到它的增广路,如果找到了,直接增广,否则,我们沿途经过的点形成了一个交错树,记左部点在交错树中的集合为\(S_1\),其他为\(S_2\),右部点中的是\(T_1,T_2\)

为了能够继续匹配下去,我们需要将\(S_1\rightarrow T_2\)的一条边加入进交错树中,我们选择让\(S_1\)中的点同时提高\(lx\)来满足\(S_1\rightarrow T_2\)中的需要期望值最小的边恰好加入相同子图。同时,为了不影响原相等子图中的边,我们需要将\(T_1\)中的点的顶标同时降低一下,至于降低的大小应当为:

\[delta=\min\{l_u+l_v-w(u,v)\}(u\in S_1,v\in T_2) \]

这个取\(min\)显然可以对每个右部点\(u\)维护\(slack(u)=\min\{l(u)+l(v)-w(u,v)\}(u\in S_1)\),然后就可以\(\mathcal O(n^3)\)做了。

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const ll inf=0x3f3f3f3f3f3f3f3f;
const int N=510;
const int M=3e5+10;
ll w[N][N],slack[N];
ll lx[N],ly[N];//左右部点期望 
bool visx[N],visy[N];//是否访问过 
int n,m,matx[N],maty[N],pre[N];

queue<int> q; 
inline bool check(int x){
	visy[x]=1;
	if(maty[x]){
		q.push(maty[x]);
		return false;
	} //检验maty[x]能否更换匹配对象,先push进队列中等待处理 
	while(x){//终于能更换了!倒回去更新增广路 
		maty[x]=pre[x];
		int t=matx[pre[x]];
		matx[pre[x]]=x;
		x=t;	
	}
	return true;
}//增广x 

inline bool bfs(){
	while(!q.empty()){
		int u=q.front();q.pop();
		if(visx[u]) continue;
		visx[u]=1;
		for(int v=1;v<=n;++v){		
			if(w[u][v]!=-inf){
				if(visy[v]) continue;
				if(lx[u]+ly[v]-w[u][v]<slack[v]){
					slack[v]=lx[u]+ly[v]-w[u][v];
					pre[v]=u;
					if(!slack[v])
						if(check(v)) return true;//找到增广路,直接跑路 
				}
			}
		}
	}
	ll delta=inf;
	for(int j=1;j<=n;++j) if(!visy[j]) delta=min(delta,slack[j]);
	for(int j=1;j<=n;++j){
		if(visx[j]) lx[j]-=delta;//左部点降低期望 
		if(visy[j]) ly[j]+=delta;//右部点提高期望 
		else slack[j]-=delta;//未遍历过的点所需期望减少 
	}	 
	for(int i=1;i<=n;++i)
		if(!visy[i]&&!slack[i])
			if(check(i)) return true;
	return false;
}
inline ll KM(){
	for(int i=1;i<=n;++i){
		ly[i]=-inf;
		for(int j=1;j<=n;++j) ly[i]=max(ly[i],w[j][i]);
	}
	for(int i=1;i<=n;++i){
		memset(slack,0x3f,sizeof(slack));//清空相等子图
		memset(visx,0,sizeof(visx));
		memset(visy,0,sizeof(visy));
		while(!q.empty())q.pop();
		q.push(i);//以i为起点找增广路
		while(!bfs());
	}//看点i是否能加进去 
	ll ret=0;
	for(int i=1;i<=n;++i) ret+=w[maty[i]][i];
	return ret; 
}

int main(){
	scanf("%d%d",&n,&m);
	for(int i=1;i<=n;++i)
		for(int j=1;j<=n;++j) w[i][j]=-inf;
	for(int i=1,u,v,x;i<=m;++i){
		scanf("%d%d%d",&u,&v,&x);
		w[u][v]=max(w[u][v],(ll)x);
	}
	printf("%lld\n",KM());
	for(int i=1;i<=n;++i) printf("%d ",maty[i]);
	return 0;
}

三、二分图重要性质

  • 二分图最小点覆盖=最大匹配

    构造最小点覆盖的方法:从右部点所有非匹配点出发找增广路,对经过的点打标记。最终右部点中没有标记的点与左部点中有标记的点组成了一个最小点覆盖。

  • 二分图最大独立集= \(n-\) 最大匹配

    这是因为在任意一张图中,将任意一个独立集取反就得到了一个点覆盖,因此最大独立集 \(=n-\) 最小点覆盖,在二分图中也就等于 \(n-\) 最大匹配

  • 二分图最小边覆盖:

    去除图中所有的孤立点,此时有最小边覆盖等于最大独立集大小。

  • DAG的最小路径覆盖(不允许相交)= \(n-\) 将每个点拆成入点和出点,将原图中的边 \((u,v)\) 改为 \(u\) 入点向 \(v\) 出点的边后形成的二分图的最大匹配。

    这是因为对于原图中的任意一条路径 $v_1\rightarrow v_2\rightarrow v_3\dots \rightarrow v_k $,都可以拆为若干条 \(v_1\rightarrow v_2\) 的边,因为路径不相交,所以每个点的入度出度都不超过 \(1\),可以看作一个匹配。同时对于任意一组匹配,可以设一开始场上有 \(n\)\(i\rightarrow i\) 的路径,然后依次将匹配 \(u\rightarrow v\) 边加入图中,这等价于合并了两条路径,因此最小路径覆盖大小 \(=n-\) 最大匹配。

    如果允许相交,那么先传递闭包就变成了不允许相交的情况。

  • DAG的最长反链:

    最长反链为一个集合 \(S\in G\),使得 \(\forall u\in S,v\in S\),在原图中 \(u\) 不能到达 \(v\)\(v\) 也不能到达 \(u\),且 \(|S|\) 最大。

    根据 \(Dilworth\) 定理,有DAG的最长反链等于其最小的允许相交的路径覆盖大小。

四、Hall定理

对于二分图 \(G=(V,E)\),记 \(N(s)\) 为与 \(s\) 相邻的点集,有结论:

任意满足左部点数小于等于右部点的二分图有完美匹配的充要条件是:对于左部点 \(V_l\) 的每个子集 \(S\) 都有 \(|S|\le |N(S)|\)

证明:必要性显然。对于充分性,考虑一张满足 \(Hall\) 定理但没有完美匹配的二分图,设 \(x\in V_l\) 没有被匹配,此时必有点 \(y\)\(x\) 相连,那么 \(y\) 一定已经与另一点 \(z\in V_l\) 匹配,此时 \(S=\{x,z\}\),一定存在另一个点 \(t\in V_r\) 满足 \(t\in N(S)\),于是问题转化为同时对 \(t\)\(y\) 寻找匹配,问题规模增加了 \(1\)。如此反复,直到 \(S=V_l\),此时 \(|N(S)|\ge |S|\),不可能再找到一个不在 \(S\) 中的点与 \(N(S)\) 匹配,因此这是一条增广路,\(x\) 可以被匹配,与假设矛盾。

\(Hall\) 定理推广:

一张左部点数小于等于右部点的二分图的最大匹配 \(=|V_l|-\max_{S\in {V_l}}(|S|-|N(S)|)\)

\(Hall\) 定理通常被用来证明某些贪心算法。

posted @ 2021-07-07 10:12  cjTQX  阅读(71)  评论(0编辑  收藏  举报