浅谈 Tarjan 算法之强连通分量(危
引子
果然老师们都只看标签拉题。。。
2020.8.19新初二的题集中出现了一道题目(现已除名),叫做Running In The Sky。
OJ上叫绮丽的天空
发现需要处理环,然后通过一些神奇的渠道了解到有个东西叫缩点。
紧接着搜了一下缩点,发现了 Tarjan 算法。
然后又翻了翻算法竞赛,于是一去不复返……
一些定义
给定一张有向图。对于图中任意两个节点 \(x, y\),存在从 \(x\) 到 \(y\) 的路径,也存在 \(y\) 到 \(x\) 的路径。则称该有向图为“强连通图”。
有向图的强连通子图被称为强连通分量 \(SCC\) \((Strongly\) \(Connected\) \(Component)\)。
显然,环一定是强连通图。因为如果在有向图中存在 \(x\) 到 \(y\) 的路径,且存在 \(y\) 到 \(x\) 的路径,那么 \(x, y\) 一定在同一个环中。
对于一个有向图,如果从 \(root\) 可以到达图中所有的点,则称其为“流图”,而 \(root\) 为流图的源点。
以 \(root\) 为原点对流图深度优先遍历,每个点只访问一次,在过程中,所有发生递归的边 \((x, y)\) 构成的一棵树叫做这个流图的搜索树。
在深度优先遍历时,对每个访问到的节点分别进行整数 \(1...n\) 标记,该标记被称为时间戳,记为 \(dfn[x]\)。
流图中的每条有向边 \((x, y)\) 必然是以下四种中的一种:
前向边,指搜索树中 \(x\) 是 \(y\) 的祖先节点
后向边,指搜索树中 \(y\) 是 \(x\) 的祖先节点
树枝边,指搜索树里的边,满足 \(x\) 是 \(y\) 的父节点。
其他边(好像也叫横叉边),指除了上面三种情况的边。且一定满足 \(dfn[y] < dfn[x]\)
Tarjan算法之强连通分量
Tarjan 算法基于有向图的深度优先遍历,能够在线性时间中求出一张有向图的各个强连通分量。
其核心思想就是考虑两点之间是否存在路径可以实现往返。
我们在后文中,都会结合搜索树(本身就是深度优先遍历的产物)来考虑,这样就可以在深度优先遍历的同时完成我们的目标。
对于流图,前向边作用不大,因为当前搜索树中一定存在 \(x\) 到 \(y\) 的路径。
后向边就很重要了,因为它一定可以和搜索树中 \(x\) 到 \(y\) 的路径组成环。
横叉边需要判断一下,如果这条横叉边能到达搜索树上 \(x\) 的祖先(显然,\(x\) 的祖先一定可以到达 \(x\))。记这个祖先为 \(z\),则这条横叉边一定能和它到 \(z\) 的路径,\(z\) 到 \(x\) 的路径组成环。
为了找到横叉边与后向边组成的环,我们考虑在深度优先遍历的同时维护一个栈。
当遍历到 \(i\) 节点时,栈里一定有以下一些点:
- 搜索树上 \(i\) 的所有祖先集合 \(j\)。若此时存在后向边 \((i, j)\),则 \((i, j)\) 一定与 \(j\) 到 \(i\) 的路径形成环。
- 已经访问过的点 \(k\),且满足从 \(k\) 出发一定能找到到 \(j\) 的路径。此时,\((i, k)\),\(k\) 到 \(j\) 的路径,\(j\) 到 \(i\) 的路径一定会形成环。
于是我们引入回溯值的概念。回溯值 \(low[x]\) 表示以下节点的最小时间戳:
- 该点在栈中。
- 存在一条从流图的搜索树中以 \(x\) 为根的子树为起点出发的有向边,以该点为终点。(也就是以它为起点能继续往下遍历到的点)
如果当前的 \(low[x]\) 表示的最小时间戳代表的点集全是2类点,则易得 \(low[x] = dfn[x]\) 时强连通分量存在,且 \(x\) 是此强连通分量的根(整个强连通分量中时间戳最小的节点)。
如果表示的点集存在1类点。则当前点一定属于强连通分量,且该强连通分量的根为整个强连通分量中时间戳最小的节点。
当我们判断了存在以当前点为根的强连通分量后,从栈中不断取出点,直到取出的点与当前点相等,我们就得到了整个强连通分量的信息。
整理更新回溯值的方法:
- 如果当前点第一次被访问,入栈,且 \(low[x] = dfn[x]\)。
- 遍历从 \(x\) 为起点的每一条边 \((x, y)\)。若 \(y\) 被访问过,且 \(y\) 在栈中,那么 \(low[x] = min(low[x], dfn[y])\)。若 \(y\) 没被访问过,递归访问 \(y\),在回溯之后更新 \(low[x] = min(low[x], low[y])\)。(典型\(dp\)思想)
具体实现
int dfn[MAXN], low[MAXN];
// 时间戳及回溯值。
vector <int> scc[MAXN];
// 储存最后求出的各个强连通分量的信息。
int key[MAXN];
// key[i]表示i在编号为key[i]的强连通分量中。
stack<int> st; // 栈。
bool vis[MAXN];
// 记录是否在栈中。
int num = 0, cnt = 0;
// num 时间戳标记。cnt 强连通分量标记。
void tarjan(int x) {
num++;
dfn[x] = num;
low[x] = num;
st.push(x);
vis[x] = true;
// 第一次遍历到,记录时间戳,入栈,记录当前回溯值
for(int i = 0; i < map_first[x].size(); i++) { // 枚举每条边。
int j = map_first[x][i];
if(!dfn[j]) { // 如果当前边的终点没被遍历过。
tarjan(j); // 递归遍历。
low[x] = min(low[x], low[j]);
// 维护回溯值。
}
else if(vis[j]) // 如果遍历过且在栈中。
low[x] = min(low[x], dfn[j]);
// 维护回溯值。
}
if(dfn[x] == low[x]) { // 如果存在以当前点为根的强连通分量。
cnt++;
int now = 0;
while(x != now) { // 找出栈中所有在当前强连通分量中的点。
now = st.top();
st.pop();
vis[now] = false;
key[now] = cnt; // 存所在编号。
scc[cnt].push_back(now); // 存点。
}
}
}
缩点
缩点。其实就是指的把环看成一个点来进行后面的图论算法。而把环看成的这个点的点权在题目中会具体说明。
比较常见的缩点后的点权是整个环路的所有点的点权和。
如下图:
1 -> 2 -> 4 -> 5 -> 2 -> 3
显然上图存在环路。在经过缩点后,我们可以将它变成这样:
1 -> 2(value[2] + value[4] + value[5]) -> 3
思路比较简单,我就直接分析代码了。。。
具体实现
for(int i = 1; i <= n; i++)
for(int j = 0; j < map_first[i].size(); j++) {
// 枚举原图中的每一条边。
int v = map_first[i][j];
if(key[i] == key[v])
// 如果这个边的两端属于同一个强连通分量,则直接放弃这条连边。
continue;
map_second[key[i]].push_back(key[v]);
// 将两个点对应的强连通分量的编号相连,加入新图。
// 显然,单个点也属于一个强连通分量。
}
现在,我们再来看看引子中提到的那道题吧。。。
题目描述
一天的活动过后,所有学生都停下来欣赏夜空下点亮的风筝。\(Curtis\) \(Nishikino\)想要以更近的视角感受一下,所以她跑到空中的风筝上去了(这对于一个妹子来说有点匪夷所思)! 每只风筝上的灯光都有一个亮度 \(k_i\). 由于风的作用,一些风筝缠在了一起。但是这并不会破坏美妙的气氛,缠在一起的风筝会将灯光汇聚起来,形成更亮的光源!
\(Curtis\) \(Nishikino\)已经知道了一些风筝间的关系,比如给出一对风筝 \((a,b)\), 这意味着她可以从 \(a\) 跑到 \(b\) 上去,但是不能返回。
现在,请帮她找到一条路径(她可以到达一只风筝多次,但只在第一次到达时她会去感受上面的灯光), 使得她可以感受到最多的光亮。同时请告诉她这条路径上单只风筝的最大亮度,如果有多条符合条件的路径,输出能产生最大单只风筝亮度的答案。
输入格式
第一行两个整数 \(n\) 和 \(m\)。 \(n\) 是风筝的数量, \(m\) 是风筝间关系对的数量。
接下来一行 \(n\) 个整数 \(k_i\)。
接下来 \(m\) 行, 每行两个整数 \(a\) 和 \(b\), 即 \(Curtis\) 可以从 \(a\) 跑到 \(b\)。
输出格式
一行两个整数。\(Curtis\) 在计算出的路径上感受到的亮度和,这条路径上的单只风筝最大亮度。
样例输入
5 5
8 9 11 6 7
1 2
2 3
2 4
4 5
5 2
样例输出
41 11
分析
显然这道题是有环的对吧。
如果我们不考虑环的情况,其实就是一个很板白菜的题目。
我们可以采用拓扑排序的思路,遍历整个图,然后对于每个路径维护一下到当前点的最大距离,并维护一个这个路径上的最大值。
然后考虑有环,很简单,事先 Tarjan 缩点嘛/xyx
并且这道题是要累加路径上的点的和。所以缩点后的点就是当前强连通分量包含的所有点的点权和。
AC代码
#include <cstdio>
#include <stack>
#include <queue>
#include <vector>
#include <cstring>
#include <algorithm>
using namespace std;
const int MAXN = 200005;
int n, m;
int value[MAXN];
vector<int> map_first[MAXN];
// 原图。
int dfn[MAXN], low[MAXN];
struct data {
int ma, sum;
data() {
ma = 0;
sum = 0;
}
data(int Ma, int S) {
ma = Ma;
sum = S;
}
} scc[MAXN];
// Tarjan 求出的强连通分量中,维护两个信息。
// 1.ma 表示整个强连通分量的最大值。
// 2.sum 表示整个强连通分量的点权和,即缩点后的点权。
int key[MAXN];
stack<int> st;
bool vis[MAXN];
int num = 0, cnt = 0;
void tarjan(int x) { // tarjan 算法。
num++;
dfn[x] = num;
low[x] = num;
st.push(x);
vis[x] = true;
for(int i = 0; i < map_first[x].size(); i++) {
int j = map_first[x][i];
if(!dfn[j]) {
tarjan(j);
low[x] = min(low[x], low[j]);
}
else if(vis[j])
low[x] = min(low[x], dfn[j]);
}
if(dfn[x] == low[x]) {
cnt++;
int now = 0;
while(x != now) {
now = st.top();
st.pop();
vis[now] = false;
key[now] = cnt;
scc[cnt].ma = max(scc[cnt].ma, value[now]);
scc[cnt].sum += value[now];
// 维护一下强连通分量的两个信息。
}
}
}
vector<int> map_second[MAXN]; // 新图。
int in[MAXN]; // 拓扑排序,统计点的入度。
int dp[MAXN][2];
// dp[i][0]表示到i点的最长路径。
// dp[i][1]表示路径上的最大点权。
void T_Sort() { // 拓扑。
queue<int> q;
for(int i = 1; i <= cnt; i++) {
dp[i][0] = scc[i].sum;
dp[i][1] = scc[i].ma;
}
for(int i = 1; i <= cnt; i++)
if(!in[i])
q.push(i);
while(!q.empty()) {
int x = q.front();
q.pop();
for(int i = 0; i < map_second[x].size(); i++) {
int v = map_second[x][i];
in[v]--;
if(!in[v])
q.push(v);
if(dp[v][0] < dp[x][0] + scc[v].sum) {
dp[v][0] = dp[x][0] + scc[v].sum;
dp[v][1] = max(dp[x][1], scc[v].ma);
// 更新最长路径及最大点权。
}
if(dp[v][0] == dp[x][0] + scc[v].sum)
dp[v][1] = max(dp[v][1], dp[x][1]);
// 如果有两条最长路径则记录两条路径中的最大值。
}
}
}
int main() {
scanf ("%d %d", &n, &m);
for(int i = 1; i <= n; i++) {
scanf ("%d", &value[i]);
key[i] = i;
}
for(int i = 1; i <= m; i++) {
int u, v;
scanf ("%d %d", &u, &v);
map_first[u].push_back(v);
}
for(int i = 1; i <= n; i++)
if(!dfn[i]) // 如果当前点没被遍历,则跑一遍 Tarjan。
tarjan(i);
for(int i = 1; i <= n; i++)
for(int j = 0; j < map_first[i].size(); j++) {
int v = map_first[i][j];
if(key[i] == key[v])
continue;
map_second[key[i]].push_back(key[v]);
in[key[v]]++;
}
// 缩点,存新图。
T_Sort();
int ans = 1;
for(int i = 2; i <= cnt; i++) // 统计答案。
if(dp[i][0] > dp[ans][0] || (dp[i][0] == dp[ans][0] && dp[i][1] > dp[ans][1]))
ans = i;
printf("%d %d", dp[ans][0], dp[ans][1]);
return 0;
}
浅谈 Tarjan 算法之强连通分量(危的更多相关文章
- 浅谈Tarjan算法
从这里开始 预备知识 两个数组 Tarjan 算法的应用 求割点和割边 求点-双连通分量 求边-双连通分量 求强连通分量 预备知识 设无向图$G_{0} = (V_{0}, E_{0})$,其中$V_ ...
- 浅谈 Tarjan 算法
目录 简述 作用 Tarjan 算法 原理 出场人物 图示 代码实现 例题 例题一 例题二 例题三 例题四 例题五 总结 简述 对于初学 Tarjan 的你来说,肯定和我一开始学 Tarjan 一样无 ...
- 浅谈Tarjan算法及思想
在有向图G中,如果两个顶点间至少存在一条路径,称两个顶点强连通(strongly connected).如果有向图G的每两个顶点都强连通,称G是一个强连通图.非强连通图有向图的极大强连通子图,称为强连 ...
- 浅谈分词算法(5)基于字的分词方法(bi-LSTM)
目录 前言 目录 循环神经网络 基于LSTM的分词 Embedding 数据预处理 模型 如何添加用户词典 前言 很早便规划的浅谈分词算法,总共分为了五个部分,想聊聊自己在各种场景中使用到的分词方法做 ...
- 浅谈分词算法(4)基于字的分词方法(CRF)
目录 前言 目录 条件随机场(conditional random field CRF) 核心点 线性链条件随机场 简化形式 CRF分词 CRF VS HMM 代码实现 训练代码 实验结果 参考文献 ...
- 浅谈分词算法(3)基于字的分词方法(HMM)
目录 前言 目录 隐马尔可夫模型(Hidden Markov Model,HMM) HMM分词 两个假设 Viterbi算法 代码实现 实现效果 完整代码 参考文献 前言 在浅谈分词算法(1)分词中的 ...
- 浅谈分词算法基于字的分词方法(HMM)
前言 在浅谈分词算法(1)分词中的基本问题我们讨论过基于词典的分词和基于字的分词两大类,在浅谈分词算法(2)基于词典的分词方法文中我们利用n-gram实现了基于词典的分词方法.在(1)中,我们也讨论了 ...
- 有向图tarjan算法求连通分量的粗浅讲解、证明, // hdu1269
打算开始重新复习一遍相关算法.对于有向图tarjan算法,通过学习过很多说法,结合自己的理解,下面给出算法自己的观点. 算法总模型是一个dfs,结合一个stack(存放当前尚未形成SCC的点集合),记 ...
- 浅谈Manacher算法与扩展KMP之间的联系
首先,在谈到Manacher算法之前,我们先来看一个小问题:给定一个字符串S,求该字符串的最长回文子串的长度.对于该问题的求解.网上解法颇多.时间复杂度也不尽同样,这里列述几种常见的解法. 解法一 ...
随机推荐
- 世界碰撞算法原理和总结(sat gjk)
序言 此文出于作者的想法,从各处文章和论文中,总结和设计项目中碰撞结构处理方法.如有其它见解,可以跟作者商讨.(杨子剑,zijian_yang@yeah.net). 在一个世界中,有多个物体,物体可以 ...
- 【转】Event Driven Programming
FROM: http://lazyfoo.net/tutorials/SDL/03_event_driven_programming/index.php Event Driven Programmin ...
- SDOI征途--斜率优化
题目描述 给定长为 n 的数列 a, 要求划分成 m 段,使得方差最小, 输出方差\(*m^2\) 题解 斜率优化好题 准备部分 设第 i 段长为 \(len_i\) 先考虑方差(\(S^2\))的式 ...
- Java学习的第四十八天
1.例8.4找出整形数组中的最大值 import java.util.Scanner; public class Cjava { public static void main(String[]arg ...
- 基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪
原文链接:基于SLF4J的MDC机制和Dubbo的Filter机制,实现分布式系统的日志全链路追踪 一.日志系统 1.日志框架 在每个系统应用中,我们都会使用日志系统,主要是为了记录必要的信息和方便排 ...
- 面试题:对NotNull字段插入Null值 有啥现象?
Hi,大家好!我是白日梦. 今天我要跟你分享的话题是:"对NotNull字段插入Null值有啥现象?" 一. 推荐阅读 首发地址:https://mp.weixin.qq.com/ ...
- deepin 20 镜像源
deepin 20 镜像源 ## Generated by deepin-installerdeb [by-hash=force] https://community-packages.deepin. ...
- 剑指offer刷题(算法类_2)
排序 035-数组中的逆序对(归并排序) 题目描述 题解 代码 复杂度 029-最小的K个数(堆排序) 题目描述 题解 代码 复杂度 029-最小的K个数(快速排序) 题目描述 题解 代码 复杂度 位 ...
- Spring源码之注解的原理
https://blog.csdn.net/qq_28802119/article/details/83573950 https://www.zhihu.com/question/318439660/ ...
- this.getClass().getResource("") url path file 区别
首先注意 "/word/appointDismiss.docx" 前面一定要加 /,有一次我就是忘记加/ 查了半天错, 不能写成 "word/appointDismiss ...