「算法筆記」二分圖匹配 入門


可能會有鍋 QwQ。相關內容:「算法筆記」霍爾定理(判斷二分圖是否存在完美匹配)。

一、相關定義

二分圖:如果一個圖的頂點能夠被分為兩個集合 X,Y,滿足每一個集合內部都沒有邊相連,那么這張圖被稱作是一張二分圖。

匹配:在圖論中,一個匹配(matching)是一個邊的集合,其中任意兩條邊都沒有公共頂點。

對於一組匹配 S(S 是一個邊集),屬於 S 的邊被稱為“匹配邊”,匹配邊的端點被稱為“匹配點”。剩余的邊或點被稱為“非匹配邊”和“非匹配點”。

最大匹配:一個圖所有匹配中,所含匹配邊數最多的匹配。

完美匹配:如果一個圖的某組匹配中,圖中所有的頂點都是匹配點(顯然同時也符合最大匹配),那么它就是一個完美匹配。

二、最大匹配——匈牙利算法

1. 基本思想

在進行匈牙利算法之前,我們先做兩個比較重要的定義:

交替路:從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊、……形成的路徑叫交替路。(換言之,交替路是圖的一條簡單路徑,滿足任意相鄰的兩條邊,一條在匹配內,一條不在匹配內。)

增廣路:從一個未匹配點出發,依次經過非匹配邊、匹配邊、非匹配邊、……、非匹配邊,最后到達一個未匹配點形成的路徑叫增廣路。(換句話說,增廣路是一個始點與終點都為非匹配邊的交錯路。)

注意到,一旦我們找出了一條增廣路,將這條路徑上所有匹配邊和非匹配邊取反,就可以讓匹配邊數 +1,即可以得到一個更大的匹配。進一步可以得到推論:二分圖的一組匹配 S 是最大匹配,當且僅當圖中不存在 S 的增廣路。匈牙利算法就是基於這個原理。

假設我們已經得到了一個匹配,希望找到一個更大的匹配。我們從一個未匹配點出發進行 dfs,如果找出了一個增廣路,就代表增廣成功,我們找到了一個更大的匹配。如果增廣失敗,可以證明此時就是最大匹配。

2. 具體實現

匈牙利算法的主要過程:

  1. 設 S=∅,即所有邊都是非匹配邊。
  2. 找到一條增廣路,把路徑上所有邊的匹配狀態取反,得到一個更大的匹配 S'。
  3. 重復第 2 步,直至圖中不存在增廣路。

該算法的關鍵在於如何找到一條增廣路。具體實現如下:

從二分圖的 X 部中取出一個為未匹配的頂點 u 開始,找一個未訪問的鄰接點 v(v 一定屬於 Y 部的頂點)。對於 v,分兩種情況:

  • 若 v 是未匹配點,此時無向邊 (u,v) 本身就是非匹配邊,自己構成一條長度為 1 的增廣路。
  • 若 v 是已匹配點,則取出 v 的匹配頂點 w(w 一定是 X 部的頂點),邊 (w,v) 目前是匹配邊,根據“取反”的想法,要將 (w,v) 改為未匹配,(u,v) 改為匹配,條件為以 w 為起點可以新找到一條增廣路徑 P',那么 u→v→P' 就是一條以 u 為起點的增廣路徑。

代碼片段:

bool find(int x){     //mp 存儲匹配邊 
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;    //如果訪問過就跳過 
        vis[y]=1;    //打上訪問標記 
        if(!mp[y]||find(mp[y])){mp[y]=x;return 1;}    //如果 v 是非匹配邊或從 v 出發能夠找到增廣路,則設置為匹配邊 
    }
    return 0;    //找不到增廣路了,返回 0 
} 

匈牙利算法模板題:HDU 2063 過山車(求二分圖的最大匹配數)

#include<bits/stdc++.h>
#define int long long
#define MEM(x,y) memset(x,y,sizeof(x))
using namespace std;
const int N=1e3+5;
int k,n,m,x,y,cnt,hd[N],to[N<<1],nxt[N<<1],mp[N],ans;
bool vis[N];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
bool find(int x){
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;
        vis[y]=1;
        if(!mp[y]||find(mp[y])){mp[y]=x;return 1;}
    }
    return 0;
} 
signed main(){
    while(~scanf("%lld",&k)&&k){
        scanf("%lld%lld",&n,&m),cnt=0,ans=0,MEM(hd,0),MEM(mp,0);
        for(int i=1;i<=k;i++)
            scanf("%lld%lld",&x,&y),add(x,y);
        for(int i=1;i<=n;i++){
            MEM(vis,0);
            if(find(i)) ans++;
        }
        printf("%lld\n",ans);
    }
    return 0;
}

 三、二分圖最大權匹配——KM 算法

現在我們把所有的邊都帶上權值,希望求出所有最大匹配中權值之和最大的匹配。

我們的思路是給每一個點賦一個“期望值”,也叫作頂標函數 c,對於邊 (u,v),只有 c(u)+c(v)=w(u,v) 的時候,才能被使用。

容易發現,此時的答案就是 c(i) 之和。

初始,我們令左邊所有點的 c(u)=maxv w(u,v),也就是說最理想的情況下,每一個點都被權值最大的出邊匹配。

接下來開始增廣,每次只找符合要求的邊。我們定義只走這些邊訪問到的子圖為相等子圖。

如果能夠找到增廣路就直接增廣,否則,就把這次增廣訪問到的左邊的所有點的 c-1,右邊所有點的 c+1。

經過這樣一通操作,我們發現原來的匹配每一條邊仍然滿足條件。同時由於訪問到的點左邊比右邊多一個(其余的都匹配上了),所以這樣會導致總的權值 -1。

接下來再嘗試進行增廣,重復上述過程。直接這樣做時間復雜度是 O(n3c) 的。(進行 n 次增廣,每次修改 c 次頂標,訪問所有 n2 條邊)

一些優化:

  • 由於修改頂標的目標是讓相等子圖變大,因此可以每次加減一個最小差值 delta。這樣每次增廣只會被修改最多 n 次頂標,時間復雜度降到 O(n4)。
  • 注意到每次重新進行 dfs 太不優秀了,可以直接進行 bfs,每次修改完頂標之后接着上一次做。時間復雜度降到 O(n3)。

四、一般圖的情況?

一般圖最大匹配?O(n3) 帶花樹。然鵝我不會。

一般圖最大權匹配?帶權帶花樹?顯然我也不會。

五、二分圖的覆蓋與獨立集

1. 二分圖的最小點覆蓋

最小點覆蓋:選取最少的點,使得每一條邊的兩端至少有一個點被選中(也就是,“點”覆蓋了所有的“邊”)。

二分圖的最小點覆蓋=最大匹配。具體地說,二分圖最小點覆蓋包含的點數等於二分圖最大匹配包含的邊數。

證明:

  1. 由於最大匹配中的邊必須被覆蓋,因此匹配中的每一個點對中都至少有一個被選中。
  2. 選中這些點后,如果還有邊沒有被覆蓋,則找到一條增廣路,矛盾。

2. 二分圖的最大獨立集

最大獨立集:選取最多的點,使得任意兩個點不相鄰。其中,選取最多的點的點集被稱作最大獨立集。

最大獨立集=點數-最小點覆蓋。(又因為二分圖的最小點覆蓋=最大匹配,所以最大獨立集=點數-最大匹配)

證明:

  1. 由於最小點覆蓋覆蓋了所有邊,因此選取剩余的點一定是一個合法的獨立集。
  2. 若存在更大的獨立集,則取補集后得到了一個更小的點覆蓋,矛盾。

3. 二分圖的最小邊覆蓋

最小邊覆蓋:選取最少的邊,使得每一個點都被覆蓋。

最小邊覆蓋=點數-最大匹配。

證明:

  1. 先選取所有的匹配邊,然后對剩下的每一個點都選擇一條和它相連的邊,可以得到一個邊覆蓋。
  2. 若存在更小的邊覆蓋,則因為連通塊數量=點數-邊數,這個邊覆蓋在原圖上形成了更多的連通塊,每一個連通塊內選一條邊,我們就得到了一個更大的匹配。

4. 二分圖的路徑覆蓋

最小不相交路徑覆蓋:一張有向圖,用最少的鏈覆蓋所有的點,鏈之間不能有公共點。

將點和邊分別作為二分圖的兩邊,然后跑匹配,最小鏈覆蓋=原圖點數-最大匹配。

最小可相交路徑覆蓋:一張有向圖,用最少的鏈覆蓋所有的點,鏈之間可以有公共點。

先跑一遍 Floyd 傳遞閉包,然后變成最小不相交路徑覆蓋。

六、二分圖的應用

1.「ZJOI 2007」矩陣游戲

題目大意:給一個 n×n 的黑白方陣,你可以任意交換兩行或者兩列。求是否能夠讓主對角線上都是黑色。n≤200,多測最多 20 組。

Solution:

假設我們選出了 n 個點的坐標。如果這 n 個點所在的行、列互不相同,那么一定可以通過交換來完成任務。(考慮到兩個同行的格子不可能最終到不同行上,列也一樣。最終我們要讓主對角線上都是黑色,也就是說,我們要讓每行每列都有黑色。所以我們最終找到的 n 個點必然在一開始橫縱坐標就互不相同。)

轉化為二分圖匹配。建一個二分圖,左邊是行,右邊是列。如果 (i,j) 是黑色就從 i 到 j 連邊。

最后看是否存在完美匹配。可以用匈牙利算法解決。

#include<bits/stdc++.h>
#define int long long
#define MEM(x,y) memset(x,y,sizeof(x))
using namespace std;
const int N=1e5+5;
int t,n,x,cnt,hd[N],to[N<<1],nxt[N<<1],mp[N],ans;
bool vis[N];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
bool find(int x){
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;
        vis[y]=1;
        if(!mp[y]||find(mp[y])){mp[y]=x;return 1;}
    }
    return 0;
} 
signed main(){
    scanf("%lld",&t);
    while(t--){
        scanf("%lld",&n),cnt=0,ans=0,MEM(hd,0),MEM(mp,0);
        for(int i=1;i<=n;i++)
            for(int j=1;j<=n;j++){
                scanf("%lld",&x);
                if(x) add(i,j);    //如果 (i,j) 是黑色就從 i 到 j 連邊
            }
        for(int i=1;i<=n;i++){
            MEM(vis,0);
            if(find(i)) ans++;
        }
        puts(ans==n?"Yes":"No");
    }
    return 0;
}

2. POJ 2446 Chessboard

題目大意:一個 n×m 的網格,其中有若干個位置被刪掉了。你需要用 1×2 的骨牌覆蓋其余所有的位置,判斷是否可行。1≤n,m≤32。

Solution:

將整個棋盤進行黑白染色,則一個骨牌一定覆蓋了相鄰兩個顏色不同的位置。

相鄰位置連邊,然后跑二分圖匹配,如果存在完美匹配那就可以,否則不行。

#include<cstdio>
#include<cstring>
#define MEM(x,y) memset(x,y,sizeof(x))
using namespace std;
const int N=40,M=1e5+5;
int n,m,K,x,y,mp[M],cnt,hd[M],to[M<<1],nxt[M<<1],ans,dx[4]={1,-1,0,0},dy[4]={0,0,1,-1};
bool a[N][N],vis[M];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
bool find(int x){
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;
        vis[y]=1;
        if(!mp[y]||find(mp[y])){mp[y]=x;return 1;}
    }
    return 0;
} 
signed main(){
    while(~scanf("%d%d%d",&n,&m,&K)){
        cnt=ans=0,MEM(hd,0),MEM(a,0),MEM(mp,0);
        for(int i=1;i<=K;i++)
            scanf("%d%d",&y,&x),a[x][y]=1;    //標記被刪掉的位置 
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                if(a[i][j]||(i+j)&1) continue;
                for(int k=0;k<4;k++){
                    x=i+dx[k],y=j+dy[k];
                    if(a[x][y]||x<1||x>n||y<1||y>m) continue;
                    add(i*m+j,x*m+y);    //相鄰位置連邊 
                }
            }
        for(int i=1;i<=n;i++)
            for(int j=1;j<=m;j++){
                if(a[i][j]||(i+j)&1) continue;
                MEM(vis,0);
                if(find(i*m+j)) ans++;
            }
        puts(2*ans==n*m-K?"YES":"NO"); 
    }
    return 0;
} 

3. ZOJ 3988 Prime Set

題目大意:給定 n 個整數 a1,...,an,你需要從中選出 k 個數對 {ax1,ay1 },{ax2,ay2 },..., {axk,ayk}(數對之間可以有重復元素),使得每一對數對的和都是質數。
你需要最大化這 k 個數對求並后集合里的元素個數。 1≤n≤3×103,1≤k≤n(n-1)/2。

Solution:

首先跑素數篩,得出所有可以形成質數的數對。

先忽略 1+1=2 的情況,那么所有合法的數對一定奇偶不同(除了 2 外的質數都是奇數,奇數+偶數=奇數)。

建一個二分圖,左邊是奇數,右邊是偶數。跑一遍最大匹配,然后再把 1+1=2 的情況考慮進去,剩下的貪心選取。

注意 1 的位置需要特殊處理。

#include<bits/stdc++.h>
#define int long long
#define MEM(x,y) memset(x,y,sizeof(x)) 
using namespace std;
const int N=3e3+5,M=2e6+5;
int t,n,k,a[N],p[M],cnt,hd[M],to[M<<1],nxt[M<<1],mp[M],ans,sum,tmp;
bool vis[M],v[M];
void add(int x,int y){
    to[++cnt]=y,nxt[cnt]=hd[x],hd[x]=cnt;
}
bool find(int x){
    for(int i=hd[x];i;i=nxt[i]){
        int y=to[i];
        if(vis[y]) continue;
        vis[y]=1;
        if(mp[y]==-1||find(mp[y])){mp[x]=y,mp[y]=x;return 1;}
    }
    return 0;
} 
signed main(){
    vis[0]=vis[1]=1;
    for(int i=1;i<=2e6;i++){    //線性篩 
        if(!vis[i]) p[++cnt]=i,v[i]=1;
        for(int j=1;j<=cnt&&i*p[j]<=2e6;j++){
            vis[i*p[j]]=1;
            if(i%p[j]==0) break;
        }
    } 
    scanf("%lld",&t);
    while(t--){
        scanf("%lld%lld",&n,&k),cnt=ans=sum=tmp=0,MEM(hd,0);
        for(int i=1;i<=n;i++)
            scanf("%lld",&a[i]),mp[i]=-2;
        for(int i=1;i<=n;i++)    //建二分圖 
            for(int j=i+1;j<=n;j++)
                if(v[a[i]+a[j]]){ 
                    mp[i]=mp[j]=-1;
                    if(a[i]==1&&a[j]==1) continue;
                    a[i]&1?add(i,j):add(j,i);
                } 
        for(int i=1;i<=n;i++)
            if(mp[i]==-1&&(a[i]&1)) MEM(vis,0),ans+=find(i);
        for(int i=1;i<=n;i++){
            if(mp[i]<0&&a[i]==1) sum++;     //特殊處理 1+1=2 的情況 
            if(mp[i]==-2) tmp++;
        } 
        ans+=sum/2,tmp=n-tmp-ans*2;    //ans:數對個數  tmp:剩余的未被匹配的元素個數 
        if(ans>=k) printf("%lld\n",k*2);    //至多選出 k 個兩兩不相交的集合,每個集合有 2 個元素,所以答案為 2*k 
        else printf("%lld\n",2*ans+min(tmp,k-ans));    //若 i 未被匹配,表示 (i,j) 中 j 已匹配其他元素,故若選擇 (i,j) 只會得到 1 個不同元素 i,所以答案為 2*ans+min(tmp,k-ans)。 
    }
    return 0;
} 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM