1. 程式人生 > >帶花樹算法學習筆記

帶花樹算法學習筆記

cst 筆記 urn sin 擴展 led jpg 表示 log

帶花樹算法學習筆記

難得yyb寫了一個這麽正式的標題

Q:為啥要學帶花樹這種東西啊?
A:因為我太菜了,要多學點東西才能不被吊打
Q:為啥要學帶花樹這種東西啊?
A:因為我做自己的專題做不動了,只能先去“預習”ppl的專題了
Q:為啥要學帶花樹這種東西啊?
A:因為可以用來做題啊,比如某WC題目

先推薦一個很皮很皮的帶花樹講解:
戳這裏嗷

QaQ
言歸正傳
帶花樹的算法用來解決一般圖的最大匹配問題
說起來,是不是想起來網絡流裏面的最小路徑覆蓋?
或者二分圖的最大匹配的問題?
的確,帶花樹解決一般圖的最大匹配問題類似於這些東西。
但是肯定是有不同的。

比方說:
我們用匈牙利的思路來解決一般圖
我們是可以很容易就讓算法掛掉的

只需要一個奇環就可以啦
(讓我偷張圖片過來)
技術分享圖片

看見沒有
有了一個奇環,在匹配的時候黑白就會翻轉過來。
所以我們當然不能直接用匈牙利來做。

但是,這樣的問題當然需要解決,
所以就有了帶花樹算法。
你可以理解為:
帶花樹算法=匈牙利算法+處理奇環

因為不打算長篇大論,
我按照帶花樹的步驟來寫寫這個算法。
(隨時對比匈牙利算法)

匈牙利算法第一步:找到一個未被匹配的點,從這個點開始匹配
帶花樹算法第一步:找到一個未被匹配的點,從這個點開始匹配

貌似沒有區別。。。
接下來匈牙利算法會用\(dfs\)來尋找增廣路
帶花樹算法使用\(bfs\)
將當前點丟進隊列裏面
我們將他染個色,比如說黑色
然後開始\(bfs\)


首先取出隊首的黑點\(u\)
找找和它相鄰的點\(v,(u,v)\in E\)
如果\(v\)是白點並且在當前的這一次匹配中已經被訪問過,則不管這個點
否則,如果當前點\(v\)沒有被訪問過,並且\(v\)沒有匹配點
那麽就是找到了一條增廣路
記錄每一個點的前驅\(pre\),每個點的匹配點\(match\)
從當前的點\(v\)開始,每個點都和他的前驅兩兩匹配
沿著增廣路全部修改回去就行了,
然後這一次的匹配結束。(這個跟匈牙利是一樣的啊)
如果這個點已經有匹配點的話,則去嘗試能否修改它的匹配點
因此,這個時候把\(v\)的前驅置為\(u\),然後把\(v\)的匹配點丟進隊列裏面。(這也是和匈牙利一樣的啊)
繼續\(bfs\),嘗試能否修改它的匹配點。

對於上面的情況,明顯和匈牙利算法是一模一樣的,
但是出現了匈牙利不能解決的情況,也就是奇環。

如果當前黑點\(u\)的相鄰點擴展出來了一個黑點\(v\)
意味著\(u-v-u\)構成了一個奇環
那麽我們就要縮環啦,這就是帶花樹算法的重點。

對於一個奇環,它的點的個數一定是\(2k+1\)的形式
意味著,在奇環內最多只有\(k\)組匹配,
同時,一定有一個點會向外匹配(匹配點不在環內)
現在,如果我們把整個奇環都看成一個點
如果某個增廣路找到了奇環上去,我們一定能夠重置奇環內的匹配
無非是把增廣路找到的奇環上的那個點和增廣路上的其他點匹配。
然後奇環剩下的\(2k\)個點兩兩匹配。

所以,我們可以直接把奇環看成一個點來縮,這個就是開花啦
如果增廣路找到了奇環上,我們就把奇環展開重新更新一下匹配就好。

可是,問題是,怎麽縮奇環???
我們額外維護一個並查集,將同朵花中的節點在並查集中合並
我們先求出他們的最近花祖先
這個要怎麽理解?
我們的匹配(\(match\))和前驅(\(pre\))都是邊
如果把已經縮好的奇環都看成一個點
那麽,這些邊和點,就是一棵樹。
假設現在出現了\(u-v\)這條邊
意味著在樹上出現了一個基環(當然也是奇環)
那麽,從當前的\(u,v\)所在的奇環開始(如果只有一個點就是它自己啦)
不斷的向上走交替地沿著\(match\)\(pre\)邊向上
當然了,每次走當然要走到他所在的奇環(並查集的根節點)所代表的那個位置啦(這是樸素的、暴力的\(lca\)求法)

所以求\(lca\)的代碼如下:

int lca(int u,int v)
{
    ++tim;u=getf(u);v=getf(v);
    while(dfn[u]!=tim)
    {
        dfn[u]=tim;
        u=getf(pre[match[u]]);
        if(v)swap(u,v);
    }
    return u;
}

\(dfn\)就是一個標記而已,你在向上跳的時候一邊跳一邊打標記
如果你在跳完另外一個點後發現這個位置已經被打了標記,
那麽就意味著這個點就是\(lca\)

好的,我們求出來了\(LCA\),考慮怎麽縮環(開花)
先上代碼我再來解釋

void Blossom(int x,int y,int w)
{
    while(getf(x)!=w)
    {
        pre[x]=y,y=match[x];
        if(vis[y]==2)vis[y]=1,Q.push(y);
        if(getf(x)==x)f[x]=w;
        if(getf(y)==y)f[y]=w;
        x=pre[y];
    }
}

\(x,y\)是要開花的奇環的兩個點(也就是上面的\(u,v\)
\(w\)是他們的\(LCA\)
此時\(x,y\)之間可以匹配,但是他們都是黑點。

因為整朵花縮完都是一個黑點
因此,我們把\(x->lca\),\(v->lca\)的路徑全部處理即可
因為兩部分相同,因此只需要寫一個\(Blossom\)函數
看看這個開花是怎麽執行的
首先把\(x,y\)\(pre\)連接起來(默認一朵花中未匹配的點就是\(lca\),也就是花根)
然後沿著\(x\)(或者\(y\))向上一個個點往上跳
如果跳到某個點是白點,但是花中的所有點都是黑點
所以把白點暴力染黑,然後丟進隊列中增廣

在跳的過程中,很可能中間跳的是若幹個已經縮完的花(縮過的花也是點,但是在維護\(pre\)的時候,還是需要沿著這朵花暴跳,因為還需要維護每個點的匹配信息,只考慮一朵花的話沒法維護所有點的信息)
所以在跳躍的過程中,暴力把所有訪問到的節點和花的並查集全部合並到\(lca\)上面,表示他們的花根是\(lca\)

感覺我寫的很不清晰

總而言之,我們來總結一下帶花樹算法的流程

1.每次找一個未匹配的點出來增廣
2.在增廣過程中,如果相鄰點是白點,或者是同一朵花中的節點,則直接跳過這個點
3.如果相鄰點是一個未被匹配過的白點,證明找到了增廣路,沿著原有的\(pre\)\(match\)路徑,對這一次的匹配結果進行更新
4.如果相鄰點是一個被匹配過的白點,那麽把這個點的匹配點丟進隊列中,嘗試能否讓這個點的匹配點找到另外一個點進行匹配,從而可以增廣。
(以上步驟同匈牙利算法)
5.如果相鄰點是一個被匹配過的黑點,證明此時出現了奇環,我們需要將這個環縮成一個黑點。具體的實現過程是:找到他們的最近花公共祖先,也就是他們的花根,同時,沿著當前這兩個點一路到花根,將花上的所有節點全部染成黑點(因為一朵花都是黑點),將原來的白點丟進棧中。同時,修改花上所有點的\(pre\),此時,只剩下花根並不與花內的節點相匹配。

以下是\(UOJ79\)模板題的代碼

#include<iostream>
#include<cstdio>
#include<cstdlib>
#include<cstring>
#include<cmath>
#include<algorithm>
#include<set>
#include<map>
#include<vector>
#include<queue>
using namespace std;
#define ll long long
#define RG register
#define MAX 555
#define MAXL 255555
inline int read()
{
    RG int x=0,t=1;RG char ch=getchar();
    while((ch<'0'||ch>'9')&&ch!='-')ch=getchar();
    if(ch=='-')t=-1,ch=getchar();
    while(ch<='9'&&ch>='0')x=x*10+ch-48,ch=getchar();
    return x*t;
}
struct Line{int v,next;}e[MAXL];
int h[MAX],cnt=1;
inline void Add(int u,int v){e[cnt]=(Line){v,h[u]};h[u]=cnt++;}
int match[MAX],pre[MAX],f[MAX],vis[MAX],tim,dfn[MAX];
int n,m,ans;
int getf(int x){return x==f[x]?x:f[x]=getf(f[x]);}
int lca(int u,int v)
{
    ++tim;u=getf(u);v=getf(v);
    while(dfn[u]!=tim)
    {
        dfn[u]=tim;
        u=getf(pre[match[u]]);
        if(v)swap(u,v);
    }
    return u;
}
queue<int> Q;
void Blossom(int x,int y,int w)
{
    while(getf(x)!=w)
    {
        pre[x]=y,y=match[x];
        if(vis[y]==2)vis[y]=1,Q.push(y);
        if(getf(x)==x)f[x]=w;
        if(getf(y)==y)f[y]=w;
        x=pre[y];
    }
}
bool Aug(int S)
{
    for(int i=1;i<=n;++i)f[i]=i,vis[i]=pre[i]=0;
    while(!Q.empty())Q.pop();Q.push(S);vis[S]=1;
    while(!Q.empty())
    {
        int u=Q.front();Q.pop();
        for(int i=h[u];i;i=e[i].next)
        {
            int v=e[i].v;
            if(getf(u)==getf(v)||vis[v]==2)continue;
            if(!vis[v])
            {
                vis[v]=2;pre[v]=u;
                if(!match[v])
                {
                    for(int x=v,lst;x;x=lst)
                        lst=match[pre[x]],match[x]=pre[x],match[pre[x]]=x;
                    return true;
                }
                vis[match[v]]=1,Q.push(match[v]);
            }
            else
            {
                int w=lca(u,v);
                Blossom(u,v,w);
                Blossom(v,u,w);
            }
        }
    }
    return false;
}
int main()
{
    n=read();m=read();
    for(int i=1;i<=m;++i)
    {
        int u=read(),v=read();
        Add(u,v);Add(v,u);
    }
    for(int i=1;i<=n;++i)if(!match[i])ans+=Aug(i);
    printf("%d\n",ans);
    for(int i=1;i<=n;++i)printf("%d ",match[i]);puts("");
    return 0;
}

帶花樹算法學習筆記