算法_有向图

一.定义以及和无向图的区别

  一幅有向图是由一组顶点和一组有方向的边组成的,每条有方向的边都连接着有序的一对顶点.有向边是由第一个顶点指出并指向第二个顶点,用v->w来表示有向图中一条由顶点v指向顶点w的一条边.当存在从v->w的有向路径的时候,称顶点w能够由顶点v达到.和无向图不同的是,在有向图中由v能够到达w,并不意味着由w也能到达v.下图为一个有向图举例.

                      

二.有向图的数据类型

  使用Bag表示有向图,其中边v->w表示为顶点v所对应的邻接链表中包含一个w顶点,与无向图不同的是,这里每条边只会出现一次.有向图的数据结构类型如下:

/**
 * 有向图的数据类型
 * @author Administrator
 *
 */
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,由于是有向图只要添加一条边就可以了
    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;
    }
}

三.有向图中的可达性

  利用深度优先搜索可以解决有向图中的单点可达性问题:即:给定一幅有向图和一个起点s,回答是否存在一条从s到达给定顶点v的有向路径的问题.

/**
 * 从指定的图中查找从s可达的所有顶点.
 * 判断一个点是否是从s可达的.
 * @author Administrator
 *
 */
public class DirectedDFS {
    private boolean[] marked;
    //从G中找出所有s可达的点
    public DirectedDFS(Digraph G,int s) {
        marked=new boolean[G.V()];
        dfs(G,s);
    }
    //G中找出一系列点可达的点
    public DirectedDFS(Digraph G,Iterable<Integer> sources) {
        marked=new boolean[G.V()];
        for(int s:sources) {
            if(!marked[s]) dfs(G,s);
        }
    }
    //深度优先搜素判断.
    private void dfs(Digraph G, int v) {
        marked[v]=true;
        for(int w:G.adj(v)) {
            if(!marked[w]) dfs(G,w);
        }
    }
    //v是可达的吗
    public boolean marked(int v) {
        return marked[v];
    }
}

四.寻找有向环

  下面的代码可以用来检测给定的有向图中是否含有有向环,如果有,则按照路径的方向返回环上的所有顶点.在执行dfs的时候,查找的是从起点到v的有向路径,onStack数组标记了递归调用的栈上的所有顶点,同时也加入了edgeTo数组,在找到有向环的时候返回环中的所有顶点.

/**
 * 有向图G是否含有有向环
 * 获取有向环中的所有顶点
 * @author Administrator
 *
 */
public class DirectedCycle {
    private boolean[] marked;
    private int[] edgeTo;
    private Stack<Integer> cycle;    //有向环中的所有顶点
    private boolean[] onStack;        //递归调用的栈上的所有顶点

    public DirectedCycle(Digraph G) {
        edgeTo=new int[G.V()];
        onStack=new boolean[G.V()];
        marked=new boolean[G.V()];
        for(int v=0;v<G.V();v++) {
            if(!marked[v]) dfs(G,v);
        }
    }
/**
 * 该算法的关键步骤在于onStack数组的运用.
 * onStack数组标记的是当前遍历的点.如果对于一个点指向的所有点中的某个点
 * onstack[v]=true.代表该点正在被遍历也就是说
 * 该点存在一条路径,指向这个点.而这个点现在又可以指向该点,
 * 即存在环的结构~
 * @param G
 * @param 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!=w;x=edgeTo[x])
                    cycle.push(x);
                cycle.push(w);
                cycle.push(v);
            }
        }
        //dfs方法结束,对于该点的递归调用结束.该点指向的所有点已经遍历完毕
        onStack[v]=false;
    }

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

五.顶点的深度优先次序以及拓补排序

  拓补排序:给定一幅有向图,将所有的顶点排序,使得所有的有向边均从排在前面的元素指向排在后面的元素.如果存在有向环的话,那么拓补排序无法完成.

  要实现有向图的拓补排序,利用标准深度优先搜索顺序即可完成任务.这里顶点会有三种排列顺序:

  1.前序:在递归调用前将顶点加入队列

  2.后序:在递归调用之后将顶点加入队列

  3.逆后序:在递归调用之后将顶点压入栈.

  具体的操作见下面的代码:

//有向图中基于深度优先搜索的拓补排序
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<>();
        post=new Queue<>();
        reversePost=new Stack<>();
        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);
        marked[v]=true;
        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;
    }
}

  拓补排序的实现依赖于上面的API,实际上拓补排序即为所有顶点的逆后序排列,证明如下:

  对于任意边v->w,在调用dfs(v)的时候,下面三种情况必然有一种成立:

  1.dfs(w)被调用且返回.(此时w被标记)

  2.dfs(w)还没有被调用,因此v->w会返回dfs(w),且dfs(w)在dfs(v)之前返回

  3.dfs(w)已经被调用没有返回,在有向无环图中这种情况不可能!

  在1,2这两种情况中dfs(w)都在dfs(v)之前返回,也就是说在逆后序排列中,顺序为v,w.满足拓补排序的要求!

  拓补排序的代码如下:

public class Topological {
    private Iterable<Integer> order;    //顶点的拓补排序
    public Topological(Digraph G) {
        DirectedCycle cyclefinder=new DirectedCycle(G);
        if(!cyclefinder.hasCycle()) {//只有无环才能进行拓补排序
            DepthFirstOrder dfs=new DepthFirstOrder(G);
            order=dfs.reversePost();
        }
    }
    public Iterable<Integer> order() {
        return order;
    }
    public boolean isDAG() {
        return order!=null;
    }
}

六.有向图的强连通性

  定义:如果两个顶点v和w是互相可达的,则称它们为强连通的.也就是说既存在一条从v到w的有向路径也存在一条从w到v的有向路径.如果一幅有向图中的任意两个顶点都是强连通的,则称这副有向图也是强连通的.任意顶点和自己都是强连通的.

    强连通性将顶点分为了一些等价类,每个等价类都是由相互均为强连通的顶点的最大子集组成的.我们将这些子集称为强连通分量.需要注意的是强连通分量是基于顶点的,而并非基于边.

  下面的代码采用如下步骤来计算强连通分量以及两个点是否是强连通的:

  1.在给定的有向图中,使用DepthFirsetOrder来计算它的反向图GR的逆后序排列

  2.按照第一步计算得到的顺序采用深度优先搜索来访问所有未被标记的点

  3.在构造函数中,所有在同一个递归dfs()调用中被访问到的顶点都是在同一个强连通分量中.

  下面的代码实现遵循了上面的思路:

/**
 * 该算法实现的关键:
 * 使用深度优先搜索查找给定有向图的反向图GR.根据由此得到的所有顶点的逆后序
 * 再次用深度优先搜索处理有向图G.其构造函数的每一次递归调用所标记的顶点都在
 * 同一个强连通分量中.
 * 解决问题:
 * 判断两个点是否是强连通的
 * 判断总共有多少个连通分量
 * @author Administrator
 *
 */
public class KosarajuSCC {
    private boolean[] marked;//已经访问过的顶点
    private int[] 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];
    }
    public int id(int v) {
        return id[v];
    }
    public int count() {
        return count;
    }
}

  为了验证这个代码的正确性,需要证明这个命题,即:

  使用深度优先搜索查找给定有向图的GR,根据由此得到的所有顶点的逆后序再次用深度优先搜索处理有向图G,其构造函数的每一次递归调用所标记的顶点都在同一个强连通分量中.

  证明如下:

  首先用反证法证明每个和s强连通的顶点v都会在构造函数中调用的dfs(G,s)中被访问到.假设有一个顶点v没有被访问到.因为存在从s到v的路径,那么说明v肯定在之前被访问过了.但是因为也存在v到s的路径,那么dfs(G,v)调用中,s肯定会被标记,因此构造函数肯定不会调用dfs(G,s)的,矛盾.

  其次要证明构造函数调用的dfs(G,s)所到达的任意顶点v都必然是和s强连通的.设v为dfs(G,s)所到达的某个顶点.那么G中必然存在从s到v的路径.因此现在只需要证明在GR中存在一条从s到v的路径即可.而由于按照逆后序进行的深度优先搜索,因此在GR中进行的深度优先搜索意味着,dfs(G,v)必然在dfs(G,s)之前就结束了.这样dfs(G,v)的调用存在两种情况:

  1.调用在dfs(G,s)的调用之前(并且也在dfs(G,s)的调用之前结束.这种情况不可能存在,因为在GR中存在一条从v到s的路径.

  2.调用在dfs(G,s)的调用之后(并且也在dfs(G,s)的结束之前结束.这种情况说明GR中存在一条从s到v的路径.证明完成.

 

时间: 2024-10-29 11:14:16

算法_有向图的相关文章

STL_算法(21)_ STL_算法_填充新值

STL_算法_填充新值 fill(b, e, v) fill(b, n, v) generate(b, n, p) generate_n(b, n, p) #include<iostream> #include<algorithm> #include<vector> #include<list> // #include<string> using namespace std; int main() { list<string> sli

Kosaraju 算法检测有向图的强连通性

给定一个有向图 G = (V, E) ,对于任意一对顶点 u 和 v,有 u --> v 和 v --> u,亦即,顶点 u 和 v 是互相可达的,则说明该图 G 是强连通的(Strongly Connected).如下图中,任意两个顶点都是互相可达的. 对于无向图,判断图是否是强连通的,可以直接使用深度优先搜索(DFS)或广度优先搜索(BFS),从任意一个顶点出发,如果遍历的结果包含所有的顶点,则说明图是强连通的. 而对于有向图,则不能使用 DFS 或 BFS 进行直接遍历来判断.如下图中,

HDU 1162 Eddy&#39;s picture (prime算法_裸题)

Problem Description Eddy begins to like painting pictures recently ,he is sure of himself to become a painter.Every day Eddy draws pictures in his small room, and he usually puts out his newest pictures to let his friends appreciate. but the result i

监督学习算法_k-近邻(kNN)分类算法_源代码

因为自己想学着去写机器学习的源码,所以我最近在学习<机器学习实战>这本书. <机器学习实战>是利用Python2完成的机器学习算法的源代码,并利用机器学习方法来对实际问题进行分析与处理. (<机器学习实战>豆瓣读书网址:https://book.douban.com/subject/24703171/) 以下内容是我通过学习<机器学习实战>,以及我对k-近邻(kNN)分类算法的理解,所总结整理出的内容,其中kNN分类算法的源码为Python3的代码,希望大家

Tarjan算法求有向图强连通分量并缩点

// Tarjan算法求有向图强连通分量并缩点 #include<iostream> #include<cstdio> #include<cstring> #include<algorithm> #include<vector> #include<queue> using namespace std; const int N = 100010, M = 1000010; // int ver[M], Next[M], head[N],

算法_最短路径

一.概述  定义:在一幅加权有向图中,从顶点s到顶点t的最短路径是所有从s到t的路径中的权重的最小者.从定义可以看出单点最短路径的实现是建立在加权有向图的基础上的. 最短路径树:给定一幅加权有向图和一个顶点s,以s为起点的一颗最短路径树是图的一幅子图,它包含s和从s可达的所有顶点.这颗有向树的根节点是s,树的每条路径都是有向图中的一条最短路径.它包含了顶点s到所有可达的顶点的最短路径. 二.加权有向图和加权有向边的数据结构 加权有向图和加权有向边的数据结构和加权无向图无向边的数据结构类型基本相同

迪杰斯特拉算法_优化版

迪杰斯特拉优化版本:vector + 优先队列 △迪杰斯特拉算法的核心:每次找距离s点最短的元素 + 松弛操作 ①要用优先队列取出最短距离降低时间复杂度,用veotor减少空间 ②定义一个pair类型,作为优先队列的元素.typedef pair<int , int > P ,first是距离,second是边的终点 ③pair类型作为优先队列的元素时,默认排序是先排序first,后再排序second.(升序) 因此,距离是first,边的终点是second,不可调换 #include <

贪心算法_活动安排问题_哈弗曼编码

问题表述:设有n个活动的集合E = {1,2,…,n},其中每个活动都要求使用同一资源,如演讲会场等,而在同一时间内只有一个活动能使用这一资源.每个活i都有一个要求使用该资源的起始时间si和一个结束时间fi,且si < fi .如果选择了活动i,则它在半开时间区间[si, fi)内占用资源.若区间[si, fi)与区间[sj, fj)不相交,则称活动i与活动j是相容的.也就是说,当si >= fj或sj >= fi时,活动i与活动j相容. 由于输入的活动以其完成时间的非减序排列,所以算法

模拟算法_掷骰子游戏&amp;&amp;猜数游戏

模拟算法是用随机函数来模拟自然界中发生的不可预测的情况,C语言中是用srand()和rand()函数来生成随机数. 先来介绍一下随机数的生成: 1.产生不定范围的随机数 函数原型:int rand() 产生一个介于0~RAD_MAX间的整数,其具体值与系统有关系.Linux下为2147483647.我们可以在include文件夹中的stdlib.h中可以看到(Linux在usr目录下,Windows在安装目录下) 1 #include<stdio.h> 2 #include<stdlib