Tarjan
1. DFS
樹(深度優先搜索樹)
-
上圖右圖是左圖以
1
為起點進行DFS
時產生的生成樹。 -
有向圖的
DFS
生成樹主要有4
種邊(不一定全部出現):- 樹邊(
tree edge
):綠色邊,每次搜索找到一個還沒有訪問過的結點(白點)的時候就形成了一條樹邊。 - 返祖邊(
back edge
):黃色邊,也被叫做回邊,即指向祖先結點(灰點)的邊。 - 橫叉邊(
cross edge
):紅色邊,它主要是在搜索的時候遇到了一個已經訪問過(黑點dfn[u]>dfn[v]
)的結點,但是這個結點 並不是 當前結點的祖先時形成的。 - 前向邊(
forward edge
):藍色邊,它是在搜索的時候遇到子樹中的結點(黑點dfn[u]<dfn[v]
)的時候形成的。
- 樹邊(
-
無向圖不存在橫叉邊和前向邊。
2. Tarjan
算法求強連通分量
-
強連通分量(
Strongly Connected Components
),經常簡寫為:SCC
,有向圖中任意兩點間可達,實際上形成一個環。 -
Tarjan
基於對圖的深度優先搜索,並對每個節點引入兩個值:dfn[u]
:節點u
的時間戳,記錄點u
是DFS
過程中第幾個訪問的節點。low[u]
:記錄節點u
或u
的子樹不經過搜索樹上的邊(樹邊)能夠到達的時間戳最小的節點。- 初始時,
dfn[u]==low[u]
。
-
對於每一條與
u
相連的邊<u,v>
:- 若在搜索樹上
v
是u
的子節點,即邊<u,v>
是樹枝邊,則更新low[u]= min(low[u], low[v])
; - 若
<u,v>
不是搜索樹上的邊(反向邊),則更新low[u]= min(low[u], dfn[v])
;
- 若在搜索樹上
-
縮點
- 在有向圖中,我們經常需要把一個
SCC
縮成一個點,然后生成一個有向無環圖(DAG
),或把一個無向圖縮點后變成一棵樹,然后可以有很多優秀的性質進行解決。 - 算法實現:
- 從圖的某一點
u
開始,對圖進行DFS(u)
,點維護dfn[u]
值和low[u]
值。 DFS
時先將u
壓入棧中,然后遍歷鄰接邊,鄰接邊定點為v
:<u,v>
為樹邊:DFS(v)
,回溯時更新:low[u]=min(low[u],low[v])
。<u,v>
為返祖邊:直接更新:low[u]=min(low[u],dfn[v])
。
- 節點
u
變黑,即其所有子樹訪問結束時,若dfn[u]==low[u]
時,此時棧頂節點到節點u
,為一個SCC
。
- 從圖的某一點
- 在有向圖中,我們經常需要把一個
-
例題:縮點(洛谷p3387)
Description
- 給定一個
n
個點m
條邊有向圖,每個點有一個權值,求一條路徑,使路徑經過的點權值之和最大。你只需要求出這個權值和。 - 允許多次經過一條邊或者一個點,但是,重復經過的點,權值只計算一次。
Input
- 第一行兩個正整數
n,m
。 - 第二行
n
個整數,依次代表點權 - 第三至
m+2
行,每行兩個整數u,v
,表示一條 \(u\rightarrow v\) 的有向邊。
Output
- 共一行,最大的點權之和。
Sample Input
2 2 1 1 1 2 2 1
Sample Output
2
Hint
- 對於 \(100\%\) 的數據,\(1\le n \le 10^4\),\(1\le m \le 10^5\),點權$ \in [0,1000]$。
分析:
-
題目說可重復的經過同一個點和邊,但權值只算一次,如果圖是一個強連通圖的話,顯然每個點我們都能走一遍,答案就是所有點的點權和。
-
我們先對圖進行
Tanjan
縮點並維護每個點的權值和sum[]
,縮點后,在原圖的基礎上我們建出新的DAG
圖。 -
在新的
DAG
圖中,我們可以進行拓撲排序+dp
,定義dp[i]
表示以節點i
為起點的最大點權和,轉移方程:dp[i]=max(dp[j]+sum[i])
,j
為i
的子節點。 -
Code
#include <bits/stdc++.h> const int maxn=100000+5 struct edge{ int from,to, next; }e[maxn << 1],g[maxn << 1];//e原圖,g存儲新圖DAG int lene,leng, heade[maxn],headg[maxn];//圖e和g的邊表 int Time, dfn[maxn], low[maxn], vis[maxn],s[maxn],top;//s是模擬棧,要全局 int dp[maxn], n, m, sum[maxn], a[maxn], tot, belong[maxn]; void Inserte(int x, int y){ e[++lene].from=x;e[lene].to =y;e[lene].next=heade[x], heade[x] = lene; }//原圖 void Insertg(int x, int y){ g[++leng].from=x;g[leng].to =y;g[leng].next=headg[x], headg[x] = leng; }//新圖 void tarjan(int u){ dfn[u] = low[u] = ++Time;//初始化 vis[u] = 1;s[++top]=u;//標記並進棧 for (int i = heade[u]; i; i = e[i].next){ int v = e[i].to; if (!dfn[v]) {//v為白點 tarjan(v); low[u] = std::min(low[u], low[v]);//<u,v>為樹枝子節點low值更新父節點low值 }//否則有可能是返祖邊 else if (vis[v]) low[u] = std::min(low[u], dfn[v]); } if(dfn[u] == low[u]){//縮點 ++tot;//記錄新圖節點數 while(s[top+1]!=u){//從棧頂到u的點縮成一個新點tot int v=s[top]; belong[v]=tot;vis[v]=0;sum[tot]+=a[s[top--]]; }//belong表示強連通分量編號,vis表示是否在棧中,sum表示強連通分量權值和 } } void dfs(int u){ if (dp[u]) return; dp[u] = sum[u]; for (int i = headg[u]; i; i = g[i].next){ int v = g[i].to; dfs(v); dp[u] = std::max(dp[u], dp[v] + sum[u]);//這里是記憶化 } } int main(){ scanf("%d%d",&n,&m); for (int i = 1; i <= n; ++i) scanf("%d",&a[i]); for (int i = 1; i <= m; ++i){ int x,y;scanf("%d%d",&x,&y); Inserte(x,y); } for (int i = 1; i <= n; ++i) if (!dfn[i]) tarjan(i); leng = 0; memset(headg, 0, sizeof(headg)); for (int i = 1; i <= m; ++i){//遍歷原始邊建新圖 int u=e[i].from,v=e[i].to; if (belong[u] != belong[v]) //判斷邊的兩個端點是否在同一個新點中 Insertg(belong[u], belong[v]);//不在就建一條邊 } int ans=0; for (int i = 1; i <= tot; ++i) if (!dp[i]){ dfs(i); ans = std::max(ans, dp[i]); } printf("%d\n", ans); return 0; }
- 給定一個