《算法4》无向图和有向图的有关问题详解(DFS,BFS,环,拓扑排序,连通性和强连通性)

本文仅作为《算法》第四版图的相关知识的个人笔记。

几个概念:

1.连通图:
从任意一个顶点都存在一条路径到达另一个任意顶点。非连通图由若干连通图组成,都是极大连通子图。
2.是一个无环连通图。连通图的生成树是其一个子图,拥有图的所有顶点。
3.二分图
一种能够将所有节点分为两部分的图。简单的说,如果按双色上色,二分图的任意两个相邻的顶点的颜色不同。
4. 两个顶点通过一条边连接,称为相邻的。顶点的度数为与他相连的边的总数。有向图中的度数分为入度和出度,入度为指向该顶点的边的总数,出度为从该顶点指出的边的总数。
5. 自环:一条连接顶点和其自身的边
6. 一对顶点的两条边称为平行边

PartOne:无向图相关算法

一、无向图

1.1 数据结构
 /**
     * 顶点数目
     */
    private final int V;
    /**
     * 边的数目
     */
    private int E;
    /**
     * 邻接表,Bag为背包,为一个只入不出的队列,adj[v]即表示与顶点v相连的顶点,因此相邻的两个顶点v和w分别会出现在对方的邻接表中
     */
    private Bag<Integer>[] adj;

1.2 构造
 public Graph(int v) {
        V = v;
        this.E=0;
        adj=new Bag[V];
        for (int i = 0; i < V; i++) {
            adj[i]=new Bag<>();
        }
    }
1.3 相关方法

两顶点是否相邻:

  public boolean hasEdge(int v,int w){
  		//遍历顶点v的邻接表,查找有无顶点w
        for(int x:adj(v)){
            if (x==w){
                return true;
            }
        }
        return false;
    }

添加一条边:

 public void addEdge(int v,int w){
       //也可以通过判断v==w和hasEdge(v,w)决定是否允许自环和平行边
            adj[v].add(w);
            adj[w].add(v);
            E++;
   

    }
//顶点度数
 public  int degree(int v){
        int degree=0;
        for (int w:adj(v)){
            degree++;
        }
        return degree;
    }
    //最大度数
    public  int maxDegree(){
        int max=0;
        for (int v = 0; v < V; v++) {
            int degree = degree(v);
            if (degree>max){
                max=degree;
            }
        }
        return max;
    }
    //自环数
    public int numOfSelfLoops(Graph graph){
        int loops=0;
        for (int i = 0; i < graph.V(); i++) {
            for(int w:graph.adj(i)){
                if (w==i){
                    loops++;
                }
            }
        }
        return loops/2;
    }

二、深度优先搜索

简单的说就是 从给定顶点出发,一直向下走,直到下面没有路了,然后返回上一个顶点,继续进行同样方式遍历,直到遍历完所有的顶点。

因此我们需要如下的数据结构:


 /**
     * 标记顶点i是否被访问
     */
    private boolean[] marked;
    /**
     * 从起点到一个顶点的已知路径上的最后一个顶点
     * 如 v->w->x  则edgeTo[w]=v,edgeTo[x]=w
     */
    private int[] edgeTo;
    /**
     * 遍历的起点
     */
    private final int s;      

构造函数:

public DepthFirstPaths(Graph G, int s) {
        this.s = s;
        //初始化两个数组
        edgeTo = new int[G.V()];
        marked = new boolean[G.V()];
        dfs(G, s);
    }

深度优先遍历:

private void dfs(Graph G, int v) {
		//标记当前顶点为true
        marked[v] = true;
        //遍历与当前顶点相邻的顶点
        for (int w : G.adj(v)) {
        	//如果没有被访问
            if (!marked[w]) {
            	//标记起点到当前顶点的最后一个路径
                edgeTo[w] = v;
                //从当前节点继续进行遍历
                dfs(G, w);
            }
        }
    }

是否存在从起点到指定顶点的路径:

 public boolean hasPathTo(int v) {
        validateVertex(v);
        return marked[v];
    }

返回从起点到指定顶点的路径,如果不存在返回null:

 public  Iterable<Integer> pathTo(int v) {
        if (!hasPathTo(v)){
           //没有到达路径,返回null
            return null;
        }
        //因为edgeTo保存的是到达当前节点的父节点,因此使用栈结构,这样从当前节点到起始顶点的顶点依此入栈,遍历的时候便是从起点到该顶点的完整路径
        Stack paths=new Stack();
        for (int i = v; i !=s ; i=edgeTo[i]) {
            paths.push(i);
        }
        paths.push(s);

        return paths;
    }

测试:

 public static void main(String[] args) {
        Graph graph=new Graph(6);
        graph.addEdge(0,2);
        graph.addEdge(0,1);
        graph.addEdge(0,5);
        graph.addEdge(2,1);
        graph.addEdge(2,3);
        graph.addEdge(2,4);
        graph.addEdge(3,5);
        graph.addEdge(3,4);
        DepthFirstPaths dfs=new DepthFirstPaths(graph,0);
        Iterable<Integer> paths = dfs.pathTo(4);
        for (Integer i:paths){
            System.out.println(i.toString());
        }
    }

结果:

0
5
3
4

三、广度优先搜索:

广度优先搜索的思想是,从当前节点,先遍历与该节点相邻的所有节点,然后再对相邻顶点进行广度优先遍历
深度优先是纵向遍历,广度优先是横向遍历。

数据结构:
广度优先搜索的数据结构同深度基本一样:

public class BreadthFirstPaths {
	private final int start;
    private int[] edgeTo;
    private boolean[] marked;

    public BreadthFirstPaths(Graph graph,int start) {
        this.start = start;
        edgeTo=new int[graph.V()];
        marked=new boolean[graph.V()];
        bfs(graph,start);
    }
}    

但在遍历的时候采用队列进行辅助,思想如下:
1.将当前顶点加入队列
2.从出队出队一个顶点(第一次时出队的就是当前节点),遍历该顶点的所有相邻顶点,置marked[v]为true,并依此入队。
3.当队列非空时,循环步骤2

代码如下:

private void bfs(Graph graph, int start) {
        Queue<Integer> queue=new Queue<>();
        queue.enqueue(start);
        marked[start]=true;
        while (!queue.isEmpty()){
            int v = queue.dequeue();
            for (int w:graph.adj(v)){
                queue.enqueue(w);
                edgeTo[w]=v;
                marked[w]=true;
            }
        }
    }

四、连通分量

数据结构如下:

public class CC {
	 private boolean[] marked;   
    /**
     * 顶点属于哪个连通分量 如 顶点v属于第count个连通分量,则id[v]=count
     */
    private int[] id;
    /**
     * 第count个连通分量的顶点数
     */
    private int[] size;
    /**
     * 连通分量数
     */
    private int count;
    
}    

思想如下:
遍历所有的顶点,对每个顶点都执行dfs深度优先遍历,如果两个顶点在同一个连通分量,那么在对一个顶点进行dfs遍历的时候,两个顶点必然会在构造函数的一个dfs中被访问到,属于同一个连通分量。
构造函数:

public CC(Graph G) {
        marked = new boolean[G.V()];
        id = new int[G.V()];
        size = new int[G.V()];
        //代码1
        for (int v = 0; v < G.V(); v++) {
            if (!marked[v]) {
                dfs(G, v);
                //与顶点v连通的所有顶点都遍历完毕后,说明该连通分量所有顶点已遍历完毕,增加count
                count++;
            }
        }
 }
 private void dfs(Graph G, int v) {
        marked[v] = true;
        //顶点v属于第count个分量
        id[v] = count;
        //第count个分量的顶点数加1
        size[count]++;
        for (int w : G.adj(v)) {
            if (!marked[w]) {
                dfs(G, w);
            }
        }
    }

五、判断是否为无环图

对于无向图而言,判断其是否为无环图很简单,在对某个顶点进行dfs遍历时,假如它的相邻顶点已被访问过,那么有两种可能:

3-4-5-3为例
每次dfs都传递要遍历的节点,和当前刚刚被遍历过的节点
一开始三个顶点都未被访问
1.现在对3进行dfs,访问到5,5被标记为true;
2.再对5进行dfs,此时3已经被访问,但是此时的3属于顶点5的父级,不构成环,再访问4,标记4为true;
3.接着对4进行dfs,此时顶点5被访问,同2一样,顶点5属于顶点4的“父级”,不构成环,但是3也被访问了,且3不是顶点4的“父级”,因此此处出现环。
关键代码如下:

private boolean[] marked;
    private boolean hasCycle;

    public Cycle(Graph G) {
        marked=new boolean[G.V()];
        for (int i = 0; i < G.V(); i++) {
            if (!marked[i]){
                dfs(G,i,i);
            }
        }
    }

    private void dfs(Graph G,int v, int u){
        marked[v]=true;
        for(int w:G.adj(v)){
            if (!marked[w]){
                dfs(G,w,v);
            }else if (w!=u){//如 3:5 5:3 就不构成环
                hasCycle=true;
            }
        }
    }

六、二分图问题

这个同无环图问题类型,在对一个顶点进行dfs遍历时,将其未被访问的临接顶点置为与该顶点对立的颜色,若已经被访问,则判断该顶点的颜色与当前顶点的颜色是否不一样,如果一样,则不是二分图。

关键代码如下:

 private void dfs(Graph graph, int v) {
        marked[v]=true;
        for (int w: graph.adj(v)){
            if (!marked[w]){
                color[w]=!color[v];
                dfs(graph,w);
            }else if (color[w]==color[v]){
                isTwoColorable=false;
                break;
            }
        }
    }

PartTwo:有向图相关算法

有向图的定义和无向图基本一样,这里也是基于临接表实现,但在插入边的时候,有向图只需要操作边的始点的邻接表,不需像无向图一样两个顶点的邻接表都插入对应的顶点。

public class Digraph {
    private final int V;
    private int E;
    private Bag<Integer>[] adj;

    public Digraph(int v) {
        V = v;
        this.E=0;
        adj=new Bag[V];
        for (int i = 0; i < v; i++) {
            adj[i]=new Bag<>();
        }
    }
    public int V(){
        return V;
    }
    public int E(){
        return E;
    }
    public void addEdge(int v,int w){
        adj[v].add(w);
        E++;
    }
    public Iterable<Integer> adj(int v){
        return adj[v];
    }
    /**
    * 获取当前有向图的反转图
    */
    public Digraph reverse(){
        Digraph R=new Digraph(V);
        for (int v = 0; v < V; v++) {
            for (int w: adj(v)){
                R.addEdge(w,v);
            }
        }
        return R;
    }
}

一、有向图的可达性:

这里同无向图一样,只需将Graph换为Digraph即可:
进行DFS遍历:

public class DirectedDFS {
    private boolean[] marked;

    /**
     * 单点可达性
     * 经过此方法,从marked(int v)返回是否存在一条从s到达给定顶点v的有向路径
     * @param digraph
     * @param s
     */
    public DirectedDFS(Digraph digraph,int s) {
        marked=new boolean[digraph.V()];
        dfs(digraph,s);
    }

    /**
     * 多点可达性
     * 是否存在一条从集合中的任意顶点到达给定顶点v的有向路径
     * @param digraph
     * @param sources
     */
    public DirectedDFS(Digraph digraph,Iterable<Integer> sources) {
        marked=new boolean[digraph.V()];
        for (int s:sources){
            if (!marked[s]){
                dfs(digraph,s);
            }
        }

    }

    private void dfs(Digraph digraph, int v) {
        marked[v]=true;
        for (int w:digraph.adj(v)){
            if (!marked[w]){
                dfs(digraph,w);
            }
        }
    }

    public boolean marked(int v){
        return marked[v];
    }

    public static void main(String[] args) {
        Digraph graph=new Digraph(6);
        graph.addEdge(0,2);
        graph.addEdge(0,1);
        graph.addEdge(0,5);
        graph.addEdge(2,1);
        graph.addEdge(2,3);
        graph.addEdge(4,5);

        DirectedDFS dfs=new DirectedDFS(graph,0);
        System.out.println(dfs.marked(3));

    }

}

顶点对的可达性

借助DirectedDFS数组,我们可以实现顶点对的可达性:
无论对于稀疏还是稠密的图,它都是理想的解决方案,但不适用于实际应用中的大型有向图,因为构造函数所需的空间和V²成正比,所需时间和V(V+E)成正比

public class TransitiveClosure {
    private DirectedDFS[] directedDFS;  // tc[v] = reachable from v

    /**
     * 构造函数
     * 所需空间与V²成正比
     * 所需时间与 V(V+E)成正比
     * @param G
     */
    public TransitiveClosure(Digraph G) {
        directedDFS = new DirectedDFS[G.V()];
        for (int v = 0; v < G.V(); v++)
            directedDFS[v] = new DirectedDFS(G, v);
    }

    boolean reachable(int v,int w){
        return directedDFS[v].marked(w);
    }

}

二、单点有向路径和单点最短有向路径

同无向图一样,单点有向路径就是借助DFS,对从s到v路径上的每一个顶点,使用edgeTo数组保存从s到该顶点的路径上的最后一个顶点坐标;而单点最短路径就是以同样的方式借助BFS保存从s到指定顶点的路径,这样得出的路径就是到达该顶点的最短路径。

三、环和无环图(拓扑排序)

在调度问题中,限制条件是这些任务的执行方法和起始时间,但最重要的限制条件叫做有限制级限制,它指明了任务间执行的先后顺序。

我们需要在一个有优先级限制的任务中,按该限制条件找出安排完成任务的顺序。这也等价于一个基本问题:拓扑排序,即在一个有向图中,将所有顶点排序,满足所有的有向边均从排在前面的元素指向排在后面的元素(或指出无法做到这一点)。

以高校课程安排为例,一些课程的开课必须要求学生修完前面的某些课,而拓扑排序就是在这种限制条件下,找出学生选修课程的顺序。

而一旦一个优先级限制的问题中存在有向环,那么这个问题一定是无解的。因此我们需要进行有向环的检测。
有向无环图就是不含有有向环的有向图

检测有向环的思路如下:
对有向图的每个顶点都进行DFS遍历,假设从顶点v开始深度优先遍历,将从该顶点递归调用路径上的所有顶点marked标记后再使用一个boolean[] onStack标记是否在递归栈上,如果对一个顶点的DFS递归调用没有发现有向环,则递归结束前再让该节点的onStack状态改为false;而如果对某个顶点的递归调用时发现另一个顶点已经被标记了,且onStack为true,则说明这个路径上一定存在有向环,这时将该环上的所有点加入Stack cycle栈中。

public class DirectedCycle {
    private boolean[]marked;
    private int[] edgeTo;
    /**
     * 有向环中的所有顶点(如果存在)
     */
    private Stack<Integer>cycle;

    /**
     * 递归调用的栈上的所有顶点
     */
    private boolean[] onStack;

    public DirectedCycle(Digraph digraph) {
        marked=new boolean[digraph.V()];
        edgeTo=new int[digraph.V()];
        onStack=new boolean[digraph.V()];
        for (int v = 0; v < digraph.V(); v++) {
            if (!marked[v]){
                dfs(digraph,v);
            }
        }
    }

    /**
     * 使用不带权的Digraph
     * @param digraph
     * @param v
     */
    private void dfs(Digraph digraph, int v) {
        onStack[v]=true;
        marked[v]=true;
        for (int w: digraph.adj(v)){
            if (this.hasCycle()){
            //如果已经找到有向环,则返回
                return;
            } else if (!marked[w]){
                edgeTo[w]=v;
                dfs(digraph,w);
            }else if (onStack[w]){
                cycle=new Stack<>();
                for (int x = v; x != w; x=edgeTo[x]) {
                    cycle.push(x);
                }
                cycle.push(w);
                cycle.push(v);
            }
        }
        //对v的一次递归调用后,如果没发现环,则重置为false
        onStack[v]=false;

    }
    public Iterable<Integer>cycle(){
        return cycle;
    }
    public boolean hasCycle(){
        return cycle!=null;
    }
}

测试:

public static void main(String[] args) {
        Digraph digraph=new Digraph(4);
        digraph.addEdge(0,1);
        digraph.addEdge(2,3);
        digraph.addEdge(3,1);
        digraph.addEdge(1,2);
        //存在环3>1->2>3
        DirectedCycle cycle = new DirectedCycle(digraph);
        if (cycle.hasCycle()){
            System.out.println("存在环");
            Stack<Integer> stack = cycle.cycle;
            while (!stack.isEmpty()){
                System.out.println(stack.pop());
            }
        }
    }

结果:

存在环
3
1
2
3

《算法4》中给出:

一幅有向无环图的拓扑排序就是所有顶点的逆后排序

那么什么是逆后排序呢?
我们规定:
前序: 在对顶点v递归调用前就将该顶点加入队列,最后出队的顺序就是前序
后序:在对顶点v递归调用后将该顶点加入队列,最后出队的顺序就是后序
逆后序:在对顶点v递归调用后将该顶点加入栈中,最后出栈的顺序就是逆后序

实现如下:

/**
 * 有向图中基于深度优先搜索的顶点排序
 * @author MaoLin Wang
 * @date 2020/2/2214:55
 */
public class DepthFirstOrder {
    private boolean[]marked;
    /**
     * 所有顶点的前序遍历(递归调用前加入队列)
     */
    private Queue<Integer> pre;
    /**
     * 所有顶点的后序遍历(递归调用后加入队列)
     */
    private Queue<Integer> post;
    /**
     * 所有顶点的逆后序遍历(递归调用后压入栈)
     */
    private Stack<Integer> reversePost;

    public DepthFirstOrder(Digraph digraph) {
        pre=new Queue<>();
        post=new Queue<>();
        reversePost=new Stack<>();
        marked=new boolean[digraph.V()];

        for (int v = 0; v < digraph.V(); v++) {
            if (!marked[v]){
                dfs(digraph,v);
            }
        }

    }

     private void dfs(Digraph digraph, int v) {
        System.out.println("dfs("+v+")");

        pre.enqueue(v);
        marked[v]=true;
        for (int w: digraph.adj(v)){
            if (!marked[w]){
                dfs(digraph,w);
            }
        }
        System.out.println(v+"完成");

        post.enqueue(v);
        reversePost.push(v);
    }

    public Iterable<Integer>pre(){
        return pre;
    }

    public Queue<Integer>post(){
        return post;
    }
    public Iterable<Integer>reversePost(){
        return reversePost;
    }
}

我们先测试一下一张有向无环图的各个排序的顺序:

public static void main(String[] args) {
        Digraph digraph = new Digraph(13);
        digraph.addEdge(0,5);
        digraph.addEdge(0,1);
        digraph.addEdge(0,6);
        digraph.addEdge(2,0);
        digraph.addEdge(2,3);
        digraph.addEdge(3,5);
        digraph.addEdge(5,4);
        digraph.addEdge(6,4);
        digraph.addEdge(6,9);
        digraph.addEdge(7,6);
        digraph.addEdge(8,7);
        digraph.addEdge(9,10);
        digraph.addEdge(9,11);
        digraph.addEdge(9,12);
        digraph.addEdge(11,12);
        DepthFirstOrder depthFirstOrder = new DepthFirstOrder(digraph);
        Stack<Integer> reversePost = depthFirstOrder.reversePost;


    }

调用结果及形成的队列如下:
边的构造顺序不一样,得到的拓扑排序也会不一样,但是结果一定是满足拓扑排序优先级限制的。

			  pre     			   post				 			reversePost
dfs(0)  	  0
  dfs(6)	  0 6
    dfs(9)    0 6 9
	  dfs(12) 0 6 9 12 
	  12完成 						12							 12
	  dfs(11) 0 6 9 12 11
	  11完成						12 11						 11 12
	  dfs(10)0 6 9 12 11 10			    					
	  10完成						12 11 10    		 		 10 11 12
	9完成							12 11 10 9					 9 10 11 12
    dfs(4)	0 6 9 12 11 10 4			
	4完成							12 11 10 9 4            	 4 9 10 11 12
  6完成								12 11 10 9 4 6 				 6 4 9 10 11 12
dfs(1)		0 6 9 12 11 10 4 1	
1完成								12 11 10 9 4 6 1 			 1 6 4 9 10 11 12
dfs(5)		0 6 9 12 11 10 4 1 5
5完成								12,11,10,9,4,6,1,5 			 5 1 6 4 9 10 11 12
0完成								12,11,10,9,4,6,1,5,0    	 0,5,1,6,4,9,10,11,12
dfs(2)	   0,6,9,12,11,10,4,1,5,2		
 dfs(3)    0,6,9,12,11,10,4,1,5,2,3
 3完成								12,11,10,9,4,6,1,5,0,3  	 3,0,5,1,6,4,9,10,11,12
2完成								12,11,10,9,4,6,1,5,0,3,2     2,3,0,5,1,6,4,9,10,11,12
dfs(7)     0,6,9,12,11,10,4,1,5,2,3,7
7完成   							12,11,10,9,4,6,1,5,0,3,2,7   7,2,3,0,5,1,6,4,9,10,11,12
dfs(8)
8完成								12,11,10,9,4,6,1,5,0,3,2,7,8   8,7,2,3,0,5,1,6,4,9,10,11,12

我们发现其逆后序就是我们要的拓扑排序,那么为什么逆后序就是拓扑排序呢?

对于任意边v->w,调用dfs(v)时,一定会出现以下三种情况之一:
1.dfs(w)已调用过,且已经结束(w被标记过了)
2.dfs(w)未被调用(w没被标记),因此dfs(v)时会调用dfs(w),且dfs(w)会在dfs(v)之前返回
3.dfs(w)被调用过了,且没有返回。这个就是有有向环时会出现的情况,但进行拓扑排序的前提的没有有向环,因为该情况不会出现。
因此 情况1和2都是w在v之前结束调用,则w在后序排序中,一定在v之前,相反,在逆后序中,w一定在v之后,因此对于任意v->w都是排名较前点指向排名较后的点。

这样我们就可以得到拓扑排序的实现:

public class TopologicalSort {
    //顶点的拓扑排序
    private Iterable<Integer>order;

    public TopologicalSort(Digraph digraph) {
        DirectedCycle directedCycle=new DirectedCycle(digraph);
        //排序前进行有向环检测,没有环才可以进行拓扑排序
        if (!directedCycle.hasCycle()){
            //返回拓扑排序
            DepthFirstOrder dfs=new DepthFirstOrder(digraph);
            order=dfs.reversePost();
        }

    }


    public Iterable<Integer>order(){
        return order;
    }
    public boolean hasOrder(){
        return order!=null;
    }
}

四、有向图的强连通性

定义:
如果两个顶点是互相可达的,则称是强连通的。如果一个有向图的任意两个顶点都是强连通的,则该有向图是强连通的。
有向图的极大强连通子图,称为强连通分量(strongly connected components)。
如下:每个颜色代表一个强联通分量。
在这里插入图片描述
计算强连通分量的最常用的方法是Kosaraju算法:
其思想是:
1.使用DFS查找给定有向图G的反向图G^R
2.根据反向图G^R求得其逆后序列
3.对2求得的逆后序进行DFS遍历,访问未被标记的点
4.在构造函数中,所有在同一个dfs调用中被访问的顶点都在同一个强连通分量中,按无向图中求连通分量的方法求强连通分量。

有关该思想的证明如下:
我们只要证明以下两个问题即可:
(树上的证明猛一看可能看的不太懂,下面用自己的理解讲的细一点)
1.每个和s强连通的顶点v都会在构造函数调用的dfs(G,s)中被访问到
2.构造函数调用的dfs(G,s)所到达的任意顶点v都必然是和s强连通的。

对命题1,我们使用反证法:
1.假设一个和s强连通的顶点v在dfs(G,s)的调用中没有被访问到
2.由于存在一条从s->v的路径,所以如果顶点v没有在dfs(G,s)中被访问,就一定在之前调用了dfs(G,v),并且访问到了v。
3.又因为s和v是强连通的,所以也存在v->s的路径,在dfs(G,v)中,s一定会被标记,而s被标记,就一定不会再次进行dfs(G,s)的调用,矛盾,因此命题1成立。

对命题2:
1.要证明s和任意顶点v强连通,只要证明s->v且v->s,因为v是dfs(G,s)调用中访问到的任意顶点,所以一定存在s->v,接下来只要证明存在v->s即可。
2.要证明存在v->s,就相当于证明反向图中存在s1->v1
如下:
在这里插入图片描述
因为我们是按照逆后序进行深度优先遍历的,按照逆序,我们是在dfs(G,s)的调用中调用dfs(G,v),即先访问s再访问v,所以在反向图GR中,我们就应该是先访问v再访问s,对应上图GR中的点就是,先访问s1再访问v1。
即只要证明dfs(G,v1)在dfs(G,s1)之前结束,则这样就有如下两个情况:

1. dfs(G,v1)在调用dfs(G,s1)之前,且在dfs(G,s1)的调用开始前结束。
2. dfs(G,v1)在调用dfs(G,s1)之后,且在dfs(G,s1)的调用结束前结束。

如果出现情况1,这显然不可能,因为如果v1在s1之前就结束了,那么得出的逆后序应该是s1,v1,对应G中的v和s,先调用v再调用s,但是显然G是先调用s再调用v,所以该情况不存在。
如果是情况2,则说明存在一条路径s1>v1,即原图G中存在一条路径v->s,命题2正确。

下面是Kosaraju算法的实现:

/**
 * 计算强连通分量的Kosaraju算法
 * @author MaoLin Wang
 * @date 2020/2/2216:54
 */
public class KosarajuSCC {
    private boolean[] marked;
    /**
     * 强连通分量的标识符
     */
    private int[] id;
    /**
     * 强连通分量个数
     */
    private int count;

    public KosarajuSCC(Digraph digraph){
        marked=new boolean[digraph.V()];
        id=new int[digraph.V()];
        //求反向图的逆后序
        DepthFirstOrder order=new DepthFirstOrder(digraph.reverse());
        //对逆后序进行dfs遍历
        for(int s: order.reversePost()){
            if (!marked[s]){
                dfs(digraph,s);
                count++;
            }
        }
    }

    private void dfs(Digraph digraph, int v) {
        marked[v]=true;
        //顶点v属于第count个强连通分量
        id[v]=count;
        for (int w: digraph.adj(v)){
            if (!marked[w]){
                dfs(digraph,w);
            }
        }
    }
    /*v和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;
    }
}

在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值