本着“勿在浮沙筑高台”的原则,在这里总结一下二分图的各类知识和模板,彻底搞懂二分图
一、相关概念和性质
一些简单的概念这里就一带而过了。
极大和最大的区别:极大指的是再加入任意点或边将不再满足条件,最大指的是极大中点或边最多的集合
1、点覆盖、最小点覆盖
2、边覆盖、极小边覆盖
3、独立集、极大独立集
独立集即一个点集,集合中任两个结点不相邻,则称V为独立集。或者说是导出的子图是零图(没有边)的点集。极大独立集(maximal independent set):本身为独立集,再加入任何点都不是。最大独立集(maximum independent set):点最多的独立集。独立数(independent number):最大独立集的点。
4、团
团即一个点集,集合中任两个结点相邻。或者说是导出的子图是完全图的点集。极大团(maximal clique):本身为团,再加入任何点都不是。最大团(maximum clique):点最多的团。团数(clique number):最大团的点数。
5、边独立集、极大边独立集
边独立集即一个边集,满足边集中的任两边不邻接。极大边独立集(maximal edge independent set):本身为边独立集,再加入任何边都不是。最大边独立集(maximum edge independent set):边最多的边独立集。边独立数(edge independent number):最大边独立集的边数。
边独立集又称匹配(matching),相应的有极大匹配(maximal matching),最大匹配(maximum matching),匹配数(matching number)。
6、支配集、极小支配集
支配集即一个点集,使得所有其他点至少有一个相邻点在集合里。或者说是一部分的“点”支配了所有“点”。极小支配集(minimal dominating set):本身为支配集,其真子集都不是。最小支配集(minimum dominating set):点最少的支配集。支配数(dominating number):最小支配集的点数。
7、边支配集、极小边支配集
边支配集即一个边集,使得所有边至少有一条邻接边在集合里。或者说是一部分的“边”支配了所有“边”。极小边支配集(minimal edge dominating set):本身是边支配集,其真子集都不是。最小边支配集(minimum edge dominating set):边最少的边支配集。边支配数(edge dominating number):最小边支配集的边数。
8、最小路径覆盖
最小路径覆盖(path covering):是“路径” 覆盖“点”,即用尽量少的不相交简单路径覆盖有向无环图G的所有顶点,即每个顶点严格属于一条路径。路径的长度可能为0(单个点)。
最小路径覆盖数=G的点数-最小路径覆盖中的边数。应该使得最小路径覆盖中的边数尽量多,但是又不能让两条边在同一个顶点相交。拆点:将每一个顶点i拆成两个顶点Xi和Yi。然后根据原图中边的信息,从X部往Y部引边。所有边的方向都是由X部到Y部。因此,所转化出的二分图的最大匹配数则是原图G中最小路径覆盖上的边数。因此由最小路径覆盖数=原图G的顶点数-二分图的最大匹配数便可以得解。
9、匹配
匹配(matching)是一个边集,满足边集中的边两两不邻接。匹配又称边独立集(edge independent set)。
在匹配中的点称为匹配点(matched vertex)或饱和点;反之,称为未匹配点(unmatched vertex)或未饱和点。
交错轨(alternating path)是图的一条简单路径,满足任意相邻的两条边,一条在匹配内,一条不在匹配内。
增广轨(augmenting path):是一个始点与终点都为未匹配点的交错轨。
最大匹配(maximum matching)是具有最多边的匹配。
匹配数(matching number)是最大匹配的大小。
完美匹配(perfect matching)是匹配了所有点的匹配。
完备匹配(complete matching)是匹配了二分图较小集合(二分图X,Y中小的那个)的所有点的匹配。
增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。
所有匹配算法都是基于增广轨定理:一个匹配是最大匹配当且仅当没有增广轨。这个定理适用于任意图。
10、二分图的性质
二分图中,点覆盖数是匹配数。
(1) 二分图的最大匹配数等于最小覆盖数,即求最少的点使得每条边都至少和其中的一个点相关联,很显然直接取最大匹配的一段节点即可。
(2) 二分图的独立数等于顶点数减去最大匹配数,很显然的把最大匹配两端的点都从顶点集中去掉这个时候剩余的点是独立集,这是|V|-2*|M|,同时必然可以从每条匹配边的两端取一个点加入独立集并且保持其独立集性质。
(3) DAG的最小路径覆盖,将每个点拆点后作最大匹配,结果为n-m,求具体路径的时候顺着匹配边走就可以,匹配边i→j',j→k',k→l'....构成一条有向路径。
(4)最大匹配数=左边匹配点+右边未匹配点。因为在最大匹配集中的任意一条边,如果他的左边没标记,右边被标记了,那么我们就可找到一条新的增广路,所以每一条边都至少被一个点覆盖。
(5)最小边覆盖=图中点的个数-最大匹配数=最大独立集。
11、二分图的判定
二分图是这样一个图: 有两顶点集且图中每条边的的两个顶点分别位于两个顶点集中,每个顶点集中没有边直接相连接!
无向图G为二分图的充分必要条件是,G至少有两个顶点,且其所有回路的长度均为偶数。
判断二分图的常见方法是染色法: 开始对任意一未染色的顶点染色,之后判断其相邻的顶点中,若未染色则将其染上和相邻顶点不同的颜色, 若已经染色且颜色和相邻顶点的颜色相同则说明不是二分图,若颜色不同则继续判断,bfs和dfs可以搞定!
易知:任何无回路的的图均是二分图。
如HDU2444判断是否为二分图


const int maxn = 510; const int maxm = 250010; int mp[maxn][maxn]; int linker[maxn]; int id[maxn]; int uN, vN; int n, m; bool vis[maxn]; bool dfs(int x) { for (int i = 1; i <= n; i ++) { if (mp[x][i]) { if (id[i] == id[x]) return false; if (id[i] == 0) { id[i] = -1 * id[x]; if (!dfs(i)) return false; } } } return true; } bool dfs1(int u) { for (int v = 1; v <= n; v ++) { if (mp[u][v] && !vis[v]) { vis[v] = true; if (linker[v] == -1 || dfs1(linker[v])) {//刚开始这地方写成dfs了,罪过呀,写的有点混乱,下次注意! linker[v] = u; return true; } } } return false; } int hungry() { memset(linker, -1, sizeof(linker)); int ret = 0; for (int i = 1; i <= n; i ++) { memset(vis, 0, sizeof(vis)); if (dfs1(i)) ret ++; } return ret; } int main() { //input; int u, v; while (~scanf("%d %d", &n, &m)) { memset(mp, 0, sizeof(mp)); memset(id, 0, sizeof(id)); for (int i = 1; i <= m; i ++) { scanf("%d %d", &u, &v); mp[u][v] = 1; mp[v][u] = 1; } bool flag = true; for (int i = 1; i <= n; i ++) { if (!id[i]) { id[i] = 1; if (!dfs(i)) { flag = false; break; } } } if (!flag) { printf("No\n"); continue; } else printf("%d\n", hungry() / 2); } return 0; }
二、二分图最大匹配求解
1、匈牙利算法
1.1 匈牙利算法思想
根据一个匹配是最大匹配当且仅当没有增广路,求最大匹配就是找增广轨,直到找不到增广轨,就找到了最大匹配。遍历每个点,查找增广路,若找到增广路,则修改匹配集和匹配数,否则,终止算法,返回最大匹配数。
1.2 匈牙利算法步骤(令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)
╝
代码实现
邻接矩阵形式


/**************************************************** 二分图匹配(匈牙利算法的DFS实现) INIT:g[][]两边定点划分的情况 CALL:res=hungary();输出最大匹配数 优点:适于稠密图,DFS找增广路快,实现简洁易于理解 时间复杂度:O(VE); ****************************************************/ const int MAXN=1000; int uN,vN; //u,v数目 int g[MAXN][MAXN];//编号是0~n-1的 int linker[MAXN]; bool used[MAXN]; bool dfs(int u) { int v; for(v=0;v<vN;v++) if(g[u][v]&&!used[v]) { used[v]=true; if(linker[v]==-1||dfs(linker[v])) { linker[v]=u; return true; } } return false; } int hungary() { int res=0; int u; memset(linker,-1,sizeof(linker)); for(u=0;u<uN;u++) { memset(used,0,sizeof(used)); if(dfs(u)) res++; } return res; }
vector邻接表形式


/* ID: LinKArftc PROG: 1083.cpp LANG: C++ */ #include <map> #include <set> #include <cmath> #include <stack> #include <queue> #include <vector> #include <cstdio> #include <string> #include <utility> #include <cstdlib> #include <cstring> #include <iostream> #include <algorithm> using namespace std; #define eps 1e-8 #define randin srand((unsigned int)time(NULL)) #define input freopen("input.txt","r",stdin) #define debug(s) cout << "s = " << s << endl; #define outstars cout << "*************" << endl; const double PI = acos(-1.0); const double e = exp(1.0); const int inf = 0x3f3f3f3f; const int INF = 0x7fffffff; typedef long long ll; const int maxn = 310; int uN, vN; vector <int> vec[maxn]; int linker[maxn]; bool vis[maxn]; bool dfs(int u) { int cnt = vec[u].size(); for (int i = 0; i < cnt; i ++) { int v = vec[u][i]; if (!vis[v]) { vis[v] = true; if (linker[v] == -1 || dfs(linker[v])) { linker[v] = u; return true; } } } return false; } int hungry() { memset(linker, -1, sizeof(linker)); int ret = 0; for (int i = 1; i <= uN; i ++) { memset(vis, 0, sizeof(vis)); if (dfs(i)) ret ++; } return ret; } int main() { int T, cnt, u, v; scanf("%d", &T); while (T -- ) { scanf("%d %d", &uN, &vN); for (int i = 1; i <= uN; i ++) vec[i].clear(); for (int i = 1; i <= uN; i ++) { scanf("%d", &cnt); for (int j = 1; j <= cnt; j ++) { scanf("%d", &v); vec[i].push_back(v); } } if (uN == hungry()) printf("YES\n"); else printf("NO\n"); } return 0; }
2、Hopcroft-Karp算法
在匈牙利算法中,我们每次寻找一条增广路来增加匹配集合M.可以证明,每次找增广路的复杂度是O(E),一共需要增广O(V)次,因此总时间复杂度为O(VE)。为了降低时间复杂度,在Hopcroft-Karp算法中,我们在增加匹配集合M时,每次DFS寻找多条增广路(不相交).可以证明,这样迭代次数最多为2*V^0.5,所以,时间复杂度就降到了O(V^0.5*E)。
2.1 Hopcroft-Karp算法原理
Hopcroft-Karp算法先使用BFS查找多条增广路,然后使用DFS遍历增广路(累加匹配数,修改匹配点集),循环执行,直到没有增广路为止。
Hopcroft-Karp算法的BFS遍历只对点进行分层(不标记是匹配点和未匹配点),然后用DFS遍历看上面的层次哪些是增广路径(最后一个点是未匹配的)。
BFS过程可以看做是图像树结构一样逐层向下遍历,还要防止出现相交的增广路径。
2.2 Hopcroft-Karp算法步骤
设U和V是图G的二分图,M是从U到V的匹配
(1)使用BFS遍历对图的点进行分层,从X中找出一个未匹配点v,(所有v)组成第一层,接下的层是这样形成的——都是查找匹配点(增广路性质),直到在V中找到未匹配点才终止查找,对X其他未匹配点同样进行查找增广路径(BFS只分层不标记是否匹配点)
(2)使用DFS遍历查找(1)形成的增广路,找到就匹配数就累加1
(3)重复(1)(2)操作直到找不出增广路径为止
2.3 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; }
再给出kuangbin的模板
/* ******************************* * 二分图匹配(Hopcroft-Carp算法) * 复杂度O(sqrt(n)*E) * 邻接表存图,vector实现 * vector先初始化,然后假如边 * uN 为左端的顶点数,使用前赋值(点编号0开始) */ const int MAXN = 3000; const int INF = 0x3f3f3f3f; vector<int>G[MAXN]; int uN; int Mx[MAXN],My[MAXN]; int dx[MAXN],dy[MAXN]; int dis; bool used[MAXN]; bool SearchP() { queue<int>Q; dis = INF; memset(dx,-1,sizeof(dx)); memset(dy,-1,sizeof(dy)); for(int i = 0 ; i < uN; i++) //如果点下标从1开始这个地方要改 if(Mx[i] == -1) { Q.push(i); dx[i] = 0; } while(!Q.empty()) { int u = Q.front(); Q.pop(); if(dx[u] > dis)break; int sz = G[u].size(); for(int i = 0;i < sz;i++) { int v = G[u][i]; if(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) { int sz = G[u].size(); for(int i = 0;i < sz;i++) { int v = G[u][i]; if(!used[v] && dy[v] == dx[u] + 1) { used[v] = true; if(My[v] != -1 && dy[v] == dis)continue; if(My[v] == -1 || DFS(My[v])) { My[v] = u; Mx[u] = v; return true; } } } return false; } int MaxMatch() { int res = 0; memset(Mx,-1,sizeof(Mx)); memset(My,-1,sizeof(My)); while(SearchP()) { memset(used,false,sizeof(used)); for(int i = 0;i < uN;i++) //如果点下标从1开始这个地方要改 if(Mx[i] == -1 && DFS(i)) res++; } return res; }
匈牙利算法和Hopcroft-Karp算法细节的对比
匈牙利算法每次都以一个点查找增广路径,Hopcroft-Karp算法是每次都查找多条增广路径;匈牙利算法每次查找的增广路径的长度是随机的,Hopcroft-Karp算法每趟查找的增广路径的长度只会在原来查找到增广路径的长度增加偶数倍(除了第一趟,第一趟得到增广路径长度都是1)。
to be continued...