二分图最大匹配算法

二分图的最大匹配就是要在二分图的边集E中找到一个子集S,使S中的任两条边没有公共顶点,且|S|达到最大。二分图有很多实际应用,如工作分配问题。同时二分图最大匹配问题又可以转化成“最小顶点覆盖”、“最小路径覆盖”、“最大独立集“等问题,因此显得非常重要。下面就介绍二分图最大匹配算法。

二分图最大匹配可以转换成最大流问题来解。假设二分图的两个顶点集分别为X, Y,那么我们在图中添加一个源s,和一个汇t。同时,在s与X的每个顶点间加一条有向边(s, vx),在Y的每个顶点与t间加一条有向边(vy, t)。X与Y的每条边也变为X->Y的有向边。图中每条边的容量为1。则最大匹配问题就转换为求转换后的图G'的最大流的问题。利用Ford-Fulkerson方法求最大流,时间复杂度为O(VE), V为顶点数,E为边数。具体证明参考《算法导论》。

经典的求二分图最大匹配的算法是Edmond于1965年提出的匈牙利算法。算法的核心思想是由一个初始匹配不断找增广路,直到找不到增广路为止。这里的增广路和网络流中的增广路有些不同。这里的增广路是这样的一条路:设已有的匹配为M,它的第一条边不在M中,最后一条边也不在M中,中间为在M中的边与不在M中的边交错出现。显然,这条路起点在X中,终点在Y中,且不在M中的边比在M中的边多1。所以我们若对增广路中的边进行取反,即原来不是匹配边的边变为匹配边,原来是匹配边的边变为不是匹配边的边,则我们能获得一个更大的匹配。所以这么一直找下去,直到找不到增广路为止,我们最后得到的匹配M就是要求的最大匹配。

匈牙利算法中,初始匹配我们可以设为空,然后用DFS或者BFS找增广路。找一条增广路的复杂度为O(E),最多找V条增广路,故算法时间复杂度为O(VE)。下面给出匈牙利算法的两种实现:

1.DFS找增广路

#include <iostream>
#include <cstring>
using namespace std;

const int MAXN = 100;
int uN, vN; // u,v数目
bool g[MAXN][MAXN]; // g[i][j] 表示 xi与yj相连
int xM[MAXN], yM[MAXN]; // 输出量
bool chk[MAXN]; // 辅助量 检查某轮 y[v]是否被check

bool SearchPath(int u)
{
int v;
for(v = 0; v < vN; v++)
{
if(g[u][v] && !chk[v])
{
chk[v] = true;
if(yM[v] == -1 || SearchPath(yM[v]))
{
yM[v] = u;
xM[u] = v;
return true ;
}
}
}
return false ;
}


int MaxMatch()
{
int u;
int ret = 0 ;
memset(xM, -1, sizeof (xM));
memset(yM, -1, sizeof (yM));
for(u = 0; u < uN; u++)
{
if(xM[u] == -1)
{
memset(chk, false, sizeof (chk));
if(SearchPath(u)) ret++;
}
}
return ret;
}

优点:实现简洁,容易理解,适用于边比较多的图,DFS找增广路快。

2.BFS找增广路

#include <iostream>
#include <cstring>
using namespace std;

#define MAXN 128

int g[MAXN][MAXN], Mx[MAXN], My[MAXN], Nx, Ny;
int chk[MAXN], Q[MAXN], prev[MAXN];

int MaxMatch(void)
{
int res = 0;
int qs, qe;

memset(Mx, -1, sizeof(Mx));
memset(My, -1, sizeof(My));
memset(chk, -1, sizeof(chk));

for (int i = 0; i < Nx; i++)
{
if (Mx[i] == -1)
{
qs = qe = 0;
Q[qe++] = i;
prev[i] = -1;

bool flag = 0;
while (qs < qe && !flag)
{
int u = Q[qs];

for (int v = 0; v < Ny && !flag; v++)
{
if (g[u][v] && chk[v] != i)
{
chk[v] = i;
Q[qe++] = My[v];
if (My[v] >= 0)
prev[My[v]] = u;
else
{
flag = 1;
int d = u, e = v;
while (d != -1)
{
int t = Mx[d];
Mx[d] = e;
My[e] = d;
d = prev[d];
e = t;
}
}
}
}
qs++;
}

if (Mx[i] != -1)
res++;
}
}
return res;
}

优点:适用于稀疏二分图,边较少,增广路较短。

还有一种叫做Hopcroft-Carp的算法,时间复杂度为O(V^0.5 E)。该算法的主要思想是在找增广路的时候同时找多条不相交的增广路,形成极大增广路集,然后对极大增广路集进行增广。在寻找增广路集的每一个阶段,找到的增广路都具有相同的长度,且随着算法的进行,增广路长度不断增大。可以证明,最多只需增广V^0.5次就可以得到最大匹配。
那么怎么在O(E)的时间复杂度内找到增广路集呢。我们可以对X中的每个未匹配的顶点进行BFS,BFS时对每个顶点维护一个距离标号dis[x]、dis[y],如果某个Y节点为未匹配节点,则找到一条增广路。BFS结束后就找到了增广路集。然后我们利用DFS用与匈牙利算法类似的方法对每条增广路进行增广,就可以得到最大匹配了。具体实现参考以下代码:

二分图匹配的Hopcroft-Carp算法:

#include <iostream>
#include <cstring>
#include <vector>
#include <queue>
using namespace std;

#define MAXN 128

const int INF = 1 << 28;

int g[MAXN][MAXN], Mx[MAXN], My[MAXN], Nx, Ny;
int dx[MAXN], dy[MAXN], dis;
bool vst[MAXN];

bool searchP(void)
{
queue<int> Q;
dis = INF;
memset(dx, -1, sizeof(dx));
memset(dy, -1, sizeof(dy));

for (int i = 0; i < Nx; i++)
{
if (Mx[i] == -1)
{
Q.push(i);
dx[i] = 0;
}
}

while (!Q.empty())
{
int u = Q.front(); Q.pop();

if (dx[u] > dis)
break;

for (int v = 0; v < Ny; v++)
{
if (g[u][v] && dy[v] == -1)
{
dy[v] = dx[u]+1;
if (My[v] == -1)
dis = dy[v];
else
{
dx[My[v]] = dy[v]+1;
Q.push(My[v]);
}
}
}
}

return dis != INF;
}

bool DFS(int u)
{
for (int v = 0; v < Ny; v++)
{
if (!vst[v] && g[u][v] && dy[v] == dx[u]+1)
{
vst[v] = 1;
if (My[v] != -1 && dy[v] == dis)
continue;

if (My[v] == -1 || DFS(My[v]))
{
My[v] = u;
Mx[u] = v;
return 1;
}
}
}
return 0;
}

int MaxMatch(void)
{
int res = 0;

memset(Mx, -1, sizeof(Mx));
memset(My, -1, sizeof(My));

while (searchP())
{
memset(vst, 0, sizeof(vst));
for (int i = 0; i < Nx; i++)
{
if (Mx[i] == -1 && DFS(i)) //if (dx[i] == 0 && DFS(i))
res++;
}
}

return res;
}

时间复杂度:O(sqrt(V)E)。

例题:
HOJ 1056 Machine Schedule
有两台机器A和B及N个需要运行的任务。每台机器有M种不同的模式,而每个任务i都恰好在一台机器上运行。如果它在机器A上运行,则机器A需要设置模式为ai,如果在机器B上运行,机器B需要设置为模式bi。每台机器上的任务可以按照任意顺序执行,但是每台机器每转换一次模式需要重新启动一次。开始时,两台机器都处于模式0,问合理安排任务的执行,最少的重启机器的次数为多少。

分析:显然,机器重启次数是两台机器需要使用的不同模式的个数。把每个任务看成一条边,即A机器的每个模式看成一个X节点,B机器的每个模式看成一个Y节点,任务i为边(ai, bi)。本题即求最少的点让每条边至少与其中的一点关联,即求一个点的最小覆盖。可以证明,这个最小覆盖就是该二分图的最大匹配数。故二分图匹配的模型就建好了。注意到开始时机器都处于0模式,所以如果某个任务可以在0模式下执行,则我们可以不考虑该任务,假定它已经被完成即可,也就是建图的时候不要把与0关联的边加到二分图中就可以得到正确的解。

代码如下:

#include <iostream>
#include <cstring>
using namespace std;

const int MAXN = 100;
int uN, vN; // u,v数目
bool g[MAXN][MAXN]; // g[i][j] 表示 xi与yj相连
int xM[MAXN], yM[MAXN]; // 输出量
bool chk[MAXN]; // 辅助量 检查某轮 y[v]是否被check



bool SearchPath(int u)
{
if (u == -1)
return false;
int v;
for(v = 0; v < vN; v++)
{
if(g[u][v] && !chk[v])
{
chk[v] = true;
if(yM[v] == -1 || SearchPath(yM[v]))
{
yM[v] = u;
xM[u] = v;
return true;
}
}
}
return false ;
}


int MaxMatch()
{
int u;
int ret = 0 ;
memset(xM, -1, sizeof(xM));
memset(yM, -1, sizeof(yM));
for(u = 0; u < uN; u++)
{
if(xM[u] == -1)
{
memset(chk, false, sizeof(chk));
if(SearchPath(u)) ret++;
}
}
return ret;
}

int main()
{
int k, i, x, y;

while (scanf("%d", &uN) && uN)
{
memset(g, 0, sizeof(g));

scanf("%d %d", &vN, &k);
for (int j = 0; j < k; j++)
{
scanf("%d %d %d", &i, &x, &y);
if (x*y != 0)
g[x][y] = 1;
}

printf("%d\n", MaxMatch());
}
return 0;
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值