前言
Tarjan 算法是图论中非常著名和常用的算法之一,能解决最近公共祖先(LCA),强连通分量,割点和割边(桥),双连通分量等问题。
Tarjan 是基于dfs搜索的算法,依据递归栈,时间戳等信息能解决多种问题。在dfs中每个点只递归一次,边也只利用一次,因此时间复杂度为 O ( n + m ) {O(n+m)} O(n+m)。
本文主要以记录模板为主,不做详细教学,需要读者有一定的dfs基础,希望读者能总结出属于自己的模板。
在求强联通分量处,给出了另一种kosaraju算法的模板。
本文并没有求双联通分量的模板,以后有机会会给出。
最近公共祖先 (LCA)
LCA - Lowest Common Ancestors
- 建树的边 (双向)
- 建查询的边 (双向)
- dfs
- 入 vis 遍历子节点dfs
- 回 并查集nex存cur
- 离 枚举查询组 借助并查集的路径压缩
视频讲解:
练习题:
// P3379 【模板】最近公共祖先(LCA)
#include <bits/stdc++.h>
using namespace std;
const int M = 10 + 500000;
vector<vector<int>> graph(M); // 存图(树)
vector<vector<pair<int, int>>> query(M); // 询问
int father[M]; // 并查集
bool vis[M]; // vis
int ans[M]; // 第几组询问的答案
void initUnionFind(int n) {
for (int i = 0; i <= n; i++) {
father[i] = i;
}
}
// 必须路径压缩
int unionFind(int x) {
return x == father[x] ? x : father[x] = unionFind(father[x]);
}
// Tarjan 算法
void Tarjan(int cur) {
// dfs 入
vis[cur] = true;
for (int& nex : graph[cur]) {
if (!vis[nex]) {
Tarjan(nex);
// dfs 回
father[nex] = cur;
}
}
// dfs 离开
for (auto& it : query[cur]) {
int &to = it.first, &idx = it.second;
// 若访问过,则可以记录
if (vis[to]) {
ans[idx] = unionFind(to);
}
}
}
int main() {
// 边数 询问数 根节点
int n, m, root;
scanf("%d %d %d", &n, &m, &root);
initUnionFind(n);
// 存无向图
for (int i = 1, a, b; i <= n - 1; i++) {
scanf("%d %d", &a, &b);
graph[a].emplace_back(b);
graph[b].emplace_back(a);
}
// 询问 也要双向存
for (int i = 1, a, b; i <= m; i++) {
scanf("%d %d", &a, &b);
query[a].emplace_back(b, i);
query[b].emplace_back(a, i);
}
Tarjan(root);
for (int i = 1; i <= m; i++) {
printf("%d\n", ans[i]);
}
return 0;
}
强连通分量
tarjan
视频讲解:
思想:
在递归站中的low值相等的点是一个强联通分量
dfn值和low值相等的是该分量的代表点
练习题:
杭电:迷宫城堡 - 1269
洛谷:P3387 【模板】缩点
/**
* https://www.luogu.com.cn/problem/P3387
* P3387 【模板】缩点
*
* tarjan 求强连通分量
* 然后缩点构造DAG图
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
// tarjan 标配
vector<vector<int>> graph; // 存图
vector<int> dfn; // dfs被访问的时间点
vector<int> low; // 通过回溯可以到达的最早时间点
int timestamp = 1; // 时间戳
// 求强连通分量 标配
vector<int> scc; // 强联通分量
stack<int> stk; // 递归栈
vector<bool> inStk; // 快速辨别是都在递归栈中
void tarjan(int cur) {
dfn[cur] = low[cur] = timestamp++;
stk.push(cur);
inStk[cur] = true;
for (int& nex : graph[cur]) {
if (dfn[nex] == 0) {
// 未访问则搜索一次
tarjan(nex);
low[cur] = min(low[cur], low[nex]);
} else if (inStk[nex]) {
// 在栈中,也要松弛一次
low[cur] = min(low[cur], dfn[nex]);
}
}
// 自己的dfn和low相同,则构成一个强联通分量
if (dfn[cur] == low[cur]) {
int x = -1;
do {
x = stk.top();
stk.pop();
inStk[x] = false;
scc[x] = cur;
} while (x != cur);
}
}
signed main() {
int n, m;
cin >> n >> m;
graph.resize(n + 1);
dfn.resize(n + 1);
low.resize(n + 1);
timestamp = 1;
scc.resize(n + 1, -1);
inStk.resize(n + 1);
vector<int> val(n + 1); // 点权
vector<int> from(m + 1); // 出发点
vector<int> to(m + 1); // 到达点
// 点权
for (int i = 1; i <= n; i++) {
cin >> val[i];
}
// 建图,单向图
for (int i = 1; i <= m; i++) {
cin >> from[i] >> to[i];
graph[from[i]].emplace_back(to[i]);
}
// 跑tarjan 获得强连通分量
for (int i = 1; i <= n; i++) {
if (dfn[i] == 0) {
tarjan(i);
}
}
/** ******** tarjan 跑完,获得强连通分量 ****************************/
/** ******** 根据强连通分量,缩点构造DAG图 ***************************/
// 先将每个强联通分量的点权集中到代表点上
for (int i = 1; i <= n; i++) {
if (scc[i] != i) { // 代表点不用重复加
val[scc[i]] += val[i];
}
}
unordered_map<int, vector<int>> dagGraph;
for (int i = 1; i <= m; i++) {
int &u = from[i], &v = to[i];
// 不在一个分量中
if (scc[u] != scc[v]) {
dagGraph[scc[u]].emplace_back(scc[v]);
}
}
function<int(int)> dfsDag = [&](int cur) -> int {
int sum = 0;
for (int& nex : dagGraph[cur]) {
// 获得子树的最大值,还可以记忆化优化下
sum = max(sum, dfsDag(nex));
}
return val[cur] + sum;
};
int maxx = 0;
// 遍历每个点,可能有某点的是单独的分量没有边
for (int i = 1; i <= n; i++) {
if (scc[i] == i) {
maxx = max(maxx, dfsDag(scc[i]));
}
}
cout << maxx << endl;
return 0;
}
kosaraju
另一种求强连通分量的方法
- 建立正向和反向图
- 先dfs反向图
- 在正向图中逆序dfs反向图的结果,并记录每个点在哪个联通分量中
/**
* https://www.luogu.com.cn/problem/P3387
* P3387 【模板】缩点
*
* kosaraju 求强连通分量
* 然后缩点构造DAG图
*/
#include <bits/stdc++.h>
#define int long long
using namespace std;
// kosaraju 标配
vector<vector<int>> forwardGraph; // 正向图
vector<vector<int>> reverseGraph; // 反向图
vector<int> scc; // 强连通分量
vector<int> vis; // vis标记
stack<int> stk; // 反图入栈
void reverseDFS(int cur) {
vis[cur] = true;
for (int& nex : reverseGraph[cur]) {
if (!vis[nex]) {
reverseDFS(nex);
}
}
// 访问的点依次入栈
stk.push(cur);
}
void forwardDFS(int cur, int father) {
vis[cur] = true;
scc[cur] = father; // 记录是哪个强连通分量
for (int& nex : forwardGraph[cur]) {
if (!vis[nex]) {
forwardDFS(nex, father);
}
}
}
signed main() {
int n, m;
cin >> n >> m;
forwardGraph.resize(n + 1);
reverseGraph.resize(n + 1);
scc.resize(n + 1);
vis.resize(n + 1);
vector<int> val(n + 1); // 点权
vector<int> from(m + 1); // 出发点
vector<int> to(m + 1); // 到达点
// 记录点权
for (int i = 1; i <= n; i++) {
cin >> val[i];
}
// 正向反向图同时建立
for (int i = 1; i <= m; i++) {
cin >> from[i] >> to[i];
forwardGraph[from[i]].push_back(to[i]);
reverseGraph[to[i]].push_back(from[i]);
}
fill(vis.begin(), vis.end(), false);
// 反向图遍历
for (int i = 1; i <= n; i++) {
if (!vis[i]) {
reverseDFS(i);
}
}
fill(vis.begin(), vis.end(), false);
// 逆序遍历反向图的结果
// 目的是获得强连通分量scc
while (!stk.empty()) {
int cur = stk.top();
stk.pop();
if (!vis[cur]) {
forwardDFS(cur, cur);
}
}
/** ************** kosaraju 跑完,获得强连通分量 *********************/
/** ******** 根据强连通分量,缩点构造DAG图 ***************************/
// 先将每个强联通分量的点权集中到代表点上
for (int i = 1; i <= n; i++) {
if (scc[i] != i) { // 代表点不用重复加
val[scc[i]] += val[i];
}
}
unordered_map<int, vector<int>> dagGraph;
for (int i = 1; i <= m; i++) {
int &u = from[i], &v = to[i];
// 不在一个分量中
if (scc[u] != scc[v]) {
dagGraph[scc[u]].emplace_back(scc[v]);
}
}
function<int(int)> dfsDag = [&](int cur) -> int {
int sum = 0;
for (int& nex : dagGraph[cur]) {
// 获得子树的最大值,还可以记忆化优化下
sum = max(sum, dfsDag(nex));
}
return val[cur] + sum;
};
int maxx = 0;
// 遍历每个点,可能有某点的是单独的分量没有边
for (int i = 1; i <= n; i++) {
if (scc[i] == i) {
maxx = max(maxx, dfsDag(scc[i]));
}
}
cout << maxx << endl;
return 0;
}
割点 割边
视频讲解:
算法轻松掌握tarjan割点&桥算法_哔哩哔哩_bilibili
算法轻松掌握tarjan割点&桥算法_5_code实现_哔哩哔哩_bilibili
割点:
- cur != root && cur 有儿子 && low[nex] >= dfn[cur]
- cur == root && cur 有儿子数量 >= 2
割边:
- low[nex] > dfn[cur]
割点
练习题:
// P3388 【模板】割点(割顶)
// 写法挺多的,以后总结出自己的模板
#include <bits/stdc++.h>
using namespace std;
const int M = 10 + 2 * 10000;
vector<vector<int>> graph(M); // 存图
int dfn[M]; // dfs被访问的时间点
int low[M]; // 通过回溯可以到达的最早时间点
int father[M]; // 记录父节点
bool cut[M]; // 是否是割点
int timestamp = 1; // 时间戳
void tarjan(int cur) {
dfn[cur] = low[cur] = timestamp++;
int child = 0;
for (auto& nex : graph[cur]) {
if (dfn[nex] == 0) {
// 未访问则搜索一次
child++, father[nex] = cur;
tarjan(nex);
// 是根节点,并且孩子个数大于等于2
if (-1 == father[cur] && child >= 2) {
cut[cur] = true;
}
// 不是根节点,但是孩子不可以回溯的更高
if (-1 != father[cur] && low[nex] >= dfn[cur]) {
cut[cur] = true;
}
// // 是割边(桥)
// if (low[nex] > dfn[cur]) {
// // 每条边只会被访问一次,直接存
// //
// 子树的回溯值比当权点的时间戳大于等于,则表示不会回溯到更先的节点,则是割边
// }
low[cur] = min(low[cur], low[nex]);
} else if (nex != father[cur]) {
// 访问过,但是并不是父节点
low[cur] = min(low[cur], dfn[nex]);
}
}
}
int main() {
memset(dfn, 0, sizeof(dfn));
memset(low, 0, sizeof(low));
memset(father, -1, sizeof(father));
memset(cut, false, sizeof(cut));
int n, m;
scanf("%d %d", &n, &m);
// 无向图
for (int i = 1, from, to; i <= m; i++) {
scanf("%d %d", &from, &to);
graph[from].emplace_back(to);
graph[to].emplace_back(from);
}
for (int i = 0; i <= n; i++) {
if (dfn[i] == 0) {
tarjan(i);
}
}
// 统计割点数量
int sum = 0;
for (int i = 1; i <= n; i++) {
sum += cut[i];
}
printf("%d\n", sum);
for (int i = 1; i <= n; i++) {
if (cut[i]) {
printf("%d ", i);
}
}
return 0;
}
割边 (桥)
练习题:
class Solution {
private:
vector<vector<int>> graph;
vector<int> dfn; // 时间戳
vector<int> low; // 回溯值
int timestamp = 1;
vector<vector<int>> ans;
void tarjan(int cur, int pre) {
dfn[cur] = low[cur] = timestamp++;
for (int& nex : graph[cur]) {
// 因为是图,防止环
if (nex == pre) {
continue;
}
if (dfn[nex] == 0) {
// 还未dfs过
tarjan(nex, cur);
// 子节点回溯不到cur之前
// 注意没有=因为可能有环
if (low[nex] > dfn[cur]) {
ans.emplace_back(vector<int>{cur, nex});
}
// 利用子节点的回溯值更新
low[cur] = min(low[cur], low[nex]);
} else {
// 不是父节点
low[cur] = min(low[cur], dfn[nex]);
}
}
}
public:
vector<vector<int>> criticalConnections(int n, vector<vector<int>>& connections) {
graph.resize(n);
dfn.resize(n);
low.resize(n);
// 存无向图
for (vector<int>& arr : connections) {
graph[arr[0]].emplace_back(arr[1]);
graph[arr[1]].emplace_back(arr[0]);
}
for (int i = 0; i < n; i++) {
if (dfn[i] == 0) {
tarjan(i, -1);
}
}
return ans;
}
};