有向图的强联通分量(Tarjan算法)

2023-05-16

连通分量:

        在一个有向图G中的子图中V,对于任意两个点 u , v u,v uv来说,如果 u u u可以走到 v v v,且 v v v可以走到 u u u的话,我们就称V是一个强连通子图。

强联通分量:

        即最大强联通子图(加入任何一个点都会使得当前的强联通子图被破坏)。

      求有向图的强联通分量实际上就是一个缩点的过程,如果把所有的强联通分量都视作一个点,建立一个新图的话,我们会发现新图一定是一个DAG图(有向无环图),具有拓扑性。并且拓扑序就是强联通分量按编号降序排列。

Tarjan算法求强联通分量:

模板:

void Tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp; //更新时间戳
    sta[++ top] = u, is_stack[u] = true;

    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(!dfn[to]) Tarjan(to), low[u] = min(low[u], low[to]);  //(注意要先递归完,再更新)如果这个点没有被遍历过,那么j能到的地方u也能到
        else if(is_stack[to]) low[u] = min(low[u], dfn[to]);  // 如果这个点被遍历过了,而且还在栈中,说明这个点是某个还
                                                              // 未遍历完的强联通分量中的点。
    }

    if(low[u] == dfn[u]) {  //说明u是其所在强联通分量的最高点,即时间戳最小的点
        scc_cnt ++;  //强联通分量数量加一
        int y;
        do{
            y = sta[top --];
            is_stack[y] = false;
            id[y] = scc_cnt;  //将所有属于此强联通分量的点都标记成其编号
            sz[scc_cnt] ++;   // 记录此强联通分量所包含的点的数量
        }while(y != u);  
    }
}

例题:

ACwing1174. 受欢迎的牛:

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
using namespace std;

const int N = 1e4 + 100, M = 5e4 + 100;
const int inf = 0x3f3f3f3f;
typedef long long ll;

int h[N], e[M], ne[M], idx, sta[N], top;
int id[N], sz[N], scc_cnt, timestamp, low[N], dfn[N], du[N];
// dfn表示时间戳,low[u]表示从u开始走,所能走到的最小时间戳是多少
bool is_stack[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp; //更新时间戳
    sta[++ top] = u, is_stack[u] = true;

    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(!dfn[to]) Tarjan(to), low[u] = min(low[u], low[to]);  //(注意要先递归完,再更新)如果这个点没有被遍历过,那么j能到的地方u也能到
        else if(is_stack[to]) low[u] = min(low[u], dfn[to]);  // 如果这个点被遍历过了,而且还在栈中,说明这个点是某个还
                                                              // 未遍历完的强联通分量中的点。
    }

    if(low[u] == dfn[u]) {  //说明u是其所在强联通分量的最高点,即时间戳最小的点
        scc_cnt ++;  //强联通分量数量加一
        int y;
        do{
            y = sta[top --];
            is_stack[y] = false;
            id[y] = scc_cnt;  //将所有属于此强联通分量的点都标记成其编号
            sz[scc_cnt] ++;   // 记录此强联通分量所包含的点的数量
        }while(y != u);  
    }
}

void solve() {
    int n, m, a, b;
    int ans = 0, zeros = 0;
    memset(h, -1, sizeof h);
    cin >> n >> m;
    while(m --) cin >> a >> b, add(a, b);
    
    for(int i = 1; i <= n; ++ i) //注意要对每一个未遍历过的点都Tarjan
        if(!dfn[i]) Tarjan(i);

    for(int i = 1; i <= n; ++ i)  // 遍历所有边
        for(int j = h[i]; ~j; j = ne[j]) {
            int to = e[j];
            a = id[i], b = id[to];
            if(a != b) //如果属于两个强联通分量
                du[a] ++;  //在两个强联通分量之间建一条新的边
        }
    
    for(int i = 1; i <= scc_cnt; ++ i) {  // 遍历所有强联通分量
        if(!du[i]) zeros ++, ans += sz[i]; 
        if(zeros > 1) {
            ans = 0;
            break;
        }
    }

    cout << ans << endl;
}

int main() {
    #ifndef ONLINE_JUDGE
        freopen("1.in", "r", stdin);
        freopen("1.out", "w", stdout);
    #endif
    IOS;
    solve();
    return 0;
}

ACwing 367. 学校网络

把题目里问的两个问题转换一下,可以变成这样:

  • 第一个问题:有多少个强联通分量的入度为0,即作为起点。
  • 第二个问题:先把原图转换成求了强连通分量缩点之后的图,考虑最少加多少条边可以使得这个图变成一个强连通分量。
第一个问题很容易解决,只要求一次强联通分量之后统计每一个强联通分量的入度就可以。
第二个问题需要使用一个结论,具体的证明这里不给出。
结论:设 q q q是入度为0的强联通分量(即作为“起点”)的数量, p p p是初读为0的强联通分量(即作为“终点”)的数量。要使得当前图变成一个强联通分量所需要加入的最少边的数量就是 m a x ( q , p ) max(q, p) max(q,p)
需要特判当前图已经是一个强联通分量了的情况,这个时候起点的数量是1,终点的数量也是1,但是不用加任何边。

AC code:

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
using namespace std;

const int N = 1e4 + 100, M = 5e4 + 100;
const int inf = 0x3f3f3f3f;
typedef long long ll;

int h[N], e[M], ne[M], idx, sta[N], top;
int id[N], sz[N], scc_cnt, timestamp, low[N], dfn[N], indu[N], outdu[N];
// dfn表示时间戳,low[u]表示从u开始走,所能走到的最小时间戳是多少
bool is_stack[N];

void add(int a, int b) {
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp; //更新时间戳
    sta[++ top] = u, is_stack[u] = true;

    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(!dfn[to]) Tarjan(to), low[u] = min(low[u], low[to]);  //(注意要先递归完,再更新)如果这个点没有被遍历过,那么j能到的地方u也能到
        else if(is_stack[to]) low[u] = min(low[u], dfn[to]);  // 如果这个点被遍历过了,而且还在栈中,说明这个点是某个还
                                                              // 未遍历完的强联通分量中的点。
    }

    if(low[u] == dfn[u]) {  //说明u是其所在强联通分量的最高点,即时间戳最小的点
        scc_cnt ++;  //强联通分量数量加一
        int y;
        do{
            y = sta[top --];
            is_stack[y] = false;
            id[y] = scc_cnt;  //将所有属于此强联通分量的点都标记成其编号
            sz[scc_cnt] ++;   // 记录此强联通分量所包含的点的数量
        }while(y != u);  
    }
}

void solve() {
    int n, a;
    int ans = 0, s = 0, t = 0;
    memset(h, -1, sizeof h);
    cin >> n;
    for(int i = 1; i <= n; ++ i) 
        while(cin >> a && a != 0) add(i, a);
    
    for(int i = 1; i <= n; ++ i)
        if(!dfn[i]) Tarjan(i);
    
    for(int i = 1; i <= n; ++ i)
        for(int j = h[i]; ~j; j = ne[j]) {
            int to = e[j];
            int x = id[i], y = id[to];
            if(x != y) indu[y] ++ , outdu[x] ++;
        }

    for(int i = 1; i <= scc_cnt; ++ i) {
        if(!indu[i]) ans ++, s ++;
        if(!outdu[i]) t ++;
    }
    cout << ans << endl;
    if(scc_cnt == 1) cout << 0 << endl;
    else cout << max(s, t) << endl;
}

int main() {
    #ifndef ONLINE_JUDGE
        freopen("1.in", "r", stdin);
        freopen("1.out", "w", stdout);
    #endif
    IOS;
    solve();
    return 0;
}

ACwing 1175. 最大半连通子图

       首先我们可以发现,对于一个强联通分量来说,从其中任意选出两个点 u u u v v v u u u v v v都是一个半联通的点对。
       既然我们想要找出最大的半联通子图,那么肯定要尽可能的多选这些半联通的点对,所以我们需要选一整个强联通分量,然后我们会发现,如果一个强联通分量可以走到另一个强联通分量,那么这个强联通分量里的所有点就可以和它能走到的强联通分量里的点构成一个更大的半联通子图。
       所以其实我们就是要找出在缩完点之后的图中,点的个数最大的链,这条链就是我们的最大半联通子图。
       因为我们发现当我们缩完点之后,整个图变成了一张拓扑图,这样我们所需要求解的就是在拓扑图上求最长路径并且统计最长路径的情况数,由拓扑序进行状态转移就可以,非常简单。

缩完点之后的拓扑序就是强联通分量按编号降序排列。
因为状态只转移一次,所以需要避免重边!!!

AC code:

#include <bits/stdc++.h>
#define endl '\n'
#define IOS ios::sync_with_stdio(0); cin.tie(0); cout.tie(0)
using namespace std;

const int N = 1e5 + 100, M = 2e6 + 100;
const int inf = 0x3f3f3f3f;
typedef long long ll;
typedef pair<int, int> PII;

int h[N], h1[N], e[M], ne[M], idx, top;
int id[N], sz[N], scc_cnt, timestamp, low[N], dfn[N], f[N], g[N];
// dfn表示时间戳,low[u]表示从u开始走,所能走到的最小时间戳是多少
bool is_stack[N];
int sta[N];

void add(int h[], int a, int b) {  //这样写就可以按表头来区分是新图还是旧图
    e[idx] = b, ne[idx] = h[a], h[a] = idx ++;
}

void Tarjan(int u) {
    dfn[u] = low[u] = ++ timestamp; //更新时间戳
    sta[++ top] = u, is_stack[u] = true;

    for(int i = h[u]; ~i; i = ne[i]) {
        int to = e[i];
        if(!dfn[to]) Tarjan(to), low[u] = min(low[u], low[to]);  //(注意要先递归完,再更新)如果这个点没有被遍历过,那么j能到的地方u也能到
        else if(is_stack[to]) low[u] = min(low[u], dfn[to]);  // 如果这个点被遍历过了,而且还在栈中,说明这个点是某个还
                                                              // 未遍历完的强联通分量中的点。
    }

    if(low[u] == dfn[u]) {  //说明u是其所在强联通分量的最高点,即时间戳最小的点
        scc_cnt ++;  //强联通分量数量加一
        int y;
        do{
            y = sta[top --];
            is_stack[y] = false;
            id[y] = scc_cnt;  //将所有属于此强联通分量的点都标记成其编号
            sz[scc_cnt] ++;   // 记录此强联通分量所包含的点的数量
        }while(y != u);  
    }
}

void solve() {
    int n, m, x;
    memset(h, -1, sizeof h);
    memset(h1, -1, sizeof h1);
    cin >> n >> m >> x;
    while(m --) {
        int a, b;
        cin >> a >> b;
        add(h, a, b);
    }

    for(int i = 1; i <= n; ++ i)
        if(!dfn[i]) Tarjan(i);

    unordered_set<ll> S;  //hash用,因为u和v的值都只到100000,
                         //所以(u, v) ---> u * 1000000 + v,一定是唯一值
    
    //unordered_set要比unordered_map快
    for(int i = 1; i <= n; ++ i)  // 建立新图
        for(int j = h[i]; ~j; j = ne[j]) {
            int to = e[j], a = id[i], b = id[to];
            ll hs = a * 1000000ll + b;
            if(a != b && !S.count(hs))  // 处理重边
                add(h1, a, b), S.insert(hs);
        }
    
    for(int i = scc_cnt; i; -- i) {  //强联通分量按编号降序即为拓扑序,可进行转移
        if(!f[i]) f[i] = sz[i], g[i] = 1; //如果是入度为0的点
        for(int j = h1[i]; ~j; j = ne[j]) {
            int to = e[j];
            if(f[to] < f[i] + sz[to]) f[to] = f[i] + sz[to], g[to] = g[i];
            else if(f[to] == f[i] + sz[to]) g[to] = (g[to] + g[i]) % x;
        }
    }

    int ans = 0, sum = 0;
    for(int i = scc_cnt; i; -- i) 
        if(ans < f[i]) ans = f[i], sum = g[i];
        else if(ans == f[i]) sum = (sum + g[i]) % x;
    
    cout << ans << endl << sum << endl;
}

int main() {
    IOS;
    solve();
    return 0;
}
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

有向图的强联通分量(Tarjan算法) 的相关文章

  • 不同操作系统及CPU字长、寻址能力、指针宽度的理解

    不同操作系统及CPU字长 寻址能力 指针宽度的理解 字长CPU位宽CPU的寻址能力操作系统32bit 64bit指针大小 字长 64位CPU和32位CPU中64和32的含义 xff1a 64和32指的是CPU中的寄存器 通用 的字长 xff
  • new和malloc的区别

    new和malloc的区别 1 new从自由存储区上分配内存 xff0c malloc从堆上分配内存 自由存储区是C 43 43 基于new操作符的一个抽象概念 xff0c 凡是通过new操作符进行内存申请 xff0c 该内存即为自由存储区
  • 程序中的负数存储及类型转换

    程序中的负数存储及类型转换 负数在计算机中怎样存储什么是原码 反码 补码为什么要设置反码 xff0c 补码剖析本质 C语言数据类型转换 xff08 自动类型转换 43 强制类型转换 xff09 自动类型转换强制类型转换类型转换只是临时性的自

随机推荐

  • Java Collections singleton()方法与示例

    集合类singleton 方法 Collections Class singleton method singleton method is available in java util package singleton 方法在java
  • 找素数问题

    span class token macro property span class token directive hash span span class token directive keyword include span spa
  • 嵌入式面试题

    面试题 字符串能直接比较大小吗typedef定义数组类型用法 字符串能直接比较大小吗 C 43 43 中字符串分两种 xff0c 一种是C语言的字符串 xff0c 一种是string字符串 C语言字符串是不可以直接比较大小的 xff0c s
  • 解决Endnote插入参考文献时导致word闪退问题

    问题描述 xff1a 通过endnote插入参考文献时 xff0c 会使得word闪退 原因分析 有像域代码之类的交互 xff0c 与endnote冲突 解决方法把word文档clean下 xff0c 即将域代码删除 解决方法 Ctrl 4
  • 音视频基础

    音视频基础 写在前面基础概念音视频直播推流和拉流什么是推流什么是拉流推流和拉流的区别 协议层 封装格式层 编解码层 像素层RTP RTCP RTMP RTSP区别RTP Real time Transport Protocol 实时传输协议
  • 回车和换行的区别

    回车和换行的区别 回车和换行的概念不同的系统间传递文件会涉及格式的转换Unix gt WindowsUnix lt Windows 回车和换行的概念 首先介绍一下 回车 xff08 carriage return r xff09 和 换行
  • 强大的PubMed插件Scholarscope

    强大的PubMed插件Scholarscope 学术基础 SCI分区什么是Pubmed什么是ScholarscopeScholarscope在不同浏览器下安装指南插件使用 学术基础 SCI分区 SCI是有两个分区 一个是JCR的划分 一般称
  • 反客STM32F4核心板DAP无法下载程序解决

    反客STM32核心板DAP无法下载程序解决 问题解决 问题 反客STM32F407ZGT6核心板使用反客的DAP下载器下载程序 xff0c 无法识别下载器 xff0c 说明下载器没有正常工作 xff08 这里是已经换过杜邦线了 xff0c
  • 有人物联网485转网口模块网口调试助手1035未知错误

    有人物联网485转网口模块网口调试助手1035未知错误 问题解决 问题 项目使用有人物联网485转网口模块USR TCP232 304 xff0c 将模块接入实验室路由器 xff0c IP地址设置为动态IP xff0c 路由器上查得IP为1
  • 1.半导体基础知识

    1 半导体基础知识 本征半导体什么是半导体 xff1f 什么是本征半导体 xff1f 本征半导体的结构本征半导体中的两种载流子为什么将自然界导电性能中等的半导体材料制成本征半导体 杂质半导体N型半导体P型半导体 PN结PN结中的扩散运动漂移
  • 2.半导体二极管

    2 半导体二极管 二极管的组成二极管和PN结伏安特性的区别二极管的伏安特性及电流方程为什么反向饱和电流越小 xff0c 单向导电性能越强 二极管的等效电路二极管的主要参数稳压二极管 xff08 又称齐纳二极管或反向击穿二极管 xff09 稳
  • Python | 从另一个列表的指定开始到结束索引创建一个列表

    Given a list start and end index we have to create a list from specified index of the list in Python 给定一个列表 xff0c 开始和结束索
  • EDA基础概念

    EDA基础概念 EDA和CADCAD工具EDA工具 EDA技术实现目标可编程逻辑器件简称PLD发展历程CPLD简介FPGA简介FPGA和CPLD区别是否需要同时学习FPGA和CPLDXilinx xff08 赛灵思 xff09 公司介绍 x
  • 半导体存储电路

    半导体存储电路 SR锁存器和触发器寄存器存储器存储器分类RAMSRAMDRAM ROMMROMPROMEPROMEEPROMFLASH原理发现者应用工作原理存储单元 磁盘硬盘机械硬盘 xff08 HDD xff09 固态硬盘 xff08 S
  • python编写简单的EXE启动器

    exe启动器 放假到现在一直憋在家里 xff0c 最近实在无聊 xff0c 就下了两个游戏玩 xff0c 玩的时候 xff0c 因为快捷方式放桌面感觉有点麻烦 xff0c path文件下图标有太多 xff0c 就想起了自己编写一个exe启动
  • mysql的left join和inner join的详细用法

    join用法 1 inner join xff0c 内连接 显示两个表中有联系的所有数据 2 left join xff0c 左链接 以左表为参照 显示所有数据 右表中没有则以null显示 3 right join xff0c 右链接 以右
  • 原来我的Ubuntu20.04桌面假死,按Alt+Ctrl+F1就可以恢复!

    配置 联想Y9000PUbuntu20 04双系统显卡驱动 NVIDIA Linux x86 64 525 89 02 xff08 下载自官网 xff09 浏览器 Edge 问题 双系统使用过一段时间之后 xff0c 偶尔会出现桌面假死的情
  • 如何查看python安装路径

    在使用python的时候 xff0c 有时候会需要找到python包的安装位置 xff0c 来找其他安装的第三方包 下面我们来看看 xff0c 在不同平台上 xff0c 怎么找到python的安装路径 很多运行的系统软件都是建立在pytho
  • ftp文件操作

    FTP中的文件操作 如何对ftp文件系统进行操作 文章目录 FTP中的文件操作前言一 ftp是什么 xff1f 二 使用步骤1 本地创建搭建ftp系统2 操作 总结 前言 公司中运用到了ftp小文件系统 xff0c 自己在本地学习了一下网上
  • 有向图的强联通分量(Tarjan算法)

    连通分量 在一个有向图G中的子图中V xff0c 对于任意两个点 u xff0c v u xff0c v u xff0c v 来说 xff0c 如果 u