二分图最大匹配

Posted cherrypill

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了二分图最大匹配相关的知识,希望对你有一定的参考价值。

问题越学越多...

 

二分图又称作二部图,是图论中的一种特殊模型。

设G=(V,E)是一个无向图,如果顶点V可分割为两个互不相交的子集(A,B),并且图中的每条边(i,j)所关联的两个顶点i和j分别属于这两个不同的顶点集(i in A,j in B),则称图G为一个二分图。

也就是说,只要两个点之间有边,那么这两个点就不能同属一个集合,必须分在两边。(就是不能相处的人连一条边

(并不是所有的无向图G都能转化为二分图。

定理:无向图G为二分图的充要条件是,该图至少有两个顶点且其所有回路(环)的长度均为偶数。|就是随便找个环,长度都是偶数

二分图染色|判定 模板:

https://codeforces.ml/problemset/problem/687/A

题目大意:有n个点,m条边,求是否能将所有的点分成两组,其中与每条边相连的两个点位于不同组。孤立的点不用管。如果不能输出-1.

for (auto iter:Data)
    iter.second = 5;//这里不会修改map的值
for (auto &iter:Data)
    iter.second = 5;//这里map内的值会变成5

#include<bits/stdc++.h>
using namespace std;
const int N=1e5+5;
int n,m,x,y,use[N];
vector < int > f[N];
set <int> s1,s2;
void dfs(int x,int col)
{
    if(use[x]==0)
        use[x]=col;
    else
    {
        if(use[x]!=col)
        {
            printf("-1
");
            exit(0);
        }
        if(use[x]==col)
            return ;
    }
    for(auto i:f[x])
        dfs(i,col%2+1);
}
int main()
{
    scanf("%d %d",&n,&m);
    for(int i=1;i<=m;i++)
    {
        scanf("%d %d",&x,&y);
        f[x].emplace_back(y);
        f[y].emplace_back(x);
    }
    for(int i=1;i<=n;i++)
    {
        if(!f[i].size()||use[i]!=0)
            continue;
        dfs(i,1);
    }
    for(int i=1;i<=n;i++)
    {
        if(use[i]==1)
            s1.insert(i);
        if(use[i]==2)
            s2.insert(i);
    }
    printf("%d
",int(s1.size()));
    for(auto i:s1)
        printf("%d ",i);
    printf("
%d
",int(s2.size()));
    for(auto i:s2)
        printf("%d ",i);
    return 0;
}

二分图最大匹配:

定义:在一个无向图中,定义一条边覆盖的点为这条边的两个端点。

找到一个边集S包含最多的边,使得这个边集覆盖到的所有顶点中的每个顶点只被一条边覆盖。S的大小叫做图的最大匹配。

so是时候掏出最大匹配算法

匈牙利算法:

建立有向图G,分为二分图的左侧和右侧。 优先选择左侧序号更小的连接可能的边。 对于两个点的目标点“冲突”的时候,采取“协商”的办法。 即序号小的连接可能连接的另一条边。 若“协商”失败,则放弃序号较大的点的边。

概念看的都好绕,实例比较好懂:

转自https://blog.csdn.net/qq_41730082/article/details/81162561


你拥有的大概就是下面这样一张关系图,每一条连线都表示互有好感。

技术图片

本着救人一命,胜造七级浮屠的原则,你想要尽可能地撮合更多的情侣,匈牙利算法的工作模式会教你这样做:

一: 先试着给1号男生找妹子,发现第一个和他相连的1号女生还名花无主,got it,连上一条蓝线

技术图片

:接着给2号男生找妹子,发现第一个和他相连的2号女生名花无主,got it

技术图片

:接下来是3号男生,很遗憾1号女生已经有主了,怎么办呢?

我们试着给之前1号女生匹配的男生(也就是1号男生)另外分配一个妹子。

(黄色表示这条边被临时拆掉)

技术图片

与1号男生相连的第二个女生是2号女生,但是2号女生也有主了,怎么办呢?我们再试着给2号女生的原配重新找个妹子(注意这个步骤和上面是一样的,这是一个递归的过程)

技术图片

此时发现2号男生还能找到3号女生,那么之前的问题迎刃而解了,回溯回去

2号男生可以找3号妹子~~~                  1号男生可以找2号妹子了~~~                3号男生可以找1号妹子

技术图片 技术图片 技术图片

所以第三步最后的结果就是:

技术图片

: 接下来是4号男生,很遗憾,按照第三步的节奏我们没法给4号男生腾出来一个妹子,我们实在是无能为力了……香吉士同学走好。


技术图片图源来自https://www.cnblogs.com/jianglangcaijin/p/6035945.html

根据上面的图,可以有2种建图方法,然而每种方法的结果取决于你枚举的点集单向边和双向边

1:编号从1~9连的边为双向边,枚举点集是1~9则输出的是6,1~4输出3

2:编号从1~9连的边是单向边,(A={1,2,3,4},B={5,6,7,8,9})枚举点集1~9输出的是3,1~4仍然是3

算法主体都是一样,感觉是数据太水,因为后面做题连单向边全都出问题了

bool dfs(int x)
{
	for(auto i:f[x])
	{
		if(use[i]==0)
		{
			use[i]=1;
			if(match[i]==0||dfs(match[i]))
			//如果fx所连的节点i暂时没有cp||现在与i匹配的那个人的cp还可以更换,则把i匹配为x 
			{
				match[i]=x;
				return 1;//可以被匹配
			}
		}
	}
	return 0;
}

由此可以总结,连2. 3.单向边结果与枚举集合无关|点的编号也无关,输出的都是最大匹配的边集S 但是2.情况建图是要你已经知道边了才方便,3.情况你要建立的图是要能有 ’1‘‘ 的

连双向边结果与枚举点集有关点的编号不能重复,如果枚举的是全部点集则输出的是能被匹配到点的个数2*S对建图没多大要求最方便

这样就方便理解下面的概念了

如果不说清楚建图方法很网上说的很多矛盾的啊 真的把我弄晕了

先别忘了最大匹配的定义:

在一个无向图中,定义一条边覆盖的点为这条边的两个端点。找到一个边集S包含最多的点,即使这个边集覆盖到的所有顶点中的每个顶点只被一条边覆盖。S的大小叫做图的最大匹配。

二分图的最小顶点 覆盖:

定义:假如选了一个点就相当于覆盖了以它为端点的所有边。最小顶点覆盖就是选择最少的点来覆盖所有的边

证我就不会了,结论就是大小刚好等于最大匹配的数值(取的点完全不一样好吧)

证明可以看(我觉得写的很清楚https://www.cnblogs.com/jianglangcaijin/p/6035945.html

无向图的最小 路径 覆盖:

无向二分图的最小路径覆盖=顶点数-分匹配(因为无向图就是双向的一条边等于两次入图正向和反向,最后得到的匹配数多了一倍所以要除以2才是原本的匹配数)

二分图的最大独立集:

定义:选出一些顶点使得这些顶点两两不相连,则这些点构成的集合称为独立集。找出一个包含顶点数最多的独立集称为最大独立集。

最大独立集=所有顶点数-最小顶点覆盖

二分图的最大团:(感觉不怎么用因为就和独立集相反嘛)

定义:就是选出一些顶点,这些顶点两两之间都有边。最大团就是使得选出的这个顶点集合最大。(与最大独立集相反)

最大团=补图的最大独立集

因为最大独立集是两两不相连,所以最大独立集的补图两两相连。

所以如果你一道题建图不同,最大团刚好会和独立集反过来虽说答案没差了

下面来道例题:

https://vjudge.net/contest/70017#problem/B

这里就是如果你要选的集合中有某个数,那么就不能出现它(某个素数)倍的数。

因为是有a不能有b所以可以反向建图(a-b),最后就是求两两不相连的最大子集(最大独立集) (双向边枚举所有点)

记得清空match等数组

#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5,M=5e5+5;
int n,T,k,maxn,x,b[M],ans,ss[M],match[N];
bool use[N],vis[M];
vector <int> f[N];
map <int ,int> mp;
bool dfs(int x)
{
	for(auto i:f[x])
	{
		if(use[i]==0)
		{
			use[i]=1;
			if(match[i]==0||dfs(match[i]))
			//如果fx所连的节点i暂时没有cp||现在与i匹配的那个人的cp还可以更换,则把i匹配为x 
			{
				match[i]=x;
				return 1;
			}
		}
	}
	return 0;
}
void pd()
{
	use[1]=1;
	for(int i=2;i<=M-3;i++)
	{
		if(use[i]==0)
			ss[++k]=i;
		for(int j=1;j<=k&&ss[j]*i<=M-3;j++)
		{
			use[ss[j]*i]=1;
			if(i%ss[j]==0)
				break;
		}
	}
}
int main()
{
	freopen("data.in","r",stdin);
	freopen("data.out","w",stdout);
	pd();
	scanf("%d",&T);
	for(int l=1;l<=T;l++)
	{
		mp.clear();
		memset(match,0,sizeof(match));
		memset(b,0,sizeof(b));
		memset(use,0,sizeof(use));
		maxn=-1;
		ans=0;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&x);
			maxn=max(maxn,x);
			b[x]=i;
			mp.insert(make_pair(x,i));
		}
		for(auto i:mp)
		{
			for(int j=1;j<=k&&i.first*ss[j]<=maxn;j++)
			{
				int temp=i.first*ss[j];
				if(b[temp]!=0)
				{
					f[i.second].emplace_back(mp[temp]);
					f[mp[temp]].emplace_back(i.second);
				}
			}
		}
		for(int i=1;i<=n;i++)
		{
			memset(use,0,sizeof(use));
			if(dfs(i))
				ans++;
		}
		printf("Case %d: %d
",l,n-ans/2);
		for(int i=1;i<=n;i++)
			f[i].clear();
	} 
	return 0;
} 
/*
3
5
2 4 8 16 32
5
2 3 4 6 9
3
1 2 3
*/

然而过不了,因为这题时间卡的紧(居然卡sqrt的复杂度)

再也不用匈牙利了

Hopcroft_Karp算法:

hk是预先找到多条路径最短的增广路,然后再用匈牙利算法就沿着bfs找的路径去找增广路。不知道理解的对不对

增广路:

举例来说,二分图G={A,B},增广路由集合A中一个点通向集合B中一个点,再由B中这个点通向A中一个点……交替进行)。

也就是说用Hopcroft_Karp算法,你必须要想办法将集合分成两个集合,而不是像匈牙利算法那样可以遍历所有的点,它只遍历A|B集合中的点

所以之前找的规律1

1:编号从1~9连的边为双向边,枚举点集是1~9则输出的是2*S,1~4输出S

下面模板我都连的是双向边,因为连单向边不知道为什么答案会多一|少一 建图姿势不正确?

这是比较非主流的模板感觉

因为它用一个数组dep换了dx,dy,match换了A_B,B_A...,还有一个常数 dis 不知到干嘛的,枚举只枚举A集合,所以输出S,这个枚举所有点又有问题...

#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int M=5e5+5,N=4e4+5,INF=0x3f3f3f3f;
int match[N],ans,dep[N],a[N],T,b[M],maxn,ss[M],k,dis,n;
int vis[N],use[M];
vector <int> f[N];
map <int,int> mp;
set <int> s[3];
queue<int>q;
bool searchP()
{
    while(!q.empty()) 
	q.pop();
    memset(dep,0,sizeof(dep));
    dis=INF;
    for(auto i:s[0])
        if(match[i]==0) 
	    q.push(i);
    while(!q.empty())
    {
        int x=q.front();
	    q.pop();
        if(dep[x]>dis) 
	    break;
        for(auto i:f[x])
        {
            if(!dep[i])
            {
                dep[i]=dep[x]+1;
                if(match[i]==0) 
		    dis=dep[i];
                else		 
                    dep[match[i]]=dep[i]+1,q.push(match[i]);
            }
        }
    }
    return dis!=INF;
}
bool dfs(int x)
{
    for(auto i:f[x])
    {
        if(vis[i]==0&&dep[i]==dep[x]+1)
        {
            vis[i]=1;
            if(match[i]&&dep[i]==dis) 
				continue;
            if(match[i]==0||dfs(match[i]))
            {
                match[x]=i;
		match[i]=x;
                return 1;
            }
        }
    }
    return 0;
}
void pd()
{
	use[1]=1;
	for(int i=2;i<=M-3;i++)
	{
		if(use[i]==0)
			ss[++k]=i;
		for(int j=1;j<=k&&ss[j]*i<=M-3;j++)
		{
			use[ss[j]*i]=1;
			if(i%ss[j]==0)
				break;
		}
	}
}
int check(int x)//判断x是属于s[0]|s[1]
{
	if(use[x]==0)
		return 1;
	int tot=0;
	for(int j=1;j<=k&&ss[j]<=x;j++)
	{
		while(x%ss[j]==0)
		{
			tot++;
			x/=ss[j];
		}
	}
	tot%=2;
	return tot; 
}
int main()
{
//	freopen("data.in","r",stdin);
//	freopen("test.out","w",stdout);
    pd();
    scanf("%d",&T);
    for(int l=1;l<=T;l++)
    {
    	mp.clear();
    	s[0].clear();
    	s[1].clear();
    	memset(match,0,sizeof(match));
    	memset(b,0,sizeof(b));
    	memset(a,0,sizeof(a));
    	maxn=-1;
    	ans=0;
        scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]); 
			maxn=max(maxn,a[i]);
			b[a[i]]=i;	
		}
		for(int i=1;i<=n;i++)
		{
			s[check(a[i])].insert(i);
			for(int j=1;j<=k&&a[i]*ss[j]<=maxn;j++)
			{
				int temp=a[i]*ss[j];
				if(b[temp]!=0)
				{
					f[i].emplace_back(b[temp]);
					f[b[temp]].emplace_back(i);
				}
			}
		}
        int ans=0;
        while(searchP())//HK
        {
            memset(vis,0,sizeof(vis));
            for(auto i:s[0])
                if(!match[i]&&dfs(i))
                    ans++;
        }
        for(int i=1;i<=n;i++)
        	f[i].clear();
        printf("Case %d: %d
",l,n-ans);
    }
}

这个是主流的 数组真的多

它bfs&&while(search_p())里面枚举的是所有点,所以输出的是2*S。这个只枚举A集合又会有问题...

#include<bits/stdc++.h>
using namespace std;
const int N=4e5+5,M=5e5+5,inf=0x3f3f3f3f;
int n,T,k,maxn,x,ans,dis,a[N],b[M],ss[M],dx[M],dy[M],A_B[M],B_A[M];
bool use[N],vis[M];
vector <int> f[N];
set <int> s[3]; 
queue <int> q;
bool dfs(int x)
{
	for(auto i:f[x])
	{
		/* 找寻所有深度右边是左边+1的点,这样就相当于找增广路,看看增广路能否增广 */
		if(vis[i]==0&&dy[i]==dx[x]+1)
		{
			vis[i]=1;
			if(B_A[i]!=0&&dy[i]==dis)//已经被匹配了 
				continue;
			if(B_A[i]==0||dfs(B_A[i]))
			//如果fx所连的右节点i暂时没有匹配||现在与i匹配的那个人的cp还可以更换,则把i匹配为x 
			{
				A_B[x]=i;// x -> i
				B_A[i]=x;// i -> x
				return 1;
			}
		}
	}
	return 0;
}
void pd()
{
	use[1]=1;
	for(int i=2;i<=M-3;i++)
	{
		if(use[i]==0)
			ss[++k]=i;
		for(int j=1;j<=k&&ss[j]*i<=M-3;j++)
		{
			use[ss[j]*i]=1;
			if(i%ss[j]==0)
				break;
		}
	}
}
bool find_expath()
{
	memset(dx,0,sizeof(dx));
	memset(dy,0,sizeof(dy)); 
	dis=inf; 
	while(!q.empty())
		q.pop();	 
	for(int i=1; i<=n;i++)
	{
		if(A_B[i]==0)//将所有未匹配的A集合的点加入队列 
			q.push(i);
	} 
	while(!q.empty())
	{
		int temp=q.front();
		q.pop();
		if(dx[temp]>dis)
			break;
		for(auto i:f[temp])
		{
			if(dy[i]==0)//没访问过 
			{
				dy[i]=dx[temp]+1;
				if(B_A[i]!=0)
				{
					dx[B_A[i]]=dy[i]+1;
					q.push(B_A[i]);
				}
				else
					dis=dy[i];
			}
		}	
	} 
	return dis!=inf;
} 
int check(int x)
{
	if(use[x]==0)
		return 1;
	int tot=0;
	for(int j=1;j<=k&&ss[j]<=x;j++)
	{
		while(x%ss[j]==0)
		{
			tot++;
			x/=ss[j];
		}
	}
	tot%=2;
	return tot; 
}
int main()
{
	freopen("data.in","r",stdin);
	freopen("test.out","w",stdout);
	pd();
	scanf("%d",&T);
	for(int l=1;l<=T;l++)
	{
		s[0].clear();
		s[1].clear();
		memset(A_B,0,sizeof(A_B));
		memset(B_A,0,sizeof(B_A));
		memset(vis,0,sizeof(vis));
		memset(b,0,sizeof(b));
		memset(a,0,sizeof(a));
		maxn=-1;
		ans=0;
		scanf("%d",&n);
		for(int i=1;i<=n;i++)
		{
			scanf("%d",&a[i]); 
			maxn=max(maxn,a[i]);
			b[a[i]]=i;	
		}
		for(int i=1;i<=n;i++)
		{
			s[check(a[i])].insert(i);
			for(int j=1;j<=k&&a[i]*ss[j]<=maxn;j++)
			{
				int temp=a[i]*ss[j];
				if(b[temp]!=0)
				{
					f[i].emplace_back(b[temp]);
					f[b[temp]].emplace_back(i);
				}
			}
		}
		while(find_expath())
		{
			memset(vis,0,sizeof(vis));
			for(int i=1;i<=n;i++)
			{
				if(A_B[i]==0&&dfs(i))
				{
					ans++;
				}
			}
		}
		printf("Case %d: %d
",l,n-ans/2);
		for(int i=1;i<=n;i++)
			f[i].clear();
	} 
	return 0;
} 
/*
3
5
2 4 8 16 32
5
2 3 4 6 9
3
1 2 3
*/

我一脸懵,还是无脑套模板吧

以上是关于二分图最大匹配的主要内容,如果未能解决你的问题,请参考以下文章

匈牙利算法-二分图的最大匹配

二分图最大权完美匹配KM算法模板

网络流24题-飞行员配对方案问题-二分图最大匹配

bzoj 1854: [Scoi2010]游戏 (并查集||二分图最大匹配)

HDU - 1083 Courses (二分图最大匹配模板)

HDU-1179-Ollivanders(二分图最大匹配)