最近开始认真学习算法,用的是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[] 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[])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();
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;ifor(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数组帮助判断该边是否为后向边.因此不管有向图还是无向图,相同之处就是都是判断该边是否为后向边,只是需要的信息不同.