一、
二分圖基礎:
參考鏈接:https://blog.csdn.net/jeryjeryjery/article/details/79596922
https://www.cnblogs.com/penseur/archive/2013/06/16/3138981.html
什么叫二分圖:給你一些點,其中這些點之間的某兩個點是相連的。如果你可以把全部點分成兩個集合 且 每個集合的任意兩個點之間沒有連線
也就是說對於任意一條邊它的兩個點不能來自於一個集合,就比如:
1,2,3一個集合,其他點一個集合就是二分圖!
染色法判斷是否為一個二分圖:https://blog.csdn.net/li13168690086/article/details/81506044
算法過程:用兩種顏色,對所有頂點逐個染色,且相鄰頂點染不同的顏色,如果發現相鄰頂點染了同一種顏色,就認為此圖不為二分圖。 當所有頂點都 被染色,且沒有發現同色的相鄰頂點,就退出。
以NYOJ 1015 二部圖為例題:
代碼:

1 #include <iostream> 2 #include <algorithm> 3 #include <string.h> 4 #include <stdio.h> 5 #include <math.h> 6 using namespace std; 7 const int N = 505; 8 int m,n; 9 int color[N]; 10 int edge[N][N]; 11 bool dfs(int v, int c){ 12 color[v] = c; //將當前頂點塗色 13 for(int i = 0; i < n; i++){ //遍歷所有相鄰頂點,即連着的點 14 if(edge[v][i] == 1){ //如果頂點存在 15 if(color[i] == c) //如果顏色重復,就返回false 16 return false; 17 if(color[i] == 0 && !dfs(i,-c)) //如果還未塗色,就染上相反的顏色-c,並dfs這個頂點,進入下一層 18 return false; //返回false 19 } 20 } 21 return true; //如果所有頂點塗完色,並且沒有出現同色的相鄰頂點,就返回true 22 } 23 void solve(){ 24 for(int i = 0; i < n; i++){ 25 if(color[i] == 0){ 26 if(!dfs(i, 1)){ 27 printf("NOT BICOLORABLE.\n"); 28 return; 29 } 30 } 31 } 32 printf("BICOLORABLE.\n"); 33 } 34 int main(){ 35 int u,v; 36 while(cin >> n >> m){ 37 memset(color, 0, sizeof(color)); 38 memset(edge, 0, sizeof(edge)); 39 for(int i = 0; i < m; i++){ 40 cin >> u >> v; //因為是無向圖,所以要往兩個方向添加邊 41 edge[u][v] = 1; //正着添加 42 edge[v][u] = 1; //反着添加 43 } 44 solve(); 45 } 46 return 0; 47 }
什么叫匹配:圖中匹配的定義是指,這個圖的一個邊的集合,集合中任意兩條邊都沒有公共的頂點,則稱這個邊集為一個匹配。我們稱匹配中的邊匹 配邊,邊中的點為匹配點;未在匹配中的邊為非匹配邊,邊中的點為未匹配點。就比如:
這就是一個匹配,每一個點只能使用一次,但是它並不是最大匹配,因為我們可以讓2和8相連,這樣就會多一條匹配邊3->5
要注意所有匹配邊都是原來圖中存在的邊,只是我們選擇了這些邊中的某幾條
最大匹配: 一個圖中所有匹配中,所含匹配邊數最大的匹配稱為最大匹配。
完美匹配: 如果一個圖的某個匹配中,圖的所有頂點都是匹配點,那么這個匹配就是完美匹配。很顯然,完美匹配一定是最大匹配,但是並不是所有的 圖都存在完美匹配。
交替路: 從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊、匹配邊…,形成這樣的交替進行的路徑成為交替路。
什么是增廣路: 從一個未匹配點出發,走交替路,如果途徑一個未匹配點(出發點不算),則這樣一條交替路稱為增廣路。增廣路有一個重要的特性,就 是非匹配邊要比匹配邊多一條(從未匹配點出發,以未匹配點結束,走交替路,顯然非匹配邊多一條),此時,我們可以讓增廣路中的匹 配邊和非匹配邊調換位置,匹配邊變為非匹配邊,非匹配邊變為匹配邊,這樣匹配邊的數量就會加1,並且由於中間的匹配節點不存在其 他相連的匹配邊,所以這樣做不會破壞匹配的性質,保證了交換的正確性。
匈牙利算法: 算法就是根據增廣路的思想,以一個未匹配的節點出發,遍歷圖,不斷的尋找增廣路來擴充匹配的邊數,直到不能擴充為止。根據遍歷圖的 方式不同,匈牙利算法可以分為dfs(深度遍歷)和bfs(廣度遍歷)的實現。
匈利牙利算法復雜度:這個問題既可以利用最大流算法解決也可以用匈牙利算法解決。如果用最大流算法中的Edmonds-karp算法解決,因為時間復雜度 為O(n*m*m),n為點數,m為邊數,會超時,利用匈牙利算法,時間復雜度為O(n*m),時間復雜度小,不會超時。
以HDU - 1083 Courses為例子:
題意:給你p個課程和n個學生,你可不可以從這k個學生中找出來p個學生,使得每一個學生負責一個課程。后面會給出課程和上這個課的學生
只要這個學生上這個課就可以負責這個課程
代碼:

1 #include<stdio.h> 2 #include<algorithm> 3 #include<string.h> 4 #include<iostream> 5 #include<queue> 6 using namespace std; 7 const int maxn=305; 8 int match[maxn],visit[maxn],n,m,grap[maxn][maxn]; 9 int dfs_solve(int x) 10 { 11 for(int i=1;i<=n;++i) 12 { 13 if(grap[x][i] && !visit[i]) 14 { 15 visit[i]=1; 16 if(match[i]==0 || dfs_solve(match[i])) 17 { 18 match[i]=x; 19 return 1; 20 } 21 } 22 } 23 return 0; 24 } 25 int hungran() 26 { 27 memset(match,0,sizeof(match)); 28 int sum=0; 29 for(int i=1;i<=m;++i) 30 { 31 memset(visit,0,sizeof(visit)); 32 sum+=dfs_solve(i); 33 } 34 return sum; 35 } 36 int main() 37 { 38 int t; 39 scanf("%d",&t); 40 while(t--) 41 { 42 scanf("%d%d",&m,&n); 43 memset(grap,0,sizeof(grap)); 44 for(int i=1;i<=m;++i) 45 { 46 int q; 47 scanf("%d",&q); 48 while(q--) 49 { 50 int w; 51 scanf("%d",&w); 52 grap[i][w]=1; 53 } 54 } 55 int ans=hungran(); 56 if(ans==m) 57 { 58 printf("YES\n"); 59 } 60 else printf("NO\n"); 61 } 62 return 0; 63 }
Hopcroft-Karp算法:這個算法的時間復雜度為O(n^(1/2)*m)。該算法是對匈牙利算法的優化。利用匈牙利算法一次只能找到一條增廣路徑,Hopcroft-K arp就提出一次找到多條不相交的增廣路徑(不相交就是沒有公共點和公共邊的增廣路徑),然后根據這些增廣路徑添加多個匹配。 說白了,就是批量處理!
具體過程看下圖:
還是以上一道題為例子:

1 #include<iostream> 2 #include<queue> 3 using namespace std; 4 const int MAXN=500;// 最大點數 5 const int INF=1<<28;// 距離初始值 6 int bmap[MAXN][MAXN];//二分圖 7 8 int cx[MAXN];//cx[i]表示左集合i頂點所匹配的右集合的頂點序號 9 int cy[MAXN]; //cy[i]表示右集合i頂點所匹配的左集合的頂點序號 10 11 int nx,ny; 12 int dx[MAXN]; 13 int dy[MAXN]; 14 int dis; 15 bool bmask[MAXN]; 16 //尋找 增廣路徑集 17 bool searchpath() 18 { 19 queue<int>Q; 20 dis=INF; 21 memset(dx,-1,sizeof(dx)); 22 memset(dy,-1,sizeof(dy)); 23 for(int i=1;i<=nx;i++) 24 { 25 //cx[i]表示左集合i頂點所匹配的右集合的頂點序號 26 if(cx[i]==-1) 27 { 28 //將未遍歷的節點 入隊 並初始化次節點距離為0 29 Q.push(i); 30 dx[i]=0; 31 } 32 } 33 //廣度搜索增廣路徑 34 while(!Q.empty()) 35 { 36 int u=Q.front(); 37 Q.pop(); 38 if(dx[u]>dis) break; 39 //取右側節點 40 for(int v=1;v<=ny;v++) 41 { 42 //右側節點的增廣路徑的距離 43 if(bmap[u][v]&&dy[v]==-1) 44 { 45 dy[v]=dx[u]+1; //v對應的距離 為u對應距離加1 46 if(cy[v]==-1) dis=dy[v]; 47 else 48 { 49 dx[cy[v]]=dy[v]+1; 50 Q.push(cy[v]); 51 } 52 } 53 } 54 } 55 return dis!=INF; 56 } 57 58 //尋找路徑 深度搜索 59 int findpath(int u) 60 { 61 for(int v=1;v<=ny;v++) 62 { 63 //如果該點沒有被遍歷過 並且距離為上一節點+1 64 if(!bmask[v]&&bmap[u][v]&&dy[v]==dx[u]+1) 65 { 66 //對該點染色 67 bmask[v]=1; 68 if(cy[v]!=-1&&dy[v]==dis) 69 { 70 continue; 71 } 72 if(cy[v]==-1||findpath(cy[v])) 73 { 74 cy[v]=u;cx[u]=v; 75 return 1; 76 } 77 } 78 } 79 return 0; 80 } 81 82 //得到最大匹配的數目 83 int MaxMatch() 84 { 85 int res=0; 86 memset(cx,-1,sizeof(cx)); 87 memset(cy,-1,sizeof(cy)); 88 while(searchpath()) 89 { 90 memset(bmask,0,sizeof(bmask)); 91 for(int i=1;i<=nx;i++) 92 { 93 if(cx[i]==-1) 94 { 95 res+=findpath(i); 96 } 97 } 98 } 99 return res; 100 } 101 102 103 int main() 104 { 105 int num; 106 scanf("%d",&num); 107 while(num--) 108 { 109 110 memset(bmap,0,sizeof(bmap)); 111 scanf("%d%d",&nx,&ny); 112 for(int i=1;i<=nx;i++) 113 { 114 int snum; 115 scanf("%d",&snum); 116 int u; 117 for(int j=1;j<=snum;j++) 118 { 119 scanf("%d",&u); 120 bmap[i][u]=1; 121 // bmap[u][i]=1; 122 } 123 } 124 // cout<<MaxMatch()<<endl; 125 if(MaxMatch()==nx) 126 { 127 printf("YES\n"); 128 } 129 else 130 { 131 printf("NO\n"); 132 } 133 } 134 //system("pause"); 135 return 0; 136 } 137 138 /* 139 140 4 141 1 3 142 1 3 4 143 2 144 145 146 */
二、
二分圖的使用:
1、最小頂點覆蓋:
在圖中所有的點中找出來最小的點集合,使得圖中的每一條邊的兩個頂點至少 有一個在這個點集中
結論:最小頂點覆蓋 == 最大匹配
證明:假設當前存在一條兩個端點都不在最小頂點覆蓋點集中,那么這么這條邊一定可以增大最大匹配邊集,與最大匹配矛盾。
2、最小路徑覆蓋:在圖中找一個最小的邊集合,使得圖中的每一個點都可以在這個邊集合的斷點處找到。
結論:最小路徑覆蓋 == 頂點數 - 最大匹配
證明:因為一條邊最多可以包含兩個頂點,所以我們選邊的時候讓這樣的邊盡量 多,也就是說最大匹配的邊集合中邊的數目。剩下的點就 只能一個邊連上一 個點到集合里。
注意:最大匹配得到的是邊的個數,(2*最大匹配)才是點的個數。所以(頂點數 - 最大匹配)得到的是(最大匹配后剩余的點)和(最大匹 配的邊)
3、最大獨立集:在N個點中選出來一個最大點集合,使這個點集合中的任意兩點之間都沒有邊。
結論:最大獨立集 == 頂點數 - 最大匹配
證明:因為去掉最大匹配兩端的頂點去掉以后,剩下的點肯定是獨立集。我們再從每個匹配里面挑選出來一個點加入到獨立集中,也是不會破 壞原有獨立集的獨立性的。