Syins的二分圖匹配理解

二分圖是什麼?

定義

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

理解

在這裏插入圖片描述
其實就是把點分爲u,v兩塊,而其內部不存在連線。

二分圖的判定

我們可以這樣想,從任意一個點出發,沿着給定的邊走
如果該圖是一個二分圖,那我們必定是在走交錯路的,也就是一下在u這邊,一下在v這邊
不會從u中的點開始,下一個點仍然是u裏的點。
那要怎麼做就很清楚了
從一個點開始,給它上一個狀態,比如0,到下一個點記一個相反狀態比如1
爲什麼要相反狀態呢?
這是因爲方便改變值
比如這裏提到的0和1就可以通過"!"(非 運算符)來更新,也可以用1-x來更新
如果下一個點的狀態跟當前點重複了,那說明不是二分圖
這個很好理解:可以看一下圖👇
在這裏插入圖片描述

代碼

在我看來代碼挺重要,但是也不是那麼重要,我還是更看重理解一些,所以給了個3級標題233

#include<bits/stdc++.h>

using namespace std;

const int N=505;

bool f=true;//答案
bool G[N][N];//邊
int m,n,pd[N+5];//邊數,點數,結點標記
queue<int>q;
void eft_bfs(int x)//用bfs判斷
{
    q.push(x);//初始點
    pd[x]=0;//標記爲0
    while(!q.empty())
    {
        int p=q.front();//取出首結點
        q.pop();
        for(int i=0;i<n;++i)//枚舉
        {
            if(G[p][i])
            {
                if(pd[i]==pd[p])//如果枚舉出的結點本來就有標記,而且與該結點一樣,則形成環,非二分圖
                {
                    f=false;
                    return;
                }
                else if(pd[i]==-1)
                {
                    pd[i]=1-pd[p];//1-0=1....1-1=0
                    q.push(i);
                }
            }
        }
    }
}
int readin()
{
    int x=0,f=1;
    char ch=getchar();
    while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();}
    while(ch>='0'&&ch<='9'){x=x*10+ch-'0';ch=getchar();}
    return x*f;
}
int main()
{
    memset(pd,-1,sizeof pd);//初始化
    n=readin(),m=readin();
    for(int i=0;i<m;++i)
    {
        int u,v;
        u=readin(),v=readin();
        G[u-1][v-1]=G[v-1][u-1]=true;
    }
    for(int i=0;i<n;++i)//枚舉,考慮有不連通的情況
    {
        if(pd[i]==-1)eft_bfs(i);
        if(!f)break;
    }
    if(!f)puts("No");
    else puts("Yes");
    return 0;
}
/*
9 8
1 6
2 6
2 7
3 8
3 9
4 9
5 7
5 9
*/

二分圖匹配(匈牙利算法)

什麼是匹配和我們要做什麼

匹配

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

極大匹配

無法再通過增加未完成匹配的邊的方式來增加匹配的邊數的匹配就是極大匹配

最大匹配(也就是我們現在要得到的)

所有極大匹配當中邊數最大的一個匹配。
通俗的說就是匹配數最大的匹配

增廣路(關鍵)

網上有些博客是描述爲”騰“,說的其實不錯,但實際上還是求增廣路,不過是學會之後的一個理解罷了。我個人感覺還是從增廣路出發更明白些
上面是廢話

什麼是增廣路呢?
若P是圖G中一條連通兩個未匹配頂點的路徑,並且屬於M的邊和不屬於M的邊(即已匹配和待匹配的邊)在P上交替出現,則稱P爲相對於M的一條增廣路徑(舉例來說,有A、B集合,增廣路由A中一個點通向B中一個點,再由B中這個點通向A中一個點……交替進行)。

通俗的說就是從一個未匹配的點開始,在u,v兩塊地方走交錯路
而且最後一個點也必須是未匹配點

鼠標繪圖,能看就行233
在這裏插入圖片描述
這裏紅線是已經連接的,那麼增廣路可以是1-2-3-6(全黑線就是未匹配)

性質

觀察上面的增廣路可以發現紅線(已匹配邊)爲1條,黑線(未匹配邊)爲2條
那麼取反之後呢?就變成紅2黑1了
誒,那不就是邊數增加了嗎?
在這裏插入圖片描述
仔細想想其實不難證明:增廣路的未匹配邊比匹配邊多一條(不然也不叫增廣路了

也就是說如果有增廣路存在,那麼取反後必然會增加邊數
這也就是匈牙利算法的主要想法
這裏其實就可以說講完了匈牙利算法
但還是要往下講些詳細的

算法流程

根據剛纔的增廣路性質
我們要找最大匹配也就是找增廣路有多少條
這裏如果前面沒看進去的話有可能無法理解
這麼說吧,一開始所有邊都是未匹配的邊,也就是一開始是0匹配數,而找到一條增廣路意味着我們可以取反來讓匹配數+1;
所以說找到一條增廣路就可以增加一個匹配數,也就是增廣路的數量就是最大匹配數
怎麼找呢?
dfs是一個不錯的選擇
從X部一個未匹配的頂點u開始,找一個未訪問的鄰接點v(v一定是Y部頂點)。
對於 v,分兩種情況:
(1)如果v未匹配,則已經找到一條增廣路。
(2)如果v已經匹配,則取出v的匹配頂點w(w一定是X部頂點),邊 (w,v)目前是匹配的,根據“取反”的想法,要將(w,v)改爲未匹配, (u,v)設爲匹配,能實現這一點的條件是看從w爲起點能否新找到一 個未匹配點。如果行,則u-v-P’ 就是一條以u爲起點的增廣路徑。
幾個注意點
會不會死循環?
不處理當然會,就像下面這個圖👇(可以自行模擬)
在這裏插入圖片描述
所以要開一個標記的數組
另外一個點就是給的圖,它不一定是全連通的
這麼說吧,它可能是多個連通的二分圖放一起的
所以要跑一個for循環來判斷所有的點
當然,如果該點在之前的點求增廣路的時候就已經有匹配了,那麼當然不必判斷,所以這裏可以有個標記的東西

代碼

主函數就建圖和答案處理

#include<bits/stdc++.h>
using namespace std;
#define N 120
int jlx[N],jly[N];
int n,m,k,w[N][N],match[N];
bool sy[N];
bool c[N];
bool search_path(int u)
{
    for(int v=0;v<m;++v)
    {
        if(w[u][v]&&(!sy[v]))
        {
            sy[v]=true;
            if(match[v]==-1||search_path(match[v]))
            {
                match[v]=u;
                c[u]=true;
                return true;
            }
        }
    }
    return false;
}
int Hungary()
{
    int jg=0;
    memset(c,false,sizeof c);
    memset(match,-1,sizeof match);
    for(int i=0;i<n;++i)
    {
        if(!c[i])
        {
            memset(sy,false,sizeof sy);
            if(search_path(i))jg++;
        }
    }
    return jg;
}

最優匹配(K_M算法)

很多時候各個邊的優先級是不一樣的,也就是他們有不同的權重,這個時候就不能簡單的應用匈牙利算法了
現在就來看看K_M算法

前置

——————————————————————————————————————————————————
這個算法的關鍵思想其實是儘量取大邊
先取最大的情況,如果不行,那麼把減小最少的一種情況拿來考慮

——————————————————————————————————————————————————
我覺得這個是非常重要的

完備匹配

完備匹配就是所有的點都存在匹配,這也就意味着完備匹配一定是最大匹配,同時如果二分圖存在完備匹配那麼最大匹配也一定是完備匹配

最優匹配

就是帶權最大匹配,也就是我們的解決目標

一個定理(關鍵)

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的一個最佳匹配。
我也不知道算不算定理奧(滑稽)
這裏初略的證明一下,因爲頂標滿足在G中x頂標加上y頂標是要大於等於兩點間的邊權的
而在其完備匹配中,有滿足x頂標加上y頂標等於兩點間的邊權
前面說了完備匹配是所有點都匹配了,那麼,這裏就滿足了兩個條件:1.單個邊權是M中的大於等於G中的。2.M中形成的匹配(也就是要加起來的邊)是最多的。
所以很簡單就得到了:最多的最大邊權相加那就是最優匹配。
無非就是存不存在M的問題了。

算法流程

先不考慮其他的,就根據這個定理來說,我們很容易想到這樣的做法:
1 要設頂標

2 要該頂標下是否存在完備匹配(匈牙利算法實現)

3 如果不存在那麼修改頂標

頂標設立

首先想到的就是怎麼設一個頂標
前面提到對所有的邊(i,j) in G,都有lx[i]+ly[j]>=w[i,j]成立
那我們就讓lx[i]=max(w[i,j]);讓ly[i]=0;
你當然可以這麼想,不過從我前置目錄下寫的關鍵思想方面考慮,會發現更加合理的解釋
這麼說吧:我們要最優匹配,就是要最大邊權和
那怎麼取最大呢?
因爲一個點只能取一條邊(一個匹配)所以我們要考慮最大的情況就是每個點都與權最大的邊相連(當然這個最大是與該點相連的邊)
也是因此我們讓lx[i]爲與i點相關的最大邊權值

代碼

啊,沒錯這裏就是在水長度

for(int i=0;i<n;++i)//初始化頂標,lx(i)變爲與xi關聯的邊的邊權最大值,ly爲0
    {
        ly[i]=0;
        lx[i]=-0x7fffffff;
        for(int j=0;j<n;++j)
        {
            if(lx[i]<w[i][j])lx[i]=w[i][j];
        }
    }

怎麼找完備匹配

第一個問題解決了(也許還沒有(滑稽))
那麼第二個點就是找完備匹配了
怎麼找呢?
完備匹配就是所有點都匹配了
那我們完全可以去找,每個點是否可以有匹配
這個很容易想到匈牙利算法
之前提過增廣路的性質,取反之後會加1
然後還要想到的一點是:取反之後,未匹配點匹配了。
也就是說我們從未匹配點出發,如果能找到增廣路,那麼我們取反之後該點就會有匹配,所以只要跑增廣路就可以判斷每個點是否有匹配。
當然我們要的不只是匹配,而是要滿足lx[u]+ly[v]==w[u][v]的完備匹配
所以還要加上這個條件來限制
具體流程這裏就不贅述了,我尋思着已經是非常詳細了(如果還沒理解就看代碼將就一下吧/(ㄒoㄒ)/~~)

代碼

假設i在y裏
那麼match[i]就是與i形成匹配的在x裏的點

bool search_pp(int u)
{
    sx[u]=true;
    for(int v=0;v<n;++v)
    {
        if(!sy[v]&&lx[u]+ly[v]==w[u][v])//這裏sy同樣有防止死循環的效果
        {
            sy[v]=true;
            if(match[v]==-1||search_pp(match[v]))//如果該點未匹配或者能找到下一個未匹配點,同匈牙利算法
            {
                match[v]=u;
                return true;
            }
        }
    }
    return false;
}

找不到的情況下怎麼修改

當然理想情況是很少的,現實很殘酷
那麼如果不存在完備匹配我們就要修改頂標(雖然說用K_M算法其實必須又完備匹配,但我們畢竟還有個限制條件)
怎麼修改呢?
你可以這麼理解:
因爲我們一開始設置的是最大的情況
所以現在肯定是要進行減少
那麼對於一次尋找的過程有4種情況
1:x在找增廣路時找到過,y也是,lx[i]-inc+ly[i]+inc是不變的
2:x在找增廣路時找到過,y沒有,lx[i]-inc+ly[i]這樣是減少的,也是有可能的
3:x沒有,y有,lx[i]+ly[i]+inc是增加的,也是不可能的
4:都沒有當然也是不變的
很多人可能看不懂(比如我/(ㄒoㄒ)/~~)
這裏我說一下:
爲什麼找增廣路時找到過的lx要加而ly要減,我們要改變條件來得到匹配也就意味着頂標的值要改變,那怎麼改呢?
lx之前說了取了最大,那不成立怎麼辦,當然是減小,而lx小了ly要加,不然這兩點之間的值就變了,所以有上面4種情況的考慮
而爲什麼沒有找到過的不用改?都沒見到過爲什麼要管他們(滑稽)而且本來他們相加也不是邊權,所以不必改
那麼inc取什麼呢?
前面提到過一個條件lx[i]+ly[j]>=w[i][j]
那麼lx[i]-inc+ly[j]>=w[i][j];
所以取inc=lx[i]+ly[j]-MAX(w[i][j])

很好,看完前面的抽象理解之後你可以考慮一下前置目錄下的內容
我說說我的理解
我們要修改頂標,是爲什麼呢?
這裏我解釋一下:不成立的話,我們就得換掉一條邊,再看看是不是成立
怎麼換呢?很容易想到,我們要換一條邊要使得減少的量最少,這樣可以保證,如果成立,那麼一定是最優匹配
這樣一來是不是頓時感覺上面都是廢話。(這也是我看重思想的一個原因,明白思想的話,寫代碼其實很容易,而且代碼容易忘,思想卻是深刻的)
我們要使得減少的量最少所以就取inc=lx[i]+ly[j]-MAX(w[i][j])
關鍵要明白一點,不成立的情況下,我們要減小值讓它成立,而減小值就是要儘量減少的小,讓下一個要計算的值儘可能的大

代碼

for(int u=0;u<n;u++)//對x裏每一個點找匹配
    {
        while(1)
        {
            memset(sx,0,sizeof sx);//每一個點找匹配都要重新判斷是否找到過這個點,所以每次都要初始化
            memset(sy,0,sizeof sy);
            if(search_path(u))break;//如果找到匹配就下一個
            int inc=0x7fffffff;
            for(int i=0;i<n;++i)//如果找不到匹配就修改頂標
            {
                if(sx[i])//因爲是u找匹配,所以只要修改過程中找到過的點的值就可以了
                {
                    for(int j=0;j<n;++j)
                    {
                        if(!sy[j]&&((lx[i]+ly[j]-w[i][j])<inc))inc=lx[i]+ly[i]-w[i][j];
                    }
                }
            }
            for(int i=0;i<n;++i)
            {
                if(sx[i])lx[i]-=inc;
                if(sy[i])ly[i]+=inc;
            }
        }
    }

求答案

emm,這沒啥好說的,前面都明白了就很容易

代碼

int sum=0;
    for(int i=0;i<n;++i)
    {
        if(match[i]!=-1)sum+=w[match[i]][i];//因爲是完備匹配所有if的條件其實沒什麼用
    }
    if(!max_weight)sum=-sum;

拓展應用

1.要求最小權完備匹配只需將所有的邊權值取其相反數,求最大權完備匹配,匹配的值再取相反數即可。
2.Kuhn-Munkers算法的運行要求是必須存在一個完備匹配,如果求一個最大權匹配(不一定完備)可以把不存在的邊權值賦爲0。
3.若要邊權之積最大則每條邊權取自然對數,然後求最大和權匹配,求得的結果a再算出e^a就是最大積匹配。

優化(加入鬆弛數)

你可以發現前面修改頂標找inc用了for嵌套
其實如果我們把lx[i]+ly[i]-w[i][j];記錄,完全可以寫成一層for
這就不細講了

bool search_pp(int u)
{
    int tem;
    sx[u]=true;
    for(int i=0;i<n;++i)
    {
        if(sy[i])continue;
        tem=lx[u]+ly[i]-w[u][i];
        if(tem=0)pp
        {
            sy[i]=true;
            if(match[i]==-1||search_pp(match[i]))
            {
                match[i]=u;
                return true;
            }
        }
        else if(slack[i]>tem)slack[i]=tem;
    }
    return false;
}


for(int u=0;u<n;++u)
{
     for(int j=0;j<n;++j)slack[j]=0x7fffffff;
     while(1)
     {
         memset(sx,false,sizeof sx);
         memset(sy,false,sizeof sy);
         if(search_path(u))break;
         int inc=0x7fffffff;
         for(int j=0;j<n;++j)//因爲如果不是找增廣路時找到過,那麼slack是非常大的,不影響結果
                 if(!sy[j]&&inc>slack[j])inc=slack[j];
         for(int i=0;i<n;++i)
         {
             if(sx[i])lx[i]-=inc;
             if(sy[i])ly[i]+=inc;
         }
     }
 }

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章