快乐地打牢基础(2)——强连通分量

2023-12-30 06:38

本文主要是介绍快乐地打牢基础(2)——强连通分量,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

一、定义


在有向图 G {G} G中,如果两个顶点 u , v {u,v} u,v间尊在一条 u {u} u v {v} v的路径且也存在一条 v {v} v u {u} u的路径,则称这两个顶点 u , v {u,v} uv是强联通的(strongly
connected)。如果有向图 G {G} G是一个强连通图,有向非强连通图的极大强连通子图,称为强连通分量(strongly connected components)。

极大强连通子图: G {G} G是一个极大强连通子图,当且仅当 G {G} G是一个强连通子图且不存在另一个强连通子图 G ′ {G'} G,使得 G {G} G G ′ {G'} G的真子集。

举个例子:
在下图中,子图 { 1 , 2 , 3 , 4 } {\{1,2,3,4\}} {1,2,3,4}是一个强连通分量,因为顶点 1 , 2 , 3 , 4 {1,2,3,4} 1,2,3,4两两可达, { 5 } , { 6 } {\{5\},\{6\}} {5},{6}也是两强连通分量。
在这里插入图片描述
应用:若将原图所有的强连通分量都缩为一个点,那么原图就会形成一个DAG(有向无环图)

在这里插入图片描述

二、Kosaraju算法


基于两次DFS的有向图强连通子图的算法。时间复杂度 O ( n + m ) {O(n+m)} O(n+m)

算法过程:
Ⅰ.对原有向图 G {G} G进行DFS,记录结点访问完的顺序 d [ i ] {d[i]} d[i], d [ i ] {d[i]} d[i]表示第 i {i} i个访问完的结点是 d [ i ] {d[i]} d[i]

Ⅱ.选择具有最晚访问完的顶点,对反向图 G T {GT} GT进行DFS,删除能够遍历到的顶点,这些顶点构成一个强连通分量。

Ⅲ.如果还有顶点没有删除,继续第二步,否则算法结束。

举个栗子:
在这里插入图片描述
第一次遍历的访问次序是1,2,3,4;访问完成的顺序是3,2,1,4.。

第二次遍历是按照“访问完成的顺序”从后向前访问,所以第二次是先从4开始,然后是1,2,最后是3。

最后我们可以求出三个强连通分量: { ( 4 ) , ( 1 , 2 ) , ( 3 ) } {\{(4),(1,2),(3)\}} {(4),(1,2),(3)}

算法模板:

#include<bits/stdc++.h>
using namespace std;const int Max = 101;
int vis[Max]={0},mapp[Max][Max],d[Max],n,t=0;
void dfsone(int x) {vis[x] = 1;for(int i = 1; i <= n; i++) {if(!vis[i] && mapp[x][i]) dfsone(i);}d[++t] = x;
}
void dfstwo(int x) {vis[x] = t;for(int i = 1; i <= n; i++)if(!vis[i] && mapp[i][x]) dfstwo(i);
}void Kosaraju() {int i;t = 0;//进行第一次DFS遍历for(i = 1; i<= n; i++)if(!vis[i]) dfsone(i);memset(vis,0,sizeof(vis));t = 0;printf("\nd[i]:\n");for(int i = 1; i <= n; i++)printf("%d ",d[i]);//进行第二次DFS遍历for(i = n; i >= 1; i--)if(!vis[d[i]]) {t++;dfstwo(d[i]);}printf("\nt = %d\n",t);
}
int main() {scanf("%d",&n);//使用邻接矩阵存储for(int i = 1; i <= n; i++) {getchar();for(int j = 1; j <= n; j++)scanf("%d",&mapp[i][j]);}Kosaraju();printf("\nvis[i]:\n");for(int i = 1; i <= n; i++)printf("%d ",vis[i]);return 0;
}
/*
4
0 1 0 0
1 0 1 0
0 0 0 0
0 0 0 0*/

上述例子的运行结果:
在这里插入图片描述

三、Tarjan算法


1.一些概念和定义

Tarjan算法,由罗伯特·塔扬(Robert Tarjan)提出,是基于对图深度优先搜索(DFS)的算法,每个强连通分量为搜索树的一棵子树。搜索时,把当前搜索树中未处理的结点加入到一个栈,回溯时可以判断栈顶到栈中的结点是否构成一个强连通分量。

DFS过程中会遇到的四种边:

树枝边:DFS时经过的边,即DFS搜索树上的边
前向边:与DFS方向一致,从某个结点指向其某个子孙的边
后向边:与DFS方向相反,从某个结点指向其某个祖先的边
横叉边:从某个结点指向搜索树中另一子树中某结点的边

定义 D F N ( u ) {DFN(u)} DFNu为结点 u {u} u的搜索次序编号(时间戳), L o w ( u ) {Low(u)} Lowu u {u} u或者 u {u} u的子树能够回溯到的最早的栈中结点的DFN值,也就是点 u {u} u及其子孙结点的中最小的DFN值。

由上面四种边的定义可以得到:

如果 ( u , v ) {(u,v)} (u,v)为树枝边, u {u} u v {v} v的父结点,则 L o w ( u ) = M i n { L o w ( u ) , L o w ( v ) } {Low(u) =Min\{Low(u),Low(v)\}} Lowu=Min{Low(u),Low(v)}

如果 ( u , v ) {(u,v)} (u,v)为后向边或者指向栈中的结点的横叉边,则 L o w ( u ) = M i n { L o w ( u ) , D F N ( v ) } {Low(u)=Min\{Low(u),DFN(v)\}} Lowu=Min{Low(u),DFN(v)}

当结点 u {u} u的搜索过程结束后,若 D F N ( u ) = L o w ( u ) DFN(u) = Low(u) DFN(u)=Low(u),则以 u {u} u为根的搜索子树上所有还在栈中的结点(即 u {u} u和栈中在 u {u} u之后的所有结点)是一个强连通分量,可退栈。为什么呢?通俗地说,若 u {u} u为强连通分量的根,那么它的子孙中最高祖宗就是它本身。

2.算法的主要过程
  1. 数组初始化:当首次搜索到点 u {u} u时, D F N ( u ) {DFN(u)} DFN(u)为结点 u {u} u的搜索次序编号(时间戳)。

  2. 堆栈:将 u {u} u压入栈顶。

  3. 更新 L o w ( u ) {Low(u)} Low(u)
    如果 ( u , v ) {(u,v)} (u,v)为树枝边( v {v} v不在栈中), u {u} u v {v} v的父结点,则 L o w ( u ) = M i n { L o w ( u ) , L o w ( v ) } {Low(u)= Min\{Low(u),Low(v)\}} Lowu=Min{Low(u),Low(v)}

    如果 ( u , v ) {(u,v)} (u,v)为后向边或者指向栈中结点的横叉边( v {v} v在栈中),则 L o w ( u ) = M i n { L o w ( u ) , D F N ( v ) } {Low(u)=Min\{Low(u),DFN(v)\}} Lowu=Min{Low(u),DFN(v)}

  4. 如果 u {u} u的子树已经全部遍历后 L o w ( u ) = D F N ( u ) {Low(u) = DFN(u)} Low(u)=DFN(u),则将 u {u} u和栈中在 u {u} u之后的所有结点弹出栈。这些出栈的元素组成一个强连通分量。

  5. 继续搜索(或许会更换搜索的起点,因为整个有向图可能分为多个不连通的部分),直到所有的点被遍历。

3.算法流程演示

下面直接用例子来演示算法过程,更为直观。
先复习一下定义:

树枝边:DFS时经过的边。
前向边:与DFS方向一致,从某个结点指向其某个子孙的边。
后向边:与DFS方向相反,从某个结点指向其某个祖先的边。
横叉边:从某个结点指向搜索树中另一子数中的某结点的边。
DFN( u {u} u):结点 u {u} u的搜索次序编号(时间戳)。
Low( u {u} u): u {u} u或者 u {u} u的子树能够回溯到的最早的栈中结点的DFN值。

从结点1开始DFS
从1进入 DFN[1]= Low[1]= ++cnt = 1
入栈 1
由1进入3 DFN[3]=Low[3]= ++cnt = 2
入栈 1 3
之后由2进入5 DFN[4]=Low[5]= ++cnt = 3
入栈 1 3 5
之后由4进入 6 DFN[6]=Low[6]=++cnt = 4
入栈 1 3 5 6
栈中结果是 1 → 3 → 5 → 6 {1 \rightarrow3 \rightarrow5 \rightarrow6} 1356。搜索到结点 u = 6 {u = 6} u=6时,DFN[6]= Low[6],找到一个强连通分量。将 u {u} u u {u} u后面所有结点弹出栈,因为 u {u} u后面没有元素,弹出 u {u} u { 6 } {\{6\}} {6}为一个强连通分量。
在这里插入图片描述
返回结点5,发现DFN[5] = Low[5],退栈后, { 5 } {\{5\}} {5}为一个强连通分量。
在这里插入图片描述返回结点3,继续搜索,到达结点4,把4加入栈。发现结点4向结点1有后向边,结点1还在栈中,所以 L o w [ 4 ] = M i n { L o w ( 4 ) , D F N ( 1 ) } = 1 {Low[4] = Min\{Low(4),DFN(1)\} = 1} Low[4]=Min{Low(4),DFN(1)}=1。结点6已经出栈,(4,6)是指向非栈中结点的横叉边,因此不更新 L o w [ 4 ] {Low[4]} Low[4]返回3,(3,4)为树枝边,所以 L o w [ 3 ] = M i n { L o w [ 3 ] , L o w [ 4 ] } {Low[3] = Min\{Low[3],Low[4]\}} Low[3]=Min{Low[3],Low[4]}
在这里插入图片描述继续回到结点1,最后访问结点2。访问边(2,4)此边是指向栈中结点的横叉边,4还在栈中,所以 L o w [ 2 ] = M i n { L o w [ 2 ] , D F N [ 4 ] } = 5 {Low[2] = Min\{Low[2],DFN[4]\} = 5} Low[2]=Min{Low[2],DFN[4]}=5.返回1后,发现 D F N [ 1 ] = L o w [ 1 ] {DFN[1] = Low[1]} DFN[1]=Low[1],把栈中1及以后的所有结点弹出,组成一个强连通分量 { 1 , 3 , 4 , 2 } {\{1,3,4,2\}} {1,3,4,2}
在这里插入图片描述

到此,算法结束。一共求出了三个强连通分量 { 6 } , { 5 } , { 1 , 3 , 4 , 2 } {\{6\},\{5\},\{1,3,4,2\}} {6},{5},{1,3,4,2}

在运行Tarjan算法的过程中,每个顶点被访问了一次,且只进出一次栈,每条边也只访问了一次,所以该算法的时间复杂度为 O ( n + m ) {O(n+m)} O(n+m),n为点数,m为边数。
模板代码:

#include <bits/stdc++.h>
using namespace std;
const int N = 1e4+10;
const int M = 5e4+10;
vector<int> G[M];
//  sta :栈     bk[u] : 结点u是否在栈中  dfn和low如提议描述
//  top是栈顶  col是染色标记  cnt:计数   n:点个数  m:边个数 
int dfn[N],low[N],sta[N],bk[N],cnt,top,color = 0;
int n,m;
void Tarjan(int u){dfn[u] = low[u] = ++cnt;//打上时间戳 并初始化low sta[++top] = u;//u入栈int len = G[u].size(); for(int i = 0 ; i < len; i++){int v = G[u][i];if(!dfn[v]){//如果后继点没有访问过就从后继点开始继续搜索 Tarjan(v);low[u] = min(low[u],low[v]);//树枝边,更新low[u] }else if(!bk[v]){//若v在栈中 low[u] = min(low[u],dfn[v]); //后向边或者指向栈内结点的横叉边 }}if(low[u] == dfn[u]){bk[u] = ++color;while(sta[top] != u){bk[sta[top]] = color;//将sta[top]弹出,并染色为color --top;}--top;//将u弹出 }
} 
int main(){scanf("%d %d",&n,&m);int x,y;for(int i = 1; i <= m; i++){scanf("%d %d",&x,&y);G[x].push_back(y);}for(int i = 1; i <= n; i++)if(!dfn(i))Tarjan(i);for(int i = 1; i <= n; i++)cout<<bk[i]<<" "; return 0;
}
/*
6 8
1 2
1 3
2 4
3 4
3 5
4 1
4 6
5 6
*/
4.操作原理

Tarjan算法的操作原理如下:
Tarjan算法基于定理:在任何DFS中,同一强连通分量内的所有顶点均在同一棵深度优先搜索树中。 也就是说,强连通分量一定是有向图的某一棵深度优先搜索树的子树。

可以证明,当一个点既是强连通子图Ⅰ中的点,又是强连通子图Ⅱ中的点时,它是强连通子图Ⅰ ⋃ {\bigcup} 图Ⅱ中的点。

这样,我们用Low值记录该点所在强连通子图对应的搜索子树的根结点的DFN值。注意,该子树中的元素在栈中一定是相邻的,且根结点在栈中一定位于所有子树元素的最下方。

强连通分量是由若干个环组成的。所以,当有环形成时(也就是搜索的下一个点已在栈中),我们将这一条路的Low值统一,即这条路径上的点属于同一个强连通分量。

如果遍历完整棵搜索树后,某个点的DFN的值等于Low的值,则它是该搜索子树的根。 这时,它以上(包括它自己)一直到栈顶的所有元素组成一个强连通分量。

四、Tarjan算法的用途


1.有向图的缩点

将同一个强连通分量中的点缩成一个新结点,对于两个新节点 a , b {a,b} a,b,它们之间有边相连,当且仅当存在两个点 u {u} u属于 a {a} a v {v} v属于 b {b} b,且 < u , v > ∈ E {<u,v>\in E} <u,v>E
在这里插入图片描述

2.解决2-SAT问题

通俗的sat问题表述一般是这样的:有很多个集合,每个集合里面有若干元素,现给出一些取元素的规则,要你判断是否可行,可行则给出一个可行方案。如果所有集合中,元素个数最多的集合有k个,那么我们就说这是一个k-sat问题,同理,2-sat问题就是k=2时的情况。
(摘自博客:https://blog.csdn.net/hawo11/article/details/74908233)
【例题】 一本通 OJ 1513 (BZOJ 1051) USACO 2003 Fall 受欢迎的牛
题意:

有 N 头牛,给你 M 对整数 (A,B),表示牛 A 认为牛 B 受欢迎。这种关系是具有传递性的,如果 A 认为 B 受欢迎,B 认为 C 受欢迎,那么牛 A 也认为牛 C 受欢迎。你的任务是求出有多少头牛被除自己之外的所有牛都认为是受欢迎的。

思路:

这题的思路就比较简单

把牛看成点,把“认为受欢迎”的这种关系变成一条有向边,A认为B受欢迎,那么A → {\rightarrow} B,形成一个有向图。

因为受欢迎有传递性,所以在有向图上找出强连通子图,可以知道每个子图中的牛一定是互相喜欢的(子图中的任意一头牛都可以被子图内的其他牛喜欢),把每一个强连通子图找出来,缩点,然后整个图就变成有向无环图(DAG)。因为一头牛需要受到其他所有牛的欢迎,那么它就不能喜欢任意除了自己这个连通子图的其他牛

所以,找到一个出度为0的强连通子图(此时应该已经缩成了一个点),其中的牛的头数就是需要的答案,当然如果存在两个及两个以上的出度为0的点,那就说明这几个子图中的奶牛各自无法欢迎其他子图里的奶牛,输出0。
如下图举例
D和E就是所有牛都欢迎的。
在这里插入图片描述

#include <bits/stdc++.h>
using namespace std;const int N = 1e4 + 10;
const int M = 5e4 + 10;
vector<int> G[M];
//de[i]:表示强连通子图i的出度  si[i]:表示 强连通子图i中的结点个数 
int dfn[N],low[N],bk[N],sta[N],de[N],si[N];
int n,m,cnt = 0,top,color=0,x,y;
void Tarjan(int u){dfn[u] = low[u] = ++cnt; sta[++top] = u;int len = G[u].size();for(int i = 0 ; i < len; i++){int v = G[u][i];if(!dfn[v]){Tarjan(v);low[u] = min(low[u],low[v]);}else if(!bk[v]){low[u] = min(low[u],dfn[v]);}}if(low[u] == dfn[u]){bk[u] = ++color;++si[color];while(sta[top] != u){++si[color];bk[sta[top]] = color;--top;}--top;}
}
int main(){scanf("%d %d",&n,&m);for(int i = 0; i < m; i++){scanf("%d %d",&x,&y);G[x].push_back(y);// 建图 } for(int i = 1; i <= n; i++)if(!dfn[i])Tarjan(i);//进行出度统计 for(int i = 1; i <= n ; i++){int len = G[i].size();for(int j = 0; j < len; j++){if(bk[i] != bk[G[i][j]])de[bk[i]]++;}}//计算出度为0的强连通子图的个数 int ans = 0,u = 0;for(int i = 1; i <= color; i++)if(!de[i]){ ans = si[i];u++;//出度为0的强连通子图的个数 } if(u == 1)printf("%d\n",ans);elseprintf("0\n");return 0;
} 

练习题:一本通OJ

1515 网络协议
1516 消息的传递
1517 间谍网络
1518 抢掠计划
1519 和平委员会

本文完全照抄 参考黄新军 董永建《信息学奥赛一本通·提高》

这篇关于快乐地打牢基础(2)——强连通分量的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/552015

相关文章

零基础学习Redis(10) -- zset类型命令使用

zset是有序集合,内部除了存储元素外,还会存储一个score,存储在zset中的元素会按照score的大小升序排列,不同元素的score可以重复,score相同的元素会按照元素的字典序排列。 1. zset常用命令 1.1 zadd  zadd key [NX | XX] [GT | LT]   [CH] [INCR] score member [score member ...]

【Linux 从基础到进阶】Ansible自动化运维工具使用

Ansible自动化运维工具使用 Ansible 是一款开源的自动化运维工具,采用无代理架构(agentless),基于 SSH 连接进行管理,具有简单易用、灵活强大、可扩展性高等特点。它广泛用于服务器管理、应用部署、配置管理等任务。本文将介绍 Ansible 的安装、基本使用方法及一些实际运维场景中的应用,旨在帮助运维人员快速上手并熟练运用 Ansible。 1. Ansible的核心概念

AI基础 L9 Local Search II 局部搜索

Local Beam search 对于当前的所有k个状态,生成它们的所有可能后继状态。 检查生成的后继状态中是否有任何状态是解决方案。 如果所有后继状态都不是解决方案,则从所有后继状态中选择k个最佳状态。 当达到预设的迭代次数或满足某个终止条件时,算法停止。 — Choose k successors randomly, biased towards good ones — Close

线性因子模型 - 独立分量分析(ICA)篇

序言 线性因子模型是数据分析与机器学习中的一类重要模型,它们通过引入潜变量( latent variables \text{latent variables} latent variables)来更好地表征数据。其中,独立分量分析( ICA \text{ICA} ICA)作为线性因子模型的一种,以其独特的视角和广泛的应用领域而备受关注。 ICA \text{ICA} ICA旨在将观察到的复杂信号

音视频入门基础:WAV专题(10)——FFmpeg源码中计算WAV音频文件每个packet的pts、dts的实现

一、引言 从文章《音视频入门基础:WAV专题(6)——通过FFprobe显示WAV音频文件每个数据包的信息》中我们可以知道,通过FFprobe命令可以打印WAV音频文件每个packet(也称为数据包或多媒体包)的信息,这些信息包含该packet的pts、dts: 打印出来的“pts”实际是AVPacket结构体中的成员变量pts,是以AVStream->time_base为单位的显

C 语言基础之数组

文章目录 什么是数组数组变量的声明多维数组 什么是数组 数组,顾名思义,就是一组数。 假如班上有 30 个同学,让你编程统计每个人的分数,求最高分、最低分、平均分等。如果不知道数组,你只能这样写代码: int ZhangSan_score = 95;int LiSi_score = 90;......int LiuDong_score = 100;int Zhou

c++基础版

c++基础版 Windows环境搭建第一个C++程序c++程序运行原理注释常亮字面常亮符号常亮 变量数据类型整型实型常量类型确定char类型字符串布尔类型 控制台输入随机数产生枚举定义数组数组便利 指针基础野指针空指针指针运算动态内存分配 结构体结构体默认值结构体数组结构体指针结构体指针数组函数无返回值函数和void类型地址传递函数传递数组 引用函数引用传参返回指针的正确写法函数返回数组

【QT】基础入门学习

文章目录 浅析Qt应用程序的主函数使用qDebug()函数常用快捷键Qt 编码风格信号槽连接模型实现方案 信号和槽的工作机制Qt对象树机制 浅析Qt应用程序的主函数 #include "mywindow.h"#include <QApplication>// 程序的入口int main(int argc, char *argv[]){// argc是命令行参数个数,argv是

【MRI基础】TR 和 TE 时间概念

重复时间 (TR) 磁共振成像 (MRI) 中的 TR(重复时间,repetition time)是施加于同一切片的连续脉冲序列之间的时间间隔。具体而言,TR 是施加一个 RF(射频)脉冲与施加下一个 RF 脉冲之间的持续时间。TR 以毫秒 (ms) 为单位,主要控制后续脉冲之前的纵向弛豫程度(T1 弛豫),使其成为显著影响 MRI 中的图像对比度和信号特性的重要参数。 回声时间 (TE)

Java基础回顾系列-第七天-高级编程之IO

Java基础回顾系列-第七天-高级编程之IO 文件操作字节流与字符流OutputStream字节输出流FileOutputStream InputStream字节输入流FileInputStream Writer字符输出流FileWriter Reader字符输入流字节流与字符流的区别转换流InputStreamReaderOutputStreamWriter 文件复制 字符编码内存操作流(