图论连通性

news/2024/9/19 16:14:38/文章来源:https://www.cnblogs.com/FlyPancake/p/18303948

【学习笔记】图论连通性

啊啊啊啊啊!

先引用一篇犇的:)))

缩点

弱连通

对于有向图中两点 \(x\)\(y\),它们在所有边为无向时存在一个环使它们相连。

强连通

对于有向图中两点 \(x\)\(y\),存在一个环使它们相连。

强连通子图

对于有向图 \(G = (V, E)\),如果对于一个 \(V\) 的子集 \(V_0\) 满足,\({\forall x, y \in V_0}\)\(x\)\(y\) 均满足强连通,则称 \(V_0\) 为一个强连通子图

强连通分量(SCC)

极大的强连通子图。(一个图中可以有多个)

  • 极大的的含义:对于一个强连通子图 \(V_0\),满足 \(\forall V_0 \subset V_1\),且 \(V_1\) 都不是强连通子图。

Tarjan 算法

一些定义:

  • 在 dfs 过程中遍历的边称为树边,未遍历的边称为非树边

  • \(dfn_x\) 表示图中 \(x\) 号节点在 dfs 算法中是第 \(x\) 个遍历到的。

  • \(low_x\) 表示 \(x\) 号节点经过若干条树边后再经过至多一条满足条件的非树边能到达的 \(dfn\) 最小的点的值。

实现方法:

  • 算出 \(dfn_x\) 并初始化 \(low_x\):可以全局记录一个时间戳 \(T\),并使 dfn[x] = low[x] = ++T

  • 枚举 \(x\) 的所有出边指向的点 \(y\)。有两种情况:

  1. \(y\) 还没有被遍历过。可以直接调用 dfs(y),因为该边是树边,所以直接更新 low[x] = min(low[x], low[y])

  2. \(y\) 已经被遍历过了。如果 \(y\)\(x\) 在同一个 SCC(\(y\) 存在到 \(x\) 的路径),则更新 low[x] = min(low[x], dfn[y])

    2.1. 如何知道是否在一个 SCC 里呢?(判断横叉边)

    我们可以考虑开一个栈,在进入 dfs(x) 是将 \(x\) 入栈,并在找到 \(x\) 所在的 SCC 的所有点后\(x\) 弹出栈。

    所以,此时如果 \(y\) 在这个栈中,则证明 \(y\)\(x\) 存在路径,否则不存在。因此如果 \(y\) 此时在栈中则更新 low[x] = min(low[x], dfn[y])。否则不用更新。

  • 结束 dfs 时,判断 \(x\) 是否为其所在 SCC 内 \(dfn\) 最小的点。
  1. 如果 \(dfn_x = low_x\),则 \(x\)\(dfn\) 最小的点。此时将栈中 \(x\) 及以上的元素弹出。这些元素即是跟 \(x\) 处于同一 SCC 的点。

  2. 否则 \(x\) 不是 \(dfn\) 最小的点,直接结束 dfs。

代码如下:

vector<int> g[N];
vector<int> scc[N];
int w[N], dfn[N], low[N], T, cnt;
int st[N], top;
bool ins[N];void tarjan(int u){dfn[u] = low[u] = ++T;st[++top] = u, ins[u] = 1;for(int v : g[u]){if(!dfn[v]){tarjan(v);low[u] = min(low[u], low[v]);} else if(ins[v]){low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){scc[cnt].push_back(st[top]);ins[st[top--]] = 0;}scc[cnt].push_back(st[top]);ins[st[top--]] = 0;}
}

一个 SCC 实际上就对应着一个可以单点经过多次的非简单环,那么如果我们把 SCC 缩成一个点,也就意味着把一个一般有向图变成了 DAG。

由于 Tarjan 算法以及后续的缩点只需要遍历一次图,所以算法的总时间复杂度为 \(O(n+m)\)

然后 DAG 上可以用拓扑排序 DP 来解决问题。

trick:统计出来的 SCC 里 \(cnt\) 由大到小就是拓扑序。


P3387 【模板】缩点

板子。注意 trick 运用方式。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 1e4+5;int n, m;
vector<int> g[N];
vector<int> scc[N]; // scc 里 cnt 由大到小就是拓扑序
vector<int> gn[N]; // scc 新图
int w[N], dis[N], dfn[N], low[N], T, cnt, ans;
int st[N], top;
bool ins[N];
int inscc[N], dp[N];void tarjan(int u){dfn[u] = low[u] = ++T;st[++top] = u, ins[u] = 1;for(int v : g[u]){if(!dfn[v]){tarjan(v);low[u] = min(low[u], low[v]);} else if(ins[v]){low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){scc[cnt].push_back(st[top]);inscc[st[top]] = cnt;dis[cnt] += w[st[top]];ins[st[top--]] = 0;}scc[cnt].push_back(st[top]);inscc[st[top]] = cnt;dis[cnt] += w[st[top]];ins[st[top--]] = 0;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=n; i++)cin>>w[i];for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);}for(int i=1; i<=n; i++)if(!dfn[i]) tarjan(i);for(int i=1; i<=n; i++){for(int j : g[i]){if(inscc[i] != inscc[j])gn[inscc[i]].push_back(inscc[j]);}}// dpfor(int i=cnt; i>=1; i--){  dp[i] = max(dp[i], dis[i]);for(int j : gn[i]){dp[j] = max(dp[j], dp[i] + dis[j]);}ans = max(dp[i], ans);}cout<<ans;return 0;
}

割点与割边

割点

将某点从图中去掉后,得到了一个非连通图,则称这个点是割点

割边

将某边从图中去掉后,得到了一个非连通图,则称这条边是割边

dfs 树

对于一个无向图,通过 dfs 算法得到的一颗生成树

  • 在 dfs 过程中遍历的边称为树边,未遍历的边称为环边
  • 所有的环边一定是返祖边。
  • 没有横叉边。

一些定义:

  • \(dfn_x\) 表示图中 \(x\) 号节点在 dfs 算法中是第 \(x\) 个遍历到的。

  • \(low_x\) 表示 \(x\) dfs 树的子树内,通过环边能回到的 \(dfn\) 最小的点。

实现方法(如何判断一个点 \(x\) 是割点):

  • \(x\) 是 dfs 树的根,\(x\) 是割点当且仅当 \(x\) 有不少于两个儿子。

  • 否则,\(x\) 是割点当且仅当存在一个 \(x\) dfs 树的儿子 \(y\) 使得 \(low_y \ge dfn_x\)

    证明:若删去 \(x\),则 \(y\) 子树内没有向 \(x\) 祖先以及其它子树连接的边,故 \(y\) 子树与其它点会形成两个连通块。故 \(x\) 是割点。

实现方法(如何判断边 \((x, y)\) 是割边):

  • \((x, y)\) 是环边,则该边一定不是割边,因为删除该边之后生成树不受影响。

  • \((x, y)\) 是树边,假设 \(x\)\(y\) 的父亲,则该边是割边当且仅当 \(low_y > dfn_x\)

    证明:若 \(low_y > dfn_x\),则删除该边后,\(y\) 子树内与 \(y\) 子树外会分成两个连通块,故该边为割边。否则,在删除边后 \(y\) 子树内与 \(y\) 子树外仍然连通,故该边不是割边。

点双与边双

割点决定点双,割边决定边双。

点双连通图

如果一个无向图不存在割点,则称该图是一个点双连通图

等价定义:图中任意两点都存在两条除了起点终点外点不相交的路径。

边双连通图

如果一个无向图不存在割边,则称该图是一个边双连通图

等价定义:图中任意两点都存在两条边不相交的路径。

点双连通分量

极大的点双连通子图。

边双连通分量

极大的边双连通子图。

思路:

边双连通分量缩点后得到的图为,而点双连通分量“缩点”后得到的图为圆方树

圆方树的含义是用圆点表示原图上的点,方点(新建点)表示不同的点双。(再把原点双的边去掉就得到了一棵

P8436 【模板】边双连通分量

板子。注意重边处理方式。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;int n, m;
vector<int> g[N];
vector<int> bs[N];
int dfn[N], low[N], T;
int st[N], top, cnt;
bool vis[N]; // 是否被遍历过void tarjan(int u, int fa, int id){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;for(int v : g[u]){if(v == fa && id == 0){id = 1;continue;// 处理重边}if(!vis[v]){tarjan(v, u, 0);low[u] = min(low[u], low[v]);} else{low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){bs[cnt].push_back(st[top]);top--;}bs[cnt].push_back(st[top]);top--;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0, 0);cout<<cnt<<"\n";for(int i=1; i<=cnt; i++){cout<<bs[i].size()<<" ";for(int j : bs[i]) cout<<j<<" ";cout<<"\n";}return 0;
}

P8435 【模板】点双连通分量

板子。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e5+5;int n, m;
vector<int> g[N];
vector<int> ds[N];
int dfn[N], low[N], T;
int st[N], top, cnt;
bool vis[N]; // 是否被遍历过void tarjan(int u, int fa){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;int son = 0;for(int v : g[u]){if(v == fa) continue;if(!vis[v]){son++;tarjan(v, u);low[u] = min(low[u], low[v]);if(low[v] >= dfn[u]){cnt++;while(st[top+1] != v){ds[cnt].push_back(st[top]);top--;}ds[cnt].push_back(u);}} else{low[u] = min(low[u], dfn[v]);}}if(fa == 0 && son == 0) ds[++cnt].push_back(u); // 孤立点判定
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0);cout<<cnt<<"\n";for(int i=1; i<=cnt; i++){cout<<ds[i].size()<<" ";for(int j : ds[i]) cout<<j<<" ";cout<<"\n";}return 0;
}

P2860 [USACO06JAN] Redundant Paths G

由题意很自然想到可以边双缩点,然后建新图并找到叶子节点的个数。

答案为 \(\lfloor \frac{s+1}{2} \rfloor\)\(s\) 为叶子节点数)。

转证明。

口胡一下另一种:将度数为 2 的点缩了之后,每次连距离最长的两个叶子节点即可。

#include <bits/stdc++.h>
using namespace std;
#define ll long long
const int N = 5e3+5;int n, m;
vector<int> g[N];
vector<int> bs[N];
int dfn[N], low[N], inbs[N], T;
int st[N], top, cnt, leaf;
bool vis[N]; // 是否被遍历过
int ind[N];void tarjan(int u, int fa, int id){dfn[u] = low[u] = ++T;st[++top] = u; vis[u] = 1;for(int v : g[u]){if(v == fa && id == 0){id = 1;continue;// 处理重边}if(!vis[v]){tarjan(v, u, 0);low[u] = min(low[u], low[v]);} else{low[u] = min(low[u], dfn[v]);}}if(dfn[u] == low[u]){cnt++;while(st[top] != u){bs[cnt].push_back(st[top]);inbs[st[top--]] = cnt;}bs[cnt].push_back(st[top]);inbs[st[top--]] = cnt;}
}int main(){ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);cin>>n>>m;for(int i=1; i<=m; i++){int u, v; cin>>u>>v;g[u].push_back(v);g[v].push_back(u);}for(int i=1; i<=n; i++)if(!vis[i]) tarjan(i, 0, 0);for(int i=1; i<=n; i++){for(int j : g[i]){if(inbs[i] != inbs[j]){ind[inbs[j]]++;// 每个点都会被遍历到一次,因为之前已经连的是无向边,所以统计一个就行。}}}for(int i=1; i<=cnt; i++)if(ind[i] == 1) leaf++;cout<<(leaf+1)/2;return 0;
}

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

如若内容造成侵权/违法违规/事实不符,请联系编程知识网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

CS144 2024 winter 作业笔记

CS144 2024 winter 作业笔记 cs144 homework notes Created: 2024-05-14T10:24+08:00 Published: 2024-07-15T20:16+08:00 Categories: ComputerNetwork 目录checkpoint0配环境wegetreliable byte stream疑问checkpoint1checkpoint2bugs:checkpoint3model of input_Sendertimer…

解锁网络无限可能:揭秘微软工程师力作——付费代理IP池深度改造与实战部署指南

"揭秘微软工程师力作:付费代理IP池深度改造,四大模块精讲,含实战部署指南。掌握高效、稳定代理IP资源,解锁网络无限可能。从筛选管理到安全加密,详细步骤助您快速搭建专属代理网络。尊享付费阅读,获取深度技术洞察与实践指导。"基于付费代理的代理IP池 项目来源…

SpringIOC 容器

SpringIOC 容器 一、组件的概念 什么是组件? 常规的三层架构处理请求流程:划分为组件后:组件就是所有可以重用的java对象,组件一定是对象,对象不一定是组件二、Spring 进行组件管理 Spring框架替代了程序员原有的new对象和对象属性赋值的动作组件对象实例化组件属性赋值组…

javap和字节码

javap字节码的基本信息public class Test {private int age = 10;public int getAge() {return age;} }在 class 文件的同级目录下输入命令 javap -v -p Test.class 来查看一下输出的内容// 字节码文件的位置 Classfile /D:/Code/code/JavaCode/JavaSourceLearn/out/production…

Prometheus之钉钉

要实现Prometheus通过Alertmanager发送告警到钉钉,您可以按照以下步骤进行配置:创建钉钉机器人:首先,您需要在钉钉群中添加一个自定义机器人,并获取机器人的Webhook地址。创建机器人时,您可以设置安全验证方式,如加签。创建完成后,保存好Webhook地址和加签后的秘钥(如…

Java——N以内累加求和

2024/07/15 1.题目 2.错误 3.分析 4.答案 1.题目2.错误 import java.util.Scanner;public class Main {public static void main(String[] args) {Scanner scanner = new Scanner(System.in);int N = scanner.nextInt();int sum = 0;while (N<=1000) {for (int i=1;i<=N;…

SQLCoder部署和应用

SQLCoder文生SQL部署和应用主页个人微信公众号:密码应用技术实战 个人博客园首页:https://www.cnblogs.com/informatics/SQLCoder简介 SQLCoder是一个用于生成SQL语句的工具,可以通过输入自然语言描述的需求,生成对应的SQL语句。SQLCoder支持连接数据库,对生成的SQL语句可…

NOIP 十三连测 #2 补题

逆天输出文件 .ans reverse 水题,随便自己造两组数据都能看出规律: \[\begin{cases}a_n a_{n - 2} \dots a_1 + a_2 a_4 \dots a_{n - 1}(n \mod 2 = 1) \\ a_n a_{n - 2} \dots a_2 + a_1 a_3 \dots a_{n - 1 } (n \mod 2 = 0)\end{cases} \]logistics 先求出最小生成树的 \…

MySQL时间戳转成日期格式

将时间戳转换为日期格式:-- 如果时间戳为毫秒级长度为13位,需要先除以1000 SELECT id, `task_name` ,FROM_UNIXTIME(`task_register_begin_time`/1000,%Y-%m-%d %H:%i:%s) as task_register_begin_time,FROM_UNIXTIME(`task_register_end_time`/1000,%Y-%m-%d %H:%i:%s) as t…

使用GSAP制作动画视频

GSAP 3Blue1Brown给我留下了深刻印象。利用动画制作视频,内容简洁,演示清晰。前两天刚好碰到一件事,我就顺便学习了一下怎么用代码做动画。 以javascrip为例,有两个动画引擎,GSAP和Animajs。由于网速的原因,询问了GPT后,我选择了GSAP来制作我的第一个动画视频。 制作动画…

log4cpp的安装及使用

介绍开源库log4cpp的安装及使用目录前言安装使用示例代码配置文件编译链接输出 前言 本文的操作均在ubuntu20.04下进行 安装 本文仅介绍从源码编译安装log4cpp的过程。 ①在开始编译前,首先要确保系统中安装了g++,make,autoconf和libtool ②下载log4cpp源码 下载log4cpp的特…

Intellij springboot远程调试

服务器端配置: java -jar -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=*:5005 XXXX.jar 说明: address:IP:端口;*代表所有IP地址都可访问,5005需要可IDEA远程请求的端口一致 XXXX.jar:你的springboot程序 IDEA端配置: