Tarjan
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> using namespace std; const int maxn = 1e4 + 5, maxm = 1e5 + 5; struct Edge{ int from, to, next; }e1[maxm], e2[maxm]; int len1, len2, head1[maxn], head2[maxn]; void Insert(int u, int v, Edge e[], int &len, int head[]){ e[++len].from = u; e[len].to = v; e[len].next = head[u]; head[u] = len; } int n, m, a1[maxn], a2[maxn]; void Read(){ scanf("%d%d", &n, &m); for(int i = 1; i <= n; ++i) scanf("%d", &a1[i]); for(int i = 1; i <= m; ++i){ int u, v; scanf("%d%d", &u, &v); Insert(u, v, e1, len1, head1); } } int dfn[maxn], low[maxn], belong[maxn], Time, cnt; bool vis[maxn];//vis[i]=1表示i节点在栈里 int st[maxn], top;//数组模拟栈 void Tarjan(int u){ dfn[u] = low[u] = ++Time; st[++top] = u; vis[u] = 1; for(int i = head1[u]; i; i = e1[i].next){ int v = e1[i].to; if(!dfn[v]){ Tarjan(v); low[u] = min(low[u], low[v]); }//v不在栈是反向边 else if(vis[v]) low[u] = min(low[u], dfn[v]); } if(low[u] == dfn[u]){ cnt++;//增加一个新图节点 while(st[top + 1] != u){ int v = st[top--]; vis[v] = 0; a2[cnt] += a1[v]; belong[v] = cnt;//记录原图节点v在新图中的节点编号 } } } int dp[maxn];//dp[i]表示以节点i开始的路径的最大点权和 int res = 0;//存储答案 void dfs(int u){ if(dp[u]) return;//记忆化 dp[u] = a2[u]; for(int i = head2[u]; i; i = e2[i].next){ int v = e2[i].to; dfs(v); dp[u] = max(dp[u], dp[v] + a2[u]); // res = max(res, dp[u]); } res = max(res, dp[u]);//不要写在上面,避免欧拉回路 } /* 3 4 1 2 3 1 3 1 2 2 1 3 2 */ void Print(int a[]){ for(int i = 1; i <= n; ++i) printf("%d ", a[i]); printf("\n--------\n"); } void sol(){ Read(); for(int i = 1; i <= n; ++i){ if(!dfn[i]) Tarjan(i); } for(int i = 1; i <= len1; ++i){ int u = belong[e1[i].from], v = belong[e1[i].to]; if(u != v) Insert(u, v, e2, len2, head2); } for(int i = 1; i <= cnt; ++i) if(!dp[i]) dfs(i); printf("%d\n", res); } int main(){ sol(); return 0; }
- 给定一个
hzoi