文本内容框架:
§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时,就是最大完备匹配问题)
看到这里,估计已经有点晕乎了,那么下面帮你理顺下思路: