1. 程式人生 > >二分圖大合集——二分圖最大匹配(最小覆蓋數),完美匹配以及最優匹配(帶權最大匹配)

二分圖大合集——二分圖最大匹配(最小覆蓋數),完美匹配以及最優匹配(帶權最大匹配)

二分圖:

定義:二分圖又稱作二部圖,是圖論的一種特殊模型。設G=(V, E)是一個無向圖,如果頂點V可分割為兩個互不相交的子集(A , B),且圖中的每條邊(i, j)所關聯的兩個定點分別屬於這兩個不同的頂點集(i in A, j in B),則稱圖G為一個二分圖。

簡單的說,一個圖被分成了兩部分,相同的部分沒有邊,那這個圖就是二分圖,二分圖是特殊的圖。(不含奇環)
由定義可知,二分圖沒有自迴路(關聯於同一結點的一條邊);零圖,平凡圖(指僅有一個結點的圖)可以看成是特殊的二分圖

判斷是否為二分圖的方法:
①定義法
關鍵是看點集是否能分成兩個獨立的點集。
這裡寫圖片描述
上圖中U和V構造的點集所形成的迴圈圈不為奇數,所以是二分圖。
這裡寫圖片描述


上圖中U和V和W構造的點集所形成的的迴圈圈為奇數,所以不是二分圖。

②定理:無向圖 G= < V, E >為二分圖的充要條件是G的所有迴路的長度均為偶數。

#include<bits/stdc++.h>
using namespace std;

const int M = 10050;
int color[M], line[M][M];

//0為白,1為黑
bool bfs(int s, int n){
    int i, beg;
    queue <int> q;
    q.push(s);
    color[s] = 1;
    while
(!q.empty()){ beg = q.front(); q.pop(); for(i = 1; i <= n; i++){ if(line[beg][i] && color[i] == 0){ q.push(i); color[i] = !color[beg];//染成不同顏色 } if(line[beg][i] && color[beg] == color[i]){//顏色相同則不是二分圖
return 0; } } } return 1; } int main(){ int i, j, n, m, a, b;//n表示有多少個點,m表示有多少條邊 bool flag = 1;//初始化無向圖是二分圖 memset(color, 0, sizeof(color)); scanf("%d%d", &n, &m); for(i = 1; i <= m; i++) { cin >> a >> b; line[a][b] = line[b][a] = 1;//儲存無向圖的邊 } for(i = 1; i <= n; i++) if(color[i] == 0 && !bfs(i, n)) {//對每個連通分支染色,如果兩個相鄰的點顏色相同,則不是二分圖。 flag = 0; break; } if(flag) cout << "YES" <<endl; else cout << "NO" <<endl; return 0; }

匹配:

  1. 定義:給定一個二分圖G,在G的一個子圖M中,M的邊集{E}中的任意兩條邊都不依附於同一個頂點,則稱M是一個匹配。
    匹配點:匹配邊上的兩點

  2. 極大匹配(Maximal Matching):是指在當前已完成的匹配下,無法再通過增加未完成匹配的邊的方式來增加匹配的邊數。

  3. 最大匹配(maximum matching):是所有極大匹配當中邊數最大的一個匹配,設為M。選擇這樣的邊數最大的子集稱為圖的最大匹配問題。

  4. 完美匹配(完備匹配):一個圖中所有的頂點都是匹配點的匹配,即2|M| = |V|。完美匹配一定是最大匹配,但並非每個圖都存在完美匹配。

  5. 最優匹配:最優匹配又稱為帶權最大匹配,是指在帶有權值邊的二分圖中,求一個匹配使得匹配邊上的權值和最大。一般X和Y集合頂點個數相同,最優匹配也是一個完備匹配,即每個頂點都被匹配。如果個數不相等,可以通過補點加0邊實現轉化。一般使用KM演算法解決該問題。(KM(Kuhn and Munkres)演算法,是對匈牙利演算法的一種貪心擴充套件。)

  6. 最小覆蓋
    二分圖的最小覆蓋分為最小頂點覆蓋和最小路徑覆蓋:

    ①最小頂點覆蓋是指最少的頂點數使得二分圖G中的每條邊都至少與其中一個點相關聯
    注:二分圖的最小頂點覆蓋數=二分圖的最大匹配數

    ②最小路徑覆蓋也稱為最小邊覆蓋,是指用盡量少的不相交簡單路徑覆蓋二分圖中的所有頂點。
    注:二分圖的最小路徑覆蓋數=|V|-二分圖的最大匹配數

  7. 最大獨立集
    最大獨立集是指尋找一個點集,使得其中任意兩點在圖中無對應邊。對於一般圖來說,最大獨立集是一個NP完全問題,對於二分圖來說最大獨立集=|V|-二分圖的最大匹配數。最大獨立集S 與 最小覆蓋集T 互補

如果在圖G左右兩邊加上源匯點後,圖G等價於一個網路流,二分圖最大匹配問題可以轉為最大流的問題。解決此問題的匈牙利演算法的本質就是尋找最大流的增廣路徑。
注意:匈牙利演算法,除了二分圖多重匹配外在二分圖匹配中都可以使用。
二分圖匹配中還有一個hk演算法,複雜度為o(sqrt(n)*e)複雜度降低較低,程式碼量飆升,不過hdu2389卡這個演算法

交替路:從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊…形成的路徑叫交替路。

增廣路:從一個未匹配點出發,走交替路,如果途徑另一個未匹配點(出發的點不算),則這條交替路稱為增廣路(agumenting path)。

增廣路有一個重要特點:非匹配邊比匹配邊多一條。因此,研究增廣路的意義是改進匹配。只要把增廣路中的匹配邊和非匹配邊的身份交換即可。由於中間的匹配節點不存在其他相連的匹配邊,所以這樣做不會破壞匹配的性質。交換後,圖中的匹配邊數目比原來多了 1 條。

我們可以通過不停地找增廣路來增加匹配中的匹配邊和匹配點。找不到增廣路時,達到最大匹配(這是增廣路定理)。匈牙利演算法正是這麼做的。

匈牙利演算法講解:

初始時最大匹配為空
while 找得到增廣路徑
do 把增廣路徑加入到最大匹配中去

(注:匈牙利演算法雖然根本上是最大流演算法,但是它不需要建網路模型,所以圖中不再需要源點和匯點,僅僅是一個二分圖。每條邊也不需要有方向。)

匈牙利樹一般由 BFS 構造(類似於 BFS 樹)。從一個未匹配點出發運行 BFS(唯一的限制是,必須走交替路),直到不能再擴充套件為止。
注:匈牙利樹中所有葉子節點均為匹配點

匈牙利演算法是由匈牙利數學家Edmonds於1965年提出,因而得名。匈牙利演算法是基於Hall定理中充分性證明的思想,它是部圖匹配最常見的演算法,該演算法的核心就是尋找增廣路徑,它是一種用增廣路徑求二分圖最大匹配的演算法。

——-等等,看得頭大?那麼請看下面的版本:

通過數代人的努力,你終於趕上了剩男剩女的大潮,假設你是一位光榮的新世紀媒人,在你的手上有N個剩男,M個剩女,每個人都可能對多名異性有好感(驚訝-_-||暫時不考慮特殊的性取向),如果一對男女互有好感,那麼你就可以把這一對撮合在一起,現在讓我們無視掉所有的單相思(好憂傷的感覺快哭了),你擁有的大概就是下面這樣一張關係圖,每一條連線都表示互有好感。
這裡寫圖片描述
本著救人一命,勝造七級浮屠的原則,你想要儘可能地撮合更多的情侶,匈牙利演算法的工作模式會教你這樣做:

===============================================================================

一: 先試著給1號男生找妹子,發現第一個和他相連的1號女生還名花無主,got it,連上一條藍線
這裡寫圖片描述

===============================================================================

二:接著給2號男生找妹子,發現第一個和他相連的2號女生名花無主,got it

這裡寫圖片描述

三:接下來是3號男生,很遺憾1號女生已經有主了,怎麼辦呢?

我們試著給之前1號女生匹配的男生(也就是1號男生)另外分配一個妹子。

(黃色表示這條邊被臨時拆掉)
這裡寫圖片描述
與1號男生相連的第二個女生是2號女生,但是2號女生也有主了,怎麼辦呢?我們再試著給2號女生的原配(發火發火)重新找個妹子(注意這個步驟和上面是一樣的,這是一個遞迴的過程)
這裡寫圖片描述
此時發現2號男生還能找到3號女生,那麼之前的問題迎刃而解了,回溯回去

2號男生可以找3號妹子~~ 1號男生可以找2號妹子了~~~ 3號男生可以找1號妹子
這裡寫圖片描述這裡寫圖片描述這裡寫圖片描述
所以第三步最後的結果就是:

這裡寫圖片描述

四: 接下來是4號男生,很遺憾,按照第三步的節奏我們沒法給4號男生騰出來一個妹子,我們實在是無能為力了……香吉士同學走好。

===============================================================================
這就是匈牙利演算法的流程,其中找妹子是個遞迴的過程,最最關鍵的字就是“騰”字

其原則大概是:有機會上,沒機會創造機會也要上

二分圖最大匹配/最小覆蓋數之dfs實現

#include<bits/stdc++.h>
using namespace std;

typedef long long ll;
const int inf=0x3f3f3f3f;
const int maxn = 10005;
int edge[maxn][maxn];//edge[i][j] == 1 代表i,j可以匹配
int vis[maxn];//用來記錄該點是否被訪問過
int cx[maxn], cy[maxn];//用來記錄x集合中匹配的y元素是哪個
int nx, ny;//nx表示x集合的頂點個數, ny表示y集合的頂點個數

int line(int u){
    int v;
    for(v = 0; v < ny; v++){
        if(edge[u][v] && !vis[v]){
            vis[v] = 1;
            if(cy[v] == -1 || line(cy[v])){//如果y集合中的v沒有匹配或v已經匹配,但從cy[v]中可以找到一條增廣路
                cx[u] = v;//找到增廣路,修改匹配
                cy[v] = u;
                return 1;
            }
        }
    }
    return 0;
}

int maxmatch(){
    int sum = 0, i;
    memset(cx, 0xff, sizeof(cx));//初始值為-1表示兩個集合中都沒有匹配的元素
    memset(cy, 0xff, sizeof(cy));
    for(i = 0; i <= nx; i++){
        if(cx[i] == -1){//還沒被匹配就執行內部程式碼
            memset(vis, 0, sizeof(vis));//重置標記為未訪問
            sum += line(i);//以 i 為起點開始查詢增廣路,返回true ,匹配數+1
        }
    }
    return sum;
}
int main(){
    int x, y, t;
    while(scanf("%d", &t) && t){
        scanf("%d%d", &nx, &ny);
        memset(edge, 0, sizeof(edge));
        for(int i = 0; i < t; i++){
            scanf("%d%d", &x, &y);
            edge[x][y] = 1;
        }
        int sum = maxmatch();
        printf("%d\n", sum);
    }
    return 0;
}

Kuhn-Munkers演算法流程

(1)初始化可行頂標的值
(2)用匈牙利演算法尋找完備匹配
(3)若未找到完備匹配則修改可行頂標的值
(4)重複(2)(3)直到找到相等子圖的完備匹配為止

#include<bits/stdc++.h>
using namespace std;

const int MAXN = 305;
vector<int> mymap[MAXN];
int N, M, mymatch[MAXN];
bool visited[MAXN];

void init() {
    for(int i = 0; i < N; i++) {
        mymap[i].clear();
    }
}

bool dfs(int k) {
    int t, I;
    for(int i = 0; i < (int)mymap[k].size(); i++) {
        I = mymap[k][i];
        if(!visited[I]) {
            visited[I] = true;
            t = mymatch[I];
            mymatch[I] = k;
            if(t == -1 || dfs(t)) {
                return true;
            }
            mymatch[I] = t;
        }
    }
    return false;
}

int hungary () {
    memset(mymatch, -1, sizeof(mymatch));
    int ans = 0;
    for (int i = 0; i < N; i++) {
        memset(visited, false, sizeof(visited));
        if (dfs(i)) {
            ans++;
        }
    }
    return ans;
}

bool buildgraph() {
    int t, K;
    init();
    for (int i = 0; i < N; i++) {
        scanf("%d", &K);
        for(int k = 0; k < K; k++) {
            scanf("%d", &t);
            mymap[i].push_back(t - 1);
        }
    }
    return true;
}

int main() {
    int T, P;
    scanf("%d", &T);
    while(T--) {
        scanf("%d%d", &N, &P);
        buildgraph();
        puts(hungary() == N ? "YES" : "NO");
    }
    return 0;
}
 #include <iostream>  
#include <cstdio>  
#include <memory.h>  
#include <algorithm>   

using namespace std;  

#define MAX 100  

int n;  
int weight[MAX][MAX];           //權重  
int lx[MAX],ly[MAX];                //定點標號  
bool sx[MAX],sy[MAX];          //記錄尋找增廣路時點集x,y裡的點是否搜尋過  
int match[MAX];                       //match[i]記錄y[i]與x[match[i]]相對應  

bool search_path(int u) {          //給x[u]找匹配,這個過程和匈牙利匹配是一樣的  
    sx[u]=true;  
    for(int v=0; v<n; v++){  
        if(!sy[v] &&lx[u]+ly[v] == weight[u][v]){  
            sy[v]=true;  
            if(match[v]==-1 || search_path(match[v])){  
                match[v]=u;  
                return true;  
            }  
        }  
    }  
    return false;  
}  

int Kuhn_Munkras(bool max_weight){  
    if(!max_weight){ //如果求最小匹配,則要將邊權取反  
        for(int i=0;i<n;i++)  
            for(int j=0;j<n;j++)  
                weight[i][j]=-weight[i][j];  
    }  
    //初始化頂標,lx[i]設定為max(weight[i][j] | j=0,..,n-1 ), ly[i]=0;  
    for(int i=0;i<n;i++){  
        ly[i]=0;  
        lx[i]=-0x7fffffff;  
        for(int j=0;j<n;j++)  
            if(lx[i]<weight[i][j])  
                lx[i]=weight[i][j];  
    }  

    memset(match,-1,sizeof(match));  
    //不斷修改頂標,直到找到完備匹配或完美匹配  
    for(int u=0;u<n;u++){   //為x裡的每一個點找匹配  
        while(1){  
            memset(sx,0,sizeof(sx));  
            memset(sy,0,sizeof(sy));  
            if(search_path(u))       //x[u]在相等子圖找到了匹配,繼續為下一個點找匹配  
                break;  
            //如果在相等子圖裡沒有找到匹配,就修改頂標,直到找到匹配為止  
            //首先找到修改頂標時的增量inc, min(lx[i] + ly [i] - weight[i][j],inc);,lx[i]為搜尋過的點,ly[i]是未搜尋過的點,因為現在是要給u找匹配,所以只需要修改找的過程中搜索過的點,增加有可能對u有幫助的邊  
            int inc=0x7fffffff;  
            for(int i=0;i<n;i++)  
                if(sx[i])  
                    for(int j=0;j<n;j++)  
                        if(!sy[j]&&((lx[i] + ly [j] - weight[i][j] )<inc))  
                            inc = lx[i] + ly [j] - weight[i][j] ;  
            //找到增量後修改頂標,因為sx[i]與sy[j]都為真,則必然符合lx[i] + ly [j] =weight[i][j],然後將lx[i]減inc,ly[j]加inc不會改變等式,但是原來lx[i] + ly [j] !=weight[i][j]即sx[i]與sy[j]最多一個為真,lx[i] + ly [j] 就會發生改變,從而符合等式,邊也就加入到相等子圖中  
            if(inc==0)  cout<<"fuck!"<<endl;  
            for(int i=0;i<n;i++){  
                if(sx[i])   //  
                    lx[i]-=inc;  
                if(sy[i])  
                    ly[i]+=inc;  
            }  
        }  

    }  
    int sum=0;  
    for(int i=0;i<n;i++)  
        if(match[i]>=0)  
            sum+=weight[match[i]][i];  

    if(!max_weight)  
        sum=-sum;  
    return sum;  


}  
int main(){  

    scanf("%d",&n);  
    for(int i=0;i<n;i++)  
        for(int j=0;j<n;j++)  
            scanf("%d",&weight[i][j]);  
    printf("%d\n",Kuhn_Munkras(1));  
    system("pause");  
    return 0;  
}

Kuhn-Munkers演算法的幾種變形應用

1.要求最小權完備匹配只需將所有的邊權值取其相反數,求最大權完備匹配,匹配的值再取相反數即可。

2.Kuhn-Munkers演算法的執行要求是必須存在一個完備匹配,如果求一個最大權匹配(不一定完備)可以把不存在的邊權值賦為0。

3.若要邊權之積最大則每條邊權取自然對數,然後求最大和權匹配,求得的結果a再算出e^a就是最大積匹配。

學的有點噁心。。。到了完全匹配那裡已經不想學了。。。先這樣半原創半轉載的寫吧。。。讓我先緩兩天~