有向图与无向图判断有环

最近开始认真学习算法,用的是Sedgewick的《Algorithms》.很多内容都与数据结构相同,不同的是对算法的内容更多的讲解.我会经常记录自己学习算法时遇到的困难和如何解决困难.
在学习拓扑排序的时候遇到了判断存在环的问题.而判断环问题又分为有向图与无向图,我会分别对无向图和有向图判断环问题进行阐述,然后比较他们之间的不同.


首先介绍一下无向图,无向图的边没有方向,或者说每一条无向图的边都是双向的,即u-v等价于u->v & v->v.这在后面讲解具体算法时很有帮助.

解决无向图的环问题最明显的方法就是使用深度优先搜索(Depth Frist Search)进行处理.
首先了解一下深度优先搜索,深度优先搜索是一种遍历图的算法.

深度优先遍历图的方法是,从图中某顶点v出发: (1)访问顶点v;
(2)依次从v的未被访问的邻接点出发,对图进行深度优先遍历;直至图中和v有路径相通的顶点都被访问;
(3)若此时图中尚有顶点未被访问,则从一个未被访问的顶点出发,重新进行深度优先遍历,直到图中所有顶点均被访问过为止。

从这个算法的描述上就可以看到,深度优先搜索类似于树的先序遍历,而事实上二叉树就是图的一类罢了.
下面给出深度优先搜索的算法

public class SearchUseDFS {
    Graph g;
    int s;
    boolean[] marked;
    int count;  //连通个数
    public SearchUseDFS(Graph g, int s) {
        this.g = g;
        marked = new boolean[g.getV()];
        this.s = s;
        dfs(s);
    }

    public void dfs(int v) {
        marked[v] = true;
        count++;
        for(int w:g.adj(v)) {
            if(!marked[w])
                dfs(w);
        }
    } 

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

    public int count() {
        return count;
    }
}

上面的代码是通过深度优先搜索解决连通性问题,这个内容很好理解,一次深度优先搜索只能找出与给定点连通的点.
而这与我们要讨论的无向图有环问题有什么关系呢.事实上我们可以利用dfs判断有环.
我们知道,dfs在进行图的遍历时会在逻辑上构建一棵树,很多人叫他作深度优先搜索树.
在搜索树中存在4种边树边,前向边,后向边,横叉边.
《算法导论》334页有这4种边的准确定义,在此不累述.

DFS过程中,对于一条边u->v
vis[v] = 0,说明v还没被访问,v是首次被发现,u->v是一条树边
vis[v] => 1,说明v已经被访问,但其子孙后代还没有被访问完(正在访问中),而u又指向v说明u就是v的子孙后代,u->v是一条后向边,因此后向边又称返祖边
vis[v] = 2,说明v已经被访问,其子孙后代也已经全部访问完,u->v这条边可能是一条横叉边,或者前向边.

而这里所指的是有向图中的边,在无向图中其实只存在两种边:树边与后向边.可以证明,不存在前向边与横叉边.原因很简单,假设存在一条横叉边或者前向边,那么一定满足u->v且vis[v]=2,而此时u与v之间的边为双向边(这里就是前文说的无向边可以理解为双向边),且v之间已经被访问,那么根据dfs的定义,v一定会通过u与v的边访问u,所以证明了无向图中一定只有树边和后向边.

树边的一边是正在访问的节点,另一边是未访问的节点,所以树边是正常的边;而后向边的一边是正在访问的节点,另一边也是正在访问的节点,例如 a-b-c-a,则c-a这条边中c正在访问,a也正在访问(递归函数正在进行),这就说明后向边指向的是它的父节点,而这种情况下一定是存在环.这就是dfs判断环的思想.
直接上代码

        // DFS,发现回路(返回true)则不可序列化,返回false
        for (int i = 1; i <= n; i++) {
            if (dfsCheckCircuit(i))
                return false;
        }

    // 如果发现回路则返回true,否则遍历结束返回false
    private boolean dfsCheckCircuit(int current) {
        if (marked[current]) {
            return true;
        }
        marked[current] = true;
        for (int i = 1; i <= n; i++)
            if (digraph[current][i]) {
                if (dfsCheckCircuit(i)) {
                    return true;
                }
            }
        marked[current] = false;
        return false;
    }

后面会看到有向图的判断环算法其实仅仅多了几行代码.这里有一点需要注意的是marked[] == true的时候一定存在环.因为前文讲到树边不会碰到marked[] == true的点,而无向图中之后树边和后向边,所以一旦碰到被标记的点可以立即判断有环.


有向图与无向图类似,最大的区别在于有向图中的边是单向的,这也导致了dfs树中就存在了前向边和横叉边,不难想到遇到这两种边在marked[]上的情况与后向边是一样的(这里如果不懂的话参见前文关于4种边的解释),所以这就导致了我们仅仅通过对marked数组进行判断无法保证得到正确的结果,因为这两种边不会得到环.
而细心观察会发现,后向边指向的点其实都是dfs树中的父节点,而dfs其实是一个利用栈进行操作的算法,在dfs递归调用的栈中保存的点正好就是该节点的所有祖先节点,因此我们可以利用这一点,实现判断有向图的环路问题.

有向图判断有环通过dfs遍历到任意节点时,如果该节点有边指向祖先节点,则存在环,且目前找到的环即从指向的祖先节电到该节点的路径+祖先节点,而我们需要的路径正好保存在dfs递归栈中,因此我们不仅找到了一个判断是否存在环的算法,也找到了找出这个环经过的点的算法!

import java.util.Stack;

public class DirectedCycle {
    private boolean[] marked;
    private boolean[] inStack;
    private int paths[];
    private Digraph dg;
    private int v;
    private boolean hasCycle;
    private Stack<Integer>[] s;
    private int pos;
    public DirectedCycle(Digraph dg, int v) {
        marked = new boolean[dg.getV()];
        inStack = new boolean[dg.getV()];
        paths = new int[dg.getV()];
        this.dg = dg;
        this.v = v;
        s = (Stack<Integer>[])new Stack[dg.getV()];
        dfs(v);
    }

    public void dfs(int v) {
        marked[v] = inStack[v] = true;

        for(int w:dg.adj(v)) {
            if(!marked[w]) {
                paths[w] = v;
                dfs(w); 
            } else if(inStack[w]) {
                hasCycle = true;

                s[pos] = new Stack<Integer>();
                for(int x = v;x!=w;x=paths[x]) {
                    s[pos].push(x);
                }
                s[pos].push(w);
                s[pos++].push(v);
                hasCycle = true;
            }
        }
        inStack[v] = false;

    }

    public static void main(String[] args) {
        Digraph dg = new Digraph(5);
        dg.addEdge(0, 1);
        dg.addEdge(1, 2);
        dg.addEdge(2, 0);
        dg.addEdge(3, 4);
        dg.addEdge(2, 3);
        dg.addEdge(4, 1);
        DirectedCycle dc = new DirectedCycle(dg, 0);
        for(int i = 0;i<dc.pos;i++) {
            for(int w : dc.s[i])
                System.out.print(w + ":");
            System.out.println();
        }
        System.out.println(dc.pos);
    }
}

而这两个算法其实都是对dfs进行了小改造,不难看出算法复杂度与传统dfs的复杂度相同,为O(V+E).

其实对于有向图与无向图判断有环的算法,有很大的共同点.都是使用 dfs生成的搜索树的后向边存在,则一定有环这一事实,而不同的就是在无向图中不存在前向边与横叉边,所以marked[v]==true等价于有环,而有向图中需要使用inStack数组帮助判断该边是否为后向边.因此不管有向图还是无向图,相同之处就是都是判断该边是否为后向边,只是需要的信息不同.

  • 25
    点赞
  • 21
    收藏
    觉得还不错? 一键收藏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值