二分图匹配
1.相关概念理解:
二分图:是这样一个图,它的顶点可以分为X和Y两个集合,所有边关联的两个顶点恰好分属于X和Y;
二分图匹配:给定二分图G,在G的一个子图M中,M的边集中任意两条边都不依附于同一个顶点,则称M是一个匹配;
最大匹配:包含边数最多的匹配称为最大匹配;
完美匹配:若所有点都在匹配边上,则称这个最大匹配为完美匹配;
未盖点:在G中不与M中任何匹配边相关联的顶点就是未盖点;
交错轨:设P是图G的一条轨,若P的任意两条相邻边一定一条属于M而另一条不属于M,则称P为交错轨;
增广路:两端点都是未盖点的交错轨称为增广路;
2.二分图最大匹配-匈牙利算法:
算法核心思想在于不断寻找增广路,以增加匹配数目,而增广路根据边的状态可抽象为“010……10”形式,如果能找到并将其反转成“101……01”形式,则匹配数增加,另外,单独一条连接两个未匹配点的边也是增广路,因为从“0”反转成“1”,也会增加匹配数;如果不能再找到增广路,此时就得到最大匹配。
图1-图2:依次从x1-x3出发寻找增广路
从x1出发后,因为y1~y7均被置为未访问,且与x1边关联的节点包括y1,y2,y3,可以看到最先考察的y1是未盖点,即找到一条增广路“x1-y1”即“0”形式,执行link[y1] = x1操作将两个节点匹配,在图中表现为边的反转,find(x1)返回true;
从x2出发后,……
从x3出发后,在满足“未访问且边关联”条件的节点中先考察y1,因为此时link[y1] = x1,即y1和x1已经匹配不再是未盖点;但可以想象,如果此时能从link[y1]即x1出发能找到一条增广路即“010……10”形式,拼接在“x3-y1-x1”即“01”形式后面,组成“01010……10”形式,显然对x3来说找到了一条增广路,执行link[y1] = x操作更新匹配,在图中同样表现为“x3-y1-x1”边的反转,find(x3)返回true;
(注:在find(link[y1])时用到递归思想,此时再次从x1出发寻找增广路,不同的是y1已被x3访问过,所以考察y2……最后在栈的最深层确实能找到“x2-y5”这条增广路,然后在回溯过程中在每一层都会对当前层次所考虑的片段执行更新匹配操作,完全退出栈后整条增广路就都已完成了重新匹配,或者说完全反转)
图3-图4:依次从x4-x6出发寻找增广路
从x4,x5出发后,情况同x1,x2;
从x6出发后,仅需考察y4,然而从link[y4]即x5处出发找不到增广路,find(link[y4])返回false,所以x6无法和y集合中任何节点匹配;
#include <stdio.h> #include <string.h> #include <iostream> using namespace std; #define N 102 int n1, n2, m, ans; // 1~n1,1~n2分别编号x,y两类节点,m为总边数 bool g[N][N]; // 邻接矩阵描述二分图 bool y[N]; // y集合中各点访问标记 int link[N]; // link[y]表示当前与y节点相连的x节点 void init() { int x, y; // 用x,y分别代表x,y集合中某考察点,下面一样 memset(g, false, sizeof(g)); memset(link, -1, sizeof(link)); scanf("%d%d%d", &n1, &n2, &m); for(int i=1; i<=m; i++) { scanf("%d%d", &x, &y); g[x][y] = true; } } bool find(int x) { for(int j=1; j<=n2; j++) { if(g[x][j]==true && y[j]==false) { y[j] = true; if(link[j]==-1 || find(link[j])==true) { link[j] = x; return true; } } } return false; } int main() { init(); // 初始化g,link数组 ans = 0; for(int i=1; i<=n1; i++) { memset(y, false, sizeof(y)); if(find(i)) ans++; } printf("ans = %d\n", ans); for(int j=1; j<=n2; j++) // 打印输出匹配结果 { if(link[j] != -1) printf("link[%d] = %d\n", j, link[j]); } return 0; }