1. 程式人生 > >淺談二分圖基本用法及例題推薦

淺談二分圖基本用法及例題推薦

目錄:

前言

本文中,使用mtc表示match,即對應的匹配點。 check表示本輪是否已經被操作。

A 求解最大匹配 匈牙利演算法(Hungarian演算法)

參考文獻:http://www.renfei.org/blog/bipartite-matching.html
具體來說,就是一個找增廣路的過程。
對於一個二分圖,我們想辦法為其左側的每一個點選擇一個右側的點進行匹配。
那麼成功找到一個匹配的方式有兩種:
1.當前搜到節點還未被匹配,直接匹配上即可。
2.當前搜到的節點已被匹配,嘗試讓 當前點的匹配點 去匹配其他點,從而把當前點讓出來用於匹配。
特別注意:對於每一條邊,要連線兩次,連雙向邊!!(左右點的標號不能重複!)

求解的演算法為匈牙利演算法(Hungarian演算法)。他的實現方式有兩種,DFS與BFS。

.

- Hungarian演算法(DFS)

DFS演算法簡單好打,思路清晰,缺點為時間效率較慢。

bool Hungarian(RG int u,RG int vis){
    for(RG int i = head[u];i;i = t[i].next){
        int v = t[i].to;
        if(check[v] != vis){
            check[v] = vis;                         //標記為這一輪已經走過
if(!mtc[v] || Hungarian(mtc[v],vis)){ //未匹配 或者 可以讓步 mtc[v] = u; mtc[u] = v; //匹配 return true; } } }return false; } int main() { freopen("testdate.in","r",stdin); N = gi(); M = gi(); U = gi(); RG int Ans = 0; for
(RG int i = 1; i <= U; i ++){ RG int d = gi(),e = gi()+N; if(d>N || e>M+N)continue; t[++cnt] = (Road){e,head[d]}; head[d] = cnt; t[++cnt] = (Road){d,head[e]}; head[e] = cnt; } for(RG int i = 1; i <= N; i ++)if(!mtc[i])if(Hungarian(i,i))Ans++; printf("%d",Ans); return 0; }
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

.

- Hungarian演算法(BFS)

BFS演算法效率超級高,但是難打,容易打錯。

IL int Hungarian(){
    RG int Ans = 0;
    memset(check,-1,sizeof(check)); memset(mtc,-1,sizeof(mtc));
    for(RG int st = 1; st <= N; st ++){
        if(mtc[st] != -1)continue;                                        //找到左側的一個未匹配點。
        while(!Q.empty())Q.pop();
        pre[st] = -1; Q.push(st);                           // 設 st 為路徑起點
        flag = false;                                       // 尚未找到增廣路
        while(!flag && !Q.empty()){
            RG int u = Q.front(); Q.pop(); //cout<<u<<endl;
            for(RG int i = head[u]; i && !flag;i = t[i].next){
                RG int v = t[i].to;
                if(check[v] == st)continue;         //確保本輪沒搞過 
                check[v] = st; 
                Q.push(mtc[v]);
                if(mtc[v] >= 0){pre[mtc[v]] = u;}   // 此點為匹配點(已經匹配);看看能否讓步
                else{                               // 找到未匹配點,交替路變為增廣路,開始修改 
                    flag = true; RG int d = u , e = v;          //當前的d、e 
                    while(d != -1){
                        RG int tmp = mtc[d];      //d 的原來匹配點可以與 其前驅匹配 
                        mtc[d] = e; mtc[e] = d;
                        d = pre[d]; e = tmp;      //d為 d的前驅 , e為 d的原來匹配 
                    }
                }
            }
        }
        if(mtc[st] != -1)Ans ++;               //st成功新增了匹配。 
    }return Ans;
}

int main()
{
    freopen("testdate.in","r",stdin);
    N = gi(); M = gi(); U = gi(); 
    for(RG int i = 1; i <= U; i ++){
        RG int d = gi(),e = gi()+N; if(d>N || e>M+N)continue;
        t[++cnt] = (Road){e,head[d]}; head[d] = cnt;
        t[++cnt] = (Road){d,head[e]}; head[e] = cnt;
    }
    printf("%d",Hungarian()); return 0;
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

.

- DFS 與 BFS 的效能比較

兩個版本的時間複雜度均為O(V⋅E)
。DFS 的優點是思路清晰、程式碼量少,但是效能不如 BFS。我測試了兩種演算法的效能。對於稀疏圖,BFS 版本明顯快於 DFS 版本;而對於稠密圖兩者則不相上下。在完全隨機資料 9000 個頂點 4,0000 條邊時前者領先後者大約 97.6%,9000 個頂點 100,0000 條邊時前者領先後者 8.6%, 而達到 500,0000 條邊時 BFS 僅領先 0.85%。

- 字典序的相關處理

如果需要匹配呈字典序,那麼我們應該確保連邊為從小到大。
然後逆序,從後往前匹配。 應為這樣的話,可以確保前面的點優先順序最大。
詳細可以見例題:
Lougu P1963 變換序列 https://www.luogu.org/problemnew/show/P1963

B 二分圖 的相關定理

最大匹配數:最大匹配的匹配邊的數目

最小點覆蓋數:選取最少的點,使任意一條邊至少有一個端點被選擇

定理1:最小點覆蓋數 = 最大匹配數(這是 Konig 定理)

最小路徑覆蓋數:對於一個 DAG(有向無環圖),選取最少條路徑,使得每個頂點屬於且僅屬於一條路徑。路徑長可以為 0(即單個點)。

二分圖模型:把所有頂點 i 拆成兩個:X集合中的 i 和Y集合中的 i’。若有邊 i->j,則在二分圖中引入邊 i->j’。

定理2:最小路徑覆蓋數 = 頂點數 - 最大匹配數

最大獨立集:選出一些頂點使得這些頂點兩兩不相鄰,則這些點構成的集合稱為獨立集。找出一個包含頂點數最多的獨立集稱為最大獨立集。

定理3:最大獨立集 = 所有頂點數-最小點覆蓋數

最大團:簡單說,就是選出一些頂點,這些頂點兩兩之間都有邊。最大團就是使得選出的這個頂點集合最大。

定理4二分圖的最大團 = 補圖的最大獨立集
補圖的定義是:對於二分圖中左邊一點x和右邊一點y,若x和y之間有邊,那麼在補圖中沒有,否則有。

C 求解帶權最佳匹配 KM演算法(Kuhn-Munkres演算法)

參考文獻:http://blog.csdn.net/yulin11/article/details/4385207
定義:
可行頂標L[x] :對於任意x∈X,任意y∈Y,有L[x] + L[y] >= W[x][y]成立。
相等子圖: M*={(x,y)|(x,y)∈M,L(x)+L(y)=W(x,y)} ,稱 M*為邊集的M之生成子圖 為 相等子圖

重要定理: M*的完備匹配 即為 M的最佳帶權匹配。

那麼KM演算法就是通過不斷改變L(x),L(y),使得相等子圖的體積不斷擴大,使得其完備匹配更大。
具體來說,流程為:

假設我們匹配{X}與{Y}。 Lx[i],Ly[i]分別為其節點的可行頂標。

1.Memset()
Lx[i] = max(  W[x][i] ,i∈{Y}  ); Ly[i] = 0;

2.KM()
對於每一個節點進行嘗試(Hungarian演算法<建議DFs>):
匹配時記錄S[i],T[i],分別表示{X},{Y}中的點是否到過。
如果匹配成功,跳出即可。  否則進行Modify(),然後再次嘗試。

3.Modify()
求解得 修改值chg = min{W[i][j]};  (i,j)∈{(i,j) | S[i] && !T[j]};
進行修改:if(S[i])Lx[i]-=chg;  if(T[i])Ly[i]+=chg;

4.輸出最大路徑和,略。
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15

.
程式碼:


IL void Memset(){                             //初始化Lx,Ly。 
    for(RG int i = 1; i <= N; i ++){
        Lx[i] = 0;
        for(RG int j = 1; j <= N; j ++)
            Lx[i] = max(Lx[i],W[i][j]);
    }
    for(RG int j = 1; j <= N; j ++)Ly[j] = 0; return;
}

bool Hungarian(RG int x){                     //匈牙利演算法 嘗試匹配。 
    S[x] = true;
    for(RG int j = 1; j <= N; j ++)
        if(!T[j] && W[x][j] == Lx[x] + Ly[j]){       //沒到過並且符合相等子圖條件 
            T[j] = true; 
            if(!mtc[j] || Hungarian(mtc[j])){
                mtc[j] = x;  return true;            //修改(注:mtc[i]為{Y}中的i 所匹配的 {X}中的節點編號) 
            }
        }
    return false;
}

IL void Modify(){                                      //修改Lx、Ly 
    RG long long chg = INF;
    for(RG int i = 1; i <= N; i ++)if(S[i])              //{X}中到過的 
        for(RG int j = 1; j <= N; j ++)if(!T[j])         //{Y}中沒到過的 
           chg = min( chg , Lx[i] + Ly[j] - W[i][j] );    //求解chg 
    for(RG int i = 1; i <= N; i ++){
        if(S[i])Lx[i] = Lx[i] - chg;                   //Lx[i] -= chg; 
        if(T[i])Ly[i] = Ly[i] + chg;                   //Ly[i] += chg; 
    }return;
}

IL void KM(){
    for(RG int i = 1; i <= N; i ++){
        for(;;){                                       //不斷嘗試 
            for(RG int j = 1; j <= N; j ++)S[j] = T[j] = false;   //清零訪問陣列 
            if(Hungarian(i))break;  else Modify();            //匹配與調整 
        }
    }return;
}

IL long long GetSum(){                               //求解路徑和。 
    RG long long Sum = 0;
    for(RG int i = 1; i <= N; i ++)
        if(mtc[i]!=0)Sum += W[mtc[i]][i];            
    return Sum;
}

int main()
{
    freopen("testdate.in","r",stdin);
    N = gi();                                     //設{X},{Y}大小都為 N 
    for(RG int i = 1; i <= N; i ++)
        for(RG int j = 1; j <= N; j ++)
            W[i][j] = gi();                       //X -> Y的距離 
    Memset(); KM(); cout<<GetSum();  
    return 0;

}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47
  • 48
  • 49
  • 50
  • 51
  • 52
  • 53
  • 54
  • 55
  • 56
  • 57
  • 58
  • 59
  • 60

D 穩定婚姻問題 Gale-Shapley演算法

a 問題的引出:

__有N男N女,每個人都按照他對異性的喜歡程度排名。現在需要寫出一個演算法安排這N個男的、N個女的結婚,要求兩個人的婚姻應該是穩定的。
__何為穩定?
__有兩對夫妻M1 F2,M2 F1。M1心目中更喜歡F1,但是他和F2結婚了,M2心目中更喜歡F2,但是命運卻讓他和F1結婚了,顯然這樣的婚姻是不穩定的,隨時都可能發生M1和F1私奔或者M2和F2私奔的情況。所以在做出匹配選擇的時候(也就是結婚的時候),我們需要做出穩定的選擇,以防這種情況的發生。

b 演算法步驟描述:

__第一輪,每個男人都選擇自己名單上排在首位的女人,並向她表白。這種時候會出現兩種情況:
(1)該女士還沒有被男生追求過,則該女士接受該男生的請求。
(2)若該女生已經接受過其他男生的追求,那麼該女生會將該男士與她的現任男友進行比較,若更喜歡她的男友,那麼拒絕這個人的追求,否則,拋棄其男友……
__第一輪結束後,有些男人已經有女朋友了,有些男人仍然是單身。
__在第二輪追女行動中,每個單身男都從所有還沒拒絕過他的女孩中選出自己最中意的那一個,並向她表白,不管她現在是否是單身。
__這種時候還是會遇到上面所說的兩種情況,還是同樣的解決方案。直到所有人都不在是單身。

習題練習

A 部分習題 —(Hungarian演算法):

B 部分習題 —(二分圖相關定理):

C 部分習題 —(KM演算法):