1. 程式人生 > >算法模板——二分圖匹配

算法模板——二分圖匹配

以及 步驟 匈牙利算法 客戶 ram 二分圖最大匹配 最優 amp 思想

一.引入

二分圖匹配算法是一個非常有用的算法,我們首先從一個簡單的題目引入。

給你n個水果,m個箱子,每個水果只能被放在指定的幾個箱子裏,每個盒子只能放一個水果,問如何安排能使的放在盒子裏的水果最多。

怎麽寫?暴力,可以試試。但不管是暴力還是什麽算法,都需要面對一個情況——後面的水果如果沒盒子放了,不能不管不顧,應該空間。對,

二分圖算法的最重要思想就是

二.算法流程

二分圖有兩種主流算法,一個是匈牙利算法,一個是KM算法。
我們先講解匈牙利算法(KM算法見目錄第5項)
為了方便理解,我們先畫個圖
技術分享圖片
\(X_{1}\)~\(X_{4}\)指的是水果,\(Y_{1}\)~\(Y_{4}\)

指的是盒子,每個水果有它自己的喜好。
這時,匈牙利算法帶著你找到了\(X_{1}\)水果,你發現\(Y_{1}\)是個空盒子,於是你把\(X_{1}\)放入\(Y_{1}\)中(連一條紅線)
技術分享圖片
接著,匈牙利算法帶著你找到了\(X_{2}\),你發現\(Y_{2}\)是個空盒子,於是你把\(X_{2}\)放入了\(Y_{2}\)中(連一條紅線)
技術分享圖片
你接著往後面找,發現了\(X_{3}\),但是你發現\(Y_{1}\)\(Y_{2}\)都已經放了東西了,怎麽幫,就這麽放手不管嗎?
不,匈牙利算法告訴你,你需要做一些嘗試。於是你試著把\(X_{3}\)放入\(Y_{2}\),並且準備給\(X_{2}\)
重新找個盒子,但是你發現\(Y_{1}\)同樣有水果了……於是你故技重施,這樣你就要給\(X_{1}\)重新找個盒子了,正巧,\(Y_{3}\)是空著的,於是你便將\(X_{1}\)放入了\(Y_{3}\)
那麽我們在不斷尋找的過程中,經歷了\(X_{?}\)-->\(Y_{?}\)--->\(X_{?}\)--->……的路徑,我們稱其為增廣路
(下圖中,黃邊是臨時拆卸的邊,表示正在嘗試)
技術分享圖片技術分享圖片技術分享圖片
於是皆大歡喜,大家都有了自己的歸宿。
接著找到了\(X_{4}\),但是不管怎麽都沒辦法了,只能把\(X_{4}\)放外面吹西北風了

所以二分圖算法最重要的一點就是,能就要大力

三.代碼(匈牙利算法)

Zju1140 Courses 課程
Description
給出課程的總數P(1<=p<100),學生的總數N(1<=N<=300)
每個學生可能選了一門課程,也有可能多門,也有可能沒有.
要求選出P個學生來組成一個協會,每個學生代表一門課程,
且每門課程都有一個學生來代表它。

Input
先是測試數據的個數。
再是P,N
下列P行,代表從第一門課到第P門課程的學生選修的情況。先給出有多少人選修,
再是這些人的學號.

Output
可否組成一個這樣的協會

Sample Input
2
3 3
3 1 2 3
2 1 2
1 1
3 3
2 1 3
2 1 3
1 1

Sample Output
YES
NO

裸的二分圖最大匹配,直接上代碼

/*program from Wolfycz*/
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x7f7f7f7f
using namespace std;
typedef long long ll;
typedef unsigned int ui;
typedef unsigned long long ull;
inline int read(){
    int x=0,f=1;char ch=getchar();
    for (;ch<‘0‘||ch>‘9‘;ch=getchar())  if (ch==‘-‘)    f=-1;
    for (;ch>=‘0‘&&ch<=‘9‘;ch=getchar())    x=(x<<1)+(x<<3)+ch-‘0‘;
    return x*f;
}
inline void print(int x){
    if (x>=10)     print(x/10);
    putchar(x%10+‘0‘);
}
const int N=1e2,M=3e2;
int path[M+10],pre[N*M+10],now[N*M+10],child[N*M+10];
int tot,n,m;
bool use[M+10];
void join(int x,int y){pre[++tot]=now[x],now[x]=tot,child[tot]=y;}
bool check(int x){
    for (int p=now[x],son=child[p];p;p=pre[p],son=child[p]){
        if (use[son])   continue;
        use[son]=1;
        if (path[son]<0||check(path[son])){path[son]=x;return 1;}
    }
    return 0;
}
void work(){
    int res=0;
    for (int i=1;i<=n;i++){
        memset(use,0,sizeof(use));
        if (check(i))   res++;
    }
    printf(res==n?"YES\n":"NO\n");
}
void init(){
    tot=0;
    memset(now,0,sizeof(now));
    memset(path,255,sizeof(path));
}
int main(){
    for (int Data=read();Data;Data--){
        init();
        n=read(),m=read();
        for (int i=1;i<=n;i++)   for (int x=read(),j;x;x--)  j=read(),join(i,j);
        work();
    }
    return 0;
}

四.關於二分圖的一些證明

(環的最大匹配方式有多種這裏不予討論)

設最大匹配數為\(K\) ,點數為\(N\)

最小點覆蓋集:就是用最少的點集\(G\),使這個圖上的所有線段的左端點或右端點屬於\(G\)
證明:
由於所有最大匹配的線段都不相交,只要取左端點或右端點就可以,所以最大匹配的每一個線段都對應了一個點,一共有\(K\)
因為是最大匹配,不存在增廣路,當某兩條線段的要取的端點相交時,可以知道那一定不是最大匹配。

最大點獨立集:在一個圖\(M\)中,取最多的點,使得每個點都互不相鄰
證明:
當一條線段\(AB\)屬於最大匹配中的邊時,它一定有一個端點沒有連其他的邊,所以最大匹配的線段上一共有\(K\)個,
而不屬於最大匹配的線段上的有\(N-2*K\)個(每條最大匹配的線段都對應了兩個點)
因為我們取的是沒有連其他邊的端點,自然也就不存在不在最大匹配的線段上的點與其相鄰
所以一共有\(N-2*K+K=N-K\)個點

最小邊覆蓋:取最少的邊集\(G\),使得所有的頂點都在\(G\)
證明:
其實與最大點獨立集差不多,只是點也可以視為邊,最大匹配中的邊覆蓋了\(2*k\)點,
剩下的\(N-2*K\)個點又要用\(N-2*K\)條線段去覆蓋,所以一共是\(N-2*K+K=N-K\)

最小路徑覆蓋:在一個有向圖中,用不相交的路徑覆蓋整個圖
證明:
因為每個點最初都是一條路徑,總共有N條不相交路徑。我們每次在二分圖裏加一條邊就相當於把兩條路徑合成了一條路徑,因為路徑之間不能有公共點,所以加的邊之間也不能有公共點,而最多能合並\(K\)次,所以答案是\(N-K\)

五.KM算法

匈牙利算法雖好,也有一定的局限性,如果題目要求的不是最大匹配而是最大權值匹配的話,匈牙利算法就顯得無能為力了,這樣我們就需要用到一種新算法,KM算法

一般對KM算法的描述,基本上可以概括成以下幾個步驟:
(1) 初始化可行標桿
(2) 用匈牙利算法尋找完備匹配
(3) 若未找到完備匹配則修改可行標桿
(4) 重復(2)(3)直到找到相等子圖的完備匹配

完備匹配:如果一個匹配中,圖中的每個頂點都和圖中某條邊相關聯,則稱此匹配為完全匹配,也稱作完備匹配。

定理:設\(M\)是一個帶權完全二分圖\(G\)的一個完備匹配,給每個頂點一個可行頂標(第\(i\)\(x\)頂點的可行標用\(lx[i]\)表示,第\(j\)\(y\)頂點的可行標用\(ly[j]\)表示),如果對所有的邊(i,j) in G,都有\(lx[i]+ly[j]>=w[i][j]\)成立(\(w[i][j]\)表示邊的權),且對所有的邊(i,j) in M,都有\(lx[i]+ly[j]=w[i][j]\)成立,則M是圖G的一個最優匹配。

那麽可行標桿是什麽?為了方便理解,我們通過一個實例,和一些比方來解釋
技術分享圖片
我們假定\(X_{?}\)為一些客人,\(Y_{?}\)為一些商品,客人們對商品有一定的期望值,你需要讓客人買一些商品,使得他們的期望值最大(不考慮經濟問題)

那麽這些客人在進行商品分配的時候難免會有一些爭端,於是我們可以找到一條增廣路,增廣路上一定有奇數個節點,並且必定是客人點多出來一個。因此,我們需要對客人和商品的標桿進行些許的調整,是的二分圖中能加進來一些新的點滿足客戶的需求,解決他們的糾紛。
我們設\(lx\)為客人的標桿,\(ly\)為商品的標桿。
客人的標桿必然是他們的期望值,那麽商品的標桿呢,肯定就是它們的期望增高值
因此,\(lx\)的初值就是某客戶對所有商品的最大期望值,\(ly\)的初值為零。
當當前標桿找不到完備匹配時,我們就要讓多出來客戶能買其他未被選中的商品,所以令\(d=min\){\(lx[i]+ly[j]-w[i][j]\)}(\(i\)為增廣路上客戶點,\(j\)為不在增廣路上的商品點)i,j的位置很好理解,肯定是有爭執的客人找沒有爭執的商品
那麽\(d\)就是我要降低的值,於是我給增廣路上所有的客人的最大期望值全部降低\(d\),但是我們不能減多了,每次整體只能減\(d\),所以我們要將增廣路上的所有商品期望都加上\(d\),這樣就能保證整體的穩定,並且能夠加進來新的商品,以滿足客戶的需求。
(講的不好,讀者們還是邊看看代碼吧)

六.代碼(KM算法)

Description
You were just hired as CEO of the local junkyard.One of your jobs is dealing with the incoming trash and sorting it for recycling.The trash comes every day in n containers and each of these containers contains certain amount of each of the n types of trash.Given the amount of trash in the containers find the optimal way to sort the trash.Sorting the trash means putting every type of trash in separate container.Each of the given containers has infinite capacity.The effort for moving one unit of trash from container i to j is 1 if i != j otherwise it is 0.You are to minimize the total effort.
你受聘於當地的垃圾處理公司任CEO,你的一項工作是處理引進的垃圾,以及分類垃圾以進行循環利用。每天,垃圾會有幾個集裝箱運來,每一個集裝箱都包含幾種垃圾。給定集裝箱裏垃圾的數目,請找出最佳的途徑去分類這些垃圾。分類垃圾即把每種垃圾分開裝到不同的集裝箱中。每個集裝箱的容量都是無限的。搬動一個單位的垃圾需要耗費代價,從集裝箱i到j是1(i<>j,否則代價為0),你必須把代價減到最小。

Input
The first line contains the number n (1 <= n <= 150), the rest of the input file contains the descriptions of the containers.The 1 + i-th line contains the desctiption of the i-th container the j-th amount (0 <= amount <= 100) on this line denotes the amount of the j-th type of trash in the i-th container.
第一行為n (1 <= n <= 150),以下為集裝箱的情況。第i+1行為第i個集裝箱中的j種垃圾的數量amount(0 <= amount <= 100)。

Output
You should write the minimal effort that is required for sorting the trash.
分類這些垃圾的所需的最小代價。

Sample Input
4
62 41 86 94
73 58 11 12
69 93 89 88
81 40 69 13

Sample Output
650

/*program from Wolfycz*/
#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x7f7f7f7f
using namespace std;
typedef long long ll;
typedef unsigned int ui;
typedef unsigned long long ull;
inline int read(){
    int x=0,f=1;char ch=getchar();
    for (;ch<‘0‘||ch>‘9‘;ch=getchar())  if (ch==‘-‘)    f=-1;
    for (;ch>=‘0‘&&ch<=‘9‘;ch=getchar())    x=(x<<1)+(x<<3)+ch-‘0‘;
    return x*f;
}
inline void print(int x){
    if (x>=10)     print(x/10);
    putchar(x%10+‘0‘);
}
const int N=1.5e2;
int path[N+10],lx[N+10],ly[N+10];
int map[N+10][N+10];
bool visx[N+10],visy[N+10];
int n,ans;
bool check(int x){
    visx[x]=1;
    for (int y=1;y<=n;y++){
        if (!visy[y]&&lx[x]+ly[y]==map[x][y]){
            visy[y]=1;
            if (path[y]<0||check(path[y])){
                path[y]=x;
                return 1;
            }
        }
    }
    return 0;
}
int Km(){
    int sum=0;
    memset(path,-1,sizeof(path));
    for (int i=1;i<=n;i++){
        while (1){
            memset(visx,0,sizeof(visx));
            memset(visy,0,sizeof(visy));
            if (check(i))   break;
            int d=inf;
            for (int i=1;i<=n;i++)  if (visx[i])
                for (int j=1;j<=n;j++)  if (!visy[j])   
                    d=min(d,lx[i]+ly[j]-map[i][j]);
            for (int i=1;i<=n;i++)  if (visx[i])    lx[i]-=d;
            for (int i=1;i<=n;i++)  if (visy[i])    ly[i]+=d;
        }
    }
    for (int i=1;i<=n;i++)  if (path[i]!=-1)    sum+=map[path[i]][i];
    return sum;
}
int main(){
    n=read();
    for (int i=1;i<=n;i++){
        lx[i]=-inf;
        for (int j=1;j<=n;j++)  ans+=map[i][j]=read(),lx[i]=max(lx[i],map[i][j]);
    }
    printf("%d\n",ans-Km());
    return 0;
}

算法模板——二分圖匹配