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是一个强连通图.非强连通图有向图的极 ... 
随机推荐
- 循环神经网络 —— LSTM 图片
- CyberDog测试视频 —— 【开箱】小米"限量"机器狗!被我玩坏了...
			地址: https://www.youtube.com/watch?v=3ntAhy3thXM PS. 现在的智能机器人其实真的没有人们想象中的那么智能.感觉现在的智能机器人最为有用的功能一个是倒地自 ... 
- gym.ActionWrapper使用时的注意点——step函数可以覆盖observation函数
			本文说的这个gym.ActionWrapper继承类的问题和gym.ObservationWrapper继承类的问题性质是一样的,具体看: gym.ObservationWrapper使用时的注意点- ... 
- Apache DolphinScheduler如何开启开机自启动功能?
			转载自东华果汁哥 Apache DolphinScheduler 是一个分布式.去中心化的大数据工作流调度系统,支持大数据任务调度.若要设置 DolphinScheduler 开机自启动,通常需要将其 ... 
- 需要多久才能看完linux内核源码?
			代码中自由颜如玉!代码中自有黄金屋! 一.内核行数 Linux内核分为CPU调度.内存管理.网络和存储四大子系统,针对硬件的驱动成百上千.代码的数量更是大的惊人. 先说说最早的内核linux 0.11 ... 
- 开发一个MutatingWebhook
			介绍 Webhook就是一种HTTP回调,用于在某种情况下执行某些动作,Webhook不是K8S独有的,很多场景下都可以进行Webhook,比如在提交完代码后调用一个Webhook自动构建docker ... 
- Ubuntu 16.04 部署Mariadb
			默认上MariaDB的包并没有在Ubuntu仓库中.要安装MariaDB,我们要设置MariaDB仓库. sudo apt-get install software-properties-common ... 
- 基于python的文字转图片工具
			地址 https://hub.docker.com/r/rainsccc/strtoimg 拉取镜像后,可以启动一个容器来运行该应用程序.以下命令会启动容器并将其端口映射到主机上: docker ru ... 
- Python wheel
			在 Python 的生态系统中,wheel 是一种打包格式,用于分发和安装 Python 项目.它是 Python 包的标准格式之一,旨在提高安装速度和可靠性. Wheel 的优势 快速安装:因为 w ... 
- pip 安装包时提示 "WARNING: Skipping xxx due to invalid metadata entry 'name'"
			我最近在使用 pip 安装包的时候经常遇到如下警告: WARNING: Skipping /opt/homebrew/lib/python3.11/site-packages/numpy-1.26.3 ... 
