Kosaraju算法一看这个名字很奇怪就可以猜到它也是一个根据人名起的算法,它的发明人是S. Rao Kosaraju,这是一个在图论当中非常著名的算法,可以用来拆分有向图当中的强连通分量

背景知识

这里有两个关键词,一个是有向图,另外一个是强连通分量。有向图是它的使用范围,我们只能使用在有向图当中。对于无向图其实也存在强连通分量这个概念,但由于无向图的连通性非常强,只需要用一个集合维护就可以知道连通的情况,所以也没有必要引入一些算法。

有向图我们都了解,那么什么叫做强连通分量呢?强连通分量的英文是strongly connected components。这是一个很直白的翻译,要理解它我们首先需要理解强连通的概念。在有向图当中,如果两个点之间彼此存在一条路径相连,那么我们称这两个点强连通。那么推广一下,如果一张图当中的一个部分中的每两个点都连通,那么这个部分就称为强连通分量。

强连通分量一般是一张完整的图的一个部分,比如下面这张图当中的{1, 2, 3, 4}节点就可以被看成是一个强连通分量。

其实求解强连通分量的算法并不止一种,除了Kosaraju之外还有大名鼎鼎的Tarjan算法可以用来求解。但相比Tarjan算法,Kosaraju算法更加直观,更加容易理解。

算法原理

Kosaraju算法的原理非常简单,简单到只有三个步骤

  1. 我们通过后序遍历的方式遍历整个有向图,并且维护每个点的出栈顺序
  2. 我们将有向图反向,根据出栈顺序从大到小再次遍历反向图
  3. 对于点u来说,在遍历反向图时所有能够到达的v都和u在一个强连通分量当中

怎么样,是不是很简单?

下面我们来详细阐述一下细节,首先后序遍历和维护出栈顺序是一码事。也就是在递归的过程当中当我们遍历完了u这个节点所有连通的点之后,再把u加入序列。其实也就是u在递归出栈的时候才会被加入序列,那么序列当中存储的也就是每个点的出栈顺序。

这里我用一小段代码(python)演示一下,看完也就明白了。

popped = [] # 存储出栈节点

def dfs(u):
for v in Graph[u]:
dfs(v)
popped.append(u)

我们在访问完了所有的v之后再把u加入序列,这也就是后序遍历,和二叉树的后序遍历是类似的。

反向图也很好理解,由于我们求解的范围是有向图,如果原图当中存在一条边从u指向v,那么反向图当中就会有一条边从v指向u。也就是把所有的边都调转反向。

我们用上面的图举个例子,对于原图来说,它的出栈顺序我们用红色笔标出。

也就是[6, 4, 2, 5, 3, 1],我们按照出栈顺序从大到小排序,也就是将它反序一下,得到[1, 3, 5, 2, 4, 6]。1是第一个,也就是最后一个出栈的,也意味着1是遍历的起点。

我们将它反向之后可以得到:

我们再次从1出发可以遍历到2,3, 4,说明{1, 2, 3, 4}是一个强连通分量。

怎么样,整个过程是不是非常简单?

我们将这段逻辑用代码实现,也并不会很复杂。

// Cpp
// g 是原图,g2 是反图
void dfs1(int u) {
vis[u] = true;
for (int v : g[u])
if (!vis[v])
dfs1(v);
s.push_back(u);
} void dfs2(int u) {
color[u] = sccCnt;
for (int v : g2[u])
if (!color[v])
dfs2(v);
} void kosaraju() {
sccCnt = 0;
for (int i = 1; i <= n; ++i)
if (!vis[i])
dfs1(i);
for (int i = n; i >= 1; --i)
if (!color[s[i]]) {
++sccCnt;
dfs2(s[i]);
}
}
# python
N = 7
graph, rgraph = [[] for _ in range(N)], [[] for _ in range(N)]
used = [False for _ in range(N)]
popped = [] # 建图
def add_edge(u, v):
graph[u].append(v)
rgraph[v].append(u) # 正向遍历
def dfs(u):
used[u] = True
for v in graph[u]:
if not used[v]:
dfs(v)
popped.append(u) # 反向遍历
def rdfs(u, scc):
used[u] = True
scc.append(u)
for v in rgraph[u]:
if not used[v]:
rdfs(v, scc) # 建图,测试数据
def build_graph():
add_edge(1, 3)
add_edge(1, 2)
add_edge(2, 4)
add_edge(3, 4)
add_edge(3, 5)
add_edge(4, 1)
add_edge(4, 6)
add_edge(5, 6) if __name__ == "__main__":
build_graph()
for i in range(1, N):
if not used[i]:
dfs(i) used = [False for _ in range(N)]
# 将第一次dfs出栈顺序反向
popped.reverse()
for i in popped:
if not used[i]:
scc = []
rdfs(i, scc)
print(scc)

思考

算法讲完,代码也写了,但是并没有结束,仍然有一个很大的疑惑没有解开。算法的原理很简单,很容易学会,但问题是为什么这样做就是正确的呢?这其中的原理是什么呢?我们似乎仍然没有弄得非常清楚。

这里面的原理其实很简单,我们来思考一下,如果我们在正向dfs的时候,u点出现在了v点的后面,也就是u点后于v点出栈。有两种可能,一种可能是u点可以连通到v点,说明u是v的上游还有一种可能是u不能连通到v,说明图被分割成了多个部分。对于第二种情况我们先不考虑,因为这时候u和v一定不在一个连通分量里。对于第一种情况,u是v的上游,说明u可以连通到v。

这时候,我们将图反向,如果我们从u还可以访问到v,那说明了什么?很明显,说明了在正向图当中v也有一条路径连向u,不然反向之后u怎么连通到v呢?所以,u和v显然是一个强连通分量当中的一个部分。我们再把这个结论推广,所有u可以访问到的,第一次遍历时在它之前出栈的点,都在一个强连通分量当中。

如果你能理解了这一点,那么整个算法对你来说也就豁然开朗了,相信剩下的细节也都不足为虑了。

到这里,整个算法流程的介绍就算是结束了,希望大家都可以enjoy今天的内容。

算法学习笔记:Kosaraju算法的更多相关文章

  1. C / C++算法学习笔记(8)-SHELL排序

    原始地址:C / C++算法学习笔记(8)-SHELL排序 基本思想 先取一个小于n的整数d1作为第一个增量(gap),把文件的全部记录分成d1个组.所有距离为dl的倍数的记录放在同一个组中.先在各组 ...

  2. Manacher算法学习笔记 | LeetCode#5

    Manacher算法学习笔记 DECLARATION 引用来源:https://www.cnblogs.com/grandyang/p/4475985.html CONTENT 用途:寻找一个字符串的 ...

  3. [ML学习笔记] XGBoost算法

    [ML学习笔记] XGBoost算法 回归树 决策树可用于分类和回归,分类的结果是离散值(类别),回归的结果是连续值(数值),但本质都是特征(feature)到结果/标签(label)之间的映射. 这 ...

  4. 学习笔记 - Manacher算法

    Manacher算法 - 学习笔记 是从最近Codeforces的一场比赛了解到这个算法的~ 非常新奇,毕竟是第一次听说 \(O(n)\) 的回文串算法 我在 vjudge 上开了一个[练习],有兴趣 ...

  5. Johnson算法学习笔记

    \(Johnson\)算法学习笔记. 在最短路的学习中,我们曾学习了三种最短路的算法,\(Bellman-Ford\)算法及其队列优化\(SPFA\)算法,\(Dijkstra\)算法.这些算法可以快 ...

  6. 某科学的PID算法学习笔记

    最近,在某社团的要求下,自学了PID算法.学完后,深切地感受到PID算法之强大.PID算法应用广泛,比如加热器.平衡车.无人机等等,是自动控制理论中比较容易理解但十分重要的算法. 下面是博主学习过程中 ...

  7. Johnson 全源最短路径算法学习笔记

    Johnson 全源最短路径算法学习笔记 如果你希望得到带互动的极简文字体验,请点这里 我们来学习johnson Johnson 算法是一种在边加权有向图中找到所有顶点对之间最短路径的方法.它允许一些 ...

  8. 算法学习笔记——sort 和 qsort 提供的快速排序

    这里存放的是笔者在学习算法和数据结构时相关的学习笔记,记录了笔者通过网络和书籍资料中学习到的知识点和技巧,在供自己学习和反思的同时为有需要的人提供一定的思路和帮助. 从排序开始 基本的排序算法包括冒泡 ...

  9. 二次剩余Cipolla算法学习笔记

    对于同余式 \[x^2 \equiv n \pmod p\] 若对于给定的\(n, P\),存在\(x\)满足上面的式子,则乘\(n\)在模\(p\)意义下是二次剩余,否则为非二次剩余 我们需要计算的 ...

随机推荐

  1. zookeeper核心之ZAB协议就这么简单!

    背景 我们都知道 Zookeeper 是基于 ZAB 协议实现的,在介绍 ZAB 协议之前,先回顾一下 Zookeeper 的起源与发展. Zookeeper 究竟是在什么样的时代背景下被提出?为了解 ...

  2. RocketMQ单节点搭建

    RocketMQ服务搭建 下载RocketMQ源码: http://mirror.bit.edu.cn/apache/rocketmq/4.4.0/rocketmq-all-4.4.0-source- ...

  3. 没事学学KVM(一)

    学习KVM肯定要找来一台虚机来学习呀,通过VMware workstation创建虚机,现在的电脑CPU,包括INTER,AMD都支持,公司发的电脑CPU为inter,通过开启inter VT-X可在 ...

  4. 单片机串口通信电平不匹配的解决电路,5V 3.3V串口通讯

    很早的时候调试串口通讯遇到单片机和模块电压不匹配,信号无法传输,所以整理后来遇到的转换电路.1.最简单的用转换电平IC,可以去淘宝上搜索,有四路的有两路的,比如这个双向电平转换模块 2.根据接触的开发 ...

  5. day76:luffy:项目前端环境搭建&轮播图的实现

    目录 1.项目前端环境搭建 1.创建项目目录 2.前端初始化全局变量和全局方法 3.跨域CORS 4.axios配置 2.轮播图功能的实现 1.安装依赖模块 2.上传文件相关配置 3.注册home子应 ...

  6. SQL注入的一些学习

    -------------------------------------- 这是我之前发布到的其他的一个网址,这里的话我重新总结 可能分很多,我也不是很清楚,只是皮毛的研究了一下 1,sql按数据库 ...

  7. Django中间件(Middleware)处理请求

    关注公众号"轻松学编程"了解更多. 1.面向切面编程 切点(钩子) 切点允许我们动态的在原有逻辑中插入一部分代码 在不修改原有代码的情况下,动态注入一部分代码 默认情况,不中断传播 ...

  8. think PHP5.1使用时 session重定向丢失问题

    查了很多资料,也看了redirect底层代码,具体来说,还是多个用的地方不太对.做个笔记防忘记: 遇重定向后丢失session时: 1.php.ini配置文件,不要自动启动,默认是0,session. ...

  9. C#3新增语法特性

    C#3,.Net Framework 3.5 ,Visual Studio 2008, CLR 3.0 C#3.0新引进的语法基于.Net Framework 3.5.主要引进的语法:Linq,隐式类 ...

  10. 3.4 MyArrayList 类的实现

    3.4 MyArrayList 类的实现 这节提供一个便于使用的 MyArrayList 泛型类的实现,这里不检测可能使得迭代器无效的结构上的修改,也不检测非法的迭代器 remove 方法. MyAr ...