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是一个强连通图.非强连通图有向图的极 ...
随机推荐
- 大语言模型(LLM)运行报错:AttributeError: module 'streamlit' has no attribute 'cache_resource'
解决方法: https://blog.csdn.net/javastart/article/details/130785100 (图:https://blog.csdn.net/javastart/a ...
- 从0实现基于Linux socket聊天室-多线程服务器一个很隐晦的错误-2
根据 <0 基于socket和pthread实现多线程服务器模型>所述,server创建子线程的时候用的是以下代码: pconnsocke = (int *) malloc(sizeof( ...
- 【2】Kaggle 医学影像数据读取
赛题名称:RSNA 2024 Lumbar Spine Degenerative Classification 中文:腰椎退行性病变分类 kaggle官网赛题链接:https://www.kaggle ...
- Windows中Powershell中的 rm -rf 等效命令
Remove-Item -Recurse -Force <要删除的目录> 可以简写为: rm -r -fo <要删除目录>
- CentOS7 压缩及打包的常用命令
gzip gzip 文件名 压缩文件 gzip -d 文件名 解压文件 gunzip 文件名 解压文件 gzip -1 #压缩级别 最高到9 默认是6级别 gzip -f # 强制覆盖同名压缩包 gz ...
- 【Python自动化】之特殊的自动化定位操作
今天有时间了,想好好的把之前遇到过的自动化问题总结一下,以后有新的总结再更新 目录: 一.上传文件(4.11) 二.下拉框选择(4.11) 1.Select下拉框 2.非Select下拉框 三.下拉框 ...
- Spring框架之IOC介绍
Spring之IOC 简介 首先,官网中有这样一句话:Spring Framework implementation of the Inversion of Control (IoC) princip ...
- Cloudflare D1 - 免费数据存储
前言 自从上次将博客项目的图片从 七牛云 迁到了 Cloudflare R2 之后就发现,Cloudflare 这个赛博菩萨的产品是真的不错,非常的适合白嫖,DevNow 项目作为一个开源博客,整体来 ...
- Angular Material 18+ 高级教程 – Custom Themes for Material Design 3 (自定义主题 Material 3)
v18 更新重要说明 从 Angular Material v18 开始,默认使用的是 Material 3 Design (简称 M3). 而且是正式版,不再是 experimental previ ...
- CSS & JS Effect – Hero Banner Swiper
效果 重点 1. 一张图片, 一个 content 定位居中作为一个 slide 2. slider 用了 JavaScript Library – Swiper 3. 当 slide active ...