難得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\)的**如下:
\(dfn\)就是乙個標記而已,你在向上跳的時候一邊跳一邊打標記int lca(int u,int v)
return u;
}
如果你在跳完另外乙個點後發現這個位置已經被打了標記,
那麼就意味著這個點就是\(lca\)啦
好的,我們求出來了\(lca\),考慮怎麼縮環(開花)
先上**我再來解釋
\(x,y\)是要開花的奇環的兩個點(也就是上面的\(u,v\))void blossom(int x,int y,int w)
}
\(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#include#include#include#include#include#include#include#include#includeusing namespace std;
#define ll long long
#define rg register
#define max 555
#define maxl 255555
inline int read()
struct linee[maxl];
int h[max],cnt=1;
inline void add(int u,int v);h[u]=cnt++;}
int match[max],pre[max],f[max],vis[max],tim,dfn[max];
int n,m,ans;
int getf(int x)
int lca(int u,int v)
return u;
}queueq;
void blossom(int x,int y,int w)
}bool aug(int s)
vis[match[v]]=1,q.push(match[v]);
}else
}} return false;
}int main()
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;
}
帶花樹演算法學習筆記
帶花樹演算法大概就是解決一般圖的最大匹配 回顧匈牙利演算法解決二分圖匹配 我們每次增廣左側的a,其實是找到與它有邊相連的右側的乙個點b 看b是否在匹配中,如果不在那麼增廣成功 如果在那麼就增廣b的匹配點c看是否成功 但我們發現這是因為我們把點集分成兩個內部無交的點集才可以這麼做 但是對於一般圖是不滿...
帶花樹演算法學習小記
眾所周知,帶花樹演算法用來解決一般圖最大匹配問題。因為人很菜理解不清楚,所以建議對板理解。以下都是本人的感性理解,有不嚴謹的說法請包涵。同樣是找增廣路。增廣路定義為一條路徑 p 1,p 2,dots,p k 滿足 p 2,p 3 匹配,p 4,p 5 匹配 p p 匹配。p 1 和 p k 不在匹配...
帶花樹學習筆記
帶花樹是一種用於解決一般圖最大匹配的演算法 不是資料結構 一般圖與二分圖最大的不同是一般圖中存在奇環,所以不能和二分圖一樣的隨便找到一條交錯路就增廣。帶花樹的做法是把奇環縮起來 開花 讓環上的點找到乙個增廣路之後能沿著正確的方向往回退,除此之外在思路上與匈牙利差不多。具體看 的注釋 include ...