表示无向图的数据类型
邻接表:将每个顶点的所有相邻顶点都保存在该顶点对应的元素所指向的一张链表中。
深度优先搜索
搜索类API:
要搜索一幅图,只需用一个递归方法来遍历所有的顶点。在访问其中一个顶点时:
- 将它标记为已访问;
- 递归地访问它的所有没有被标记过的邻居顶点。
这种方法称为深度优先搜索(DFS)。它使用一个boolean数组来记录和起点想通的所有顶点。递归方法会标记给定的顶点并调用自己来访问该顶点的相邻顶点列表中所有没有被标记过的点。如果图是连通的,每个邻接链表中的元素都会被检查到。
注意,在无向图的深度优先搜索中,每条边都会被访问两次,且在第二次时总会发现这个顶点已经被标注过。
package Graph;
public class DepthFirstSearch {
private int count;
private boolean[] marked;
DepthFirstGraph(Graph G, int s){
marked = new boolean[G.V()];
dfs(G, s);
}
private 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 v){
return marked[v];
}
public int count(){
return count;
}
}
寻找路径
路径的API:
构造函数接受一个起点s作为参数,计算s到与s连通的每个顶点之间的路径。
实现
在DepthFirstSearch中添加一个实例变量edgeTo[]整型数组来记录每个顶点到起点的路径。在由v-w第一次访问任意w时,将edgeTo[w]设为v来记住这条路径。换句话说,v-w是从s到w的路径上的最后一条已知的边。这样,搜索的结果是一棵以起点为根结点的树,edgeTo[]是一棵由父链接表示的树。
package Graph;
import java.util.LinkedList;
import java.util.Queue;
public class DepthFirstPaths {
private int s;
private boolean[] marked;
private int[] edgeTo;
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){
Queue<Integer> queue = new LinkedList<>();
queue.add(s); //别忘了加上起点
for(int x = edgeTo[v]; x != edgeTo[x]; x = edgeTo[x]){
queue.add(x);
}
return queue;
}
}
广度优先搜索
对于单点最短路径,解决的经典方法叫做广度优先搜索。深度优先搜索在这个问题上没有什么作为,因为它遍历整个图的顺序和找出最短路径的目标没有任何关系。
使用一个队列来保存所有已经被标记过但其邻接表还未被检查过的顶点。先将起点加入队列,然后重复一下步骤直到队列为空:
- 取队列中的下一个顶点v并标记它。
- 将与v相邻的所有未被标记过的顶点加入队列。
和深度优先搜索一样,它的结果也是一个数组edgeTo[],也是一棵用父链接表示的根结点为s的树。
DFS和BFS的对比
在搜索中我们都会先将起点存入数据结构中,然后重复以下步骤直到数据结构被清空:
- 取其中的下一个顶点并标记它;
- 将v的所有相邻而又未被标记的顶点加入数据结构。
这两个算法的不同之处仅在于从数据结构找那个获取下一个顶点的规则(对于广度优先搜索来说是最早加入的顶点,对于深度优先搜索来说是最晚加入的顶点)。
连通分量
实现
CC的实现使用了marked[]数组来寻找一个顶点作为每个连通分量重深度优先搜索的起点。递归的深度优先搜索第一次调用的参数是顶点0——它会标记所有与0连通的顶点。然后构造函数中的for循环会查找每个没有被标记的顶点并递归调用dfs()来标记和它相邻的所有顶点。另外,它还使用了一个以顶点作为索引的数组id[],将同一个连通分量中的顶点和连通分量的标识符连接起来。
public class CC {
private boolean marked[];
private int id[];
private int count;
CC(Graph G){
marked = new boolean[G.V()];
id = new int[G.V()];
for(int s = 0, n = G.V(); s < n; s++){
if(!marked[s]){
dfs(G, s);
count++;
}
}
}
private void dfs(Graph G, int v){
id[v] = count;
marked[v] = true;
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 count(){
return count;
}
public int id(int v){
return id[v];
}
}
与union-find算法的对比
理论上,深度优先搜索比union-find算法快,因为它能保证所需的时间是常数而union-find算法不行;但在实际应用中,这点差异微不足道。union-find算法其实更快,因为它不需要完整地构造并表示一幅图。更重要的是,union-find算法是一种动态算法,但深度优先搜索则必须要对图进行预处理。
应用
G是无环图吗?(假设不存在自环或平行边)
对于每一个未被标记的顶点,调用dfs方法进行深度优先搜索,搜索时记录上一层遍历的顶点u,对当前顶点的邻接表中的每一个顶点w做如下操作:
- 如果w未被遍历,则调用dfs方法遍历该点。
- 如果w已被遍历且不是上一层遍历的顶点u,则说明图中存在环。
public class Cycle {
private boolean[] marked;
private boolean hasCycle = false;
Cycle(Graph G){
int N = G.V();
marked = new boolean[N];
for(int s = 0; s < N; 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;
return;
}
}
}
public boolean hasCycle(){
return hasCycle;
}
}
G是二分图吗(双色问题)
在深度优先搜索的基础上加上一个布尔型的color数组,用以存储各个点的颜色。
对于每一个未被标记的顶点,调用dfs方法进行深度优先搜索,对当前顶点v的邻接表中的每一个顶点w做如下操作:
- 如果w未被遍历,则将w的颜色置为与color[v]相反。
- 如果该点已被遍历且color[w]等于color[v],则说明该图不是二分图。
public class TwoColor {
private boolean[] marked;
private boolean[] color;
private boolean isTowColorable = true;
TwoColor(Graph G){
int N = G.V();
marked = new boolean[N];
color = new boolean[N];
for(int s = 0; s < N; 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]){
color[w] = !color[v];
dfs(G, w, v);
}else if(color[w] == color[v]){
isTowColorable = false;
return;
}
}
}
public boolean isBipartite(){
return isTowColorable;
}
}