DFS算法分析

深度优先算法与广度优先算法可谓是图论中的两个基础算法。本科时候被这两个算法折磨的也是很惨。今天来分析一下DFS的过程。

首先我们介绍一下图的表示方式。图是由一组顶点和一组能够将两个顶点相连的边组成的。

我们可以用邻接矩阵,边的数组和邻接表数组三种方式来表示图。这里我们使用邻接表的形式,使用一个以顶点为索引的列表数组,其中的每个元素都是和该顶点相邻的顶点列表。

邻接表的数据结构如下所示:

public class Graph {
	private final int V;	//顶点数目
	int E;
	private List<Integer>[] adj;	//邻接表

	public Graph(int V) {
		this.V=V;
		this.E=0;
		adj=new ArrayList[V];		//创建邻接表
		for(int v=0;v<V;v++)		//将所有链表初始化为空
			adj[v]=new ArrayList<>();
	}

	void addEdge(int v, int w) {
		adj[v].add(w);
		adj[w].add(v);
		E++;
	} // 向图中添加一条边v-w

	Iterable<Integer> adj(int v) {
		return adj[v];
	} // 和v相邻的所有顶点

	public static int degree(Graph G, int v) {
		int degree = 0;
		for (int w : G.adj(v))
			degree++;
		return degree;
	}
}

其中,主要的数据结构为List<Integer>[] adj邻接表数组,它是一个链表数组,每当插入一条边时,再相应的链表中加入对应的节点。

图的很多性质和路径有关,因此一种很自然的想法是沿着图的边从一个顶点移动到另一个顶点。

走迷宫:

思考图的搜索过程的一种有益的方法是,考虑另一个和它等价但历史悠久而又特别的问题——在一个由各种通道和路口组成的迷宫中找到出路。用迷宫代替图,通道代替边,路口代替顶点。一种古老的方法叫做Tremaux搜索:要探索迷宫中的所有通道,我们需要:

选择一条没有标记过的通道,在你走过的路上铺一条绳子;

标记所有你第一次路过的路口和通道;

当来到一个标记过的路口时(用绳子)回退到上个路口;

当回退到的路口已经没有可走的通道时继续回退。

绳子可以保证你总能找到一条出路,标记则能保证你不会两次经过同一条通道或者同一个路口。我们来看看图的搜索算法。

public class DepthFirstSearch {
	private boolean[] marked;
	private int count;

	public DepthFirstSearch(Graph G, int s) {
		marked = new boolean[G.V()];
		dfs(G, s);
	}

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

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

搜索连通图的经典递归算法(遍历所有的顶点的和边)和Tremaus搜索类似,但描述起来更简单。要搜索一幅图,只需用一个递归方法来遍历所有顶点。在访问其中一个顶点时:

将它标记为已访问;

递归的访问它的所有没有被标记过的邻居顶点。

这种方法称为深度优先搜索(DFS)。它使用一个boolean数组来记录和起点连通的所有顶点。递归方法会标记给定的顶点并调用自己来访问该顶点的相邻列表中所有没有被标记过的顶点。

代码方法的调用和返回机制对应迷宫中绳子的作用:当已经处理过依附于一个顶点的所有边时(搜索了路口连接的所有通道),我们只能“返回”(return)。在图中我们会经过每条边两次(在它的两个端点各一次)。

在无向图的深度优先搜索中,在碰到边v-w时,要么进行递归调用(w没有被标记过),要么跳过这条边(w已经被标记过)。第二次从另一个方向w-v遇到这条边时,总是会忽略它,因为它的另一端v肯定已经被访问过了。

dfs寻找路径

单点路径问题在图的处理领域十分重要。Paths类构造函数Paths(Graph G,int s)在G中找出所有起点为s的路径。

下面的算法基于dfs实现Path。在dfs中增加了一个实例变量edgeTo[]整型数组来起到Tremaux搜索中绳子的作用。这个数组可以找到从每个与s连通的顶点回到s的路径。它会记住每个顶点到起点的路径,而不是记录当前顶点到起点的路径。为了做到这一点,在由边v-w第一次访问任意w时,将edgeTo[w]设为v来记住这条路径。换句话说,v-w是从s到w的路径上最后一条已知的边。这样,搜索的结果是一颗以起点为根结点的树,edgeTo[]是一颗由父链接表示的树。

public class DepthFirstPaths {
	private boolean[] marked;	//这个顶点上调用过dfs()了吗?
	private int[] edgeTo;		//从起点刀一个顶点的已知路径上的最后一个顶点
	private final int s;		//起点
	public DepthFirstPaths(Graph G,int s) {
		marked=new boolean[G.V()];
		edgeTo=new int[G.V()];
		this.s=s;
		dfs(G,s);
	}

	private void dfs(Graph G,int v){
		marked[v]=true;
		for(int w:G.adj(v))
			if(!marked[w]){
				edgeTo[w]=v;
				dfs(G, w);
			}
	}
	
	public boolean hasPathTo(int v){
		return marked[v];
	}
	
	public Iterable<Integer> pathTo(int v){
		if(!hasPathTo(v)) return null;
		Stack<Integer> path=new Stack<>();
		for(int x=v;x!=s;x=edgeTo[x])
			path.push(x);
		path.push(s);
		return path;
	}
}

dfs另一个应用是找出一幅图的所有连通分量。它能够将所有顶点切分为等价类(连通分量)。

用例可以用id()方法将连通分量用数组保存,使用了一个Bag对象数组。

CC的实现使用了marked[]数组来寻找一个顶点作为每个连通分量中深度优先搜索的起点。递归的深度优先搜索第一次调用的参数世顶点0——它回标记所有与0连通的顶点。然后构造函数中的for循环回查找每个没有被标记的顶点并递归调用dfs()来标记和它相邻的所有顶点。

public class CC {
	private boolean[] marked;
	private int[] id;
	private int count;
	
	public CC(Graph G) {
		marked=new boolean[G.V()];
		id=new int[G.V()];
		for(int s=0;s<G.V();s++)
			if(!marked[s]){
				dfs(G,s);
				count++;
			}
	}
	
	private void dfs(Graph G,int v){
		marked[v]=true;
		id[v]=count;
		for(int w:G.adj(v))
			if(!marked[w])
				dfs(G,w);
	}
	
	public boolean connected(int v,int w){
		return id[v]==id[w];
	}
	
	public int id(int v){
		return id[v];
	}
	
	public int count(){
		return count;
	}
}

dfs应用:判断G是无环图么?

public class Cycle {
	private boolean[] marked;
	private boolean hasCycle;

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

	private void dfs(Graph G, int v, int u) {
		marked[v] = true;
		for (int w : G.adj(v))
			if (!marked[w])
				dfs(G, w, v);
			else if (w != u)
				hasCycle = true;
	}

	public boolean hasCycle() {
		return hasCycle;
	}

}

dfs应用:G是二分图么?

public class TwoColor {
	private boolean[] marked;
	private boolean[] color;
	private boolean isTwoColorable = true;

	public TwoColor(Graph G) {
		marked = new boolean[G.V()];
		color = new boolean[G.V()];
		for (int s = 0; s < G.V(); s++)
			if (!marked[s])
				dfs(G, s);
	}

	private void dfs(Graph G, int v) {
		marked[v] = true;
		for (int w : G.adj(v))
			if (!marked[w]) {
				color[w] = !color[v];
				dfs(G, w);
			} else if (color[w] == color[v])
				isTwoColorable = false;
	}

	public boolean isBipartite() {
		return isTwoColorable;
	}
}

点赞