帶花樹演算法 UOJ#79. 一般圖最大匹配
帶花樹演算法
匈牙利演算法可以解決二分圖匹配的問題,但是因為二分圖有一個特殊的性質,那就是不會出現一個有奇數點的環。然而對於每一個有偶數點的環,在環裡面無論怎麼匹配都是可以的,然而假如有奇數的環就不一樣了。
不想寫廢話了。。
————————————正文——————————
時間複雜度大概是O(n^2)吧
首先,思路肯定是找增廣路,也是讓每一個沒匹配的點去嘗試進行匹配,看是否可以有新的匹配。
首先,在沒出現奇環之前,我們把他當做一個普通的二分圖來看,是沒毛病的。
於是有了id這一個陣列,id有三種不同的狀態
-1:沒訪問過 0:S點 1:T點
首先我們假設一開始要增廣的點是一個S點
由於寫的是廣搜,所以我們需要一個數組,
然後對於找到方案的這一段程式碼就很簡單了。。
那麼接下來的問題是如何來進行尋找增廣路的操作了
假設現在我們在佇列中待處理的點為x
首先,假如我們遍歷到一個以前沒有訪問過的點y,那麼我們就讓他的配偶繼續進行增廣。
Q:為什麼是讓他的配偶去呢?
A:因為我們現在是要假設x要與y相連啊,拿肯定是看看y的配偶是否可以換人啦,這一步感覺和普通的匈牙利是沒什麼太大的不同的
然後我們可以發現一個問題啊,那就是我們每一個待擴充套件的點x都是一個S點,嗯,都是S點。這就說明每一個點都是由S點擴展出來的。
那麼除了上面的情況外,剩下的就是環了
假設我們出現的是一個偶環,那麼就直接無視——至於為什麼,大概想一下就可以了
假如我們發現的是一個奇環,那就有大大的不一樣了,因為假如我們這個環中有任意一個點可以在外面找到增廣路的話,這個環就變成了一個偶數環,問題就解決了。
比如說上面這個圖,X就是我們要增廣的點,假如出現了這樣一個環,那麼只要1,2,3,4隨便一個點可以找到增廣路,那麼X就可以有配偶啦^_^
然後下文我們假設這個環的突然出現時因為出現了2————3這一條邊
鋪墊:我們在處理完一個奇環後,我們是可以把他試做一個點的(因為這個環中只要有一個點成功就行),所以這裡寫了一個並查集,來使點變成一個環,我的並查集下文用的是f,當然,這個大點是一個
那麼我們怎樣知道他是不是一個奇環呢?
也很簡單,只要我們訪問到的點也是S點就是了。這個自己想一下應該也可以想出來
那麼剩下的操作就是如何讓他們去遍歷了。
首先我們要知道這個環長什麼樣吧,比如說在上圖中,2,3肯定是由之前的同一個點增廣時所牽扯出來的,也就是有一個點使得他們間接連通現在才回出現環,上圖就是x,所以我們要先知道他是從哪裡來的。於是我們需要一個lca,當然這個lca也寫得比較巧妙~~
然後就是處理環了,分兩段進行,
一段就是處理2到x這一條路,另一段就是處理3到x這一條路,兩個的操作是一樣的,所以只需要寫一個就可以了。
其中一個操作肯定是讓圖中所有的點去增廣,因為一開始的S點肯定都已經入隊增廣了,所以我們要新加入的點就只有之前的T點了。
其次就是維護pre了,這個過程也比較簡單,就是先讓出現新邊的兩條pre互連,然後讓上面的每一個T點與擴充套件他的S點相連。然後假如我們在其中任意有一個點找到增廣路了,根據pre,你就可以得出一條沒有衝突的匹配關係。這個我覺得自己腦補一下就好了。
還是舉個栗子吧。。
還是上面的圖,假如你現在T點4找到了增廣路,那麼3就會去找2,2的原配偶1就會去找x。
假如是S點3找到了增廣路,那麼4就會找到x,1,2關係不用變,這個和沒環沒啥區別。
完結撒花~~
全程式碼大部分都是照著別人的寫的。。
#include<cstdio>
#include<cstring>
#define swap(x,y) {int tt=x;x=y;y=tt;}
const int N=505*2;
const int M=124750*2;
int f[N];
struct qq
{
int x,y;
int last;
}s[M];
int num,last[N];
int n,m;
void init (int x,int y)
{
num++;
s[num].x=x;s[num].y=y;
s[num].last=last[x];
last[x]=num;
}
int match[N];
int id[N];//這個點是什麼點
//-1:沒訪問過 0:S點 1:T點
int q[N];//要擴充套件的佇列————也就是我們要嘗試幫誰換配偶
int pre[N];//在這次過程中,x的新配偶是誰
int Tim,vis[N];//對於lca的標記以及時間軸
int find (int x)
{
if (f[x]==x) return f[x];
f[x]=find(f[x]);
return f[x];
}
int lca (int x,int y)//尋找lca
{
Tim++;
while (vis[x]!=Tim)
{
if (x!=0)
{
x=find(x);//先找到花根
if (vis[x]==Tim) return x;
vis[x]=Tim;
if (match[x]!=0) x=find(pre[match[x]]);
//因為在之前我們知道,每一個S點的配偶(也就是T點)的pre 都是指向他的父親的,於是就直接這麼跳就可以了
//還有要注意的是,一定要先去到花根,因為他們現在已經是一個點了,只有花根的pre才指向他們真正的父親
else x=0;
}
swap(x,y);
}
return x;
}
int st,ed;
void change (int x,int y,int k)//環 出現的是x---y的連邊 已知根是k
{
while (find(x)!=k)
{
pre[x]=y;
int z=match[x];
id[z]=0;q[ed++]=z;if (ed>=N-1) ed=1;
if (find(z)==z) f[z]=k;
if (find(x)==x) f[x]=k;
y=z;x=pre[y];
}
}
void check (int X)//儘量幫助x尋找增廣路
{
for (int u=1;u<=n;u++) {f[u]=u;id[u]=-1;}
st=1;ed=2;
q[st]=X;id[X]=0;
while (st!=ed)
{
int x=q[st];
for (int u=last[x];u!=-1;u=s[u].last)
{
int y=s[u].y;
if (match[y]==0&&y!=X)
//當然match[X]=0,但X(這次來尋找配偶的點)並不是一個可行的東西,所以不能算可行解
{
pre[y]=x;//先假設他與x相連
int last,t,now=y;
while (now!=0)//當然,這次來的X的match是為0,要是能更新到0就是結束
{
t=pre[now];//now新的配偶
last=match[t];//理所當然啦
match[t]=now;match[now]=t;
now=last;
}
return ;
}
if (id[y]==-1)//找到一個沒有訪問過的點————進行擴充套件
{
id[y]=1;
pre[y]=x;//先假設他與x相連
id[match[y]]=0;q[ed++]=match[y];
if (ed>=N-1) ed=1;
}
else if (id[y]==0&&find(x)!=find(y))//出現一個以前未處理過的奇環
{
int g=lca(x,y);
change(x,y,g);change(y,x,g);
}
}
st++;
if (st>=N-1) st=1;
}
}
int main()
{
memset(vis,0,sizeof(vis));Tim=0;
memset(match,0,sizeof(match));
num=0;memset(last,-1,sizeof(last));
scanf("%d%d",&n,&m);
for (int u=1;u<=m;u++)
{
int x,y;
scanf("%d%d",&x,&y);
init(x,y);init(y,x);
}
for (int u=1;u<=n;u++)
if (match[u]==0)
check(u);
int ans=0;
for (int u=1;u<=n;u++)
if (match[u]!=0) ans++;
printf("%d\n",ans/2);
for (int u=1;u<=n;u++) printf("%d ",match[u]);
return 0;
}
肯定寫錯了很多東西。。。。。。