強、雙連通學習筆記

有向圖強連通分量

有向圖強連通分量就是在一個強連通分量裏面,每個點都能到達分量裏面其他所有點。

那麼,如何求?

Tarjan算法

例題

定義

我們定義一個low數組與一個dfs數組與一個ts(時間戳,不需要過多理解,下文看了就知道功能了)

做法

我們要明白Tarjan算法其實就是利用DFS序來完成工作的。

而且一個點只能在一個強連通分量裏面。

首先,我們找到一個點x,然後low[x]=dfs[x]=++tslow[x]=dfs[x]=++ts,這個時候,然後我們用x繼續去找其他點。

找到了一個沒被找過的點(dfn[y]==0dfn[y]==0),對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完後low[x]=min(low[x],low[y]);low[x]=min(low[x],low[y]);豈不妙哉?

4、4找到了孫子或更低的3,這時,4找完了兒子,兒子也認4爲兒子…,總之4在3下面,所以4找過了,老道理,4還是在一個已經找完的強連通分量,但是我們不能像以前low[4]=dfn[3]low[4]=dfn[3]了,因爲3的DFS序大於4的,我們會發現,一個點的low必須小於等於dfn,難道要low[4]=low[3]low[4]=low[3],不用,我們會發現,等4的兒子便歷完後傳回來的low[y],其實就已經包括了low[3]low[3]了,所以我們只需讓low[4]=min(low[4],dfn[3])low[4]=min(low[4],dfn[3])

那麼,通過上面我們可以知道,如果我們找到了一個點,沒被找過,我們就讓他去DFS,並且更新,找過了,但所在的分量沒更新完,也是這樣更新。

重點來了。

一個分量怎樣算找完?

我們設立一個棧,每次找到一個點就把他推入棧,當low[x]==dfn[x]low[x]==dfn[x]時,我們就把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

1

這道題目難度有點大,我們做一遍Tarjan算法,然後把每個強連通分量當成一個點,計算每個點的入度與出度,我們需要知道,爲什麼這些點(我們已經把所有強連通分量縮點了)不在一個強連通分量裏面?

比如:
在這裏插入圖片描述
我們可以姑且的認爲,一個長得像1&gt;2&gt;3&gt;4&gt;...1-&gt;2-&gt;3-&gt;4-&gt;...的點叫僞點(非專業術語)

而一個僞點一般有一個點入度爲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

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

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的情況,那麼就成了!

並且if(!v[x])if(!v[x])可以去掉,爲什麼?因爲這是無向邊,不用你去找他,他就已經找了你了,如果你又找到了他,那你們肯定是一個分量的呀

練習

這次沒例題,直接放練習

練習

像上次那樣,我們記錄每個分量的度(無向邊),爲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了,而是大於,以及根節點不用特判(畢竟找的是邊),然後就沒有然後了。

小結

又水了一篇博客

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