远古算法笔记。

dfs 生成树

无向图

对于一张连通的无向图,我们可以从任意一点开始 dfs,得到原图的一棵生成树(以开始 dfs 的那个点为根)。

这棵生成树上的边称作树边,不在生成树上的边称作非树边

由于 dfs 的性质,我们可以保证所有边连接的两个点都满足一个是另一个的祖先。

如果存在边 \((u, v)\),假设在 dfs 中先访问到了 \(u\) 点,而 \(v\) 还没有访问过,\(u\) 开始遍历它的子树,此时有两种可能:

  1. \(u\) 在遍历 \(v\) 之前的儿子时没有到过 \(v\),则 \(u\) 就会通过 \((u,v)\) 到达 \(v\),那么 \(v\) 就成为了 \(u\) 的儿子。

  2. \(u\) 在遍历 \(v\) 之前的儿子时到过 \(v\),那么 \(v\) 就会成为 \(u\) 的某一个儿子的后代,所以 \(v\) 也是 \(u\) 的子孙。

这就证明了所有边连接的两个点都满足一个是另一个的祖先,同时我们也可以发现一个点一定他的所有后代先访问到,即 \(dfn_u \leq dfn_v(v \in T(u))\),这里 \(T(u)\) 表示 \(u\) 子树内的所有结点。

有向图

有向图的 dfs 生成树在实现上和无向图类似,也是从任意一点开始 dfs 得到的一棵生成树。

我们可以把图中的边分成 \(4\) 类:

  1. 树边(tree edge):每次搜索找到一个还没有访问过的结点的时候就形成了一条树边。

  2. 反祖边(back edge):也被叫做回边,即指向祖先结点的边,如示意图中的 \((4,1)\)。

  3. 前向边(forward edge):它是在搜索的时候遇到子树中的结点的时候形成的,如示意图中的 \((1,3)\)。

  4. 横叉边(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\),然后分两种情况讨论:

  1. 若 \(u\) 是生成树的根,\(u\) 已经没有祖先了,它的儿子能不能到达无所谓。但如果他堵住了两个以上的儿子 ,即 \(d_u \geq 2\),这些儿子之间互相不能到达,此时 \(u\) 是图的割点。

  2. 若 \(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 算法的更多相关文章

  1. 有向图强连通分量的Tarjan算法

    有向图强连通分量的Tarjan算法 [有向图强连通分量] 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G ...

  2. 点/边 双连通分量---Tarjan算法

    运用Tarjan算法,求解图的点/边双连通分量. 1.点双连通分量[块] 割点可以存在多个块中,每个块包含当前节点u,分量以边的形式输出比较有意义. typedef struct{ //栈结点结构 保 ...

  3. 割点和桥---Tarjan算法

    使用Tarjan算法求解图的割点和桥. 1.割点 主要的算法结构就是DFS,一个点是割点,当且仅当以下两种情况:         (1)该节点是根节点,且有两棵以上的子树;         (2)该节 ...

  4. Tarjan算法---强联通分量

    1.基础知识 在有向图G,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极大强连通子 ...

  5. (转载)LCA问题的Tarjan算法

    转载自:Click Here LCA问题(Lowest Common Ancestors,最近公共祖先问题),是指给定一棵有根树T,给出若干个查询LCA(u, v)(通常查询数量较大),每次求树T中两 ...

  6. 强连通分量的Tarjan算法

    资料参考 Tarjan算法寻找有向图的强连通分量 基于强联通的tarjan算法详解 有向图强连通分量的Tarjan算法 处理SCC(强连通分量问题)的Tarjan算法 强连通分量的三种算法分析 Tar ...

  7. [知识点]Tarjan算法

    // 此博文为迁移而来,写于2015年4月14日,不代表本人现在的观点与看法.原始地址:http://blog.sina.com.cn/s/blog_6022c4720102vxnx.html UPD ...

  8. Tarjan 算法&模板

    Tarjan 算法 一.算法简介 Tarjan 算法一种由Robert Tarjan提出的求解有向图强连通分量的算法,它能做到线性时间的复杂度. 我们定义: 如果两个顶点可以相互通达,则称两个顶点强连 ...

  9. 【小白入门向】tarjan算法+codevs1332上白泽慧音 题解报告

    一.[前言]关于tarjan tarjan算法是由Robert Tarjan提出的求解有向图强连通分量的算法. 那么问题来了找蓝翔!(划掉)什么是强连通分量? 我们定义:如果两个顶点互相连通(即存在A ...

  10. 有向图强连通分量 Tarjan算法

    [有向图强连通分量] 在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极 ...

随机推荐

  1. 循环神经网络 —— LSTM 图片

  2. CyberDog测试视频 —— 【开箱】小米"限量"机器狗!被我玩坏了...

    地址: https://www.youtube.com/watch?v=3ntAhy3thXM PS. 现在的智能机器人其实真的没有人们想象中的那么智能.感觉现在的智能机器人最为有用的功能一个是倒地自 ...

  3. gym.ActionWrapper使用时的注意点——step函数可以覆盖observation函数

    本文说的这个gym.ActionWrapper继承类的问题和gym.ObservationWrapper继承类的问题性质是一样的,具体看: gym.ObservationWrapper使用时的注意点- ...

  4. Apache DolphinScheduler如何开启开机自启动功能?

    转载自东华果汁哥 Apache DolphinScheduler 是一个分布式.去中心化的大数据工作流调度系统,支持大数据任务调度.若要设置 DolphinScheduler 开机自启动,通常需要将其 ...

  5. 需要多久才能看完linux内核源码?

    代码中自由颜如玉!代码中自有黄金屋! 一.内核行数 Linux内核分为CPU调度.内存管理.网络和存储四大子系统,针对硬件的驱动成百上千.代码的数量更是大的惊人. 先说说最早的内核linux 0.11 ...

  6. 开发一个MutatingWebhook

    介绍 Webhook就是一种HTTP回调,用于在某种情况下执行某些动作,Webhook不是K8S独有的,很多场景下都可以进行Webhook,比如在提交完代码后调用一个Webhook自动构建docker ...

  7. Ubuntu 16.04 部署Mariadb

    默认上MariaDB的包并没有在Ubuntu仓库中.要安装MariaDB,我们要设置MariaDB仓库. sudo apt-get install software-properties-common ...

  8. 基于python的文字转图片工具

    地址 https://hub.docker.com/r/rainsccc/strtoimg 拉取镜像后,可以启动一个容器来运行该应用程序.以下命令会启动容器并将其端口映射到主机上: docker ru ...

  9. Python wheel

    在 Python 的生态系统中,wheel 是一种打包格式,用于分发和安装 Python 项目.它是 Python 包的标准格式之一,旨在提高安装速度和可靠性. Wheel 的优势 快速安装:因为 w ...

  10. pip 安装包时提示 "WARNING: Skipping xxx due to invalid metadata entry 'name'"

    我最近在使用 pip 安装包的时候经常遇到如下警告: WARNING: Skipping /opt/homebrew/lib/python3.11/site-packages/numpy-1.26.3 ...