《算法》-图[有向图]

有向图

简单的来说有向图就是连接带方向的图。有向图的例子在现实生活中也很多,比如在一段时间内银行间的现金流动,或者在某些地方的一些道路是单向的啊,那么这些现金流以及单向的道路就要用带方向的边来描述,这时有向图就有了用武之地。一个有向图的例子如下:
[图片上传失败...(image-cfad18-1599849542047)]

有向图该怎么实现呢?,对于无向图我们使用邻接表来实现,这种表达方式非常高效,并且略加思考就可以发现,邻接表完全可以直接套用在有向图上。假设要添加一条v->w的边,只要每次调用add()函数的时候只在adj[v]所指向的Bag添加w即可,这么看来无向图可以看作是有向图的特殊情况,即凡是有边相连的两个顶点一定有两个互相来回的边,所以对于无向图,在添加一条边的时候需要在两个顶点都添加一条边。下面给出有向图的实现:

public class Digraph {
    private final int V; // number of vertex
    private  int E;      // number of edge
    private Bag<Integer>[] adj;  //adjacent list

    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 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 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;
    }


}

可以看出有向图Digraph的实现中多了一个reverse()函数,这是将所有边全部反向,这个函数在后面会有作用。

可达性

对于有向图我们依然有搜索的需要,并且我们依然可以使用深度优先以及广度优先搜索,首先可以解决的问题是可达性,从一个或一组顶点出发能否到达一点,使用深度优先或者广度优先都可以实现,然后还能找到一个路径,同时利用广度优先还能找出最短路径,这些问题的解决方式和无向图基本没有差别,只是将Graph替换为Digraph就行,所以不再详述。

DAG

DAG(Directed Acyclic Graph)即为有向无环图,就是没有有向的环路的有向图,这在现实生活中也很常见,比如一些具有优先级的任务流程。对DAG有一些处理算法,不过在那之前,先来看看怎么判断一副有向图是否为DAG。

其实很简单,首先只要有一个有向环,这个图就不是DAG,所以只要检测出一个有向环,就可以返回了,不用再找下去了。第二这里我们使用深度优先搜索,深度优先沿着一条路径一直找下的特性刚好符合这里的要求,广度优先搜索是没有用的,那么我们什么时候判断存在一个有向环呢就是我们在走的时候碰到了还没有返回的顶点,用一个布尔数组onStack为每一个顶点保存一个是否还处于堆栈上没有返回的标志,这样,一旦在深度优先搜索的时候碰到了一个还在栈上的顶点,那么就能够确认这个有向图含有有向环,即不是DAG。代码如下:

public class DirectedCycle {
    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle;
    private boolean[] onStack;

    public DirectedCycle(Digraph G){
        onStack = new boolean[G.V()];
        edgeTo = new int[G.V()];
        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 v){
        onStack[v] =true;
        marked[v] = true;
        for(int w:G.adj(v)){
            if (this.hasCycle()) return;
            else if (!marked[w] ){
                edgeTo[w] = v;
                dfs(G, w);
            }
            else if(onStack[w]){
                cycle = new Stack<Integer>();
                for(int x = v;x!=v;x=edgeTo[x])
                    cycle.push(x);

                cycle.push(w);
                cycle.push(v);
            }

        }
        onStack[v] = false;//这里要注意没有这句话,在沿着路经返回的时候会不断往cycle栈里面压入数据
    }

    public boolean hasCycle(){
        return cycle != null;
    }

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

}

代码最后能够返回一个路径。

下面先介绍一个概念拓扑排序:给定一副有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素,拓扑排序有着很多应用,比如任务调度,课程安排,类之间的继承等等。
排成之后的效果如下图:
[图片上传失败...(image-eb696f-1599849542047)]
当且仅当一幅有向图是无环图时它才能进行拓扑排序,这是显然的,存在环的话无论怎么排都不可能让图中的左右边都朝着同一方向。我们应该利用上面的方法判断是否有环,然后再进行全面的搜索,但是我们可以以一定的顺序将遍历完成后的结果保存在一个数据结构中,下面是三种典型的排序:

前序:在递归调用前将顶点加入队列
后序:在递归调用后将顶点加入队列
逆后序:在归调用后将顶点压入栈
这实现起来也很简单,只需要在深度优先搜索的代码中加入相应的对数据结构的处理。

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 v){
        pre.enqueue(v);

        for(int w:G.adj(v))
            if (!marked[w]) dfs(G,  w);

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

    }

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

}

代码中,pre(), post(), reversePost() 分别能够返回顶点的前序,后序以及逆后序。

现在我们已经很容易就能够实现拓扑排序了,因为有一个定理:一幅有向无环图的拓扑排序即为所有顶点的逆后序排序。

证明:假设我们访问到了任意一个顶点v,他有一个相邻顶点w,在dfs(v)函数内部一定会访问到w,此时只会有两种情况:

  1. w已经被访问过并且返回了,这是完全有可能的。
  2. w还未被访问,这时就会递归的调用dfs(w),还有一种情况,w已经被访问过但是没有返回,这个参照上面判断DAG的代码里面已经讨论过,这种情况正好证明存在有向环,而我们在拓扑排序时首先要保证的就是图要是DAG,所以这种情况不会出现。

然后对于逆后序我们还使用了栈来存储数据,这能够保证后压入的数据至少不会是在之前压入数据之前,并且最后栈最后返回数据的时候可以看做是链接全部向右,对应于现实中比如,返回的就是需要先安排的任务,这就很方便。对于前序它是不能实现拓扑排序的,因为它就单纯的是记录的是深度优先搜索的路径,而后序的顺序反了,队列弹出的数据是优先级较高的,就好比是给出了最后需要完成的任务,所以我们才用栈将这个顺序反过来,这也就是为什么叫“逆后序”。

下面是拓扑排序的代码:

public class Topological {
    private Iterable<Integer> order;

    public Topological(Digraph G){
        DirectedCycle cyclefinderCycle = new DirectedCycle(G);
        if (!cyclefinderCycle.hasCycle()){//只有在是DAG的时候才继续排序
            DepthFirstOrder dfs = new DepthFirstOrder(G);
            order = dfs.reversePost();//得到逆后序
        }
    }

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

}

Kosaraju算法

Kuosaraju算法是用来找出强连通分量的,前面的一些方法在这里也有一些作用,不过在那之前先介绍清楚几个概念。
强连通:对于两个顶点v和w,分别存在一条从v到w和从w到v的有向路径,那么称这两个顶点是强连通的。
强连通分量:是有向图G的一个子集,它的所有顶点都互相是强连通的。

Kosaraju算法可以给出有向图中有几个强连通分量以及两个顶点是否在一个强连通分量里面。

算法的步骤主要是:对有向图G的反向图进行拓扑排序,然后根据这个顺序使用深度优先处理有向图G,然后没一次递归所经过的顶点都在一个强连通分量里面

下面给出一个比较直观的证明:首先对于每一个强连通分量为了直观起见,我们将其视作一个点,那么我们的目标就是找出这个收缩过之后的图有几个顶点,联系之前的无向图的连通分量的算法,我们希望每个节点返回一次。这该怎么达到呢?这时拓扑排序就派上用场了,我们先对图求反,再对其做拓扑排序,这时候得到的结果,我们不考虑强连通分量中的点,在强连通分量之间就是完全的拓扑排序的反序,按照这个顺序在G中进行深度优先搜索,我们就会每次搜索完强连通分量内的点,然后无法到达其他连通分量,就只能返回,达到了我们前面说的对每个强连通分量缩成的节点每次都返回一次。


在这里插入图片描述

上图是《算法4》中的一个例子,右边的图中,每一个方框内都是一个强连通分量,然后可以发现,按照反向图的逆后序进行深度优先搜索的话,强联通分量之间的边都是逆着的,这就完成了上面所说的每个收缩后的节点返回一次的目的。

最后说一点可能不对的想法,在思考证明方法的过程中我想到一种情况,我们第一次搜索反向图是为了得到缩点之后的连通分量之间的边是逆着的图,但是好像单纯对原图搜索,取其后序好像也行,但是实验之后发现结果不对,但是百思不得其解,后来找了个小例子发现了一个可能性,关键在于强连通分量之间是可达的,这个道理好像是显而易见的,但是应该就是这种方法行不通的原因,举个极端例子,在某个顶点出发是完全有可能一次就遍历整个图,并且第一个完成遍历的顶点就和它相连,那么当我们按照后序第二次遍历的时候就会一下子遍历整个图,那么最后我们就会只得到一个连通分量,这显然是不对的。

下面是代码的实现:

public class KosarajuSCC {
    private boolean[] marked;
    private int[] id;
    private int count;

    public KosarajuSCC(MyDigraph G){
        marked = new boolean[G.V()];
        id = new int[G.V()];
//      DepthFirstOrder order=new DepthFirstOrder(G.reverse());
        MyDepthFirstOrder order=new MyDepthFirstOrder(G);
        for(int s:order.post())
            if(!marked[s]){
                dfs(G,s);
                count++;
            }
    }

    private void dfs(MyDigraph 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;
    }
}

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342