二分图大讲堂——彻底搞定最大匹配数(最小覆盖数)、最大独立数、最小路径覆盖、带权最优匹配(转)
文本内容框架:
§1图论点、边集和二分图的相关概念和性质
§2二分图最大匹配求解
匈牙利算法、Hopcroft-Karp算法
§3二分图最小覆盖集和最大独立集的构造
§4二分图最小路径覆盖求解
§5二分图带权最优匹配求解
Kuhn-Munkers算法
§6小结
每章节都详细地讲解了问题介绍,算法原理和分析,算法流程,算法实现四部分内容,力求彻底解决问题。
§1图论点、边集和二分图的相关概念和性质
点覆盖、最小点覆盖
点覆盖集即一个点集,使得所有边至少有一个端点在集合里。或者说是“点” 覆盖了所有“边”。。极小点覆盖(minimal vertex covering):本身为点覆盖,其真子集都不是。最小点覆盖(minimum vertex covering):点最少的点覆盖。点覆盖数(vertex covering number):最小点覆盖的点数。
边覆盖、极小边覆盖
边覆盖集即一个边集,使得所有点都与集合里的边邻接。或者说是“边” 覆盖了所有“点”。极小边覆盖(minimal edge covering):本身是边覆盖,其真子集都不是。最小边覆盖(minimum edge covering):边最少的边覆盖。边覆盖数(edge covering number):最小边覆盖的边数。
独立集、极大独立集
独立集即一个点集,集合中任两个结点不相邻,则称V为独立集。或者说是导出的子图是零图(没有边)的点集。极大独立集(maximal independent set):本身为独立集,再加入任何点都不是。最大独立集(maximum independent set):点最多的独立集。独立数(independent number):最大独立集的点。
团
团即一个点集,集合中任两个结点相邻。或者说是导出的子图是完全图的点集。极大团(maximal clique):本身为团,再加入任何点都不是。最大团(maximum clique):点最多的团。团数(clique number):最大团的点数。
边独立集、极大边独立集
边独立集即一个边集,满足边集中的任两边不邻接。极大边独立集(maximal edge independent set):本身为边独立集,再加入任何边都不是。最大边独立集(maximum edge independent set):边最多的边独立集。边独立数(edge independent number):最大边独立集的边数。
边独立集又称匹配(matching),相应的有极大匹配(maximal matching),最大匹配(maximum matching),匹配数(matching number)。
支配集、极小支配集
支配集即一个点集,使得所有其他点至少有一个相邻点在集合里。或者说是一部分的“点”支配了所有“点”。极小支配集(minimal dominating set):本身为支配集,其真子集都不是。最小支配集(minimum dominating set):点最少的支配集。支配数(dominating number):最小支配集的点数。
边支配集、极小边支配集
边支配集即一个边集,使得所有边至少有一条邻接边在集合里。或者说是一部分的“边”支配了所有“边”。极小边支配集(minimal edge dominating set):本身是边支配集,其真子集都不是。最小边支配集(minimum edge dominating set):边最少的边支配集。边支配数(edge dominating number):最小边支配集的边数。
最小路径覆盖
最小路径覆盖(path covering):是“路径” 覆盖“点”,即用尽量少的不相交简单路径覆盖有向无环图G的所有顶点,即每个顶点严格属于一条路径。路径的长度可能为0(单个点)。
最小路径覆盖数=G的点数-最小路径覆盖中的边数。应该使得最小路径覆盖中的边数尽量多,但是又不能让两条边在同一个顶点相交。拆点:将每一个顶点i拆成两个顶点Xi和Yi。然后根据原图中边的信息,从X部往Y部引边。所有边的方向都是由X部到Y部。因此,所转化出的二分图的最大匹配数则是原图G中最小路径覆盖上的边数。因此由最小路径覆盖数=原图G的顶点数-二分图的最大匹配数便可以得解。
匹配
匹配(matching)是一个边集,满足边集中的边两两不邻接。匹配又称边独立集(edge independent set)。
在匹配中的点称为匹配点(matched vertex)或饱和点;反之,称为未匹配点(unmatched vertex)或未饱和点。
交错轨(alternating path)是图的一条简单路径,满足任意相邻的两条边,一条在匹配内,一条不在匹配内。
增广轨(augmenting path):是一个始点与终点都为未匹配点的交错轨。
最大匹配(maximum matching)是具有最多边的匹配。
匹配数(matching number)是最大匹配的大小。
完美匹配(perfect matching)是匹配了所有点的匹配。
完备匹配(complete matching)是匹配了二分图较小集合(二分图X,Y中小的那个)的所有点的匹配。
增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。
所有匹配算法都是基于增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。这个定理适用于任意图。
二分图的性质
二分图中,点覆盖数是匹配数。
(1) 二分图的最大匹配数等于最小覆盖数,即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可。
(2) 二分图的独立数等于顶点数减去最大匹配数,很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质。
(3) DAG的最小路径覆盖,将每个点拆点后作最大匹配,结果为n-m,求具体路径的时候顺着匹配边走就可以,匹配边i→j',j→k',k→l'....构成一条有向路径。
(4)最大匹配数=左边匹配点+右边未匹配点。因为在最大匹配集中的任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖。
(5)最小边覆盖=图中点的个数-最大匹配数=最大独立集。
二分图的判定
二分图是这样一个图: 有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!
无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。
判断二分图的常见方法是染色法: 开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!
易知:任何无回路的的图均是二分图。
§2二分图最大匹配求解
§2.1问题简介
设G=(V,E)是一个无向图。如顶点集V可分割为两个互不相交的子集V1,V2之并,并且图中每条边依附的两个顶点都分属于这两个不同的子集。则称图G为二分图。二分图也可记为G=(V1,V2,E)。
给定一个二分图G,在G的一个子图M中,M的边集{E}中的任意两条边都不依附于同一个顶点,则称M是一个匹配。 选择这样的子集中边数最大的子集称为图的最大匹配问题(maximal matching problem)
如果一个匹配中,图中的每个顶点都和图中某条边相关联,则称此匹配为完全匹配,也称作完备,完美匹配。
§2.2匈牙利算法
匈牙利算法思想
根据一个匹配是最大匹配当且仅当没有增广路,求最大匹配就是找增广轨,直到找不到增广轨,就找到了最大匹配。遍历每个点,查找增广路,若找到增广路,则修改匹配集和匹配数,否则,终止算法,返回最大匹配数。
增广路径必须满足的性质
1.有奇数条边。
2.起点在二分图的左半边,终点在右半边。
3.路径上的点一定是一个在左半边,一个在右半边,交替出现。(其实二分图的性质就决定了这一点,因为二分图同一边的点之间没有边相连,不要忘记哦。)
4.整条路径上没有重复的点。
5.起点和终点都是目前还没有配对的点,而其它所有点都是已经配好对的。
6.路径上的所有第奇数条边都不在原匹配中,所有第偶数条边都出现在原匹配中。
7.最后,也是最重要的一条,把增广路径上的所有第奇数条边加入到原匹配中去,并把增广路径中的所有第偶数条边从原匹配中删除(这个操作称为增广路径的取反),则新的匹配数就比原匹配数增加了1个(奇数=偶数+1)。
每次查找得到的增广路径的长度都是在上一次查找到的增广路径的基础上延伸的,这样每次更新匹配数都是增加1。
匈牙利算法步骤(令G = (X,*,Y)是一个二分图,其中,X = {x1,x2,...xm}, Y = {y1,y2,...yn}。令M为G中的任一个匹配)
(1)置M为空
(2)从G中找出一个未匹配点v(增广路性质5要求的),如果没有则算法结束,否则,以v为起点,查找增广路(邻接点是为未匹配点,则返回寻找完成,若v的邻接点u是匹配点,则从u开始查找,直至查找到有未匹配点终止)即满足增广路的性质,如果没有找到增广路,则算法终止
(3)找出一条增广路径P,通过异或操作获得更大的匹配M’代替M(方便要输出增广矩阵以及进一步查找),匹配数加1(性质7得到)
(4)重复(2)(3)操作直到找不出增广路径为止
彻底理解增广路查找方法
1.总是从X集的未匹配点出发,寻找匹配点或者未匹配点,如查找到未匹配点则该增广路终止,否则以该点的增广路不存在。
2.每次查找增广路都是在之前形成的匹配(上面步骤3中异或后的匹配)的基础上进行延伸的,也就是查找匹配点总是在匹配M中,其实就是用起点和终点两个未匹配点将得到匹配的边尽可能的连接起来的增广路,这样增广路长度就延长了,当然也可以是直接就是以两个个未匹配点的边(就直接添加进匹配中)。总而言之,每次扩充匹配不是通过延伸增广路径就是新增增广路径(当然长度为1)。
╔
时间空间复杂度
时间复杂度 邻接矩阵:最坏为O(n^3) 邻接表:O(mn) 空间复杂度 邻接矩阵:O(n^2) 邻接表:O(m+n)
╝②
匈牙利算法实现
匈牙利算法只需要以每个节点为起点找一次增广路即可求得最大匹配,寻找增广路的复杂度为O(E),总的复杂度为O(VE)。
下面的实现是查找从X到Y的匹配,X中每一个点最多只能被遍历一次用于查找增广路(当已经是匹配点是就不遍历了),对DFS进行了详细的注释,要是还是不懂,可以免费提供解惑。
╔
DFS
#define maxn 10//表示x集合和y集合中顶点的最大个数! int nx,ny;//x集合和y集合中顶点的个数 int edge[maxn][maxn];//edge[i][j]为1表示ij可以匹配 int cx[maxn],cy[maxn];//用来记录x集合中匹配的y元素是哪个! int visited[maxn];//用来记录该顶点是否被访问过! int path(int u) { int v; for(v=0;v<ny;v++) { if(edge[u][v]&&!visited[v]) { visited[v]=1; if(cy[v]==-1||path(cy[v]))//如果y集合中的v元素没有匹配或者是v已经匹配,但是从cy[v]中能够找到一条增广路 { cx[u]=v; //找到增广路,修改匹配M cy[v]=u; return 1; } } } return 0; } int maxmatch() { int res=0; memset(cx,0xff,sizeof(cx));//初始值为-1表示两个集合中都没有匹配的元素! memset(cy,0xff,sizeof(cy)); for(int i=0;i<=nx;i++) { if(cx[i]==-1) //还没被匹配,执行内部代码 { memset(visited,0,sizeof(visitited)); //重置标记为为访问 res+=path(i); //以 i 为起点开始查找增广路,返回true ,匹配数+1 } } return res; }
BFS
#define maxn 10; int pred[maxn]; int cx,cy; int nx,ny; int visited[maxn]; int edge[maxn][maxn]; int queue[maxn];// 用来模拟队列 int maxmatch() { int i,j,y; int cur,tail; int res=0; memset(cx,0xff,sizeof(cx)); memset(cy,0xff,sizeof(cx)); for(i=0;i<nx;i++) { if(cx[i]!=-1) continue; //对于x集合中的每个没有匹配的点i进行一次bfs找交错轨 for(j=0;j<ny;j++) { if(edge[i][j]) { pred[j]=-1;//-1表示遍历到了, queue[tail++]=j; } } while(cur<tail) { y=queue[cur]; if(cy[y]==-1) break; for(j=0;j<ny;j++) { if(pred[j]==-2&&edge[cy[y]][j]) { pred[j]=y; queue[tail++]=j; } } } if(cur==tail) continue; while(pred[y]>-1) { cx[cy[pred[y]]]=y; cy[y]=cy[pred[y]]; y=pred[y]; } cy[y]=i; cx[i]=y; res++; } return res; }
╝①
§2.3Hopcroft-Karp算法
在匈牙利算法中,我们每次寻找一条增广路来增加匹配集合M.可以证明,每次找增广路的复杂度是O(E),一共需要增广O(V)次,因此总时间复杂度为O(VE)。为了降低时间复杂度,在Hopcroft-Karp算法中,我们在增加匹配集合M时,每次DFS寻找多条增广路(不相交).可以证明,这样迭代次数最多为2*V^0.5,所以,时间复杂度就降到了O(V^0.5*E)。
Hopcroft-Karp算法原理
Hopcroft-Karp算法先使用BFS查找多条增广路,然后使用DFS遍历增广路(累加匹配数,修改匹配点集),循环执行,直到没有增广路为止。
Hopcroft-Karp算法的BFS遍历只对点进行分层(不标记是匹配点和未匹配点),然后用DFS遍历看上面的层次哪些是增广路径(最后一个点是未匹配的)。
BFS过程可以看做是图像树结构一样逐层向下遍历,还要防止出现相交的增广路径。
Hopcroft-Karp算法步骤
设U和V是图G的二分图,M是从U到V的匹配
(1)使用BFS遍历对图的点进行分层,从X中找出一个未匹配点v,(所有v)组成第一层,接下的层是这样形成的——都是查找匹配点(增广路性质),直到在V中找到未匹配点才终止查找,对X其他未匹配点同样进行查找增广路径(BFS只分层不标记是否匹配点)
(2)使用DFS遍历查找(1)形成的增广路,找到就匹配数就累加1
(3)重复(1)(2)操作直到找不出增广路径为止
Hopcroft-Karp算法实现
下面的实现有详细的注释,该算法还是不完美,每次调用searchP()值保留了一个最小的dis值(为什么是最小,因为其是BFS遍历,当同一层次有一个v满足My[v]==-1时,dis就附上相应的层次值),也就是在长度大于dis的层在本次调用时再遍历下去,只能是下次调用searchP()查找,花了好几个小时去理解。
通过上面的分析,易知searchP()是没有遍历层次大于dis的层,也就是说没有把长度大于dis增广路径是没有找到的。当然这样做的好处——防止出现相交的增广路径。
还有个要知道的是dis在下面这个算法中的值只可能是从1逐渐增加偶数变大的,所以这样做是不可能在一次searchP()调用之后DFS出现相交的增广路径的(一定只会是长度小的那个增广路径)。
╔
Hopcroft-Carp 算法: //poj_1469 /*==================================================*\ | 二分图匹配(Hopcroft-Carp 的算法) | INIT: g[][]邻接矩阵; | CALL: res = MaxMatch(); Nx, Ny要初始化!!! | Mx,My为match | 时间复杂度为O(V^0.5 E) \*==================================================*/ /***********************Hopcroft-Carp 算法****************************************/ #include <cstdio> #include <memory.h> #include <queue> using namespace std; const int MAXN = 310; const int INF = 1 << 28; bool flag; int p,n; int Mx[MAXN], My[MAXN], Nx, Ny; int dx[MAXN], dy[MAXN], dis; bool vst[MAXN],g[110][310]; bool searchP(void) //BFS { queue <int> Q; dis = INF; memset(dx, -1, sizeof(dx)); memset(dy, -1, sizeof(dy)); for (int i = 1; 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; //说明该增广路径长度大于dis还没有结束,等待下一次BFS在扩充 for (int v = 1; v <= Ny; v++) if (g[u][v] && dy[v] == -1) { //v是未匹配点 dy[v] = dx[u]+1; if (My[v] == -1) dis = dy[v]; //得到本次BFS的最大遍历层次 else{ dx[My[v]] = dy[v]+1; //v是匹配点,继续延伸 Q.push(My[v]); } } } return dis != INF; } bool DFS(int u){ for (int v = 1; 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; //层次(也就是增广路径的长度)大于本次查找的dis,是searchP被break的情况,也就是还不确定是否是增广路径,只有等再次调用searchP()在判断。 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 = 1; i <= Nx; i++) if (Mx[i] == -1 && DFS(i)) res++; //查找到一个增广路径,匹配数res++ } return res; } /**********************************************************************/ int main() { int i,j,k,t,v,cnt; scanf("%d",&t); while (t--) { scanf("%d %d", &p, &n); for (i = 1; i <= p; i++) for (j = 1; j <= n; j++) g[i][j] = false; flag = true; for (i = 1; i <= p; i++) { scanf("%d",&k); if (k == 0) flag = false; while (k--) { scanf("%d",&v); g[i][v] = true; } } Nx = p; Ny = n; if (flag) { cnt = MaxMatch(); if (cnt == p) printf("YES\n"); else printf("NO\n"); } else printf("NO\n"); } return 0; }
╝②
该算法实现的关键点:每次使用调用BFS查找到多条增广路的路径长度都是相等的,而且都以第一次得到的dis为该次查找增广路径的最大长度。
下面给出另一个实现
╔
代码 //1550ms #include <stdio.h> #include <string.h> #define CAP 50010 int n, m; int mx[CAP], my[CAP], dis[CAP], que[CAP]; bool used[CAP]; struct Node { int id; struct Node *next; }adj[CAP]; bool BFS() { int front, rear; int i, j; front = rear = 0; for (i=1; i<=n; i++) { if (mx[i] < 0) { dis[i] = 0; que[rear++] = i; used[i] = true; }else { used[i] = false; } } bool suc = false; while (front < rear) { int u = que[front++]; struct Node *p = &(adj[u]); while (p->next) { int v = p->next->id; if (my[v] < 0) suc = true; else if (!used[my[v]]) { dis[my[v]] = dis[u]+1; used[my[v]] = true; que[rear++] = my[v]; } p = p->next; } } return suc; } bool DFS(int u) { struct Node *p = &(adj[u]); while (p->next) { int v = p->next->id; if (my[v] < 0 || dis[my[v]] == dis[u]+1 && DFS(my[v])) { my[v] = u; mx[u] = v; dis[u] = -1; return true; } p = p->next; } return false; } int main() { int i, j, P; int a, b; struct Node *p; while (scanf("%d%d%d", &n, &m, &P) != EOF) { for (i=1; i<=n; i++) adj[i].next = NULL; for (i=0; i<P; i++) { scanf("%d%d", &a, &b); p = new Node; p->id = b; p->next = adj[a].next; adj[a].next = p; } memset(mx, -1, sizeof(mx)); memset(my, -1, sizeof(my)); int match = 0; while (BFS()) { for (i=1; i<=n; i++) { if (mx[i] < 0 && DFS(i)) match++; } } printf("%d\n", match); } return 0; }
╝③
匈牙利算法和Hopcroft-Karp算法细节的对比
匈牙利算法每次都以一个点查找增广路径,Hopcroft-Karp算法是每次都查找多条增广路径;匈牙利算法每次查找的增广路径的长度是随机的,Hopcroft-Karp算法每趟查找的增广路径的长度只会在原来查找到增广路径的长度增加偶数倍(除了第一趟,第一趟得到增广路径长度都是1)。
§3二分图最小覆盖集和最大独立集的构造
╔
二分图有下面两个性质:
定理1:最小覆盖数 = 最大匹配数
定理2:最大独立集S 与 最小覆盖集T 互补
算法实现步骤:
1. 做最大匹配,没有匹配的空闲点∈S
2. 如果u∈S那么u的邻点必然属于T
3. 如果一对匹配的点中有一个属于T那么另外一个属于S
4. 还不能确定的,把左子图的放入S,右子图放入T
算法结束
╝④
§4二分图最小路径覆盖求解
最小覆盖的相关性质可查看前面的第一部分。
╔
有向无环图最小不相交路径覆盖
定义:用最少的不相交路径覆盖所有顶点。
定理:把原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By。这样就得到了一个二分图,最小路径覆盖=原图的节点数-新图最大匹配。
简单证明:一开始每个点都独立的为一条路径,总共有n条不相交路径。我们每次在二分图里加一条边就相当于把两条路径合成了一条路径,因为路径之间不能有公共点,所以加的边之间也不能有公共点,这就是匹配的定义。所以有:最小路径覆盖=原图的节点数-新图最大匹配。
有向无环图最小可相交路径覆盖
定义:用最小的可相交路径覆盖所有顶点。
算法:先用floyd求出原图的传递闭包,即如果a到b有路,那么就加边a->b。然后就转化成了最小不相交路径覆盖问题。
╝⑦
问题描述
给定有向图 G=(V,E)。设 P 是 G 的一个简单路(顶点不相交)的集合。如果 V 中每个顶点恰好在 P 的一条路上,则称 P是 G 的一个路径覆盖。P 中路径可以从 V 的任何一个顶点开始,长度也是任意的,特别地,可以为0。G 的最小路径覆盖是 G 的所含路径条数最少的路径覆盖。
算法思想
把每个顶点理解成两个顶点,一个是出发,一个是目标,建立二分图。该二分图的任何一个匹配方案,都对应了一个路径覆盖方案。如果匹配数为0,那么显然路径数=顶点数。每增加一条匹配边,那么路径覆盖数就减少一个,所以路径数=顶点数 - 匹配数。要想使路径数最少,则应最大化匹配数,所以要求二分图的最大匹配。
有向无环图最小覆盖算法流程
1.构建二分图,原图中的每个点V拆成Vx和Vy,如果有一条有向边A->B,那么就加边Ax-By。这样就得到了一个无向的二分图。
2.求最大匹配数
算法实现
╔
#include<iostream> #include<string.h> #include<stdio.h> #include<algorithm> #include<vector> #define maxn 10000 using namespace std; vector<int>node[maxn]; int mm[maxn]; int visit[maxn]; int n,m; void init() { cin>>n>>m;//输入点数,边数 for(int i=0;i<=n;i++) node[i].clear(); for(int i=0;i<m;i++) { int a,b; cin>>a>>b; node[a].push_back(b);//建边 } } int dfs(int fa) { for(int i=0;i<node[fa].size();i++) { int v=node[fa][i]; if(!visit[v]) { visit[v]=1; if(mm[v]==-1||dfs(mm[v])) { mm[v]=fa; return 1; } } } return 0; } void solve() { int cnt=0; memset(mm,-1,sizeof(mm)); for(int i=1;i<=n;i++) { memset(visit,0,sizeof(visit)); if(dfs(i))cnt++; } cout<<n-cnt<<endl;//最小路径覆盖=点数-最大匹配数 } int main() { int test; cin>>test; while(test--) { init(); solve(); } return 0; }
╝⑧
§5二分图带权最优匹配求解
问题描述
二分图带权最优匹配:对于二分图的每条边都有一个权(非负),要求一种完备匹配方案,使得所有匹配边的权和最大,记做最优完备匹配。(特殊的,当所有边的权为1时,就是最大完备匹配问题)
看到这里,估计已经有点晕乎了,那么下面帮你理顺下思路:
二分图带权最大匹配和二分图最有匹配的区别
二分最大匹配:使得匹配中的边数量不小于任何其他的匹配。
二分完备匹配:使得图G中所有点出现在该匹配中。
二分图完备匹配的判定
╔
Hall定理:设二分图中G=<V1,V2,E>中|V1|=m<=|V2|=n,G中存在从V1到V2的完备匹配当且仅当V1中任意k(k=1,2,...,m)个顶点至少与V2中k个顶点相邻(相异性条件)。
证明:
(必要性)显然成立。
(充分性)反证法。设G中不存在完备匹配,取G的一个最大匹配M,则V1中至少有一个点不在M上,且该点必至少与一条不在M中的边相连,该边的另一个顶点若也为M-非饱和点,则与M为最大匹配矛盾,若另一个顶点为M-饱和点,则考察在M中与该顶点相邻的点,利用饱和点去考察在M中相邻的饱和点(交错地考察,即交错地通过M中的边和非M中的边),直至考察完毕,由相异性条件知,最后必考察至非饱和点,此时出现一条增广路,又与假设矛盾,故充分性成立。
Hall定理的一个推论:设二部图中G=<V1,V2,E>中|V1|=m<=|V2|=n,V1中每个顶点至少关联正整数t条边,V2中每个顶点至多关联t条边(t条件),则G存在从V1到V2的完备匹配。
╝⑤
二分图完备匹配判断实现可参考http://www.cnblogs.com/moonbay/archive/2012/08/16/2642042.html。
二分图的带权匹配就是求出一个匹配集合,使得集合中边的权值之和最大或最小。
而二分图的最优匹配则一定为完备匹配,在此基础上,才要求匹配的边权值之和最大或最小。二分图的带权匹配与最优匹配不等价,也不互相包含。
使用Kuhn-Munkers算法实现求二分图的最优匹配。Kuhn-Munkers算法可以实现为O(N^3)的时间复杂度。
Kuhn-Munkers算法
╔
Kuhn-Munkers算法是通过给每个顶点一个标号(叫做顶标)来把求最大权匹配的问题转化为求完备匹配的问题的。设顶点Xi的顶标为A[i],顶点Yi的顶标为B[i],顶点Xi与Yj之间的边权为w[i,j]。在算法执行过程中的任一时刻,对于任一条边(i,j), A[i]+B[j]>=w[i,j]始终成立。
Kuhn-Munkers算法的正确性基于以下定理:
若由二分图中所有满足A[i]+B[j]=w[i,j]的边(i,j)构成的子图(称做相等子图)有完备匹配,那么这个完备匹配就是二分图的最大权匹配。
这个定理是显然的。因为对于二分图的任意一个匹配,如果它包含于相等子图,那么它的边权和等于所有顶点的顶标和;如果它有的边不包含于相等子图,那么它的边权和小于所有顶点的顶标和。所以相等子图的完备匹配一定是二分图的最大权匹配。
Kuhn-Munkers算法的基本思路
初始时为了使A[i]+B[j]>=w[i,j]恒成立,令A[i]为所有与顶点Xi关联的边的最大权,B[j]=0。如果当前的相等子图没有完备匹配,就按下面的方法修改顶标以使扩大相等子图,直到相等子图具有完备匹配为止。
我们求当前相等子图的完备匹配失败了,是因为对于某个X顶点,我们找不到一条从它出发的交错路。这时我们获得了一棵交错树,它的叶子结点全部是X顶点。现在我们把交错树中X顶点的顶标全都减小某个值d,Y顶点的顶标全都增加同一个值d,那么我们会发现:
两端都在交错树中的边(i,j),A[i]+B[j]的值没有变化。也就是说,它原来属于相等子图,现在仍属于相等子图。
两端都不在交错树中的边(i,j),A[i]和B[j]都没有变化。也就是说,它原来属于(或不属于)相等子图,现在仍属于(或不属于)相等子图。
X端不在交错树中,Y端在交错树中的边(i,j),它的A[i]+B[j]的值有所增大。它原来不属于相等子图,现在仍不属于相等子图。
X端在交错树中,Y端不在交错树中的边(i,j),它的A[i]+B[j]的值有所减小。也就说,它原来不属于相等子图,现在可能进入了相等子图,因而使相等子图得到了扩大。
现在的问题就是求d值了。为了使A[i]+B[j]>=w[i,j]始终成立,且至少有一条边进入相等子图,d应该等于min{A[i]+B[j]-w[i,j]|Xi在交错树中,Yi不在交错树中}。
以上就是Kuhn-Munkers算法的基本思路。但是朴素的实现方法,时间复杂度为O(n4)——需要找O(n)次增广路,每次增广最多需要修改O(n)次顶标,每次修改顶标时由于要枚举边来求d值,复杂度为O(n²)。实际上Kuhn-Munkers算法的复杂度是可以做到O(n3)的。我们给每个Y顶点一个“松弛量”函数slack,每次开始找增广路时初始化为无穷大。在寻找增广路的过程中,检查边(i,j)时,如果它不在相等子图中,则让slack[j]变成原值与A[i]+B[j]-w[i,j]的较小值。这样,在修改顶标时,取所有不在交错树中的Y顶点的slack值中的最小值作为d值即可。但还要注意一点:修改顶标后,要把所有的slack值都减去d。
Kuhn-Munkers算法流程
(1)初始化可行顶标的值
(2)用匈牙利算法寻找完备匹配
(3)若未找到完备匹配则修改可行顶标的值
(4)重复(2)(3)直到找到相等子图的完备匹配为止
Kuhn-Munkers算法实现
#include <cstdio> #include <queue> #include <algorithm> using namespace std; const int N = 128; const int INF = 1 << 28; class Graph { private: bool xckd[N], yckd[N]; int n, edge[N][N], xmate[N], ymate[N]; int lx[N], ly[N], slack[N], prev[N]; queue<int> Q; bool bfs(); void agument(int); public: bool make(); int KMMatch(); }; bool Graph::make() { int house[N], child[N], h, w, cn = 0; char line[N]; scanf("%d %d", &h, &w); if(w == 0) return false; scanf("\n"); n = 0; for(int i = 0; i < h; i++) { gets(line); for(int j = 0; line[j] != 0; j++) { if(line[j] == 'H') house[n++] = i * N + j; if(line[j] == 'm') child[cn++] = i * N + j; } } for(int i = 0; i < n; i++) { int cr = child[i] / N, cc = child[i] % N; for(int j = 0; j < n; j++) { int hr = house[j] / N, hc = house[j] % N; edge[i][j] = -abs(cr-hr) - abs(cc-hc); } } return true; } bool Graph::bfs() { while(!Q.empty()) { int p = Q.front(), u = p>>1; Q.pop(); if(p&1) { if(ymate[u] == -1) { agument(u); return true; } else { xckd[ymate[u]] = true; Q.push(ymate[u]<<1); } } else { for(int i = 0; i < n; i++) if(yckd[i]) continue; else if(lx[u]+ly[i] != edge[u][i]) { int ex = lx[u]+ly[i]-edge[u][i]; if(slack[i] > ex) { slack[i] = ex; prev[i] = u; } } else { yckd[i] = true; prev[i] = u; Q.push((i<<1)|1); } } } return false; } void Graph::agument(int u) { while(u != -1) { int pv = xmate[prev[u]]; ymate[u] = prev[u]; xmate[prev[u]] = u; u = pv; } } int Graph::KMMatch() { memset(ly, 0, sizeof(ly)); for(int i = 0; i < n; i++) { lx[i] = -INF; for(int j = 0; j < n; j++) lx[i] >?= edge[i][j]; } memset(xmate, -1, sizeof(xmate)); memset(ymate, -1, sizeof(ymate)); bool agu = true; for(int mn = 0; mn < n; mn++) { if(agu) { memset(xckd, false, sizeof(xckd)); memset(yckd, false, sizeof(yckd)); for(int i = 0; i < n; i++) slack[i] = INF; while(!Q.empty()) Q.pop(); xckd[mn] = true; Q.push(mn<<1); } if(bfs()) { agu = true; continue; } int ex = INF; mn--; agu = false; for(int i = 0; i < n; i++) if(!yckd[i]) ex <?= slack[i]; for(int i = 0; i < n; i++) { if(xckd[i]) lx[i] -= ex; if(yckd[i]) ly[i] += ex; slack[i] -= ex; } for(int i = 0; i < n; i++) if(!yckd[i] && slack[i] == 0) { yckd[i] = true; Q.push((i<<1)|1); } } int cost = 0; for(int i = 0; i < n; i++) cost += edge[i][xmate[i]]; return cost; } int main() { Graph g; while(g.make()) printf("%d\n", -g.KMMatch()); return 0; }
Kuhn-Munkers算法的几种变形应用
1.Kuhn-Munkers算法是求最大权完备匹配,如果要求最小权完备匹配怎么办?方法很简单,只需将所有的边权值取其相反数,求最大权完备匹配,匹配的值再取相反数即可。
2.Kuhn-Munkers算法的运行要求是必须存在一个完备匹配,如果求一个最大权匹配(不一定完备)该如何办?依然很简单,把不存在的边权值赋为0。
3.Kuhn-Munkers算法求得的最大权匹配是边权值和最大,如果我想要边权之积最大,又怎样转化?还是不难办到,每条边权取自然对数,然后求最大和权匹配,求得的结果a再算出e^a就是最大积匹配。
╝⑥
§6小结
本文详细地讲解了二分图——最大匹配数(最小覆盖数)、最大独立数、最小路径覆盖、带权最优匹配五大主题,每部分解释都做到有过之而不及的详尽,紧扣重点,简明易懂。如果你有任何建议或者批评和补充,请留言指出,不胜感激,更多参考请移步互联网。
参考:
①波风七夜: http://www.cnblogs.com/bofengqiye/archive/2012/05/02/2479809.html
②acronix: http://www.cppblog.com/acronix/archive/2010/08/21/124203.aspx
③superbin: http://www.cnblogs.com/superbin/archive/2010/06/09/1754996.html
http://hi.baidu.com/ewamihcjxublmrd/item/5729ae2346a7783395f62b2d
⑤zxfx100: http://www.cnblogs.com/zxfx100/
⑥NOCOW: http://www.nocow.cn/index.php/Kuhn-Munkres%E7%AE%97%E6%B3%95
⑦Memento: http://endlesscount.blog.163.com/blog/static/821197872012622103810976/
⑧一把刷子: http://www.cnblogs.com/cs1003/archive/2012/08/28/2660845.html