Tarjan 算法
远古算法笔记。
dfs 生成树
无向图
对于一张连通的无向图,我们可以从任意一点开始 dfs,得到原图的一棵生成树(以开始 dfs 的那个点为根)。
这棵生成树上的边称作树边,不在生成树上的边称作非树边。

由于 dfs 的性质,我们可以保证所有边连接的两个点都满足一个是另一个的祖先。
如果存在边 \((u, v)\),假设在 dfs 中先访问到了 \(u\) 点,而 \(v\) 还没有访问过,\(u\) 开始遍历它的子树,此时有两种可能:
\(u\) 在遍历 \(v\) 之前的儿子时没有到过 \(v\),则 \(u\) 就会通过 \((u,v)\) 到达 \(v\),那么 \(v\) 就成为了 \(u\) 的儿子。
\(u\) 在遍历 \(v\) 之前的儿子时到过 \(v\),那么 \(v\) 就会成为 \(u\) 的某一个儿子的后代,所以 \(v\) 也是 \(u\) 的子孙。
这就证明了所有边连接的两个点都满足一个是另一个的祖先,同时我们也可以发现一个点一定他的所有后代先访问到,即 \(dfn_u \leq dfn_v(v \in T(u))\),这里 \(T(u)\) 表示 \(u\) 子树内的所有结点。
有向图
有向图的 dfs 生成树在实现上和无向图类似,也是从任意一点开始 dfs 得到的一棵生成树。

我们可以把图中的边分成 \(4\) 类:
树边(tree edge):每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。
反祖边(back edge):也被叫做回边,即指向祖先结点的边,如示意图中的 \((4,1)\)。
前向边(forward edge):它是在搜索的时候遇到子树中的结点的时候形成的,如示意图中的 \((1,3)\)。
横叉边(cross edge):它是在搜索的时候遇到了一个已经访问过的结点,但是这个结点并不是当前结点的祖先或子孙,如示意图中的 \((6,4)\)(注意这类边在无向图中是不存在的,但在有向图中可能存在)。
因为每一个点 \(u\) 都是从 \(fa_u\) 过来的,所以也存在 \(dfn_u \leq dfn_v(v \in T(u))\)。
桥
【洛谷 P1656】
桥(bridge):在无向联通图中如果删去这条边就会使图不连通的边。
给出一张无向联通图,求该图的桥。
我们先求出图的 dfs 生成树,定义 \(fa_u\) 为结点 \(u\) 的父亲,\(dfn_u\) 为到达结点 \(u\) 的时间,\(low_u\) 为所有 \(u\) 能通过它的子孙到达的 \(dfn\) 值最小的结点的 \(dfn\) 值。
如图,设 \(dfn_i= i\),则 \(low_1 = low_2 = low_4 = 1, low_3 = 3, low_5 = 5\)。

根据 \(low\) 的定义可以得到,\(low_u \leq low_v (v \in T(u))\)。
由于一个点向上只能到达它的祖先,而它的祖先的 \(dfn\) 都小于它的 \(dfn\),那么对于一个点 \(u\),如果通过它的子孙,至少够到达它的祖先 \(v\),那么 \(low_u \leq dfn_v < dfn_u\)。
反之,如果 \(dfn_u=low_u\),说明它通过子孙不能到达他的祖先,那么如果没有一条边 \((u,fa_u)\),它和它的祖先就不连通。
所以,我们计算每一个点的 \(dfn\) 值,\(low\) 值,如果 \(dfn_u=low_u\) 且 \(fa_u\) 存在,则 \((u, fa_u)\) 是该图的桥。
Code
/**
* author: hztmax0
* created: 18.05.2023
**/
#include <iostream>
#include <algorithm>
#include <vector>
using namespace std;
using Pr = pair<int, int>;
const int N = 152;
int n, m;
int now, dfn[N], low[N];
vector<int> e[N];
vector<Pr> ans;
int Dfs (int u, int fa) {
if (!dfn[u]) {
dfn[u] = ++now;
low[u] = dfn[u];
for (auto v : e[u]) {
if (v != fa) {
low[u] = min(low[u], Dfs(v, u));
}
}
cout << u << ' ' << low[u] << ' ' << dfn[u] << '\n';
if (dfn[u] == low[u] && fa) {
ans.push_back({min(u, fa), max(u, fa)});
}
}
return low[u];
}
int main () {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
Dfs(1, 0);
sort(ans.begin(), ans.end());
for (auto i : ans) {
cout << i.first << ' ' << i.second << '\n';
}
return 0;
}
割点
【洛谷 P3388】
割点(cut vertex):若删除某点以及其所有连边后,原本其所在图被分为至少两个图,这些图互相不能到达,则该点为割点(注意图不一定联通)。
给出一个 \(n\) 个点,\(m\) 条边的无向图,求图的割点。
我们还是使用求桥的方式计算出每个点的 \(dfn, low\)。
考虑生成树上一点 \(u\),如果存在 \(u\) 的儿子 \(v\), \(low_v \geq dfn_u\),那么 \(v\) 最多只能到达 \(u\), 而不能到达 \(u\) 的祖先,此时我们称点 \(u\) 堵住了点 \(v\)。
对于每一个点 \(u\),我们计算它能堵住的点的个数,记作 \(d\),然后分两种情况讨论:
若 \(u\) 是生成树的根,\(u\) 已经没有祖先了,它的儿子能不能到达无所谓。但如果他堵住了两个以上的儿子 ,即 \(d_u \geq 2\),这些儿子之间互相不能到达,此时 \(u\) 是图的割点。
若 \(u\) 不是生成树的根,当 \(u\) 堵住了至少一个儿子,即 \(d_u \geq 1\) 时,至少有一个儿子不能到达 \(u\) 的祖先,此时 \(u\) 是图的割点。
所以,当一个非根的点 \(d\) 至少为 \(1\),或根结点的 \(d\) 值至少为 \(2\) 时,这个点是一个割点。
Code
/**
* author: hztmax0
* created: 08.06.2023
**/
#include <iostream>
#include <vector>
#include <algorithm>
using namespace std;
const int N = 2e4 + 5;
int n, m, r;
int now, dfn[N], low[N], v[N];
vector<int> e[N], ans;
int Dfs (int u, int fa) {
if (dfn[u]) {
return dfn[u];
}
dfn[u] = ++now;
low[u] = dfn[fa];
for (auto v : e[u]) {
low[u] = min(low[u], Dfs(v, u));
}
if (v[u] - (u == r) >= 1) {
ans.push_back(u);
}
v[fa] += (low[u] >= dfn[fa]);
return low[u];
}
int main () {
cin >> n >> m;
for (int i = 1; i <= m; i++) {
int u, v;
cin >> u >> v;
e[u].push_back(v);
e[v].push_back(u);
}
for (r = 1; r <= n; r++) {
if (!dfn[r]) {
Dfs(r, r);
}
}
cout << ans.size() << '\n';
sort(ans.begin(), ans.end());
for (auto u : ans) {
cout << u << ' ';
}
return 0;
}
缩点
【洛谷 P3387】
与前面一样,我们先求出每个点的 \(dfn\) 和 \(low\),考虑将一个环上的点全部缩到环上 \(dfn\) 最小的结点中,这里我们认为一个不在任何环上的点自己构成一个环。
因为是有向图,注意通过横叉边可以到达一个已经被缩的点,这时我们不能通过这个已经被缩的点更新 \(low\) 值,而前向边会通往自己的子孙,这时 \(low\) 值不会更新,这种情况可以忽略不记。
我们维护一个栈,访问到一个点就把这个点加入栈中,当栈尾构成一个环时,我们就把环上的所有元素退栈。
对于一个点 \(u\),如果 \(low_u < dfn_u\),那么 \(u\) 必定可以通过子孙到达自己的祖先,而它的祖先也可以到它自己,所以 \(u\) 与它的祖先构成一个环。
由于 \(u\) 的祖先在环内,\(u\) 肯定不是环中 \(dfn\) 最小的,所以我们把 \(u\) 留在栈中,等待它的祖先来缩掉。
否则 \(dfn_u =low_u\),说明 \(u\) 不能通过子孙到达自己的祖先,它只能和它的子孙在一个环中,那么栈中从 \(u\) 到栈尾的元素构成一个环。
且因为环上的都是 \(u\) 的子孙,所以 \(u\) 是环上 \(dfn\) 最小的,我们把环上的所有元素从栈中取出,并用一个点 \(u\) 表示这个环。
Code
/**
* author: hztmax0
* created: 28.05.2023
**/
#include <iostream>
#include <vector>
using namespace std;
const int N = 1e4 + 5;
int n, m;
int a[N];
int now, dfn[N], low[N], rt[N], d[N], f[N];
vector<int> e[N];
int st[N], tp;
int q[N], head, tail;
int Dfs (int u, int fa) {
dfn[u] = ++now;
low[u] = dfn[u];
st[++tp] = u;
for (auto v : e[u]) {
if (!dfn[v]) {
low[u] = min(low[u], Dfs(v, u));
}
else if (!rt[v]) {
low[u] = min(low[u], low[v]);
}
}
if (dfn[u] == low[u]) {
for (int v; v = st[tp--]; ) {
rt[v] = u;
if (u == v) break;
for (auto i : e[v]) {
e[u].push_back(i);
}
e[v].clear();
a[u] += a[v];
}
}
return low[u];
}
int 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;
e[u].push_back(v);
}
for (int i = 1; i <= n; i++) {
if (!dfn[i]) Dfs(i, 0);
}
head = 1, tail = 0;
for (int i = 1; i <= n; i++) {
for (auto &j : e[i]) {
j = rt[j];
d[j] += (i != j);
}
}
for (int i = 1; i <= n; i++) {
if (!d[i] && rt[i] == i) {
q[++tail] = i;
}
}
while (head <= tail) {
int i = q[head];
head++;
for (auto j : e[i]) {
d[j]--;
if (!d[j]) {
q[++tail] = j;
}
}
}
int ans = 0;
for (int k = tail; k >= 1; k--) {
int i = q[k];
for (auto j : e[i]) {
f[i] = max(f[i], f[j]);
}
f[i] += a[i];
ans = max(ans, f[i]);
}
cout << ans;
return 0;
}
Tarjan 算法的更多相关文章
- 有向图强连通分量的Tarjan算法
有向图强连通分量的Tarjan算法 [有向图强连通分量] 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G ...
- 点/边 双连通分量---Tarjan算法
运用Tarjan算法,求解图的点/边双连通分量. 1.点双连通分量[块] 割点可以存在多个块中,每个块包含当前节点u,分量以边的形式输出比较有意义. typedef struct{ //栈结点结构 保 ...
- 割点和桥---Tarjan算法
使用Tarjan算法求解图的割点和桥. 1.割点 主要的算法结构就是DFS,一个点是割点,当且仅当以下两种情况: (1)该节点是根节点,且有两棵以上的子树; (2)该节 ...
- Tarjan算法---强联通分量
1.基础知识 在有向图G,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极大强连通子 ...
- (转载)LCA问题的Tarjan算法
转载自:Click Here LCA问题(Lowest Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u, v)(通常查询数量较大),每次求树T中两 ...
- 强连通分量的Tarjan算法
资料参考 Tarjan算法寻找有向图的强连通分量 基于强联通的tarjan算法详解 有向图强连通分量的Tarjan算法 处理SCC(强连通分量问题)的Tarjan算法 强连通分量的三种算法分析 Tar ...
- [知识点]Tarjan算法
// 此博文为迁移而来,写于2015年4月14日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vxnx.html UPD ...
- Tarjan 算法&模板
Tarjan 算法 一.算法简介 Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度. 我们定义: 如果两个顶点可以相互通达,则称两个顶点强连 ...
- 【小白入门向】tarjan算法+codevs1332上白泽慧音 题解报告
一.[前言]关于tarjan tarjan算法是由Robert Tarjan提出的求解有向图强连通分量的算法. 那么问题来了找蓝翔!(划掉)什么是强连通分量? 我们定义:如果两个顶点互相连通(即存在A ...
- 有向图强连通分量 Tarjan算法
[有向图强连通分量] 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极 ...
随机推荐
- 【C】Re05 指针
一.变量 & 指针 变量 = 内存地址 + 存储值 指针变量 = 内存地址 + 存储值[变量的内存地址] 作用: 间接访问内存地址 内存地址 = 地址编号 地址编号:内存中的每个字节唯一的编号 ...
- 【OracleDB】 07 分组查询 & 分组函数
分组函数 分组函数作用于一组数据,并对一组数据返回一个值. Oracle中分组函数的种类: - 求平均值 AVG - 计数记录数 COUNT - 求最大值 MAX - 求最小值 MIN - 求和 SU ...
- 在哲学/自然科学范畴下“推理”(reason about)的类别及解释
注意,本文的解释采用Google大模型(Gemini)的答案. 翻译: 推理是运用逻辑和证据得出结论的过程.它包含批判性地思考一个主题,考虑不同的观点,以及识别事物之间的关系.以下是推理的一些方式: ...
- 神州笔记本 win11 节能模式 供电不足 自动关机
刚刚买了一个神州笔记本没几天,用着用着就出现问题了. 本人使用电脑有个极为不好的习惯,那就是会一次性打开特别多的应用,然后不关,一直留着,这个习惯虽然不好但也是一直没有啥问题的,不过最近换了个新的笔记 ...
- 阿里提供的免费DNS服务器
阿里提供的免费DNS服务器的介绍网页: https://developer.aliyun.com/mirror/DNS nameserver 223.5.5.5 nameserver 223.6.6. ...
- baselines算法库的安装——Ubuntu20.04系统使用anaconda环境配置
baselines算法库不做过多介绍,个人认为这是公开是所有reinforcement learning算法库中最权威的,虽然没有后期更新,也就是没有了学术界的state of the art , ...
- Ubuntu系统:NVIDIA显卡关闭图形显示 —— 彻底禁用NVIDIA GPU 的显示输出接口 —— ubuntu无桌面方式启动 —— NVIDIA显卡模式切换(显示模式切换为计算模式)
相关: ubuntu desktop改用无桌面方式启动 在使用Linux做异构计算等科学计算的时候一个常见的问题就是: NVIDIA显卡关闭图形显示 -- 彻底禁用NVIDIA GPU 的显示输出接口 ...
- Apache DolphinScheduler支持Flink吗?
随着大数据技术的快速发展,很多企业开始将Flink引入到生产环境中,以满足日益复杂的数据处理需求.而作为一款企业级的数据调度平台,Apache DolphinScheduler也跟上了时代步伐,推出了 ...
- SQL Server序列号的获取
建表: 1 USE [JX_IMS_CPK] 2 GO 3 4 SET ANSI_NULLS ON 5 GO 6 7 SET QUOTED_IDENTIFIER ON 8 GO 9 10 CREATE ...
- [原创] 域格CLM920模组使用pppd专网拨号
域格CLM920模组使用pppd专网拨号 参考资料 参照<9X07 模块LINUX集成用户手册版本_V1.60>的第四章节使用pppd拨号上网 编写chat脚本chat-script 要点 ...