学习笔记:tarjan

news2024/10/7 7:25:56

tarjan

引入

Robert Tarjan,计算机科学家,以 LCA、强连通分量等算法而闻名。Tarjan 设计了求解的应用领域的广泛有效的算法和数据结构。他以在数据结构和图论上的开创性工作而闻名,他的一些著名的算法有 Tarjan 最近公共祖先离线算法,Tarjan 的强连通分量算法以及 Link-Cut-Trees 算法等。其中 Hopcroft-Tarjan 平面嵌入算法是第一个线性时间平面算法。Tarjan 也开创了重要的数据结构如:斐波那契堆和 splay 树,另一项重大贡献是分析了并查集。他是第一个证明了计算反阿克曼函数的乐观时间复杂度的科学家。

Tarjan 算法,又称为 Tarjan’s algorithm,是一个用于求解图的强连通分量(Strongly Connected Component)的算法。它是由美国计算机科学家 Robert Tarjan 在 1972 年提出的。(然而实际上能求的不只有强连通分量?)

前置知识

栈是 OI 中常用的一种线性数据结构,其修改是按照后进先出的原则进行的,因此栈通常被称为是后进先出(last in first out)表,简称 LIFO 表。

可以考虑用数组模拟一个栈,定义一个变量 top 表示栈顶指针。

int stk[100005], top = 0; // 定义一个大小为 100005 的栈,初始时将指针指向栈底(即 0)
void insert(int x){ // 插入一个元素到栈顶
    top++;stk[top] = x;
}
void remove(int x){ // 删除栈顶元素
    top--;
}
int size(){ // 读取栈大小(即栈内元素个数)
    return top;
}
void clear(){ // 清空栈
    top = 0;
}
int top(){ // 读取栈顶元素
    return stk[top];
}

STL stack

简介

栈是一种先进后出的容器。

头文件
#include <stack>
初始化
stack <int> s;
stack <string> s;
stack <node> s; //node 是结构体类型
函数
函数含义
push(x) x x x 入栈 O ( 1 ) O(1) O(1)
pop()将栈顶元素出栈 O ( 1 ) O(1) O(1)
top()返回栈顶元素 O ( 1 ) O(1) O(1)
empty()检测栈是否为空 O ( 1 ) O(1) O(1)
size()返回元素个数 O ( 1 ) O(1) O(1)
访问

STL 中的栈仅支持读取栈顶元素,如果需要遍历则需要将所有元素出栈。

可以考虑用数组模拟栈,比 STL 的 stack 容器速度更快,且遍历元素更加方便。

STL vector

简介

vector 一词在英文中是向量的意思。

vector 为可变长数组(即动态数组),可以随时添加数值和删除数值。

注意

在局部区域中开 vector 是在堆空间开的

在局部区域开数组是在栈空间开的,而栈空间比较小,如果开了很大的数组就会爆栈。

所以,在局部区域中不能开大数组,但能开大 vector

头文件
#include <vector>
初始化
vector <int> a; // 定义了一个名为 a 的一维数组,数组存储 int 类型数据
vector <double> b;// 定义了一个名为 b 的一维数组,数组存储 double 类型数据
vector <node> c;// 定义了一个名为 c 的一维数组,数组存储结构体类型数据,node 是结构体类型
vector <int> v(n);// 定义一个长度为 n 的数组,初始值默认为 0,下标范围[0, n - 1]
vector <int> v(n, 1);// v[0] 到 v[n - 1] 所有的元素初始值均为 1
//注意:指定数组长度之后(指定长度后的数组就相当于正常的数组了)
vector <int> a{1, 2, 3, 4, 5};//数组 a 中有五个元素,数组长度就为 5
vector <int> a(n + 1, 0);
vector <int> b(a);// 两个数组中的类型必须相同,a 和 b 都是长度为 n + 1,初始值都为 0 的数组
vector <int> v[5];// 定义可变长二维数组
// 注意:行不可变(只有 5 行), 而列可变,可以在指定行添加元素
// 第一维固定长度为 5,第二维长度可以改变
vector <vecto <int>> v;//定义一个行和列均可变的二维数组
函数
函数含义
front()返回第一个数据 O ( 1 ) O(1) O(1)
pop_back()删除最后一个数据 O ( 1 ) O(1) O(1)
push_back(x)在尾部加一个数据 O ( 1 ) O(1) O(1)
size()返回数据个数 O ( 1 ) O(1) O(1)
clear()清空容器 O ( n ) O(n) O(n)
resize(x, y)将数组大小改为 x x x x x x 个空间赋值为 y y y,没有 y y y 默认为 0 0 0
insert(x, y)向迭代器 x x x 中插入一个数据 y y y O ( n ) O(n) O(n)
erase(x, y)删除 [ x , y ) [x, y) [x,y) 中的所有数据 O ( n ) O(n) O(n)
begin()返回首元素迭代器 O ( 1 ) O(1) O(1)
end()返回末元素后一个位置的迭代器 O ( 1 ) O(1) O(1)
empty()判断容器是否为空 O ( 1 ) O(1) O(1)
访问

可以直接和数组一样访问。

vector <int> a;
a.push_back(1);
cout << a[0] << endl;

也可以采用迭代器访问。

vector <int> a;
a.push_back(1);
vector <int>::iterator tmp = a.begin();
cout << *tmp << endl;
for(tmp = a.begin() ; tmp != a.end() ; tmp ++)
    	cout << *tmp << endl;

也可以使用智能指针,但只能一次性遍历完整个数组。

vector <int> v;
v.push_back(114514);
v.push_back(1919810);
for(auto val : v) 
    cout << val << " "; // 114514 1919810

一些概念

割点和割边

割点:在一个无向连通图 G = ( V , E ) G=(V,E) G=(V,E) 中,若存在一个点 x ∈ V x \in V xV 使得从图中删去这个点以及与这个点相连的所有边后整个图不再连通,则这个点是割点。

割边(或者叫做桥):在一个无向连通图 G = ( V , E ) G=(V,E) G=(V,E) 中,若存在一条边 x ∈ E x \in E xE 使得从图中删去这条边后整个图不再连通,则这条边是割边。

在上图中,观察可知, 3 3 3 4 4 4 是割点,边 ( 3 , 4 ) (3,4) (3,4) 是割边。

时间戳

在图的深度优先遍历过程中,按照每个节点第一次被访问的时间顺序,依次给予 n n n 个节点 1 1 1 n n n 的整数标记,该标记就被称为”时间戳“,记为 dfn[x]

搜索树

在无向连通图中任选一个节点出发进行深度优先遍历,每个点只访问一次。所有发生递归的边 ( x , y ) (x,y) (x,y)(换言之,从 x x x y y y 是对 y y y 的第一次访问)构成一棵树,我们把它称为“无向连通图的搜索树”。当然,一般无向图(不一定连通)的各个连通块的搜索树构成无向图的“搜索森林”。这棵树上的边称作树边,不在树上的边称作非树边

下图显然是上图构成的一棵搜索树。

当然,搜索树一般情况下可能会有多个,这里只给出其中一种。

追溯值

son[x] 表示以 x x x 为根的子树,则追溯值 low[x] 定义为以下节点的时间戳的最小值:

  1. son[x] 中的节点。
  2. 通过一条不再搜索树上的边能够到达 son[x] 的节点。

以上图为例。为了叙述简便,我们用时间戳代替节点编号。son[4] = {4,5,6,7}。另外,节点 6 6 6 通过不在搜索树上的边 ( 4 , 6 ) (4,6) (4,6) 能够到达 。所以 low[6] = 4

根据定义,为了计算low[x],应该先令 low[x] = dfn[x],然后考虑从 x x x 出发的每条边 ( x , y ) (x,y) (x,y)

  1. 若在搜索树上 x x x y y y 的父节点,则令 low[x] = min(low[x], low[y])

  2. 若无向边 ( x , y ) (x,y) (x,y) 不是搜索树上的边,则令 low[x] = min(low[x], dfn[y])

表格里的数值标注了每个节点的“时间戳” d f n dfn dfn 和“追溯值” l o w low low

节点 1节点 2节点 3节点 4节点 5节点 6节点 7
dfn1234567
low1114444

tarjan 算法与无向图连通性

割边、割点的判定法则

割边
判定法则

在无向图 G = ( V , E ) G=(V,E) G=(V,E) 中,边 ( x , y ) (x,y) (x,y) 是割边,当且仅当该图的搜索树中存在 x x x 以及一个 x x x 的子节点 y y y 满足:
d f n [ x ] < l o w [ y ] dfn[x]<low[y] dfn[x]<low[y]
根据定义,dfn[x] < low[y]说明从 son(y) 出发,在不经过边 ( x , y ) (x,y) (x,y) 的前提下,不管走哪条边,都无法到达 x x x 或比 x x x 更早访问的节点。若把 ( x , y ) (x,y) (x,y) 删除,则son(y) 就好像形成了一个封闭的环境,与节点 x x x 没有边相连,图断开成了两部分,因此 ( x , y ) (x,y) (x,y) 是割边。

反之,若不存在这样的子节点 y y y 使得 dfn[x] < low[y],则说明每个 son(y) 都能绕行其他边到达 x x x 或比 x x x 更早访问的节点, ( x , y ) (x,y) (x,y) 自然就不是割边。

不难发现,割边一定是搜索树中的边,并且一个简单环中的边一定都不是割边。

代码实现
#include <iostream>
#define MAXN 20005
#define MAXM 100005
using namespace std;
int n, m, x, y;
struct edge{int to, nxt;}e[MAXM << 1];
int head[MAXN], cnt = 1;
int dfn[MAXN], low[MAXN], tot;
bool isb[MAXM << 1], flag;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int u, int v){
    cnt++;e[cnt].to = v;e[cnt].nxt = head[u];head[u] = cnt;
    cnt++;e[cnt].to = u;e[cnt].nxt = head[v];head[v] = cnt;
}
void tarjan(int now, int fat){
    tot++;dfn[now] = tot;low[now] = tot;
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if(dfn[v] == 0){
            tarjan(v, i);
            low[now] = min(low[now], low[v]);
            if(dfn[now] < low[v])isb[i] = true,isb[i ^ 1] = true;
        }else if(i != (fat ^ 1))
            low[now] = min(low[now], dfn[v]);
    }
}
int main(){
    n = read();m = read();
    for(int i = 1 ; i <= m ; i ++)
        x = read(),y = read(),add(x, y);
    for(int i = 1 ; i <= n ; i ++)
        if(dfn[i] == 0)tarjan(i, 0);
    for(int i = 1 ; i <= cnt ; i += 2){
        if(isb[i] == true){
            write(e[i].to);putchar(' ');
            write(e[i ^ 1].to);putchar('\n');
        }
    }
    return 0;
}

运行程序可以得到如下结果:

PS C:\Users\tsqtsqtsq\OIer\work> g++ -o a tarjan1.cpp
PS C:\Users\tsqtsqtsq\OIer\work> time ./a
12 15
1 2
2 3
3 1
3 4
4 5
5 6
6 4
5 7
7 8
8 9
9 7
7 10
9 10
10 11
11 12
3 4
5 7
10 11
11 12
real    0m 0.40s
user    0m 0.00s
sys     0m 0.01s
PS C:\Users\tsqtsqtsq\OIer\work>

我们将输入数据绘制成一张图,则有:

不难发现割边为 ( 3 , 4 ) (3,4) (3,4) ( 5 , 7 ) (5,7) (5,7) ( 10 , 11 ) (10,11) (10,11) ( 11 , 12 ) (11,12) (11,12),证明所求是正确的。

割点
判定法则

割点的判定法则类似,只需浅浅修改成这样:
d f n [ x ] ≤ l o w [ y ] dfn[x]\le low[y] dfn[x]low[y]
那么,如何一次性地求出图中的所有割点呢?

我们考虑直接运用 tarjan 算法对图进行一次深度优先遍历,遍历时实时更新每一个节点的“时间戳”和“追溯值”。对于每一条边都判一下即可。

有一个特判,如果某个节点是这个搜索树中的根节点,那么一般的割边判定法则对此并不适用。特别地,若 x x x 是搜索树的根节点,则 x x x 是割点当且仅当搜索树上存在至少两个子节点 y 1 , y 2 y_1,y_2 y1,y2 满足上述条件。

代码实现

P3388 【模板】割点(割顶)

#include <iostream>
#define MAXN 20005
#define MAXM 100005
using namespace std;
int n, m, x, y;
struct edge{int to, nxt;}e[MAXM << 1];
int head[MAXN], cnt = 1;
int dfn[MAXN], low[MAXN], tot;
bool ans[MAXN], flag;
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int u, int v){
    cnt++;e[cnt].to = v;e[cnt].nxt = head[u];head[u] = cnt;
    cnt++;e[cnt].to = u;e[cnt].nxt = head[v];head[v] = cnt;
}
void tarjan(int now, int fat){
    tot++;dfn[now] = tot;low[now] = tot;int tmp = 0;
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if(dfn[v] == 0){
            tarjan(v, fat);
            low[now] = min(low[now], low[v]);
            if(low[v] >= dfn[now] && now != fat)ans[now] = true;
            else if(now == fat)tmp++;
        }
        low[now] = min(low[now], dfn[v]);
    }
    if(tmp >= 2 && now == fat)ans[now] = true;
}
int main(){
    n = read();m = read();
    for(int i = 1 ; i <= m ; i ++)
        x = read(),y = read(),add(x, y);
    for(int i = 1 ; i <= n ; i ++)
        if(dfn[i] == 0)tarjan(i, i);
    tot = 0;
    for(int i = 1 ; i <= n ; i ++)
        if(ans[i] == true)tot++;
    write(tot);putchar('\n');
    for(int i = 1 ; i <= n ; i ++){
        if(ans[i] == true){
            if(flag == true)putchar(' ');
            write(i);flag = true;
        }
    }
    putchar('\n');return 0;
}

双连通分量的求法

关于双连通分量

若一张无向连通图不存在割点,则称它为“点双连通图”。若一张无向连通图不存在桥,则称它为“边双连通图”。

无向连通图的极大边双连通子图被称为“边双连通分量”,简记为“e-DCC”。无向图的极大点双连通子图被称为“点双连通分量”,简记为“v-DCC”。二者统称为“双连通分量”,简记为"DCC"。

在一张连通的无向图中,对于两个点 u u u v v v,如果无论删去哪条边(只能删去一条)都不能使它们不连通,我们就说 u u u v v v 边双连通

在一张连通的无向图中,对于两个点 u u u v v v,如果无论删去哪个点(只能删去一个,且不能删 u u u v v v 自己)都不能使它们不连通,我们就说 u u u v v v 点双连通

边双连通具有传递性,即,若 x , y x,y x,y 边双连通, y , z y,z y,z 边双连通,则 x , z x,z x,z 边双连通。

点双连通 具有传递性,反例如下图, A , B A,B A,B 点双连通, B , C B,C B,C 点双连通,而 A , C A,C A,C 点双连通。

放点图方便理解(笔者语文水平太 low 不会表达 qwq

上图中,红边是割边,圈出来的是边双连通分量。

上图中,红点是割点,圈出来的是点双连通分量。

边双连通分量
求法

不难发现,任意两个直接相连的边双连通分量都是由一条割边连接起来的,且一个点只会属于一个边双连通分量。

我们可以先进行一次深度优先遍历找出给定图中的所有割边。求出割边后,再划分出所有边双连通分量:

代码实现

P8436 【模板】边双连通分量

#include <iostream>
#include <vector>
#define MAXN 500005
#define MAXM 2000005
using namespace std;
int n, m, x, y;
struct edge{int to, nxt;}e[MAXM << 1];
int head[MAXN], cnt = 1;
int dfn[MAXN], low[MAXN], vis[MAXN], tot;
bool isb[MAXM << 1];
vector <int> ans[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int u, int v){
    cnt++;e[cnt].to = v;e[cnt].nxt = head[u];head[u] = cnt;
    cnt++;e[cnt].to = u;e[cnt].nxt = head[v];head[v] = cnt;
}
void tarjan(int now, int fat){
    tot++;dfn[now] = tot;low[now] = tot;
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if(dfn[v] == 0){
            tarjan(v, i);
            low[now] = min(low[now], low[v]);
            if(dfn[now] < low[v])isb[i] = true,isb[i ^ 1] = true;
        }else if(i != (fat ^ 1))
            low[now] = min(low[now], dfn[v]);
    }
}
void dfs(int now, int tim){
    vis[now] = tim;ans[tim].push_back(now);
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if (vis[v] == 0 && isb[i] == false)
		    dfs(v, tim);
    }
}
int main(){
    n = read();m = read();
    for(int i = 1 ; i <= m ; i ++)
        x = read(),y = read(),add(x, y);
    for(int i = 1 ; i <= n ; i ++)
        if(dfn[i] == 0)tarjan(i, 0);
    tot = 0;
    for(int i = 1 ; i <= n ; i ++)
        if(vis[i] == 0)tot++,dfs(i, tot);
    write(tot);putchar('\n');
    for(int i = 1 ; i <= tot ; i ++){
        write(ans[i].size());
        for(int j = 0 ; j < ans[i].size() ; j ++)
            putchar(' '),write(ans[i][j]);
        putchar('\n');
    }
    return 0;
}
点双连通分量
求法

知道了割点怎么求,点双连通分量(接下来简称点双)就很好求了:

两个点双最多只有一个公共点(即都有边与之相连的点);且这个点在这两个点双和它形成的子图中是割点。

对于第一点,因为当它们有两个及以上公共点时,它们可以合并为一个新的点双(矩形代表一个点双,圆形代表公共点):

DIANSHUANG

当有两个及以上公共点时,删除其中一个点及其与两个点双相连的边后,这两个点双总是可以通过另一个公共点到达彼此,属于一个连通分量,所以这些公共点对于这个子图而言并不是一个割点,按照定义,这两个点双和这些公共点应该是一个更大的点双。

对于第二点,与第一点类似,当对于这个子图而言它不是一个割点时,这两个点双也可以合并为一个新的点双:

DIANSHUANG2

当这个公共点对于这个子图不是一个割点时,也就意味着这两个点双有着另外的边相连,而这些边相连的点同样也是两个点双的公共点,可以归到第一种情况里。

对于一个点双,它在 DFS 搜索树中 dfn 值最小的点一定是割点或者树根。

当这个点是割点时,它所属的点双必定不可以向它的父亲方向包括更多点,因为一旦回溯,它就成为了新的子图的一个割点,不是点双。所以它应该归到其中一个或多个子树里的点双中。

当这个点是树根时,它的 dfn 值是整棵树里最小的。它若有两个以上子树,那么它是一个割点;它若只有一个子树,它一定属于它的直系儿子的点双,因为包括它;它若是一个独立点,视作一个单独的点双。

换句话说,一个点双一定在这两类点的子树中。

我们用栈维护点,当遇到这两类点时,将子树内目前不属于其它点双的非割点或在子树中的割点归到一个新的点双。注意这个点可能还是与其它点双的公共点,所以不能将其出栈。

代码实现

P8435 【模板】点双连通分量

#include <iostream>
#include <vector>
#define MAXN 500005
#define MAXM 2000005
using namespace std;
int n, m, x, y;
struct edge{int to, nxt;}e[MAXM << 1];
int head[MAXN], cnt = 1;
int dfn[MAXN], low[MAXN], tot;
int stk[MAXN], top, sum;
vector <int> ans[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int u, int v){
    cnt++;e[cnt].to = v;e[cnt].nxt = head[u];head[u] = cnt;
    cnt++;e[cnt].to = u;e[cnt].nxt = head[v];head[v] = cnt;
}
void tarjan(int now, int fat){
    tot++;dfn[now] = tot;low[now] = tot;
    top++;stk[top] = now;int son = 0;
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if(dfn[v] == 0){
            son++;tarjan(v, now);
            low[now] = min(low[now], low[v]);
            if(low[v] >= dfn[now]){
                sum++;
                while(stk[top + 1] != v)
                    ans[sum].push_back(stk[top]),top--;
                ans[sum].push_back(now);
            }
        }else if(v != fat)
            low[now] = min(low[now], dfn[v]);
    }
    if(fat == 0 && son == 0)sum++,ans[sum].push_back(now);
}
int main(){
    n = read();m = read();
    for(int i = 1 ; i <= m ; i ++)
        x = read(),y = read(),add(x, y);
    for(int i = 1 ; i <= n ; i ++)
        if(dfn[i] == 0)top = 0,tarjan(i, 0);
    write(sum);putchar('\n');
    for(int i = 1 ; i <= sum ; i ++){
        write(ans[i].size());
        for(int j = 0 ; j < ans[i].size() ; j ++)
            putchar(' '),write(ans[i][j]);
        putchar('\n');
    }
    return 0;
}

tarjan 算法与有向图连通性

关于强连通分量

若一张有向图的节点两两互相可达,则称这张图是 强连通的 (strongly connected)

强连通分量(Strongly Connected Components,SCC)的定义是:极大的强连通子图。

上图中,一共有 5 个强连通分量 { 1 , 2 , 3 } \left\{1,2,3\right\} {1,2,3} { 4 , 5 , 6 } \left\{4,5,6\right\} {4,5,6} { 7 , 8 , 9 } \left\{7,8,9\right\} {7,8,9} { 10 } \left\{10\right\} {10} { 11 } \left\{11\right\} {11} { 12 } \left\{12\right\} {12}

求法

在 Tarjan 算法中为每个结点 u u u 维护了以下几个变量:

  1. dfn u \textit{dfn}_u dfnu:深度优先搜索遍历时结点 u u u 被搜索的次序。
  2. low u \textit{low}_u lowu:在 u u u 的子树中能够回溯到的最早的已经在栈中的结点。设以 u u u 为根的子树为 Subtree u \textit{Subtree}_u Subtreeu low u \textit{low}_u lowu 定义为以下结点的 dfn \textit{dfn} dfn 的最小值: Subtree u \textit{Subtree}_u Subtreeu 中的结点;从 Subtree u \textit{Subtree}_u Subtreeu 通过一条不在搜索树上的边能到达的结点。

一个结点的子树内结点的 dfn 都大于该结点的 dfn。

从根开始的一条路径上的 dfn 严格递增,low 严格非降。

按照深度优先搜索算法搜索的次序对图中所有的结点进行搜索,维护每个结点的 dfnlow 变量,且让搜索到的结点入栈。每当找到一个强连通元素,就按照该元素包含结点数目让栈中元素出栈。在搜索过程中,对于结点 u u u 和与其相邻的结点 v v v v v v 不是 u u u 的父节点)考虑 3 种情况:

  1. v v v 未被访问:继续对 v v v 进行深度搜索。在回溯过程中,用 low v \textit{low}_v lowv 更新 low u \textit{low}_u lowu。因为存在从 u u u v v v 的直接路径,所以 v v v 能够回溯到的已经在栈中的结点, u u u 也一定能够回溯到。
  2. v v v 被访问过,已经在栈中:根据 low 值的定义,用 dfn v \textit{dfn}_v dfnv 更新 low u \textit{low}_u lowu
  3. v v v 被访问过,已不在栈中:说明 v v v 已搜索完毕,其所在连通分量已被处理,所以不用对其做操作。

对于一个连通分量图,我们很容易想到,在该连通图中有且仅有一个 u u u 使得 dfn u = low u \textit{dfn}_u=\textit{low}_u dfnu=lowu。该结点一定是在深度遍历的过程中,该连通分量中第一个被访问过的结点,因为它的 dfn 和 low 值最小,不会被该连通分量中的其他结点所影响。

因此,在回溯的过程中,判定 dfn u = low u \textit{dfn}_u=\textit{low}_u dfnu=lowu 是否成立,如果成立,则栈中 u u u 及其上方的结点构成一个 SCC。

代码实现

#include <iostream>
#include <vector>
#define MAXN 20005
#define MAXM 100005
using namespace std;
int n, m, x, y;
struct edge{int to, nxt;}e[MAXM << 1];
int head[MAXN], cnt = 1;
int dfn[MAXN], low[MAXN], vis[MAXN], tot, sum;
int stk[MAXN], top;
bool ins[MAXN];
vector <int> ans[MAXN];
int read(){
    int t = 1, x = 0;char ch = getchar();
    while(!isdigit(ch)){if(ch == '-')t = -1;ch = getchar();}
    while(isdigit(ch)){x = (x << 1) + (x << 3) + (ch ^ 48);ch = getchar();}
    return x * t;
}
void write(int x){
    if(x < 0){putchar('-');x = -x;}
    if(x >= 10)write(x / 10);
    putchar(x % 10 ^ 48);
}
void add(int u, int v){
    cnt++;e[cnt].to = v;e[cnt].nxt = head[u];head[u] = cnt;
}
void tarjan(int now){
    tot++;dfn[now] = tot;low[now] = tot;
    top++;stk[top] = now;ins[now] = true;
    for(int i = head[now] ; i != 0 ; i = e[i].nxt){
        int v = e[i].to;
        if(dfn[v] == 0){
            tarjan(v);low[now] = min(low[now], low[v]);
        }else if(ins[v] == true)
            low[now] = min(low[now], dfn[v]);
    }
    if(dfn[now] == low[now]){
        sum++;
        while(stk[top + 1] != now){
            vis[stk[top]] = sum;
            ins[stk[top]] = false;top--;
        }
    }
}
int main(){
    n = read();m = read();
    for(int i = 1 ; i <= m ; i ++)
        x = read(),y = read(),add(x, y);
    for(int i = 1 ; i <= n ; i ++)
        if(dfn[i] == 0)tarjan(i);
    for(int i = 1 ; i <= n ; i ++)
        ans[vis[i]].push_back(i);
    for(int i = 1 ; i <= sum ; i ++){
        write(ans[i].size());
        for(int j = 0 ; j < ans[i].size() ; j ++)
            putchar(' '),write(ans[i][j]);
        putchar('\n');
    }
    return 0;
}

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1136337.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

服务器数据恢复—nas硬盘故障导致raid6失效、存储无法访问的数据恢复案例

服务器故障&分析&#xff1a; 一台nas存储中有一组由十几块硬盘组建的raid6磁盘阵列。 nas存储中的raid6阵列成员盘出现故障离线&#xff0c;磁盘阵列崩溃&#xff0c;nas存储无法正常访问。 北亚企安数据恢复工程师将nas存储内的所有硬盘编号后取出&#xff0c;经过硬件工…

SQL Server Management Studio (SSMS)的安装教程

文章目录 SQL Server Management Studio (SSMS)的安装教程从Microsoft官网下载SQL Server Management Studio安装程序。选中安装程序右键并选择“以管理员的身份运行”选项选择安装目录&#xff0c;单击“安装”按钮开始安装过程安装成功界面安装完成后&#xff0c;您可以启动S…

Blueprints - 虚幻中的行为树(Behavior Tree)

一些以前的学习笔记归档&#xff1b; 简单的说&#xff0c;行为树可以让agent&#xff08;代理&#xff0c;指可以自主活动的游戏角色等&#xff09;做出决定&#xff0c;可以理解为AI的大脑&#xff1b; 行为树自上而下的从树的根节点&#xff08;Root节点&#xff09;开始读…

双11云服务器价格多少钱?在哪买便宜云服务器

恒创科技作为香港/美国/日本云服务器、服务器、DDoS高防的老牌供应商&#xff0c;每到电商促销季&#xff0c;总有很多站长问他家的产品配置和价格。所以&#xff0c;这里整理一份恒创科技2023双十一云服务器、物理服务器、DDoS高防配置和价格汇总&#xff0c;以供站长参考。 …

Jmeter基础---while控制器举例说明

一、 While 控制器 首先创建一个While Controller (While 循环控制器) ​​ 设置界面如下&#xff1a; Condition (function or variable) &#xff1a;条件说明 条件为 Flase 的时候&#xff0c;才会跳出 While 循环&#xff0c;否则一直执行 While 控制器下的样例 1、不填…

国密改造什么意思?国密SSL证书在国密改造中有什么作用?

在网络攻击日益增多的当今&#xff0c;我国政府与企业都意识到加强网络信息安全的重要性&#xff0c;而国密改造不仅可实现密码技术升级&#xff0c;还可实现商用密码自主可控&#xff0c;是国家战略的重要组成部分&#xff0c;由此国密改造成为我国网络安全领域的重要话题。那…

基于元模型优化算法的主从博弈多虚拟电厂动态定价和能量管理(matlab代码)

目录 1 主要内容 主从博弈模型 基于元模型的均衡算法流程图 2 部分代码 3 程序结果 4 下载链接 1 主要内容 该程序复现《基于元模型优化算法的主从博弈多虚拟电厂动态定价和能量管理》模型&#xff0c;建立运营商和多虚拟电厂的一主多从博弈模型&#xff0c;研究运营商动态…

Win10中Pro/E鼠标滚轮不能缩放该怎么办?

Pro/E安装好后&#xff0c;鼠标滚轮不能缩放模型&#xff0c;该怎么办&#xff1f;问题多发生在win8/win10上&#xff0c;新装了PROE&#xff0c;发现滑动鼠标中键不能放大缩小。 彩虹图纸管理软件_图纸管理系统_图纸文档管理软件系统_彩虹EDM【官网】彩虹EDM图纸管理软件系统…

在C代码中找到栈顶的位置并访问栈空间

任务目标 在主循环中写一个任务&#xff0c;检查栈是否溢出。 思路 先找到任务的栈顶位置。在初始化时在栈顶位置写一个标志&#xff0c;运行过程中及时检查该标志是否被改写。如果标志位改变了&#xff0c;则判断为栈溢出。 问题 在RTOS中&#xff0c;任务的栈空间是自己分…

【广州华锐互动】城市水处理VR仿真实训平台

随着科技的不断发展&#xff0c;虚拟现实&#xff08;VR&#xff09;技术已经逐渐渗透到各个领域&#xff0c;为我们带来了前所未有的沉浸式体验。在教育领域&#xff0c;VR技术的应用也日益广泛&#xff0c;为传统的教学模式带来了革命性的变革。本文将以城市水处理VR仿真实训…

Python实现BrainFxxk虚拟机

Python实现BrainFxxk虚拟机 文章目录 Python实现BrainFxxk虚拟机&#x1f468;‍&#x1f3eb;前言&#xff1a;什么是BrainFxxk&#xff1f;&#x1f468;‍&#x1f52c;内容一&#xff1a;BrainFxxk的字符标识&#x1f468;‍⚖️内容二&#xff1a;经典案例——打印 Hello …

文心一言 VS 讯飞星火 VS chatgpt (121)-- 算法导论10.4 2题

二、用go语言&#xff0c;给定一个n结点的二叉树&#xff0c;写出一个 O(n)时间的递归过程&#xff0c;将该树每个结点的关键字输出。 文心一言&#xff1a; 在Go语言中&#xff0c;你可以使用递归函数来遍历二叉树的所有节点&#xff0c;并输出每个节点的关键字。以下是一个…

Vs2019 配置全局公共库和头文件

本文参考&#xff1a;Visual Studio 2019 配置全局公共库目录 背景 在程序开发过程中&#xff0c;日志和数据格式化是必不可少的。而spdlog和fmt正好可以满足这两点并且轻量。但是如果每次新建一个项目都必须引入一次显的太繁琐。那么是否可以加入vs的公共库呢? 实施 spdlog…

vector 的 capacity 增长

在对 vector 容器 push_back 操作时&#xff0c;其capacity 会以怎样的大小进行扩容呢&#xff1f; #include <stdio.h> #include <stdlib.h> #include <vector> #include <string>int main() {std::vector<std::string> stringVec;printf(&quo…

JAVA将EEE MMM dd HH:mm:ss zzz yyyy日期格式化为yyyy-MM-dd HH:mm:ss形式

1、将EEE MMM dd HH:mm:ss zzz yyyy格式的数据转换成yyyy-MM-dd HH:mm:ss 代码如下 public static void main(String[] args) throws ParseException {String dateStr "Mon Oct 26 15:19:15 CST 2020";DateFormat cstFormate new SimpleDateFormat("yyyy-MM…

【VPX610】 青翼科技基于6U VPX总线架构的高性能实时信号处理平台

板卡概述 VPX610是一款基于6U VPX架构的高性能实时信号处理平台&#xff0c;该平台采用2片TI的KeyStone系列多核DSP TMS320C6678作为主处理单元&#xff0c;采用1片Xilinx的Virtex-7系列FPGA XC7VX690T作为协处理单元&#xff0c;具有2个FMC子卡接口&#xff0c;各个处理节点之…

成绩发布必备指南

哈喽&#xff0c;亲爱的老师们&#xff01;今天我们来聊聊一个让老师们的“成绩发布大战”变得轻松自如的秘密武器——成绩查询系统&#xff01;有了它&#xff0c;学生们可以自主查询成绩&#xff0c;再也不用老师们一个个公布成绩啦&#xff01; 那么&#xff0c;这个成绩查询…

pgsql 分组查询,每组取10条

需求&#xff1a; 按照表的字段分组&#xff0c;然后每组取10条结果&#xff0c;返回即可 sql 如下&#xff1a; SELECT* FROM (SELECT chk_id,feature_id,task_id, ROW_NUMBER () OVER (PARTITION BY chk_id ORDER BY chk_id) AS row_num FROM ics_check_report WHERE task…

【电商开放平台】五大全新API接口详解来袭~

为了给用户提供更多优质的商品&#xff0c;大多数电商开放平台为广大开发者们提供五大全新接口&#xff1a;【品牌栏目】、【单个品牌详情】、【商品评论】、【店铺转链】、【短视频商品】。除此之外&#xff0c;【高效转链】接口也进行了更新优化。 一、品牌栏目 接口亮点&am…

【自动化测试教程】Java+Selenium自动化测试环境搭建

本主要介绍以Java为基础&#xff0c;搭建Selenium自动化测试环境&#xff0c;并且实现代码编写的过程。 1.Selenium介绍 Selenium 1.0 包含 core、IDE、RC、grid 四部分&#xff0c;selenium 2.0 则是在两位大牛偶遇相互沟通决定把面向对象结构化&#xff08;OOPP&#xff09…