算法随笔:强连通分量

news2025/1/13 8:05:07

概念和性质:

 

强连通:在有向图G中,如果两个点u和v是互相可达的,即从u出发可以到达v,从v出发也可以到达u,则成u和v是强连通的。

强连通分量:如果一个有向图G不是强连通图,那么可以把它分成躲个子图,其中每个子图的内部是强连通的,而且这些子图已经扩展到最大,不能与子图外的任一点强连通,成这样的一个“极大连通”子图是G的一个强连通分量(SCC)。

强连通分量的一些性质

(1)一个点必须有出度和入度,才会与其他点强连通。

(2)把一个SCC从图中挖掉,不影响其他点的强连通性。

求强连通分量的方法:

Kosaraju算法:

算法原理及步骤:

(1)将有向图G的所有边反向,建立返图rG,反图rG不会改变原图G的强连通性(也就不会改变SCC的数量)。

(2)对原图G做一次DFS,确定各点的先后顺序(可以用vector数组来记录顺序)。

(3)确定了顺序之后,在反图上做DFS,按顺序从优先级最高的点开始,到最低的点。

为什么要在反图上做DFS?这样做可以求得被隔离的岛。我们以下图为例,图a为原图G,图b为反图rG,我们将每个SCC都打上阴影,将其想象成一个个岛屿(也可以理解为一个个缩点),那么我们可以发现{a,b,e}这个SCC与其他SCC之间只有出边没有入边,所以这个SCC在第二次DFS中是要首先被遍历的,

那么我们在反图上遍历它的好处就是,可以发现在反图中,{a,b,e}这个SCC变成了只有入边没有出边,所以我们的遍历一定只会被限制在当前的SCC中,确定了第一个SCC。当我们遍历完这个SCC,我们将其删除(在代码中可以体现为将其标记为已经遍历),然后继续从剩下的优先级最高的点开始搜索,这一次从c开始搜索,因为反边,这次搜索也被限制在{c,d}内,确定了第二个SCC,删除这个SCC,然后按照这个步骤确定剩下的SCC。

参考代码:

题目来自hdu 1269,标准的模板题,判断整个图是否强连通,求出SCC数量是否为1即可。

在进行第一次dfs确定先后顺序时,我们有在递归进入时和递归返回时标记两种方法。分别给出代码:

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e4 + 10;
const int INF = 0x3fffffff;
const int mod = 1000000007;
vector<int> G[maxn], rG[maxn];  // 原图G和它的反图
vector<int> S;      // 存储第一次dfs的结果:标记点的先后顺序
int vis[maxn];      // 访问标记
int sccno[maxn];    // sccno[i]表示第i个点所属的强连通分量 
int cnt;            // 强连通分量的个数
int n, m;

void dfs1(int u) {  // 在原图G上做一次dfs,标记点的先后顺序
    S.push_back(u);     // 记录点的先后顺序
    vis[u] = true;
    for (int i = 0; i < G[u].size(); i++) {
        if (!vis[G[u][i]]) {
            dfs1(G[u][i]);
        }
    }
}

void dfs2(int u) {  // 在反图rG上做一次dfs,顺序从标记最大的点开始,到标记最小的点。
    sccno[u] = cnt;
    for (int i = 0; i < rG[u].size(); i++) {
        if (!sccno[rG[u][i]]) {
            dfs2(rG[u][i]);
        } 
    }
}

void Kosaraju() {
    cnt = 0;
    S.clear();
    memset(sccno, 0, sizeof sccno);
    memset(vis, 0, sizeof vis);
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {
            dfs1(i);
        }
    }
    for (int i = 0; i < n; i++) {
        if (!sccno[S[i]]) {
            cnt++;
            dfs2(S[i]);
        }
    }
}

void solve() {
    int u, v;
    while (cin >> n >> m, n != 0 || m != 0) {
        for (int i = 1; i <= n; i++) {
            G[i].clear();
            rG[i].clear();
        }
        for (int i = 0; i < m; i++) {
            cin >> u >> v;
            G[u].push_back(v);      // 原图
            rG[v].push_back(u);     // 反图
        }
        Kosaraju();
        cout << (cnt == 1 ? "Yes\n" : "No\n");
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cout << fixed;
    cout.precision(18);

    solve();
    return 0;
}

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e4 + 10;
const int INF = 0x3fffffff;
const int mod = 1000000007;
vector<int> G[maxn], rG[maxn];  // 原图G和它的反图
vector<int> S;      // 存储第一次dfs的结果:标记点的先后顺序
int vis[maxn];      // 访问标记
int sccno[maxn];    // sccno[i]表示第i个点所属的强连通分量 
int cnt;            // 强连通分量的个数
int n, m;

void dfs1(int u) {  // 在原图G上做一次dfs,标记点的先后顺序
    vis[u] = true;
    for (int i = 0; i < G[u].size(); i++) {
        if (!vis[G[u][i]]) {
            dfs1(G[u][i]);
        }
    }
    S.push_back(u);     // 记录点的先后顺序,标记大的放在S的后面
}

void dfs2(int u) {  // 在反图rG上做一次dfs,顺序从标记最大的点开始,到标记最小的点。
    sccno[u] = cnt;
    for (int i = 0; i < rG[u].size(); i++) {
        if (!sccno[rG[u][i]]) {
            dfs2(rG[u][i]);
        } 
    }
}

void Kosaraju() {
    cnt = 0;
    S.clear();
    memset(sccno, 0, sizeof sccno);
    memset(vis, 0, sizeof vis);
    for (int i = 1; i <= n; i++) {
        if (!vis[i]) {
            dfs1(i);
        }
    }
    for (int i = n - 1; i >= 0; i--) {
        if (!sccno[S[i]]) {
            cnt++;
            dfs2(S[i]);
        }
    }
}

void solve() {
    int u, v;
    while (cin >> n >> m, n != 0 || m != 0) {
        for (int i = 1; i <= n; i++) {
            G[i].clear();
            rG[i].clear();
        }
        for (int i = 0; i < m; i++) {
            cin >> u >> v;
            G[u].push_back(v);      // 原图
            rG[v].push_back(u);     // 反图
        }
        Kosaraju();
        cout << (cnt == 1 ? "Yes\n" : "No\n");
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cout << fixed;
    cout.precision(18);

    solve();
    return 0;
}

Tarjan算法:

算法原理及步骤:

在讲Tarjan算法之前,我们需要给出一个定理:一个SCC,从其中任意一点出发,都至少有一条路径可以回到自己。那么其实从任意一点开始DFS,这个点都会成为这个SCC的祖先。

我们可以在DFS求low值的同时把点按SCC(有相同的low值)分开,用栈分离不同的SCC。关于low值的定义可以看看我之前关于割点割边的随笔。

大体步骤:

dfn[u]表示dfs时达到顶点u的次序号(时间戳),low[u]表示以u为根节点的dfs树中次序号最小的顶点的次序号,所以当dfn[u]=low[u]时,以u为根的搜索子树上所有节点是一个强连通分量。 先将顶点u入栈,dfn[u]=low[u]=++idx,扫描u能到达的顶点v,如果v没有被访问过,则dfs(v),low[u]=min(low[u],low[v]),如果v在栈里,low[u]=min(low[u],dfn[v]),扫描完v以后,如果dfn[u]=low[u],则将u及其以上顶点出栈。

考虑最先入栈的点,每进入一个新的SCC,访问并入栈的第1个点都是这个SCC的祖先,它的num值和low值相等,这个SCC中所有的点的low值都与它相等。

参考代码:

例题仍然是hdu1269

#include <bits/stdc++.h>
using namespace std;
#define endl '\n'
typedef long long ll;
typedef unsigned long long ull;
const int maxn = 1e4 + 10;
const int INF = 0x3fffffff;
const int mod = 1000000007;
int num[maxn];          // 记录每个节点的dfs次序,时间戳
int low[maxn];          // low[v]表示v以及v的后代能退回到的节点的num值最小是多少
int sccno[maxn];        // sccno[u]表示u点属于哪个强连通分量
int st[maxn], top;   // 模拟栈
vector<int> G[maxn];    // 图
int dfn;                // dfs次序,时间戳
int cnt;                // 强连通分量的个数
int n, m;

void dfs(int u) {
    st[top++] = u;  // u入栈
    low[u] = num[u] = ++dfn;
    for (int i = 0; i < G[u].size(); i++) {
        int v = G[u][i];
        if (!num[v]) {  // 未访问过的点,继续dfs
            dfs(v);     // dfs的最底层,是最后一个SCC
            low[u] = min(low[u], low[v]);
        } else if (!sccno[v]) {     // 处理回退边
            low[u] = min(low[u], num[v]);
        }
    }
    if (low[u] == num[u]) {     // 栈底的点是SCC的祖先,它的low == num
        cnt++;
        while (1) {
            int v = st[--top];  // v弹出栈
            sccno[v] = cnt;
            if (u == v) {       // 栈底的点是SCC的祖先
                break;
            }
        }
    }
}

void tarjan() {
    cnt = top = dfn = 0;
    memset(sccno, 0, sizeof sccno);
    memset(num, 0, sizeof num);
    memset(low, 0, sizeof low);
    for (int i = 1; i <= n; i++) {
        if (!num[i]) {
            dfs(i);
        }
    }
}

void solve() {
    int u, v;
    while (cin >> n >> m, n != 0 || m != 0) {
        for (int i = 1; i <= n; i++) {
            G[i].clear();
        }
        for (int i = 0; i < m; i++) {
            cin >> u >> v;
            G[u].push_back(v);
        }
        tarjan();
        cout << (cnt == 1 ? "Yes\n" : "No\n");
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cout.tie(0);
    cout << fixed;
    cout.precision(18);

    solve();
    return 0;
}

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

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

相关文章

适用于vue3中图片全屏展示

App中的代码片段 <style scoped> .container {background: url("./assets/back2.jpg");height: 100%;width: 100%;position: fixed;background-size: 100% 100%; } </style>

vue与vueComponent的关系

创建完组件之后 就会创建一个vueComponent构造函数 当注册成功这个组件并且在页面使用之后 就会创建一个vueComponent实例对象&#xff0c; 所以为了避免组件在使用过程中data对象中的值混乱 组件中的data要写成函数&#xff0c; 使得每次创建的组件实例对象都可以返回一…

【ESD专题】电池包打ESD时的电流走向分析及防护指南

一个典型的电池包,包含了电芯、保护芯片(或电量计)、及周边的元件(如检测电阻、MOS管)、连接器等。 如下所示为高侧MOS电量计方案:在电量计(BMU)周边,有电芯、 保护 MOSFET、 电流检测电阻、 连接器等。在电量计左边的 RC 滤波电路是代表多个 RC 滤波电路,用于监控多…

Java项目-苍穹外卖-Day05

主要是写代码 完成套餐功能 和之前那个菜品类似 这里我懒得贴我的代码了 写一下改的几个bug 1.修改菜品关联套餐的时候&#xff0c;对应的菜品关联数据要setSetmealId一下&#xff0c;因为接收数据是赋不到那个关联的ID值的&#xff0c;如果不设置新增的话对应的setmealID就会为…

node安装node-sass依赖失败(版本不一致)

1.官网对应node版本 https://www.npmjs.com/package/node-sass2.node-sass版本对应表

Redis数据结构之List

Redis 中列表&#xff08;List&#xff09;类型是用来存储多个有序的字符串&#xff0c;列表中的每个字符串成为元素 Eelement&#xff09;&#xff0c;一个列表最多可以存储 2^32-1 个元素。 在 Redis 中&#xff0c;可以对列表两端插入&#xff08;push&#xff09;和弹出&am…

Matlab绘制灰度直方图

直方图是根据灰图像绘制的&#xff0c;而不是彩色图像通。查看图像直方图时候&#xff0c;需要先确定图片是否为灰度图&#xff0c;使用MATLAB2019查看图片是否是灰度图片&#xff0c;在读取图片后在MATLAB界面的工作区会显示读取的图像矩阵&#xff0c;如果是&#xff0c;那么…

DBO优化TCN的电力负荷预测,附MATLAB代码

今天为大家带来一期基于DBO-TCN的电力负荷预测。不得不说&#xff0c;TCN在时序数据中的预测效果在一定程度上要优于LSTM。 原理详解 文章对TCN网络的五个参数进行优化&#xff0c;分别是&#xff1a; 正则化参数&#xff0c;学习率&#xff0c;滤波器个数&#xff0c;滤波器大…

(二)k8s实战-深入Pod详解

一、配置文件详解 创建Pod nginx样例 apiVersion: v1 # api文档版本 kind: Pod # 资源对象类型&#xff0c;Pod, Deployment,StatefulSet metadata: # Pod相关的元数据&#xff0c;用于描述Pod的数据name: nginx-demo # Pod的名称labels: # 定义Pod的标签type: app # 自定义l…

SpringBoot 跨域问题和解决方法

Spring Boot 是一种用于构建独立的、生产级别的Java应用程序的框架。在开发Web应用程序时&#xff0c;经常会遇到跨域资源共享&#xff08;CORS&#xff09;问题。本文将详细介绍Spring Boot中的跨域问题以及相应的解决方法。 目录 什么是跨域&#xff1f;1. 使用Spring Boot…

MySQL中到全外连接(full outer join)编写方法说明

在SQL中&#xff0c;全外连接&#xff08;full outer join&#xff09;是一种无论是否匹配&#xff0c;都返回两张表中所有记录的连接: 不幸的是&#xff0c;MySQL不支持这种关联&#xff0c;我们必须以某种方式模拟它。但该如何操作呢&#xff1f; 在SQL中&#xff0c;同样的…

西瓜书第三章

广义线性模型 考虑单点可微函数 g ( ⋅ ) g(\cdot) g(⋅)&#xff0c;令 y g − 1 ( ω T x b ) yg^{-1}(\omega^{T}xb) yg−1(ωTxb)&#xff0c;这样得到的模型称为“广义线性模型”&#xff0c;其中函数 g ( ⋅ ) g(\cdot) g(⋅)称为“联系函数”。显然&#xff0c;对数线…

Maven聚合项目(微服务项目)创建流程,以及pom详解

1、首先创建springboot项目作为父项目 只留下pom.xml 文件&#xff0c;删除src目录及其他无用文件 2、创建子项目 子项目可以是maven项目&#xff0c;也可以是springboot项目 3、父子项目关联 4、父项目中依赖管理 <?xml version"1.0" encoding"UTF-8&qu…

iOS 分别对一张图的局部进行磨砂,拼接起来不能贴合

效果图 需求&#xff0c;由于视图层级的原因&#xff0c;需要对图片分开进行磨砂&#xff0c; 然后组合在一起 如图&#xff0c;上下两部分&#xff0c;上下两个UIImageVIew大小相同&#xff0c;都是和图片同样的大小&#xff0c;只是上面的UIimageVIew 只展示上半部份 &#…

基于java SpringBoot CRM客户关系管理系统设计

当今计算机技术和信息管理技术的迅速发展和广泛应用&#xff0c;为管理相关理论的应用提供了广阔的平台。IT技术和DBT技术&#xff08;数据库技术&#xff09;基于《客户信息管理系统》设计的客户信息管理系统正在逐步发展成为管理模式发展的新趋势&#xff0c;企业建立客户信息…

ModaHub魔搭社区:AI Agent在 知识图谱场景下的AgentBench基准测试

近日,来自清华大学、俄亥俄州立大学和加州大学伯克利分校的研究者设计了一个测试工具——AgentBench,用于评估LLM在多维度开放式生成环境中的推理能力和决策能力。研究者对25个LLM进行了全面评估,包括基于API的商业模型和开源模型。 他们发现,顶级商业LLM在复杂环境中表现出…

[保研/考研机试] KY207 二叉排序树 清华大学复试上机题 C++实现

题目链接&#xff1a; 二叉排序树_牛客题霸_牛客网二叉排序树&#xff0c;也称为二叉查找树。可以是一颗空树&#xff0c;也可以是一颗具有如下特性的非空二叉树&#xff1a; 1。题目来自【牛客题霸】https://www.nowcoder.com/share/jump/437195121692721757794 描述&#x…

Ae 效果:CC Light Sweep

生成/CC Light Sweep Generate/CC Light Sweep CC Light Sweep&#xff08;CC 光线扫描&#xff09;可以创建一个动态的光线扫描&#xff0c;常用于模拟一束光在图像上移动的效果。支持 Alpha 通道并能基于 Alpha 通道边缘创造逼真的光照。 ◆ ◆ ◆ 效果属性说明 Center 中心…

实时云渲染的关键技术是什么

首先&#xff0c;我们需要了解实时云渲染的概念。实时云渲染是指将原本在本地电脑上进行的渲染工作转移到云端服务器上完成&#xff0c;并实时将处理结果返回给终端用户显示。这其实是一个合成词&#xff0c;由实时、云和渲染三个关键词组成。实时表示具有时效性和同步效果&…

访问学者申请中4个难点解析

在众多申请过程中&#xff0c;成为一名访问学者可能是一个具有挑战性的目标。这个过程可能涉及到许多复杂的步骤和问题。在本文中&#xff0c;知识人网小编将解析访问学者申请中的四个难点&#xff0c;以帮助申请人更好地应对这一挑战&#xff0c;增加成功的机会。 1.研究课题的…