Tarjan算法缩点
Tarjan算法缩点
一.Tarjan算法缩点详解
在图论中,缩点是指将有向图中的强联通分量(SCCs)缩成单个节点,从而得到一个更简单的图结构,称为缩点图或SCC图。Tarjan算法不仅可以用来寻找强联通分量,还可以用来进行缩点操作。
基本概念
- 强联通分量:在一个有向图中,如果一组节点中任意两个节点都可以互相到达,那么这组节点就构成了一个强联通分量。
- 缩点:将每个强联通分量视为一个单一的节点,并重新构建图结构。
算法步骤
-
使用Tarjan算法找到所有强联通分量:
- 初始化DFS序和low值。
- 使用栈来记录当前路径上的节点。
- 递归访问每个节点,并更新low值。
- 当发现一个节点的DFS序等于其low值时,找到一个强联通分量。
-
构建缩点图:
- 为每个强联通分量创建一个新节点。
- 遍历原图中的每条边,如果边的两个端点属于不同的强联通分量,则在缩点图中添加一条对应的新边。
二.Tarjan缩点后的点排列顺序是逆拓扑序的解释
在使用Tarjan算法进行缩点后,得到的缩点图(SCC图)中的节点排列顺序实际上是逆拓扑序。为了理解这一点,我们需要回顾Tarjan算法的工作原理以及拓扑排序的概念。
基本概念
- 强联通分量(SCC):在一个有向图中,如果一组节点中任意两个节点都可以互相到达,那么这组节点就构成了一个强联通分量。
- 缩点:将每个强联通分量视为一个单一的节点,并重新构建图结构。
- 拓扑排序:在一个有向无环图(DAG)中,拓扑排序是对节点的一种线性排序,使得对于每一条有向边 (u, v),节点 u 在排序中都出现在节点 v 之前。
- 逆拓扑序:与拓扑排序相反的顺序,即对于每一条有向边 (u, v),节点 v 在排序中都出现在节点 u 之前。
Tarjan算法与逆拓扑序的关系
Tarjan算法在寻找强联通分量的过程中,会使用深度优先搜索(DFS)来遍历图中的节点。在DFS的过程中,当一个节点的所有邻接节点都被访问完毕后,该节点才会被标记为一个强联通分量的结束点。因此,最后一个被标记的强联通分量实际上是缩点图中的一个源节点(即没有入边的节点)。
由于Tarjan算法是深度优先的,最后一个被标记的强联通分量在缩点图中没有入边,这意味着它在拓扑排序中应该是第一个节点。因此,Tarjan算法标记强联通分量的顺序实际上是逆拓扑序。
解释
-
DFS遍历顺序:
- Tarjan算法从任意一个未访问的节点开始进行DFS。
- 当访问到一个节点 u 时,算法会递归访问 u 的所有邻接节点 v。
- 当所有邻接节点都被访问完毕后,节点 u 才会被标记为一个强联通分量的结束点。
-
强联通分量的标记顺序:
- 由于DFS的性质,最后一个被标记的强联通分量在缩点图中没有入边。
- 这意味着最后一个被标记的强联通分量在拓扑排序中应该是第一个节点。
-
逆拓扑序:
- 因此,Tarjan算法标记强联通分量的顺序实际上是逆拓扑序。
三.最短路径问题与拓扑排序和动态规划的结合
最短路径问题是图论中的一个经典问题,通常使用Dijkstra算法、Bellman-Ford算法或Floyd-Warshall算法来解决。然而,在某些特定情况下,特别是对于有向无环图(DAG),我们可以结合拓扑排序和动态规划来高效地求解最短路径问题。
基本概念
- 最短路径问题:在加权图中找到从一个源节点到目标节点的最短路径。
- 拓扑排序:在一个有向无环图(DAG)中,拓扑排序是对节点的一种线性排序,使得对于每一条有向边 (u, v),节点 u 在排序中都出现在节点 v 之前。
- 动态规划:一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法。动态规划通常用于优化问题,如最短路径、最长递增子序列等。
结合应用场景
在有向无环图中,由于没有环的存在,我们可以利用拓扑排序来确定节点的处理顺序,然后使用动态规划来计算最短路径。
示例:DAG中的最短路径
考虑一个有向无环图(DAG),我们需要找到从源节点到目标节点的最短路径。这个问题可以通过结合拓扑排序和动态规划来解决。
-
拓扑排序:
- 首先对图进行拓扑排序,得到节点的线性顺序。
- 拓扑排序确保了在处理每个节点时,其所有前驱节点都已经被处理过。
-
动态规划:
- 使用动态规划数组
dp[v]
表示从源节点到节点v
的最短路径长度。 - 按照拓扑排序的顺序处理每个节点
v
,并更新其所有后继节点w
的dp
值:dp[w] = min(dp[w], dp[v] + weight(v, w))
- 最终,
dp[target]
就是从源节点到目标节点的最短路径长度。
- 使用动态规划数组
伪代码
function shortest_path(graph, source, target):
// 进行拓扑排序
sorted_nodes = topological_sort(graph)
// 初始化动态规划数组
dp = array of size V initialized to infinity
dp[source] = 0
// 按照拓扑排序的顺序处理每个节点
for each v in sorted_nodes:
for each (v, w) in graph.edges:
if dp[v] != infinity:
dp[w] = min(dp[w], dp[v] + weight(v, w))
return dp[target]
结论
通过上述分析,我们可以得出结论:Tarjan算法缩点后的点排列顺序是逆拓扑序。这是因为Tarjan算法在DFS遍历过程中,最后一个被标记的强联通分量在缩点图中没有入边,从而在拓扑排序中应该是第一个节点。
题目描述
题目:缩点
给定一个 \(n\) 个点 \(m\) 条边有向图,每个点有一个权值,求一条路径,使路径经过的点权值之和最大。你只需要求出这个权值和。
允许多次经过一条边或者一个点,但是,重复经过的点,权值只计算一次。
输入格式
第一行两个正整数 \(n,m\)
第二行 \(n\) 个整数,其中第 \(i\) 个数 \(a_i\) 表示点 \(i\) 的点权。
第三至 \(m+2\) 行,每行两个整数 \(u,v\),表示一条 \(u\rightarrow v\) 的有向边。
输出格式
共一行,最大的点权之和。
样例 #1
样例输入 #1
2 2
1 1
1 2
2 1
样例输出 #1
2
提示
对于 \(100\%\) 的数据,\(1\le n \le 10^4\),\(1\le m \le 10^5\),\(0\le a_i\le 10^3\)。
代码:
#include<bits/stdc++.h>
#define int long long
using namespace std;
const int N = 100010; // 定义常量N,表示图中节点的最大数量
vector<int> g[N]; // 邻接表存储图
int dfn[N], low[N], ins[N], idx; // Tarjan算法所需变量
int bel[N], cnt; // bel[u]表示节点u属于哪个强联通分量,cnt表示强联通分量的数量
int sz[N]; // sz[i]表示第i个强联通分量中所有节点的权值之和
int n, m; // n表示节点数,m表示边数
int a[N]; // 每个节点的权值
stack<int> stk; // 栈,用于Tarjan算法
vector<int> ans[N]; // 存储每个强联通分量中的节点
int dp[N]; // dp[i]表示以第i个强联通分量为起点的最长路径的权值和
int way[N]; // way[i]表示以第i个强联通分量为起点的最长路径的数量
int vis[N]; // vis[i]表示第i个强联通分量是否已经被访问过
int res = 0, w = 0; // res表示最长路径的权值和,w表示最长路径的数量
// Tarjan算法求强联通分量
void tarjan(int u) {
dfn[u] = low[u] = idx++; // 初始化dfn和low
ins[u] = true; // 标记节点u在栈中
stk.push(u); // 将节点u入栈
for (auto v : g[u]) { // 遍历节点u的所有邻接节点
if (!dfn[v]) tarjan(v); // 如果节点v未访问过,递归访问
if (ins[v]) low[u] = min(low[u], low[v]); // 更新low值
}
if (dfn[u] == low[u]) { // 如果dfn[u]等于low[u],说明找到了一个强联通分量
cnt++; // 强联通分量数量加1
while (true) {
int t = stk.top(); // 取出栈顶节点
stk.pop(); // 弹出栈顶节点
sz[cnt] += a[t]; // 累加强联通分量的权值和
bel[t] = cnt; // 标记节点t属于当前强联通分量
ans[cnt].push_back(t); // 将节点t加入当前强联通分量
ins[t] = false; // 标记节点t不在栈中
if (t == u) break; // 如果栈顶节点是u,结束循环
}
}
}
signed main() {
cin >> n >> m; // 输入节点数和边数
for (int i = 1; i <= n; i++) cin >> a[i]; // 输入每个节点的权值
for (int i = 1; i <= m; i++) { // 输入每条边
int u, v;
cin >> u >> v;
g[u].push_back(v); // 添加边到邻接表
}
for (int i = 1; i <= n; i++) { // 对每个未访问的节点进行Tarjan算法
if (!dfn[i]) tarjan(i);
}
int tt = 0; // 用于标记访问次数
for (int i = 1; i <= cnt; i++) { // 对每个强联通分量进行处理
way[i] = 1; // 初始化路径数量为1
dp[i] = 0; // 初始化路径权值和为0
tt++; // 访问次数加1
for (auto u : ans[i]) { // 遍历当前强联通分量中的所有节点
for (auto v : g[u]) { // 遍历节点u的所有邻接节点
if (bel[u] != bel[v] && vis[bel[v]] < tt) { // 如果邻接节点v不在当前强联通分量且未被访问过
vis[bel[v]]++; // 标记邻接节点v所在的强联通分量已被访问
if (dp[i] < dp[bel[v]]) dp[i] = dp[bel[v]], way[i] = 0; // 更新最长路径权值和和路径数量
if (dp[i] == dp[bel[v]]) way[i] = (way[i] + way[bel[v]]); // 累加路径数量
}
}
}
dp[i] += sz[i]; // 加上当前强联通分量的权值和
if (dp[i] > res) res = dp[i], w = 0; // 更新最长路径权值和和路径数量
if (dp[i] == res) w = (w + way[i]); // 累加路径数量
}
cout << res << endl; // 输出最长路径的权值和
}