[Java]-有向图(拓扑排序,kosaraju算法)

原文链接

有向图

在有向图中,边是单向的,每条边所连接的两个顶点是一个有序对,这种邻接性是单向的

定义:

一幅有方向性的图(有向图),由一组顶点和有方向的边组成,每条有向边都连接着一组有序对

有向环

一条至少含有一条边且起点和终点相同的有向路径。简单有向环是一条(除了起点终点相同)不含有重复顶点和边的环。

路径或环的长度即为边的数量

[Java]-有向图(拓扑排序,kosaraju算法)_第1张图片

有向图的代码实现:

package cn.ywrby.Graph;

//有向图

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;

public class Digraph {
    private final int V;  //图中顶点数目
    private int E;   //边的个数
    private Bag<Integer>[] adj;   //邻接表
    //构造函数
    public Digraph(int V){
        this.V=V;
        this.E=0;
        adj=(Bag<Integer>[]) new Bag[V];
        for(int v=0;v<V;v++){
            adj[v]=new Bag<Integer>();
        }
    }
    public int V(){return V;}
    public int E(){return E;}
    
    //与无向图最大区别,只向v中写入w,而在w的邻接表中不包含v
    public void addEdge(int v,int w){
        adj[v].add(w);  //向v的链表中写入顶点w
        E++;  //增加边的数目
    }

    public Digraph(In in){
        this(in.readInt());
        int E=in.readInt();
        for(int i=0;i<E;i++){
            int v=in.readInt();
            int w=in.readInt();
            addEdge(v,w);
        }
    }
    
    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;
    }
}

可达性

单点可达性

给定一幅有向图与一个起点S,是否存在一条从S到达给定顶点V的有向路径

多点可达性

给定一幅有向图和一组顶点集合,是否存在一条从集合中任意顶点到达给定顶点v的有向路径

深度优先搜索

利用无向图中谈到过的深度优先搜索方法可以处理以上两种问题,并且比处理无向图中的连通性更流程简介,因为深度优先搜索与广度优先搜索本就更适应于有向图

代码实现:
package cn.ywrby.Graph;

//有向图的可达性
//利用有向图与深度优先搜索实现

import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;

public class DirectedDFS {
    private boolean[] marked;
    
    //处理单个节点可行性问题
    public DirectedDFS(Digraph G,int s){
        marked=new boolean[G.V()];
        dfs(G,s);
    }
    //利用数据结构处理多节点可行性问题
    public DirectedDFS(Digraph G,Iterable<Integer>sources){
        marked=new boolean[G.V()];
        for(int s:sources){
            dfs(G,s);
        }
    }
    private void dfs(Digraph G,int s){
        marked[s]=true;
        for(int w:G.adj(s)){
            if(!marked[w]) dfs(G,w);
        }
    }
    public boolean marked(int v) {return marked[v];}

    public static void main(String[] args) {
        Digraph G=new Digraph(new In(args[0]));
        Bag<Integer> sources=new Bag<Integer>();
        for(int i=1;i<args.length;i++){
            sources.add(Integer.parseInt(args[i]));
        }
        DirectedDFS reachable=new DirectedDFS(G,sources);
        for(int i=0;i<G.V();i++){
            if(reachable.marked(i)) StdOut.print(i+" ");
        }
        StdOut.println();
    }
}
标记-清楚垃圾收集 机制

多点可达性的一个重要应用就是类似Java中的垃圾清理机制(对运行内存使用情况的控制),在程序执行的过程中,有些对象可以被直接访问,而不能被直接访问的对象就应当被及时清楚。通过类似于深度优先搜索的机制,Java每隔一段时间就会检测程序运行过程中的不可达顶点,并对它进行清理

有向图的寻路:

只需要修改前面介绍过的广度优先搜索和深度优先搜索的相应方法的类名,就可以实现类似无向图的有向图寻路功能,单点最短有向路径等

环和有向无环图

调度问题

给定一组任务并安排他们的执行顺序(上课顺序,执行顺序等等),限制条件是这些任务的执行方法和起始时间。限制条件还可能包括任务的耗时与资源利用情况,其中最重要的一种限制条件就是优先级限制

优先级限制下的调度问题

执行任务很容易碰到优先级问题,例如在安排大学课程时,要先学习计算机理论基础,才能开始学习人工智能,先学习算法才能学习与之相关的数据库知识。基于此的调度问题就可以利用有向图来解决

但在解决过程中如果遇到有向环,例如学习A后才可以学习B,学习B后才可以学习C,学习C后才可以学习A。这种有向环明显是不符合逻辑的,也会导致问题无解

所以,怎么辨别有向图中是否包含有向环(有向图是否是有向无环图)就非常重要了

有向环检测

package cn.ywrby.Graph;

//利用深度优先寻找有向环

import cn.ywrby.dataStructure.Stack;

public class DirectedCycle {
    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle;  //有向环中所有顶点
    private boolean[] onStack;  //递归调用的栈上所有的顶点
    public DirectedCycle(Digraph G){
        marked=new boolean[G.V()];
        edgeTo=new int[G.V()];
        onStack=new boolean[G.V()];
        for(int v=0;v<G.V();v++){
            if(!marked[v]) dfs(G,v);
        }
    }
    
    
    /*
    * 在进行深度优先搜索的过程中不断把所有顶点放入栈中
    * 如果发现曾经已经存在栈中的顶点再次被别的顶点指向
    * 说明在这条有向路径中出现了一个环
    * (例如1->5,5->4,4->3,3->5  在再次放入5的过程中就发现了这个环5,4,3)
    * 我们可以通过edgeTo将这个环的路径找到
    * */
    private void dfs(Digraph G,int v){
        marked[v]=true;
        onStack[v]=true;
        for(int w:G.adj(v)){
            if(this.hasCircle()) return;  //发现一个环就结束循环
            else if(!marked[w]) {edgeTo[w]=v;dfs(G,w);}
            else if(onStack[w]){
                cycle=new Stack<Integer>();
                for(int x=v;x!=w;x=edgeTo[x]) cycle.push(x);
                cycle.push(w);
                cycle.push(v);
            }

        }
        onStack[v]=false; //结束这条路径的环的查找
    }
    public boolean hasCircle(){return cycle!=null;}
    public Iterable<Integer> cycle(){return cycle;}
}

顶点的深度优先次序与拓扑排序

拓扑排序

给定一幅有向图,将所有顶点排序,使得所有有向边均从排在前面的元素指向排在后面的元素(或者说明无法做到这一点)

[Java]-有向图(拓扑排序,kosaraju算法)_第2张图片

优先级限制下的调度问题,等价于计算有向无环图中的拓扑排序

当且仅当一幅有向图是无环图时,它才能进行拓扑排序

在实现拓扑排序前,首先要理解以下三种有向图的遍历方式:

  • 前序遍历:在递归前将顶点加入队列
  • 后序遍历:在递归完成后将顶点加入队列
  • 逆后序:在递归完成后将顶点加入 ,得到的是后序的逆序
package cn.ywrby.Graph.DepthFirstPaths;

import cn.ywrby.Graph.Digraph;
import edu.princeton.cs.algs4.Queue;
import edu.princeton.cs.algs4.Stack;

//有向图中基于深度优先搜索的顶点排序
//能够实现前序,后序,逆后序

public class DepthFirstOrder {
    private boolean[] marked;
    private Queue<Integer> pre;  //前序遍历
    private Queue<Integer> post;  //后序遍历
    private Stack<Integer> reversePost;   //逆后序

    public DepthFirstOrder(Digraph G){
        pre=new Queue<Integer>();
        post=new Queue<Integer>();
        reversePost=new Stack<Integer>();

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


    }
    private void dfs(Digraph G,int s){
        pre.enqueue(s);  //在递归之前加入队列,前序遍历
                
        marked[s]=true;
        for(int w:G.adj(s)){
            if(!marked[w]) dfs(G,w);
        }
        post.enqueue(s);  //递归完成之后加入队列,后序遍历
        reversePost.push(s);  //递归完成之后加入栈,实现反向,逆后序
    }
    public Iterable<Integer> pre(){return pre;}
    public Iterable<Integer> post(){return post;}
    public Iterable<Integer> reversePost(){return reversePost;}
    

}
拓扑排序的代码实现
package cn.ywrby.Graph;

import cn.ywrby.Graph.DepthFirstPaths.DepthFirstOrder;
import edu.princeton.cs.algs4.StdOut;

//拓扑排序  利用逆后序实现

public class Topological {
    private Iterable<Integer> order;

    public Topological(Digraph G){
        DirectedCycle cyclefinder=new DirectedCycle(G);
        if(!cyclefinder.hasCircle()){   //判断是否符合有向无环图的条件
            DepthFirstOrder dfs=new DepthFirstOrder(G);  //进行深度优先搜索
            order=dfs.reversePost();   //取出逆后序排列
        }
    }
    public Iterable<Integer> order(){return order; }
    public boolean isDAG() {return order!=null;}

    public static void main(String[] args) {
        String filename=args[0];
        String separator =args[1];
        SymbolDigraph sg=new SymbolDigraph(filename,separator);

        Topological top=new Topological(sg.G());
        for(int v:top.order()){
            StdOut.println(sg.name(v));
        }
    }

}

一幅有向无环图的拓扑顺序即为所有顶点的逆后序排列

证明:

对于任意边v->w,在调用dfs(v)时,都必然涉及下面三种情况中的一种:

  • dfs(w)已经被调用了,并且已经返回过(w已经被标记,并且进入栈中了),这就说明w一定在v的前面,符合拓扑排序的要求
  • dfs(w)还没有被调用过,说明经过v->w后,w会被标记,且在递归过程中先于v返回,所以w还是在v的前面,符合拓扑排序要求
  • dfs(w)已经被调用且没有返回,这种情况在拓扑排序过程中不可能出现,因为如果还没有返回证明v会先于w返回,也就是说会出现v<-…<-w<-v即有向环,这显然与拓扑排序的前提有向无环图相违背,所以不可能发生

综上所述,只有钱两种情况发生的前提下,v会按照拓扑排序出现在w的后方且对于所有顶点都适用,所以逆后序排列后的结果就是拓扑排序的结果

使用深度优先搜索对有向无环图进行拓扑排序所需时间与V+E成正比

代码一共进行两次遍历,第一次深度优先搜索保证不含有环结构,第二次深度优先搜索进行逆后序排列,两次都遍历了数组,所以与V+E成正比

任务调度类问题解题步骤

  1. 指明任务和确定任务优先级
  2. 不断检测图中是否包含环结构,并去除。以确保程序正常运行
  3. 利用拓扑排序对任务进行调度

有向图中的强连通性

有向图中的连通性

如果v可达w,且w可达v(存在一条路径从v到w,也存在一条路径从w到v),则称v顶点和w顶点是强连通的

如果一幅有向图中任意两点都是强连通的,则称这副图也是强连通的
两个顶点是强连通的,当且仅当它们都在有向图的一个简单环中

强连通分量

强连通性:

强连通性与无向图中的连通性一样,都是一种等价关系,它们均满足三条性质:

  • 自反性:任意顶点v和自己都是强连通的
  • 对称性:如果v和w是连通的,那么w和v也是强连通的
  • 传递性:如果v和w是强连通的,w和x是强连通的,那么v和x也是强连通的

由于强连通性是一种等价关系,它将所有顶点分成了不同的等价类,这些等价类称作强连通分量

  • 一个含有V个顶点的有向图含有1~V个强连通分量
  • 一个强连通图含有一个强连通分量
  • 一个有向无环图含有V个强连通分量

计算强连通分量的Kosaraju算法

package cn.ywrby.Graph;

import cn.ywrby.Graph.DepthFirstPaths.CC;
import cn.ywrby.Graph.DepthFirstPaths.DepthFirstOrder;
import cn.ywrby.Graph.Graph;
import edu.princeton.cs.algs4.Bag;
import edu.princeton.cs.algs4.In;
import edu.princeton.cs.algs4.StdOut;

/*
* Kosaraju算法计算有向图中强连通分量个数
* 算法流程很简单,但要真正理解需要仔细思考
* 首先运用深度优先搜索,遍历有向图,利用逆倒序排列将所有顶点储存在栈中
* 然后按照出栈顺序再次利用深度优先搜索遍历整个有向图
* 在同一次递归调用中的顶点,都是同一个强连通分量下的顶点
* */


public class KosarajuSCC {
    private boolean[] marked;
    private int[] id;  //id数组用来存储每个顶点所在的强连通分量
    private int count;  //计算图中强连通分量个数

    public KosarajuSCC(Digraph G){
        marked=new boolean[G.V()];
        id=new int[G.V()];
        //利用深度优先搜索获取逆倒序排列
        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];}
    //返回当前节点id
    public int id(int v){return id[v];}
    public int count(){return count;}


    public static void main(String[] args) {
        Digraph G=new Digraph(new In(args[0]));
        //进行深度优先搜索处理强连通分量
        KosarajuSCC cc=new KosarajuSCC(G);
        int M=cc.count();
        StdOut.println(M+" components");
        //创建一个背包,存储所有强连通分量
        Bag<Integer>[] components;
        components=(Bag<Integer>[])new Bag[M];
        for(int i=0;i<M;i++){
            components[i]=new Bag<Integer>();
        }
        //将每个顶点,按照所在强连通分量加到背包中
        for(int v=0;v<G.V();v++){
            components[cc.id(v)].add(v);
        }
        for(int i=0;i<M;i++){
            for(int v:components[i]){
                StdOut.print(v+" ");
            }
            StdOut.println();
        }
    }
}

算法流程很简单,但要真正理解需要仔细思考

首先运用深度优先搜索,遍历有向图,利用逆倒序排列将所有顶点储存在栈中

然后按照出栈顺序再次利用深度优先搜索遍历整个有向图

在同一次递归调用中的顶点,都是同一个强连通分量下的顶点

算法证明:
证明1:每个和s强连通的顶点v都会在构造函数调用的dfs(G,s)中被访问到

利用反证法,假设存在v和s强连通且在s调用dfs方法时未被访问到,因为存在s到v的路径,所以说明v已经被标记了。但在标记v时,必然调用dfs(G,v),因为s与v强连通,所以存在v到s的路径,所以s之前必然已经被标记过了,那就不会再被访问了,与论题相悖,证明1成立

证明2:构造函数调用dfs(G,s)所到达的每一个顶点v与s必然都是强连通的

v是dfs(G,s)到达的某一个顶点,证明G中必然含有s到v的路径,说明G的反向图中必然含有v到s的路径,所以此时要证明s与v具有强连通性,只要证明G中含有v到s的路径,即G的反向图中含有s到v的路径即可。根据第一条证明可知,先出现的是s顶点,所以v顶点一定先于s顶点结束调用,进入栈中。所以在反向图中有两种调用情况

  1. dfs(G,v)在dfs(G,s)调用之前开始,并在其之前结束
  2. dfs(G,v)在dfs(G,s)调用开始之后开始,并在dfs(G,s)调用结束之前结束

两种情况如下图,易知第一种情况不可能实现,第二种情况证明了我们想得到的结论

[Java]-有向图(拓扑排序,kosaraju算法)_第3张图片

算法分析:Kosaraju算法的预处理所需时间与V+E成正比且支持常数时间的有向图强连通性的查询

算法运行过程中处理有向图和与之对应的反向图,一共进行两次深度优先搜索,三步所需时间都和V+E成正比,所需空间主要用于构造反向图,也与V+E成正比


你可能感兴趣的:(算法,java,队列,数据结构,数据库)