//https://img2018.cnblogs.com/blog/1646268/201908/1646268-20190806114008215-138720377.jpg

二分图学习笔记

update:2022.7.31:调整了图片大小,修改了 KM 算法部分。

update:2022.8.1:超链接不知道为什么不显示,加括号标注

update:2022.9.4:代码崩了调整回来了

update:2022.10.5:今天听了qbxt的老师讲的,想想我讲了之后几乎没有人听懂,这么一对比我发现我真是

二分图

定义

二分图又称作二部图,是图论的一种特殊模型。设 \(G=(V,E)\) 是一个无向图,如果顶点 \(V\) 可以分割成两个互不相交的子集 \((A,B)\),且图中的每一条边 \((i,j)\) 所关联的两个定点分别属于这两个不同的顶点集 \((i\in A,j\in B)\),则称图 \(G\) 为一个二分图。

通俗一点说就是一个图的点分为两部分,每一部分之间的点没有边相连,这就是二分图。

看一下一个二分图。

这是二分图??

没错他就是一个二分图。

所以不要尝试用眼去判断二分图。

由定义可知,二分图没有自回路(关联于同一结点的一条边);零图,平凡图(指仅有一个结点的图)可以看成特殊的二分图。

二分图的判断

  1. 用眼看根据定义来判断,主要还是看能不能把点分成两部分使每一部分的点之间没有边互相链接。

  2. 染色法:这个比较好理解,给定一张图,把里面的点染为黑白两色,根据给出的边依次进行染色(初始的点没有颜色),如把第一条边的起点染为黑色,那么把与之相连的未染色点染为白色,如果已经染色为白色则跳过,如果已经染为黑色则说明此图不是二分图,全部染完后把终点更新成起点继续染色,直到出现要进行染色的终点与起点颜色相同时返回假,表示不是二分图,或者所有的点染色成功返回真,表示此图为二分图。

代码我不会就不贴了

代码如下:

#include<bits/stdc++.h>
using namespace std;
int mp[1010][1010],n,m;//mp存放图的连通情况 
int se[10010];//se存放每个点染成的颜色 
int dfs(int x,int c)//x是当前点的编号,c是当前点的颜色 
{
	se[x]=c;//先把当前点给染上色 
	for(int i=1;i<=n;i++)//枚举每一个点 
	{
		if(mp[x][i])//有边相连 
		{
			if(se[i]==c)//如果终点的颜色与起点相同 
			  return 0;//返回假 
			if(se[i]==0&&!dfs(i,-c))//如果没染色,搜终点,如果返回值为假 
			  return 0;//返回假 
		}
	}
	return 1;//没毛病就返回真 
}
int main()
{
	cin>>n>>m;
	for(int i=1;i<=n;i++)
	{
		int x,y;
		cin>>x>>y;
		mp[x][y]=1;//无向图存两遍 
		mp[y][x]=1;
	}
	for(int i=1;i<=n;i++)
	  if(se[i]==0)//当前点没有染色 
		if(!dfs(i,1))//如果返回值是假 
		{
			cout<<"NO"<<endl;//输出不是二分图 
			return 0;
		}
	cout<<"YES"<<endl;//全都搜完了没有问题那就是二分图 
	return 0;
}

二分图的匹配

定义:给定一个二分图 \(G\),在 \(G\) 的一个子图 \(M\) 中,\(M\) 的边集 \(E\) 中的任意两条边都不依附于同一个顶点,则称 \(M\) 是一个匹配。

匹配点:匹配边上的两点

通俗一点就是一个二分图,每一个点最多只连着一条边(个人理解 qwq)。

极大匹配

是指在当前已完成的匹配下,无法再增加未完成的匹配的边的方式来增加匹配的边数。

最大匹配

是指所有的极大匹配当中边数最大的一个匹配,设为 \(M\)。选择这样的边数最大的子集成为图的最大匹配问题。

完美匹配(完备匹配)

一个图中所有的顶点都是匹配点的匹配,即 \(M=2\times V\)。完美匹配一定是最大匹配。

其实就是两部分的点一一对应。

最优匹配

最优匹配又称为带权最大匹配,是指在带有权值边的二分图中,求一个匹配使得匹配边上的权值和最大。一般 \(X\)\(Y\) 集合顶点个数相同,最优匹配也是一个完美匹配,即每一个顶点都被匹配。如果个数不相等,可以通过补点加 \(0\) 边实现转化。一般会使用 KM 算法来解决此问题,这个我们后面再说。

最小覆盖

二分图的最小覆盖分为最小顶点覆盖和最小路径覆盖。

最小顶点覆盖是指最少的顶点数使得二分图 \(G\) 中的每一条边都至少与其中一个点相关联。

二分图的最小顶点覆盖数=二分图的最大匹配数。

最小路径覆盖也称为最小边覆盖,是指尽量用少的不相交简单路径覆盖二分图中的所有顶点。

二分图的最小路径覆盖数=\(V\)-二分图的最大匹配数。

最大独立集

最大独立集是指寻找一个点集,使得其中任意两点在图中无对应边。对于二分图来说最大独立集=\(V\)-二分图的最大匹配数。最大独立集 \(S\) 与最小覆盖集 \(T\) 互补。


二分图可以在左右两边加上原点和汇点,这样求二分图的最大匹配问题就成了最大流,相比之下应该好理解一些。

以后写了网络流来贴个链接。

匈牙利算法

首先来了解几个定义。

交替路:从一个未匹配点出发,依次经过非匹配边、匹配边,非匹配边......形成的路径叫做交替路。

增广路:从一个未匹配点出发,走交替路,如果途径另一个未匹配点(出发点不算),则这条交替路称之为增广路。

增广路有一个重要特点:非匹配边比匹配边多一条。因此研究增广路的意义是改进匹配。只要把增广路中的匹配边和非匹配边的身份互换即可。由于中间的匹配节点不存在其他相连的匹配边,所以这样做不会破坏匹配的性质。交换后,图中的匹配数目比原来多了一条。

我们可以通过不停的找增广路来增加匹配中的匹配边和匹配点。找不到增广路时就是最大匹配了(增广路定理)。

正文开始。

匈牙利算法的思路就是依靠上面的增广路定理来实现的,参考扶苏的博客来写的简单一些。

如上图,现在来进行匈牙利算法的模拟。

1可以和2相连,先连上。

3可以和4连,连上。

5可以和6连,连上。

7可以和6连,但6已经和5连了,断开5和6,6和7连上。

5不能和6连,但还可以和4连,把3和4断开,连上4和5。

3不能和4连了,但还可以和2连,把1和2断开,连上2和3。

1不能和2连了,但1可以和8连,连上。

至此结束(太抽象了我自己都嫌弃)。

可以看出:

如果后来的和以前的发生矛盾就把先连的给断开。

如果断开之后以前的无法与其他的匹配,就不拆了,后来的去找其他的点。

如果后来的没有能匹配的点就只能不匹配了。

代码实现(邻接矩阵):

#include<bits/stdc++.h>
#define int long long
#define N 2010
using namespace std;
int n,m,k,mi[N];//mi存放每一个点链接的点 
int vis[N][N],a[N];//vis存放图的联通情况,a存放当前搜索的每一个点是否已链接 
inline int fid(int x)//dfs寻找匹配的点对 
{
	for(int i=1;i<=m;i++)//枚举右边的每一个点 
	  if(vis[x][i])//如果和当前点是联通的 
	  {
	  	if(a[i])continue;//如果当前点已经和其他点连起来了就跳过 
	  	a[i]=1;//标记链接 
	  	if(mi[i]==0||fid(mi[i]))//如果当前点没有与之联通的点或者与当前点相连的边有其他与之相连的边 
	  	{
	  	  mi[i]=x;//链接这两个点 
	  	  return 1;//返回成功连边 
	    }
	  }
	return 0;//没连上边就返回0 
}
inline int match()//匹配函数 
{
	int cnt=0;//计数器 
	for(int i=1;i<=n;i++)//枚举每一个左半部分的点 
	{
		memset(a,0,sizeof(a));//清空标记 
		if(fid(i))//如果成功连边 
		  cnt++;//连边数加一 
	}
	return cnt;//返回边数 
}
signed main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)//枚举每一条边 
	{
		int x,y;
		cin>>x>>y;
		vis[x][y]=1;//标记 
	}
	cout<<match()<<endl;//输出 
	return 0;//好习惯 
}

链式前向星:

#include<bits/stdc++.h>//链式前向星版本 
#define N 20100
using namespace std;
struct sb{int to,next;}e[N*4];
int head[N*2],a[N*2],mi[N*2];
int n,m,k,cnt,ans;
inline void add(int u,int v)
{
	e[++cnt].to=v;
	e[cnt].next=head[u];
	head[u]=cnt;
}
inline int dfs(int x)
{
	for(int i=head[x];i;i=e[i].next)
	{
		int v=e[i].to;
		if(a[v])continue;
		a[v]=1;
		if(!mi[v]||dfs(mi[v]))
		{
			mi[v]=x;
			return 1;
		}
	}
	return 0;
}
int match()
{
	for(int i=1;i<=n;i++)
	{
		memset(a,0,sizeof(a));
		if(dfs(i))
		ans++;
	}
	return ans;
}
int main()
{
	cin>>n>>m>>k;
	for(int i=1;i<=k;i++)
	{
		int x,y;
		cin>>x>>y;
		add(x,y);
	}
	cout<<match()<<endl;
	return 0;
}

KM算法

KM 算法一般用于解决寻找最优匹配的问题,而这个算法是基于匈牙利算法的一种算法(大雾。

步骤如下:

首先要用邻接矩阵存二分图,然后用贪心算法初始化标杆,运用匈牙利算法找到完美匹配,如果找不到则修改标杆增加一些边。重复以上步骤直到找到完美匹配。

标杆分为 \(X\) 标杆和 \(Y\) 标杆,一般把比较少的点放在 \(x\) 标杆一侧。

首先要初始化两个标杆分别为 \(X\) 标杆和 \(Y\) 标杆,\(x\) 标杆初始化为与之相连的最大边权,\(Y\) 标杆初始化为 \(0\),且直接加入拥有最大边权的边。如果发现此时的匹配就是完美匹配,那么就直接退出,否则进行标杆的更改。从第一个节点开始扫描,如果有合法的增广路,那么将其反选,扩充路径,如果该节点没有合法的增广路,那么则将增广路上的所有的 \(X\) 标杆上的点加入点集 \(S\),将 \(Y\) 标杆上的所有点加入点集 \(T\),从 \(S\) 和不在 \(T\) 集合中的点里面,计算 \(d=\min\left\{L(x)+L(y)-w(x,y)\right\}\);计算后,将在 \(S\) 点集内的 \(x\) 的顶标减 \(d\),在 \(T\) 的顶标加 \(d\)。并将目前没有加入二分图的权值和等于顶标和的边作为未匹配边加入到二分图中,然后再在该节点寻找增广路,如果还是没有,则再次通过更改标杆来增加边,直到有增广路出现为止。之后重复寻找增广路的步骤以及更改标杆的步骤,如果出现了完美匹配就直接退出。

如果没有明白的话,来看一下这个:

(超链接)我不吃饼干呀的博客

本来这里是把上面的博客复制过来的,但是太长了我给阉割了。

来说一下本人对 KM 算法的理解:

KM 算法就好比是匈牙利算法的一种扩展,因为他在进行时需要用匈牙利算法的 DFS 部分来找边匹配,然后一步一步得到最优匹配的。但是我们发现,KM 算法他多了两个数组,用于存放左右标杆,而标杆一开始左部的标杆全赋初值为每一个点所链接的边的最大边权,右部的标杆全赋初值为0,当每一次匹配的时候多一个判断条件,如果左右匹配的点的标杆的值加起来等于边权的话就匹配,反之不匹配(左标杆一开始都是最大值,所以不等于的话一般就是比边权大),不匹配的时候我们需要去找其他可以选的边,所以我们要找出最少左标杆参与 dfs 的标杆最少下降多少才能有别的选择,然后参与 dfs 的左部标杆减去,右部标杆加上,然后继续 dfs 直到此点匹配成功,最后一个 for 循环,找出所有点匹配的点,最后把边权加起来就是答案(应该比上面的好理解 qwq )。

因为没有模板题所以来看一下这道题(超链接)。

直接看代码吧

#include<bits/stdc++.h>
#define int long long
#define N 21
using namespace std;
inline int read()//快读 
{
   int s=0,w=1;char ch=getchar();
   while(ch<'0'||ch>'9')
   {  if(ch=='-')  w=-1;  ch=getchar();}
   while(ch>='0'&&ch<='9')
   {  s=s*10+ch-'0'; ch=getchar();}
   return s*w;
} 
int a[N][N],lx[N],ly[N];//a存放每一个点和另一个点之间的边权,lx,ly存放标杆 
int visx[N],visy[N],m[N];//vis标记是否已匹配,m存放每一个左部点所链接的右部点 
int n,minz,o;
int dfs(int s)//dfs匹配 
{
	visx[s]=1;//标记左部已链接 
	for(int i=1;i<=n;i++)//枚举右部的每一个点 
	  if(!visy[i])//如果当前的右部的点没有标记 
	  {
	  	int t=lx[s]+ly[i]-a[s][i];//计算两边的期望值与边权的差 
	  	if(t==0)//是否为零 
	  	{
	  		visy[i]=1;//是就链接这两个点 
	  		if(m[i]==0||dfs(m[i]))//如果当前点没有值或者是还可以与其他点相连 
	  		{
	  			m[i]=s;//链接 
	  			return 1;//返回已连接 
		    }
		}
		else if(t>0)//如果t大于0 
		  minz=min(minz,t);//找出下降的最小minz值 
	  }
	return 0;//返回链接失败 
}
void KM()//KM算法主体 
{
	for(int i=1;i<=n;i++)//枚举每一个左部的点 
	{
		while(1)//只要找不到点就一直循环 
		{
			minz=1e9;//重置minz 
			memset(visx,0,sizeof(visx));//清空vis标记 
			memset(visy,0,sizeof(visy));
			if(dfs(i))break;//如果找到了就退出循环 
			for(int j=1;j<=n;j++)//枚举左部点 
			  if(visx[j])lx[j]-=minz;//参与选边的减去minz 
			for(int j=1;j<=n;j++)//枚举右部点 
			  if(visy[j])ly[j]+=minz;//参与选边的加上minz 
		}
	}
}
signed main()
{
	cin>>n;
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=n;j++)
	    a[i][j]=read();//输入 
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=n;j++)
	  {
	  	o=read();
	  	a[j][i]*=o;//计算每一条边的边权 
	  }
	for(int i=1;i<=n;i++)
	  for(int j=1;j<=n;j++)
	    lx[i]=max(lx[i],a[i][j]);//找出每一个点的x标杆 
	KM();//KM算法跑一遍 
	int ans=0;//ans一定要赋0 
	for(int i=1;i<=n;i++)//枚举每一个右部点 
	  ans+=a[m[i]][i];//累加答案 
	cout<<ans<<endl;//输出答案 
	return 0;//好习惯 
}

好的剩下的先咕掉.

好的再来看一道题:仓库管理员的烦恼(超链接)

code及解析(超链接)

posted @ 2022-09-03 10:59  北烛青澜  阅读(76)  评论(4编辑  收藏  举报