图论算法——有向图中的强连通性

引言

本文我们着重分析下有向图的强连通性以及如何计算有向图中的强连通分量。

有关图的概念可参考博文数据结构之图的概述

强连通

在一幅无向图中,如果有一条路径连接顶点vw,则它们就是连通的;然后,在一幅有向图中,如果从顶点v有一条有向路径达到w,则顶点w是从顶点v可达的,但如果从w到达v的路径可能不存在。这两个顶点不是强连通的。

如果两个顶点互相可达,则它们是强连通的。如果一幅有向图中任意两个顶点都是强连通的,则这幅有向图也是强连通的。

强连通分量

有向图中的强连通性是一种顶点之间平等关系,有着以下性质:

  • 自反性: 任意顶点和自己都是强连通的
  • 对称性:如果vw是强连通的,那么wv也是强连通的
  • 传递性:如果vw是强连通的且如果wx也是强连通的,那么vx也是强连通的

强连通性将所有顶点分成了一些平等的部分,每个部分都是由相互均为强连通的顶点的最大子集组成的。这些子集称为强连通分量(强连通分支)

在这里插入图片描述
如上图可以分为5个强连通分量。 一个含有V个顶点的有向图可能有1~V个强连通分量;一个强连通图只含一个强连通分量;而一个有向无环图中含有V个强连通分量。

那么如何通过代码找到有向图中的强连通分量呢?

在这里插入图片描述
有向图的强连通分量和无向图的连通分量不同,因为有向图带有方向,情况略为复杂。
比如考虑上图, { 0 , 1 , 2 } \{0,1,2\} {0,1,2} { 4 , 5 , 6 , 7 } \{4,5,6,7\} {4,5,6,7}是两个不同的强连通分量。但是从4有一条路径到另个一分量中的2,同时6也能到8,但是反过来2却不能到4,所以2和4属于不同的分量之中。

下面一起来学习下一种计算有向图强连通分量的算法

Kosaraju算法

强烈建议先了解下无向图的连通分量计算算法

该算法看起来很简单,但是要理解为什么起作用需要仔细考虑下,算法过程如下:

  • 在给定的一幅有向图 G G G中,使用DepthFirstOrder来计算它的反向图 G R G^R GR的逆后序序列。
  • 在有向图 G G G中(注意是在原图中操作)按照上步得到的序列进行标准的深度优先搜索。
  • 在构造函数中,所有在同一个递归dfs()调用中被访问到的顶点都在同一个强连通分量中

DepthFirstOrder算法描述见图论算法——环和有向无环图

实现起来真的很简单,反向图即图中所有路径方向逆转后的图。
关键是为啥有效?

我们一步一步来分析。

看算法过程,为什么要引入 G R G^R GR,它是干嘛的?

G R G^R GR是正向图的反向图,它和正向图有同样的强连通划分。什么?不信!

在这里插入图片描述
上图是我们要计算强连通分量的有向图,嗯,正向图来的。

通过肉眼可以看到有5个强连通分量,我通过不同颜色的矩形框起来了。下面给出它的反向图:

在这里插入图片描述
同样,通过肉眼可以观察到所有路径的方向都逆转了,但是分量和正向图的分量一样。

下面继续分析为什么用逆后序,最后才能得出为什么要计算反向图的逆后序序列。

回顾我们之前对逆后序的探讨,在对 s s s做深度优先搜索,我们发现可以访问 v v v,那么必然有 s → v s \rightarrow v sv

dfs(DiGraph g,int s) {
	...
	dfs(g,s);
		...
		dfs(g,v);
		...
		v done;
	...
	s done;
	...
}

伪代码如上所示, v v v先入栈, s s s会在它之后入栈。根据栈的后进先出特性,最后得到的顺序为: s s s, v v v
所以,如果逆后序序列为: s s s, v v v 我们可以猜测为 s → v s \rightarrow v sv

在这里插入图片描述
为什么说是猜测呢?我们以上面正向图的实例来解析,得到它的逆后序遍历顺序为: 4 , 5 , 7 , 6 , 3 , 8 , 9 , 0 , 1 , 2 4,5,7,6,3,8,9,0,1,2 4,5,7,6,3,8,9,0,1,2

首先我们看 4 , 5 , 7 , 6 4,5,7,6 4,5,7,6,诶,这个顺序没问题, 4 → 5 → 7 → 6 4 \rightarrow 5 \rightarrow 7 \rightarrow 6 4576 继续往后走, 6 , 3 6,3 6,3,但是它们之间就没有路径了。只能说明 6 6 6 3 3 3前面被访问

回顾下无向图的计算连通分量算法,假设我们先访问4,接下来可以选择56,假设先选择5吧,然后访问766可以访问484因为之前已经访问过(marked[]数组标记过),所以不会再次访问,但是如果访问8就会跑到另外一个连通分量去了。

这时,脑海里有没有浮起:如果8已经访问过就好了。

现在,再次关注反向图。
在这里插入图片描述

我们可以发现,在反向图中,6访问不到8,同时4也访问不到2了。这时可能有同学会说,那直接访问反向图就好了。非也,相反地,2也是能访问4的。也会存在同样的问题,只不过换了对象而已。

好了,到了解答这个问题的时候了

为什么要计算反向图的逆后序序列?

在这里插入图片描述

回到正向图的访问顺序,如存在一个这样的顺序,使得24先访问,86先访问,那么依次 { 4 , 5 , 7 , 6 } \{4,5,7,6\} {4,5,7,6} 得到的就是强连通分量。

没错,反向图的逆后序序列就是我们想要的顺序。以上图为例,得出它的反向图的逆后序序列为: 9 , 8 , 3 , 0 , 2 , 4 , 6 , 7 , 5 , 1 9,8 ,3, 0, 2, 4, 6, 7, 5, 1 9,8,3,0,2,4,6,7,5,1

我们根据这个序列对正向图 G G G进行dfs访问:

  • 先访问99没有邻接点,递归调用dfs(9)结束,得到分量 { 9 } \{9\} {9}
  • 访问8,8也没有邻接点,得到分量 { 8 } \{8\} {8}
  • 访问33有邻接点8,但是8刚才已经访问过了(^_^),递归调用dfs(3)结束,得到分量 { 3 } \{3\} {3}
  • 访问0,按照0,1,2的顺序访问到22有一个邻接点0,已经访问过了。递归调用dfs(0)结束,得到分量 { 0 , 1 , 2 } \{0,1,2\} {0,1,2}
  • 访问2,它已经访问过了
  • 访问44的邻接点有2,5,但是2已经访问过了,然后依次访问5,7,6,6接下来想尝试访问8,但是8早已被访问。得到分量 { 4 , 5 , 7 , 6 } \{4,5,7,6\} {4,5,7,6}
  • 反向图的逆后序序列剩下的6,7,5,1都已经被访问,就不细说了

最后就得到了5个强连通分量。

本文的分量都是对强连通分量的简称

接下来看一下代码实现

Kosaraju算法实现

本文相关代码实现涉及到栈和队列的请访问:栈和队列的实现

package com.algorithms.graph;

import com.algorithms.queue.Queue;

/**
 * 计算有向图的强连通分量
 * @author yjw
 * @date 2019/5/22/022
 */
public class KosarajuSCC {
    private boolean[] marked;
    /**
     * 标识连通分量,同一连通分量的值相同
     * 0:第一个连通分量
     * 1:第二个连通分量
     * ...
     *
     * 值为0到count - 1之间
     */
    private int[] id;
    /**
     * 连通分量数
     */
    private int count;

    public KosarajuSCC(DiGraph g) {
        marked = new boolean[g.vertexNum()];
        id = new int[g.vertexNum()];
        DepthFirstOrder order = new DepthFirstOrder(g.reverse());

        for (int s : order.reversePost()) {
            if (!marked[s]) {
                dfs(g,s);
                count++;
            }
        }
    }

    private void dfs(DiGraph g,int v) {
        marked[v] = true;
        id[v] = count;//标识连通分量
        for (int w: g.adj(v)) {
            if (!marked[w]) {
                dfs(g,w);
            }
        }
    }

    public boolean stronglyConnected(int v,int w) {
        return id[v] == id[w];
    }

    public int id(int v) {
        return id[v];
    }

    public int count() {
        return count;
    }

    @SuppressWarnings("unchecked")
    public void print() {
        System.out.println(count + " components");//count个连通分量

        Queue<Integer>[]components = (Queue<Integer>[]) new Queue[count];
        for (int i = 0; i < components.length; i++) {
            components[i] = new Queue<>();
        }


        for (int i = 0; i < id.length; i++) {
            components[id(i)].enqueue(i);
        }
        for (Queue<Integer> queue : components) {
            System.out.println(queue);
        }

    }

    public static void main(String[] args) {
        DiGraph g = new DiGraph(10);
        g.addEdge(0,1,1,2,2,0);
        g.addEdge(4,2,4,5,5,7,7,6,6,4,6,8);
        g.addEdge(8,9,3,8);

        //System.out.println(g);

        KosarajuSCC cc = new KosarajuSCC(g);
        cc.print();
    }
}

和无向图的连通分量计算算法很像,除了dfs调用的顺序不同。再次建议先了解下无向图的连通分量计算算法

执行上面的主函数,输出如下:

5 components
[9]
[8]
[3]
[0 1 2]
[4 5 6 7]

为啥是4,5,6,7而不是上面的4,5,7,6 提这个问的同学看得很仔细,是4,5,6,7的原因请看print()方法。

还有啥问题的欢迎在下面评论指出

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

愤怒的可乐

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值