淺談強連通分量(Tarjan)
阿新 • • 發佈:2020-07-27
強連通分量\(\rm (Tarjan)\)
——作者:BiuBiu_Miku
\(1.\)一些術語
· 無向圖:指的是一張圖裡面所有的邊都是雙向的,好比兩個人打電話 \(U\) 可以打給 \(V\) 同時 \(V\) 也可以打給 \(U\)( 如圖1 就是一個無向圖)
· 有向圖:指的是一張圖裡面所有的邊都是單向的,好比一條單向的公路只能從 \(U→V\) 而不能從 \(V→U\) ( 如圖2 就是一個有向圖)
· 連通:指的是在 \(\rm \color{red}{無向圖}\) 中,任意節點 \(V\) 可以到達任意節點 \(U\) , 如圖1 中點 \(1\) 和點 \(2\) 可以互相到達 ,所以點 \(1\) 和點 \(2\) 是聯通的
· 強連通:指在 \(\rm \color{red}{有向圖}\) 中,某兩個點可以互相到達,比 如圖2 中 點 \(1\) 和點 \(2\) 就是可以互相到達對方的,雖然不是直接到達,但是可以到達,就將其稱為強連通
· 弱連通:指在 \(\rm \color{red}{有向圖}\) 中,某兩個點若本身不存在強連通關係,但是通過將其看成 \(\rm \color{red}{無向圖}\) 使其連通,則將他們稱為弱連通
· 強連通分量:指在 \(\rm \color{red}{有向圖}\) 中,一些節點存在強連通關係,如圖(2) 中節點 \(1,2,3,4\) , 節點 \(5\) , 節點 \(6\) , 節點 \(7\) 分別為圖中的四個強連通分量,強連通分量也可以是單獨一個節點
\(2.\)\(\rm Tarjan\)演算法的思想簡述:
· 我們定義兩個變數:
\(dfn[i]\) 表示節點 \(i\) 的時間戳(也就是dfs後序遍歷的順序)
\(low[i]\) 表示節點 \(i\) 可以通過一些節點找到比自己時間戳早的時間戳
比如說節點 \(U\) 的時間戳 \((dfn[U])\) 是 \(1\),節點 \(V\) 的時間戳是 \(3\) ,\(V\) 可以到達 \(U\) 則 \(V\) 的 \(low[V]\) 就是 \(1\)
· 關於此演算法的流程:
\(\rm Tarjan\) 演算法是一個通過對圖進行深度優先搜尋並通過同時維護一個棧以及兩個相關時間戳 (上面提到的兩個變數) 的演算法。
第一步:建圖
可以用鄰接矩陣,鏈式前向星,或者其他東西
\(\rm \color{red}{PS:一定是單向邊}\)
第二步:跑圖
用 \(dfs\) 從一個節點開始遍歷整張圖,與此同時更新時間戳。
在 \(dfs\) 過程中每遍歷到一個元素,就將其存到棧中,其主要維護的是上文提到的 \(low\) , 因為 \(dfn\) 是 \(dfs\) 的搜尋順序的時間戳,所以從有值之後基本上就不用變化了,而 \(low\) 不能被馬上確定,因為在 \(dfs\) 的遍歷中,也許當前節點可以到達比當前的 \(low\) 更前的節點,此時我們就要更新他的 \(low\) 變為更前的節點的遍歷時間,也就是 \(dfn\) 。
第三步:存強連通分量
當我們搜尋完之後,發現某個節點的 \(dfn\) 和 \(low\) 相等時,說明我們找到了一個強連通分量,因為當前節點不能再到達比自己更小的節點了,那麼此時,以這個節點為 \(low\) 值得節點自然不會再次被更新了,因為他是按 \(dfs\) 以後序遍歷,順序搜尋過來的,因此我們此時就可以開始存強連通分量了,其手段是利用棧將棧首元素進行儲存,之後彈出,直到棧首元素為當前點的值為止
我們可以以染色的手段來儲存強連通分量,每當找到一個強連通分量,就可以將其的每一個值作為下標,在陣列 \(color\) 中進行染色,其儲存的值一般為找到的強連通分量的編號,如:假設我找到了一個強連通分量為 \(7,8,9\) ,其又是第二個被找到的強連通分量,則將 \(color[7] color[8] color[9]\) 標為 \(2\)。
\(3.\)\(\rm Tarjan\)演算法的程式碼實現:
題目:在一個有向圖中,有n個節點,m條邊,現給出這m條邊,請輸出圖中所有的強連通分量。
\(\rm Code:\)
#include<bits/stdc++.h>
using namespace std;
int n,m;
struct edge{ //定義存邊的變數
int from,to,next;
} e[10005];
int head[10005];
int cnt;
void Insert(int x,int y){ //鏈式前向星存邊
e[++cnt].from=x;
e[cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
int dfn[10005]; //上文提到的dfn
int low[10005]; //上文提到的low
int t; //當前搜尋的時間,用於給時間戳dfn與low賦初始值
stack<int> s;
int p[100005]; //判斷某個元素在不在棧裡面
// int tot;
// int color[100005];
void Tarjan(int now){
s.push(now); //講當前元素放入棧
dfn[now]=low[now]=++t; //講當前搜尋的時間,也就是當前搜過了幾個點的數量賦值給時間戳dfn,同時對low進行初始化
p[s.top()]=true;
for(int i=head[now];i;i=e[i].next){ //鏈式前向星遍歷所有節點
int get=e[i].to;
if(!dfn[get]){ //判斷當前節點有沒有被搜尋過
Tarjan(get); //如果沒有,那就搜這個節點
low[now]=min(low[now],low[get]); //更新當前節點的low,為什麼不是 low[now]=min(low[now],dfn[get]); 呢?我們不妨觀察一下,low的值是不是永遠≤dfn的?此時既然now可以到達get,那麼now自然也可以到達get節點能到達的節點
}
else if(p[get]) low[now]=min(low[now],dfn[get]); //否則判斷當前節點在不在棧裡,如果不在,就不用理他,如果在那麼就可以更新一下當前節點,因為當前節點可以到達get,但此時的low不一定是最終得到的值,所以不能寫low[now]=min(low[now],low[get]);
}
if(dfn[now]==low[now]){ //如果兩個相等,說明當前節點不能再更新了,不能再找到比自己low更小的值
// tot++;
while(s.top()!=now){ //將棧首到當前元素的所有值彈出佇列,說明這堆東西就是一個強連通分量
printf("%d ",s.top());
// color[s.top]=tot;
p[s.top()]=false; //標記其不在棧裡
s.pop();
}
printf("%d",s.top()); //因為只是彈到當前節點,當前節點也是包含在這個強連通分量內的,所以再做一次
// color[s.top]=tot;
p[s.top()]=false;
s.pop();
printf("\n");
}
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=m;i++){
int x,y;
scanf("%d%d",&x,&y);
Insert(x,y); //存邊
}
for(int i=1;i<=n;i++) //因為圖不一定保證是連通的,有可能某個節點不被其他任何節點到達,所以要用for判斷一遍如果沒有被搜過就搜,搜過了就沒他什麼事了
if(!dfn[i])
Tarjan(i);
return 0;
}
\(4.\)強連通分量的應用:
· 縮點
因為強連通分量一般為一個環,所以在一些題目中,我們可以把這些環變成一個點來簡便搜尋,然後把縮小後的點再次連線,建一張新的圖,然後開始一系列操作。
參考例題:洛谷 P3387【模板】縮點
\(\rm Code:\)
#include<bits/stdc++.h>
#define MAXN 100005
using namespace std;
long long n,m;
struct edge{
long long from,to,next;
} e[MAXN];
long long head[MAXN];
long long cnt;
long long qlt;
void Insert(long long x,long long y){
e[++cnt].from=x;
e[cnt].to=y;
e[cnt].next=head[x];
head[x]=cnt;
}
long long dfn[MAXN];
long long low[MAXN];
long long t;
stack<int> s;
long long p[MAXN];
long long color[MAXN];
long long f[MAXN];
long long u[MAXN],v[MAXN],l[MAXN];
long long dis[MAXN];
long long mmax;
const long long oo=0x7f7f7f;
void Tarjan(long long now){
s.push(now);
dfn[now]=low[now]=++t;
p[now]=false;
for(long long i=head[now];i;i=e[i].next){
long long get=e[i].to;
if(!dfn[get]){
Tarjan(get);
low[now]=min(low[now],low[get]);
}
else if(!p[get]) low[now]=min(low[now],dfn[get]);
}
if(dfn[now]==low[now]){
qlt++;
while(s.top()!=now){
color[s.top()]=qlt;
p[s.top()]=true;
f[qlt]+=l[s.top()]; //儲存縮點後單點的點權
s.pop();
}
color[s.top()]=qlt;
p[s.top()]=true;
f[qlt]+=l[s.top()]; //同上,再做一次
s.pop();
}
}
void dfs(long long now) { //做一遍記憶化搜尋來更新從某一個節點的答案
dis[now]=f[now]; //初始化,自己的點權就是自己
long long mmmax=0;
for(long long i=head[now];i;i=e[i].next){ //遍歷每個節點
long long get=e[i].to;
if(!dis[get])dfs(get); //如果節點沒被搜過就搜
mmmax=max(mmmax,dis[get]); //更新最大值
}
dis[now]+=mmmax; //更新當前值
}
int main(){
scanf("%lld%lld",&n,&m);
for(long long i=1;i<=n;i++) scanf("%lld",&l[i]);
for(long long i=1;i<=m;i++){
scanf("%lld%lld",&u[i],&v[i]);
Insert(u[i],v[i]);
}
for(long long i=1;i<=n;i++)
if(!dfn[i])
Tarjan(i);
memset(e,0,sizeof(e)); //清零重新建圖
memset(head,0,sizeof(head));
cnt=0;
for(long long i=1;i<=m;i++)
if(color[u[i]]!=color[v[i]]) //建立縮點後的圖,如果兩點不在同一個強連通分量裡,說明兩個集合不連通,所以將其連通
Insert(color[u[i]],color[v[i]]);
for(long long i=1;i<=qlt;i++)
if(!dis[i]){
dfs(i); //如果當前節點沒被搜過,就進行記憶化搜尋
mmax=max(dis[i],mmax); //更新最大值
}
printf("%lld\n",mmax); //輸出答案
return 0;
}