【蓝桥杯】tarjan算法

news/2024/7/8 0:35:31

一.概述

Tarjan 算法是基于DFS的算法,用于求解图的连通性问题。

Tarjan 算法可以在线性时间内求出:

无向图:

  • 割点与桥
  • 双连通分量

有向图:

  • 强连通分量
  • 必经点与必经边

 1.割点:

若从图中删除节点 x 以及所有与 x 关联的边之后,图将被分成两个或两个以上的不相连的子图,那么称 x 为图的割点

2.割边/桥:

若从图中删除边 e 之后,图将分裂成两个不相连的子图,那么称 e 为图的割边

3.搜索树:

在无向图中,我们以某一个节点 x 出发进行深度优先搜索,每一个节点只访问一次,所有被访问过的节点与边构成一棵树,我们可以称之为“无向连通图的搜索树”。

4.时间戳:​

时间戳是用来标记图中每个节点在进行深度优先搜索被访问的时间顺序。

dfn[x] 来表示。

5.追溯值:

追溯值用来表示从当前节点 x 作为搜索树的根节点出发,能够访问到的所有节点中,时间戳最小的值 —— low[x]。

二.主要思想

主要思想:

通过一次深度优先遍历,即可找出有向图中的强连通分量。

深度优先遍历有两种方式:

  • 先访问当前节点,再递归相邻节点。
  • 先递归相邻节点,再访问当前节点。

数据结构分析:

  • 我们需要给每个节点一个时间戳,这个时间戳代表了该节点被访问的次序。
  • 同时,还需要一个记录该节点通过自身/子孙能够回到的最早的时间节点。

实现步骤:

  • 我们将所有节点的时间戳初始化为0,构建递归树。
  • 循环所有节点,若dfn[i]==0,则递归访问该节点。
  • 每次递归访问节点时,我们需要将该节点压栈,给时间戳和回溯值赋初值,同时遍历该节点的相邻节点,如果相邻节点的dfn为0,则其还没有被访问过,递归访问该节点,访问结束时,更新回溯值;如果相邻节点已经在栈中,那么就直接更新回溯值。另一种情况是,该节点已经被访问过,但是从栈中弹出,那么不做任何处理。
  • 当遍历完该节点的所有邻接点,我们要判断,该节点的时间戳的回溯值是否相等,若相等,则该节点为强连通分量的根节点。开始弹栈,直到将该节点弹出栈。

三.具体应用

1.求无向图的割边/割点

无向图最最重要的点在于,不能够去处理该节点通向父节点的那条边。

这是有向图和无向图最大的区别。

有向图需要去管在栈里的节点,看能否通过栈里面的节点回到更早的时间,但是为什么要用栈,就是因为一个节点只能访问一次;而对于无向图来说,当前正在访问的节点是通过该节点的父节点访问的,而无向图是不能访问子节点通过父节点的,所以不需要栈。

1)割点的判断方法

  • 该点是根节点并且该点有两个及以上的儿子
  • 该点不是根节点并且该点有儿子并且该点儿子的追溯值大于或等于该点被访问的时间

//tarjan求无向图的割点
#include <iostream>
#include <cstring>
#include <vector>

using namespace std;
const int N=100000;

//链式前向星,用来表示边
struct node{
    int to,next;
}edge[N];
int head[N]={-1},cnt=1;//head[i]表示的是以节点i为始点的边,head[i]中的值表示的是第几条边

int time_flag=0,n,m,res=0,root;
int dfn[N]={0},low[N]={0};//dfn时间戳,low追溯值
bool is_articulation[N]={false};//是否是割点

//加边
void add(int u,int v){
    edge[cnt].to=v;
    edge[cnt].next=head[u];
    head[u]=cnt++;
}

void tarjan(int u){
    dfn[u]=low[u]=++time_flag;
    int child=0;
    
    int v=head[u];
    while(v!=-1){
        int a=edge[v].to;
        if(dfn[a]==0){
            child++;
            tarjan(a);
            low[u]=min(low[u],low[a]);
            if((u==root&&child>1)||(u!=root&&low[a]>=dfn[u])){
                if(!is_articulation[u]){
                    is_articulation[u]=true;
                    res++;
                }
            }
        }
        else{
            low[u]=min(low[u],dfn[a]);
        }
        v=edge[v].next;
    }
    
}


int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n>>m;//n个顶点,m条边
    
    //构建好无向图,顶点和边的下标都是从1开始
    memset(head,-1,sizeof(head));
    for(int i=0;i<m;i++){
        int x,y;
        cin>>x>>y;
        add(x,y);
        add(y,x);
    }
    
    for(int i=1;i<=n;i++){
        if(!dfn[i]){
            root=i;
            tarjan(i);
        }
    }
    cout<<res<<'\n';
    for(int i=1;i<=n;i++){
        if(is_articulation[i])
            cout<<i<<' ';
    }
    cout<<'\n';
    return 0;
}

 

2)割边的判断方法

若x->y这条边是割边,那么low[y]>dfn[x] 

2.求强连通分量(有向图)

//tarjan求强连通分量
#include <iostream>
#include <stack>

using namespace std;
const int N=100;

//链式前向星,用来表示边
struct node{
    int to,next;
}edge[N];
int head[N]={-1};//head[i]表示的是以节点i为始点的边,head[i]中的值表示的是第几条边

int time_flag=0,n,m,cnt=0;
int dfn[N]={0},low[N]={0};//dfn时间戳,low追溯值
bool instack[N]={false};
stack<int> s;

//加边
void add(int u,int v){
    edge[cnt].to=v;
    edge[cnt].next=head[u];
    head[u]=++cnt;
}

void tarjan_dfs(int u){
    dfn[u]=low[u]=++time_flag;//时间戳和追溯值赋初值
    s.push(u);//节点入栈
    instack[u]=true;
    
    int v=head[u];
    while(v!=-1){//找与u邻接的边
        int a=edge[v].to;
        if(!dfn[a]){//a还没有被访问过
            tarjan_dfs(a);//深度优先访问a节点
            low[u]=min(low[a],low[u]);
        }
        else if(instack[a]){//v已经被访问过
            low[u]=min(dfn[a],low[u]);
        }
        v=edge[v].next;
    }
    if(low[u]==dfn[u]){//表明节点u是强连通分量的根
        int x;
        do{
            x=s.top();
            cout<<x<<' ';
            s.pop();
            instack[x]={false};
        }while(x!=u);
        cout<<endl;
    }
}
int main(){
    ios::sync_with_stdio(0);cin.tie(0);cout.tie(0);
    cin>>n>>m;//n个顶点,m条边
    
    //构建好有向图,顶点和边的下标都是从1开始
    for(int i=0;i<m;i++){
        int x,y;
        cin>>x>>y;
        add(x, y);
    }
    
    for(int i=1;i<=n;i++){
        if(!dfn[i]){
            tarjan_dfs(i);
        }
    }
    return 0;
}


http://lihuaxi.xjx100.cn/news/2125454.html

相关文章

【机器学习】原理+实例, 一文掌握 精确率、召回率与F1分数,再也不迷路。

精确率、召回率与F1分数 1、引言2、定义2.1 精确率2.2 召回率2.3 F1分数2.4 二分类任务2.5 代码示例 3、总结 1、引言 小屌丝&#xff1a;鱼哥&#xff0c;你在给我详细的唠叨唠叨 精确率&#xff0c;召回率和F1分数。 小鱼&#xff1a;这…这不是机器学习基本知识吗 小屌丝&a…

Tomcat 下载以及安装

Tomcat安装及配置教程主要分为四步&#xff1a; 步骤一&#xff1a;首先确认自己是否已经安装JDK 1. cmd&#xff1a;查看java的版本 步骤二&#xff1a;下载安装Tomcat 1. 下载tomcat :Apache Tomcat - Welcome! 2. 选择对应的tomcat版本&#xff1a; 3. 进行安装&#…

【数据分享】1929-2023年全球站点的逐日平均压力(Shp\Excel\免费获取)

气象数据是在各项研究中都经常使用的数据&#xff0c;气象指标包括气温、风速、降水、能见度等指标&#xff0c;说到气象数据&#xff0c;最详细的气象数据是具体到气象监测站点的数据&#xff01; 有关气象指标的监测站点数据&#xff0c;之前我们分享过1929-2023年全球气象站…

前后端实时数据通信

实现前后端实时数据转换通常涉及到以下几个步骤&#xff1a; 后端提供数据转换接口。 前端实时数据获取。 前端实时数据转换。 前端实时展示转换后数据。 以下是一个简单的例子&#xff0c;假设后端提供了一个接口来转换某种数据格式&#xff0c;前端使用JavaScript和WebS…

Redis缓存预热,该如何实现

一、什么是缓存预热 缓存预热是一种在程序启动或缓存失效之后&#xff0c;主动将热点数据加载到缓存中的策略。这样&#xff0c;在实际请求到达程序时&#xff0c;热点数据已经存在于缓存中&#xff0c;从而减少了缓存穿透和缓存击穿的情况&#xff0c;也缓解了SQL服务器的压力…

node-fs(fileSystem)文件系统-write--02

1.简介 实现与硬盘的交互&#xff0c;文件创建&#xff0c;删除与重命名&#xff0c;移动以及文件内容的写入与读取以及文件夹相关操作。 2.fs 模块的常见用法 2.1fs.writeFile() 方法用于异步地将数据写入文件。 新建一个文件xxx.txt 写入内容 哟哟 实际应用场景&#xff0c…

【linux深入剖析】基础IO操作 | 使用Linux库函数实现读写操作 | 文件相关系统调用接口

&#x1f341;你好&#xff0c;我是 RO-BERRY &#x1f4d7; 致力于C、C、数据结构、TCP/IP、数据库等等一系列知识 &#x1f384;感谢你的陪伴与支持 &#xff0c;故事既有了开头&#xff0c;就要画上一个完美的句号&#xff0c;让我们一起加油 目录 前言1.复习C文件IO相关操…

文献阅读工具-->Adobe pdf + 有道词典

Adobe pdf 有道词典 最近一直在考虑用什么文献阅读工具&#xff0c;痛点无非就是想用翻译功能&#xff0c;Adobe pdf的添加注释已经很好用了&#xff0c;使用了zotero&#xff0c;感觉不行&#xff08;不能直接对原文件修改&#xff0c;有副本&#xff0c;麻烦&#xff09;。…