kosaraju 和 tarjan演算法詳解(強連通分量)
定 義
在有向圖G中,如果任意兩個不同的頂點相互可達,則稱該有向圖是強連通的。
有向圖G的極大強連通子圖稱為G的強連通分支。
轉置圖的定義:將有向圖G中的每一條邊反向形成的圖稱為G的轉置GT 。(注意到原圖和GT 的強連通分支是一樣的)
Korasaju演算法
1.深度優先遍歷G,算出每個結點u的結束時間f[u],起點如何選擇無所謂。
每個結點的結束時間和開始時間是dfs序,開始時間是此點第一次被遍歷到時,結束時間為此點已經沒法擴充,從棧中彈出,即已經遍歷結束,不懂dfs序,可以看這個dfs序
2.深度優先遍歷G的轉置圖GT ,選擇遍歷的起點時,按照結點的結束時間從大到小進行。遍歷的過程中,
一邊遍歷,一邊給結點做分類標記,每找到一個新的起點,分類標記值就加1。
如果此節點已經在反圖中遍歷過,就不再從它遍歷,挑選下一個結束時間晚的
3. 第2步中產生的標記值相同的結點構成深度優先森林中的一棵樹,也即一個強連通分量
至於證明。。。emmm,我沒看懂,就自己想了一下
如果正圖中 b->a,那麼反圖中為 a->b,而b的結束時間一定比a要晚,所以先遍歷b,如果b->a,則說明在原圖中a->b
#include<bits/stdc++.h>
using namespace std;
int f[1000][1000];//儲存正圖
int rf[1000][1000];//儲存反圖
int vis[1000];
stack<int> s;//用來儲存節點離開時間
stack<int> s1[1000];
int n;
//對原圖dfs,找出每個節點的離開時間,用棧儲存,直接pop,就不用再逆序
void dfs(int a){
vis[a]=1;
for(int i=1;i<=n;i++){
if(f[a][i]&&!vis[i]){
dfs(i);
}
}
s.push(a);
}
void rdfs(int a,int k){
vis[a]=1;
for(int i=1;i<=n;i++){
if(rf[a][i]&&!vis[i]){
rdfs(i,k);
}
}
s1[k].push(a);//對每個連通分量分支,記錄下來
}
int main(){
int m;
cin>>n>>m;
for(int i=1;i<=m;i++){
int x,y;
cin>>x>>y;
f[x][y]=1;
rf[y][x]=1;
}
//對原圖dfs
for(int i=1;i<=n;i++){
if(!vis[i])
dfs(1);
}
memset(vis,0,sizeof(vis));
int k=0;
//對反圖dfs
while(!s.empty()){
if(!vis[s.top()])//節點按照節點離開時間從大到小遍歷
rdfs(s.top(),++k);//計算連通分量個數
s.pop();
}
cout<<k<<endl;
//輸出
for(int i=1;i<=k;i++){
while(!s1[i].empty()){
cout<<s1[i].top();
s1[i].pop();
}
cout<<endl;
}
return 0;
}
用鄰接表寫的程式碼(老師寫的)
#include <cstdio>
#include <cstring>
const int V = 1e4 + 7, E = 5e4 + 7;
int hd[V], to[E], fr[E], nt[E], pr[E], tl[E];
int n, m, id[V], vis[V], cnt[V], scc;
bool out[V];
void dfs(int u, int &clk) {//用引用,減少全域性變數的使用
id[u] = 1;
for (int i = hd[u]; i; i = nt[i])
if (!id[to[i]]) dfs(to[i], clk);
vis[++clk] = u;//當一個節點是葉子節點時,記錄結束時間且只記錄結束時間
}
void rdfs(int v, int clk) {
id[v] = clk, ++cnt[clk];
for (int i = tl[v]; i; i = pr[i])
if (!id[fr[i]]) rdfs(fr[i], clk);
}
void Korasaju() {
for (int i = 1, clk = 0; i <= n; ++i) if (!id[i]) dfs(i, clk);//最晚結束時間必然是n
for (int i = 1; i <= n; ++i) id[i] = 0;
for (int i = n; i; --i)
if (!id[vis[i]]) rdfs(vis[i], ++scc);
}
int main() {
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nt[i] = hd[u], pr[i] = tl[v], hd[u] = tl[v] = i;
to[i] = v, fr[i] = u;//同時儲存正向與反向圖
}
int ans = 0;
Korasaju();
//自此,產生了一個以強連通分量編號作為結點,構成的一個DAG圖
for (int i = 1; i <= n; ++i)
for (int j = hd[i]; j; j = nt[j])
if (id[i] != id[to[j]]) out[id[i]] = true;
//如果度用++統計的話,需要考慮兩個連通分量之間存在重複邊(此題並不需要)
for (int i = 1; i <= scc; ++i)
if (!out[i]) {
if (ans) { puts("0"); return 0; }
ans = cnt[i];
}
printf("%d\n", ans);
return 0;
}
tarjan演算法(很重要)
low陣列和dfn陣列,dfn是第一次搜到此點的時間戳,low值是此點所連線的點中最小dfn
棧用來儲存搜尋到的點
沿著起點一直搜尋,如果搜到返祖邊,則將此點的low值更改,low[u]=min(low[u],dfn[v])
至於為什麼是dfn[v],不是low[v]?
low[v]並不是最終結果,不該使用,就算low[v]=dfn[v],也不使用他
當此點無法再搜下去,比較dfn與low值,相等則說明棧中從此點一直到棧頂為一個連通分量,將這些從棧中彈出,然後回溯,回溯時比較點的low值,再次更新low值,low[u]=min(low[u],low[v])
至於縮點只要將一個分量的染色就好了
有用的定理
有向無環圖中唯一出度為0的點,一定可以由任何點出發均可達(由於無環,所以從任何點出發往前走,必然終止於
一個出度為0的點)
因為強連通所有點都可互相到達,可以把強連通分量縮成一個點,如果有唯一的一個點的出度為0,那麼這個分量的個數即為答案
如果不止一個出度為0,則無解。
#include<iostream>
#include<vector>
#include<algorithm>
#include<stack>
using namespace std;
int n,m;
const int e=1e4+4;
//標記時間戳
int low[e],dfn[e],index;
//tarjan中的棧,和判斷是否在棧中
stack<int> s;
bool ins[e];
//標記所屬分量,用於縮點
int col[e];
//計算有幾個分量
int cnt;
//每個分量中的成員
vector<int> ve[e];
//鄰接表
struct edge{
int to,next;
};
edge f[5*e];
int head[e],id;
void add(int from,int to){
f[++id].next=head[from];
f[id].to=to;
head[from]=id;
}
//Tarjan
void Tarjan(int u){
dfn[u]=low[u]=++index;
s.push(u);
ins[u]=1;
for(int i=head[u];i;i=f[i].next){
int v=f[i].to;
if(!dfn[v])//沒有深搜的點
Tarjan(v),low[u]=min(low[u],low[v]);//回溯階段,無法繼續探索,則從底往上逐級返回//low值
else if(ins[v])
low[u]=min(low[u],dfn[v]);//在棧中的點
}
//當此點全部探索完成,low值依然沒有改變,說明為聯通分量的根節點 ,可以出棧
if(dfn[u]==low[u]){
++cnt;
int t;
do{
t=s.top();
s.pop();
ins[u]=0;//標記出棧
ve[cnt].push_back(t);
col[t]=cnt;//縮點(染色標記)
}while(t!=u);
}
}
int main(){
cin>>n>>m;
for(int i=1;i<=m;i++){
int a,b;
cin>>a>>b;
add(a,b);
}
for(int i=1;i<=n;i++){
if(!dfn[i])
Tarjan(i);
}
int out[e]={0};//統計每個聯通塊的出度
for(int i=1;i<=n;i++){
for(int j=head[i];j;j=f[i].next){
int to=f[i].to;
if(col[i]!=col[to])//不是同一個連通塊,則出度+1
++out[col[i]];
}
}
int sum=0,ans;
for(int i=1;i<=cnt;i++){
if(!out[i]) sum++,ans=ve[i].size();
}
if(sum==1)
cout<<ans<<endl;
else
cout<<0<<endl;
return 0;
}
對於tarjan中的棧的用處,有個部落格寫的特別詳細
https://blog.csdn.net/qq_38234381/article/details/79981531
用陣列模擬棧的寫法(老師寫的)
#include <cstdio>
#include <algorithm>
const int V = 1e4 + 7, E = 5e4 + 7;
//hd-head, fr-from, nt-next, pr-pre, tl-tail
int hd[V], to[E], nt[E], lbl[V], cnt[V], low[V], dfn[V], stk[V], tp, scc;
bool out[V];
void Tarjan(int u, int &clk){
low[u] = dfn[u] = ++clk;
stk[++tp] = u;
for (int i = hd[u]; i; i = nt[i])
if (not dfn[to[i]])
Tarjan(to[i], clk), low[u] = std::min(low[u], low[to[i]]);
else if (not lbl[to[i]])
low[u] = std::min(low[u], dfn[to[i]]);
if (low[u] == dfn[u])
for (++scc; stk[tp+1] != u; --tp)
lbl[stk[tp]] = scc, ++cnt[scc];
}
int main() {
int n, m;
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nt[i] = hd[u], hd[u] = i, to[i] = v;
}
for (int i = 1, clk = 0; i <= n; ++i)
if (not lbl[i]) Tarjan(i, clk);
for (int i = 1; i <= n; ++i)
for (int j = hd[i]; j; j = nt[j])
if (lbl[i] != lbl[to[j]]) out[lbl[i]] = true;
int ans = 0;
for (int i = 1; i <= scc; ++i)
if (not out[i]) {
if (ans) { puts("0"); return 0; }
ans = cnt[i];
}
printf("%d\n", ans);
return 0;
}
縮點+建圖
用pair儲存每條邊的兩個端點,重點在於處理自環邊和重複邊,用sort處理,重新建圖
#include <cstdio>
#include <queue>
#include <algorithm>
const int N = 5e5 + 7;
int hd[N], to[N], fr[N], nx[N], stk[N], tp, scc;
int low[N], dfn[N], id[N], sum[N], w[N], ans;
bool bar[N], inq[N], stop[N];
std::pair<int, int> eg[N];
void Tarjan(int u, int &clk) {
low[u] = dfn[u] = ++clk;
stk[++tp] = u;
for (int i = hd[u], v; i; i = nx[i])
if (!dfn[v=to[i]])
Tarjan(v, clk), low[u] = std::min(low[u], low[v]);
else if (!id[v])
low[u] = std::min(low[u], dfn[v]);
if (low[u] == dfn[u])
for (++scc; stk[tp+1] != u; --tp)
id[stk[tp]] = scc, sum[scc] += w[stk[tp]], stop[scc] |= bar[stk[tp]];
}
int dis[N];
void spfa(int s) {
std::queue<int> que;
que.push(s), ans = dis[s] = sum[s];
while (que.size()) {
int u = que.front(); que.pop();
inq[u] = false;
for (int i = hd[u]; i; i = nx[i]) {
int v = to[i], tot = dis[u] + sum[v];
if (dis[v] < tot) {
dis[v] = tot;
if (stop[v]) ans = std::max(ans, dis[v]);
if (not inq[v]) que.push(v);
}
}
}
}
int main() {
int n, m, s, p;
scanf("%d%d", &n, &m);
for (int i = 1, u, v; i <= m; ++i) {
scanf("%d%d", &u, &v);
nx[i] = hd[u], hd[u] = i, to[i] = v, fr[i] = u;
}
for (int i = 1; i <= n; ++i) scanf("%d", w+i);
scanf("%d%d", &s, &p);
for (int i = 1, x; i <= p; ++i)
scanf("%d", &x), bar[x] = true;
for (int i = 1, clk = 0; i <= n; ++i) if (!dfn[i]) Tarjan(i, clk);
//縮點建新圖
for (int i = 1; i <= m; ++i)
eg[i].first = id[fr[i]], eg[i].second = id[to[i]];
std::sort(eg+1, eg+m+1);
for (int i = 1; i <= scc; ++i) hd[i] = 0;
for (int i = 1; i <= m; ++i) {
int u = eg[i].first, v = eg[i].second;
int uu = eg[i-1].first, vv = eg[i-1].second;
if (u!=v && (u!=uu || v!=vv))//清除自環邊和重複邊
nx[i] = hd[u], hd[u] = i, to[i] = v;
}
//在新圖上直接跑spfa
spfa(id[s]);
printf("%d\n", ans);
return 0;
}
相關文章
- 連通圖演算法詳解之① :Tarjan 和 Kosaraju 演算法演算法
- 強連通分量-tarjan演算法模板詳解演算法
- 圖之強連通、強連通圖、強連通分量 Tarjan演算法演算法
- 強連通分量(Tarjan演算法)演算法
- Tarjan演算法(強連通分量分解)演算法
- 強連通演算法--Tarjan個人理解+詳解演算法
- 圖論——強連通分量(Tarjan演算法)圖論演算法
- Tarjan演算法求強連通分量總結演算法
- Tarjan演算法三大應用之強連通分量演算法
- 強連通分量及縮點tarjan演算法解析演算法
- 【模板】tarjan 強連通分量縮點
- Tarjan 求有向圖的強連通分量
- 尋找圖的強連通分量:tarjan演算法簡單理解演算法
- POJ 2186 Popular Cows(強連通分量縮點,Tarjan演算法)演算法
- 強連通------tarjan演算法詳解及與縮點聯合運用演算法
- 20行程式碼實現,使用Tarjan演算法求解強連通分量行程演算法
- 圖論複習之強連通分量以及縮點—Tarjan演算法圖論演算法
- 強連通分量與縮點(Tarjan演算法)(洛谷P3387)演算法
- 連通圖與Tarjan演算法演算法
- 強連通分量
- HDU 2767 Proving Equivalences Tarjan 強連通縮點UI
- 演算法學習之路|強連通分量+縮點演算法
- 【筆記】tarjian演算法 求強連通分量筆記演算法
- 【演算法學習】tarjan 強連通、點雙、邊雙及其縮點 重磅來襲!!!!演算法
- 強連通分量及縮點 演算法解析及例題演算法
- kosaraju 演算法演算法
- 全網最!詳!細!Tarjan演算法講解。演算法
- 強連通圖的演算法演算法
- Trajan演算法(強連通+縮點)演算法
- HDU2767Proving Equivalences[強連通分量 縮點]UI
- 求有向圖的強連通分量(c語言版)C語言
- Tarjan演算法及其應用 總結+詳細講解+詳細程式碼註釋演算法
- 我對Kosaraju演算法的理解演算法
- 演算法學習筆記:Kosaraju演算法演算法筆記
- Tarjan演算法_縮點演算法
- 抓間諜(強連通)
- 軟連結和硬連結詳解
- 連線池和連線數詳解