有向圖強連通分量
有向圖強連通分量就是在一個強連通分量裏面,每個點都能到達分量裏面其他所有點。
那麼,如何求?
Tarjan算法
定義
我們定義一個low數組與一個dfs數組與一個ts(時間戳,不需要過多理解,下文看了就知道功能了)
做法
我們要明白Tarjan算法其實就是利用DFS序來完成工作的。
而且一個點只能在一個強連通分量裏面。
首先,我們找到一個點x,然後,這個時候,然後我們用x繼續去找其他點。
找到了一個沒被找過的點(),對y進行dfs
於是我們得到了一個DFS序
那麼,現在怎麼做?
dfn是存DFS序的,那麼low呢?
LOOK
4現在找到了3,發現3並沒有在一個已經找完的強連通分量裏面,說明什麼?(現在可能看不懂,看下去就懂了)
1、 3號點在DFS樹中是4的祖先,3還沒便歷完,這個時候,3可以沿着DFS樹中的邊到4,4也可以到3,豈不妙哉?於是我們可以知道DFS樹中3->4上所有的點都是一個分量裏的,這個時候,我們用low[4]=dfn[3]。
2、 3號點在DFS樹上是4的祖先的另外一個分支,但是3並沒在一個分量裏面,而且按照DFS的規則,只有便利完一個分支才能便歷另一個分支,所以,3肯定與3、4的最近共同祖先是一個分量裏的,那麼,祖先可以到4,而4也可以先走到3在走到祖先,那麼low[4]=dfn[3](注:3先被發現,所以DFS序要小),情況大概如下圖。
3、 3號點爲4號點的DFS樹中兒子,其實不能說是找到了兒子,畢竟如果他沒被找過,找到了,3號纔是4號兒子,如果3號被找過了,那麼3號應該是別人兒子(除非有重邊),就是情況2,那我們就考慮假設找到一個沒被找過的點怎麼辦?
先DFS一遍3,發現3還是沒有在一個已經找完的強連通分量,說明3的low指向的是4甚至更高的祖先,難道我們又要讓low[x]=low[y]?但是,既然是兒子的,我們爲什麼不能在他DFS完後豈不妙哉?
4、4找到了孫子或更低的3,這時,4找完了兒子,兒子也認4爲兒子…,總之4在3下面,所以4找過了,老道理,4還是在一個已經找完的強連通分量,但是我們不能像以前了,因爲3的DFS序大於4的,我們會發現,一個點的low必須小於等於dfn,難道要,不用,我們會發現,等4的兒子便歷完後傳回來的low[y],其實就已經包括了了,所以我們只需讓
那麼,通過上面我們可以知道,如果我們找到了一個點,沒被找過,我們就讓他去DFS,並且更新,找過了,但所在的分量沒更新完,也是這樣更新。
重點來了。
一個分量怎樣算找完?
我們設立一個棧,每次找到一個點就把他推入棧,當時,我們就把x包括x以上所有點算在一個分量裏面,想想就知道,這時可行的。
代碼
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int belong[61000],dfn[61000],cnt,low[61000],n,m,id;
struct node
{
int y,next;
}a[210000];int len,last[61000];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}//邊目錄
int sta[61000],p;//棧
bool v[61000];//所在的分量找完沒?
void dfs(int x)
{
dfn[x]=low[x]=++id;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(dfn[y]==0)
{
dfs(y);//便歷
low[x]=min(low[x],low[y]);
}
else if(v[y]==false)low[x]=min(low[x],dfn[y]);//low[x]=min(low[x],low[y]);也不會錯
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
do
{
now=sta[p--];
v[now]=true;//找完了
belong[now]=cnt;//所在的分量
}while(now!=x && p>0);
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)dfs(i);//便歷
}
printf("%d\n",cnt);
return 0;
}
練習
1
這道題目難度有點大,我們做一遍Tarjan算法,然後把每個強連通分量當成一個點,計算每個點的入度與出度,我們需要知道,爲什麼這些點(我們已經把所有強連通分量縮點了)不在一個強連通分量裏面?
比如:
我們可以姑且的認爲,一個長得像的點叫僞點(非專業術語)
而一個僞點一般有一個點入度爲0,一個點出度爲0,當然,即使有特殊情況使得某個爲0也是沒問題的,代表他和其他僞點已經有聯繫了。
那麼,我們只需要把一個僞點沒入度的連向沒出度的(當然,只有一個分量的話要特判,直接輸出0),也就是max(rdcnt,cdcnt)。
雖然很難理解,但是畫以下圖就知道了。
#include<cstdio>
#include<cstring>
#include<cstdlib>
using namespace std;
int flog[21000],fa[21000],biao[21000],id,n,m,cnt,t;
struct node
{
int x,y,next;
}a[51000];int last[21000],len,list[21000],top;
bool v[21000];
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
inline int mymin(int x,int y){return x<y?x:y;}
inline int mymax(int x,int y){return x>y?x:y;}
void dfs(int x)
{
fa[x]=biao[x]=++id;
list[++top]=x;v[x]=true;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(biao[y]==0)
{
dfs(y);
fa[x]=mymin(fa[x],fa[y]);
}
else
{
if(v[y]==true)fa[x]=mymin(fa[x],fa[y]);
}
}
if(biao[x]==fa[x])
{
int i=0;cnt++;
while(i!=x)
{
i=list[top--];
flog[i]=cnt;
v[i]=false;
}
}
}
int rd[21000],cd[21000];
int main()
{
//freopen("b.in","r",stdin);
//freopen("1.out","w",stdout);
scanf("%d",&t);
while(t--)
{
memset(fa,0,sizeof(fa));
memset(biao,0,sizeof(biao));cnt=0;len=0;id=0;
memset(last,0,sizeof(last));top=0;
memset(rd,0,sizeof(rd));memset(cd,0,sizeof(cd));
scanf("%d%d",&n,&m);
int ans1=0,ans2=0;
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(biao[i]==0)dfs(i);
}
if(cnt==1)
{
printf("0\n");
continue;
}
for(int i=1;i<=m;i++)
{
int tx=flog[a[i].x]/*縮點*/,ty=flog[a[i].y];
if(tx!=ty)
{
rd[ty]++;cd[tx]++;
}
}
for(int i=1;i<=cnt;i++)
{
if(rd[i]==0)ans1++;
if(cd[i]==0)ans2++;
}
printf("%d\n",mymax(ans1,ans2));
}
return 0;
}
2
我們先跑一遍二分匹配,然後把原本的邊反向建(母牛連向公牛),並且連一條邊,公牛連向他匹配的母牛,那麼再跑一邊強連通,我們就會發現每個分量裏面都是公牛->母牛->公牛->母牛…
也就是說每個母牛至少有兩個選擇,公牛也是,然後我們在找公牛能****(手動打碼)的每個母牛,如果母牛跟公牛在同一分量中,那麼這個母牛原本的公牛也可以在找另外一頭母牛,是不是很厲害?
#include<cstdio>
#include<cstring>
#include<algorithm>
#include<bitset>
using namespace std;
const int cc=4010;
struct node
{
int y,next;
}a[200010];int len,last[cc];
struct trlen
{
int x,y,next;
}map[200010];int tlen,tlast[cc];
int cnt,id,p;
int sta[cc],low[cc],dfn[cc],belong[cc];
int chw[cc],match[cc],n;
bool v[cc];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}
void ins1(int x,int y)
{
tlen++;
map[tlen].x=x;map[tlen].y=y;map[tlen].next=tlast[x];tlast[x]=tlen;
}
bool find(int x)
{
for(int k=tlast[x];k;k=map[k].next)
{
int y=map[k].y;
if(chw[y]!=id)
{
chw[y]=id;
if(match[y]==0 || find(match[y])==true)
{
match[y]=x;
return true;
}
}
}
return false;
}
void dfs(int x)
{
low[x]=dfn[x]=++id;v[x]=true;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(low[y]==0)
{
dfs(y);
low[x]=min(low[x],low[y]);
}
else if(v[y]==true)low[x]=min(low[x],dfn[y]);
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
do
{
now=sta[p--];
v[now]=false;
belong[now]=cnt;
}while(now!=x);
}
}
int main()
{
scanf("%d",&n);
for(int i=1;i<=n;i++)
{
int kkk=0;scanf("%d",&kkk);
for(int j=1;j<=kkk;j++)
{
int x;scanf("%d",&x);
ins1(i,x+n);
}
}
for(int i=1;i<=n;i++)
{
id++;
find(i);
}//二分匹配
for(int i=1;i<=n;i++)ins(match[i+n],i+n);
for(int i=1;i<=tlen;i++)ins(map[i].y,map[i].x);
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i);
}
for(int i=1;i<=n;i++)
{
int jj=belong[i],ansl[cc];
ansl[0]=0;
for(int j=tlast[i];j;j=map[j].next)
{
if(belong[map[j].y]==jj)ansl[++ansl[0]]=map[j].y-n;
}
sort(ansl+1,ansl+1+ansl[0]);
for(int j=1;j<ansl[0];j++)printf("%d ",ansl[j]);
printf("%d\n",ansl[ansl[0]]);
}
//輸出
return 0;
}
3
這道題比較簡單,如果一個強連通分量有邊連向其他分量,這個分量都沒用了。
#include<cstdio>
#include<cstring>
using namespace std;
inline int mymin(int x,int y){return x<y?x:y;}
int n,m;
int low[21000],dfn[21000],belong[21000],cnt,out[21000],stp;
int sta[21000],tp=0;bool v[21000];
struct node
{
int x,y,next;
}a[21000];int last[21000],len;
int ansl[21000];
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
void dfs(int x)
{
low[x]=dfn[x]=++stp;v[x]=true;sta[++tp]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(low[y]==0)
{
dfs(y);
low[x]=mymin(low[x],low[y]);
}
else if(v[y]==true)low[x]=mymin(low[x],dfn[y]);
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
while(now!=x)
{
now=sta[tp--];
belong[now]=cnt;
v[now]=false;
}
}
}
int main()
{
memset(v,true,sizeof(v));
while(scanf("%d",&n)!=EOF)
{
if(n==0)break;
scanf("%d",&m);
memset(low,0,sizeof(low));stp=0;
memset(last,0,sizeof(last));len=0;
memset(out,0,sizeof(out));
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);
}
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i);
}
for(int i=1;i<=m;i++)
{
if(belong[a[i].x]!=belong[a[i].y])out[belong[a[i].x]]++;
}
for(int i=1;i<=n;i++)
{
if(out[belong[i]]==0)ansl[++ansl[0]]=i;
}
for(int i=1;i<ansl[0];i++)printf("%d ",ansl[i]);
printf("%d\n",ansl[ansl[0]]);ansl[0]=0;
}
return 0;
}
雙連通分量
雙聯通分量就是無向圖的強連通分量
邊-雙連通分量
橋
如果原本兩個點是連通的,截斷一條邊就使得兩個點不聯通了,這條邊叫橋。
Tarjan算法
又是這個巨佬,邊-雙連通就是沒有橋的分量,比如:1-2-3-1,有橋嗎?沒有吧。
那麼,怎麼做成了關鍵,再看Tarjan過程,我們得設立個條件,兒子不能到達父親,然後繼續看。
原本的Tarjan算法可不可以再次利用,我們繼續看。
原本的強連通分量長這樣:1->2->3->1
但是如果單向邊全變成雙向邊,貌似就是邊-雙連通了呢。
而且我們規定兒子不能到父親,也就是沒有1-2的情況,那麼就成了!
並且可以去掉,爲什麼?因爲這是無向邊,不用你去找他,他就已經找了你了,如果你又找到了他,那你們肯定是一個分量的呀
練習
這次沒例題,直接放練習
像上次那樣,我們記錄每個分量的度(無向邊),爲0,ans+=2,爲1,ans++
然後答案爲ans/2+ans%2
#include<cstdio>
#include<cstring>
using namespace std;
inline int mymin(int x,int y){return x<y?x:y;}
int low[6000],dfn[6000],belong[6000],cnt,stp;
int sta[6000],tp;
struct node
{
int x,y,next;
}a[21000];int last[6000],len;
int ax[11000],ay[11000],n,m,io[11000],ans;
void ins(int x,int y)
{
len++;
a[len].x=x;a[len].y=y;a[len].next=last[x];last[x]=len;
}
void dfs(int x,int fa)
{
low[x]=dfn[x]=++stp;
sta[++tp]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=fa)
{
if(low[y]==0)
{
dfs(y,x);
low[x]=mymin(low[x],low[y]);
}
else low[x]=mymin(low[x],dfn[y]);
}
}
if(low[x]==dfn[x])
{
int now=0;cnt++;
while(now!=x)
{
now=sta[tp--];
belong[now]=cnt;
}
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
scanf("%d%d",&ax[i],&ay[i]);
ins(ax[i],ay[i]);ins(ay[i],ax[i]);
}
for(int i=1;i<=n;i++)
{
if(low[i]==0)dfs(i,0);
}
if(cnt==1){printf("0\n");return 0;}//特判
for(int i=1;i<=m;i++)
{
if(belong[ax[i]]!=belong[ay[i]])io[belong[ax[i]]]++,io[belong[ay[i]]]++;
}
for(int i=1;i<=cnt;i++)
{
if(io[i]==0)ans+=2;
else if(io[i]==1)ans++;
}
printf("%d\n",ans/2+ans%2);
return 0;
}
點雙連通分量
點強聯通太活躍了,要根據具體題目具體定。
而且一個點強連通分量一定是個邊強連通分量
一個點可能屬於多個點連通,但只能屬於一個邊連通
割點
就是把點割掉後,原本相連兩個點(不是被割掉的點)不相連了,就割點。
這有什麼好怕的?這還真的就有這麼可怕。
點雙連通分量
沒有例題。
自己創吧:
在一個無向圖,輸出所有的點雙連通分量。
輸入:
第一行輸入點數、邊數
接下來邊數行,每行x,y描述一條邊。
輸出:
第一行,點雙連通分量數量。
接下來每行輸出一個點雙連通分量。
輸入樣例:
5 6
1 2
2 3
3 1
3 4
4 5
3 5
輸出樣例:
2
1 2 3
3 4 5
怎麼做,我們還是一個同樣的味道,Tarjan算法。
比較麻煩的事,在點分量中,一個點可以重複。
我們發現,在邊分量裏面,紅藍是同一分量,但是在點分量裏面,是兩個分量,我們容易知道,只要一個點x的兒子y的low等於x的dfn,那麼x與y這顆字樹同屬於一個點雙連通分量。
代碼
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dfn[61000],cnt,low[61000],n,m,id;
struct node
{
int y,next;
}a[210000];int len,last[61000];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}
int sta[61000],p;
int ans[130000],wl[61000],wr[61000];
void dfs(int x,int fa)
{
dfn[x]=low[x]=++id;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=fa)
{
if(dfn[y]==0)
{
dfs(y,x);
low[x]=min(low[x],low[y]);
if(low[y]==dfn[x])//剛好到我這裏,那麼你是一個點強連通
{
int now=0;cnt++;
wl[cnt]=ans[0]+1;
while(now!=x && p>0)
{
now=sta[p--];
ans[++ans[0]]=now;
}
wr[cnt]=ans[0];
sta[++p]=x;//如果我能到我的上級,我還可以包括在我的上級的分量裏
}
}
else low[x]=min(low[x],dfn[y]);
}
}
if(low[x]==dfn[x])//我到不了上面,算了吧。
{
p--;
}
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);ins(y,x);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)dfs(i,0);
}
printf("%d\n",cnt);
for(int i=1;i<=cnt;i++)
{
for(int j=wl[i];j<wr[i];j++)printf("%d ",ans[j]);
printf("%d\n",ans[wr[i]]);
}
return 0;
}
求割點與橋
割點
我們研究DFS序就會發現,只要一個不是根結點的其中一個兒子的low全部小於等於他的dfn,那麼這個點就是割點,根節點就是他的子樹數量大於等於兩顆就是割點。
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
int dfn[61000],low[61000],n,m,id;
struct node
{
int y,next;
}a[210000];int len,last[61000];
void ins(int x,int y)
{
len++;
a[len].y=y;a[len].next=last[x];last[x]=len;
}
int sta[61000],p;
int ans[130000];
void dfs(int x,int fa)
{
bool bk=false;int cnt=0;
dfn[x]=low[x]=++id;sta[++p]=x;
for(int k=last[x];k;k=a[k].next)
{
int y=a[k].y;
if(y!=fa)
{
if(dfn[y]==0)
{
cnt++;
dfs(y,x);
low[x]=min(low[x],low[y]);
if(low[y]>=dfn[x]/*橋就是>*/)bk=true;
}
else low[x]=min(low[x],dfn[y]);
}
}
if(low[x]==dfn[x])
{
int now=0;
while(now!=x)now=sta[p--];
}
if(!fa)//根節點特判
{
if(cnt>=2)ans[++ans[0]]=x;
}
else if(bk==true)ans[++ans[0]]=x;
}
int main()
{
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++)
{
int x,y;scanf("%d%d",&x,&y);
ins(x,y);ins(y,x);
}
for(int i=1;i<=n;i++)
{
if(dfn[i]==0)dfs(i,0);
}
sort(ans+1,ans+ans[0]+1);
printf("%d\n",ans[0]);
for(int i=1;i<ans[0];i++)printf("%d ",ans[i]);
if(ans[0])printf("%d\n",ans[ans[0]]);
return 0;
}
橋
其實就是low不是大於等於dfn了,而是大於,以及根節點不用特判(畢竟找的是邊),然後就沒有然後了。
小結
又水了一篇博客