算法——圖之無向圖

圖的概念

圖是算法中是樹的拓展,樹是從上向下的數據結構,結點都有一個父結點(根結點除外),從上向下排列。而圖沒有了父子結點的概念,圖中的結點都是平等關係,結果更加複雜。


圖的分類

圖可以分爲無向圖(簡單連接),有向圖(連接有方向),加權圖(連接帶權值),加權有向圖(連接既有方向又有權值)。


這篇討論無向圖。

無向圖的表示方法:

1.鄰接矩陣
2.邊的數組
3.鄰接表數組


1.鄰接矩陣

我們可以使用一個V*V的布爾矩陣graph來表示圖。當頂點v和頂點w之間有邊相連時,則graph[v][w]和graph[w][v]爲true,否則爲false。

但是這種方法需要佔用的空間比較大,因爲稀疏圖更常見,這就導致了很多空間的浪費。V*V的矩陣很多時候我們是不能接受的。


2.邊的數組

我們可以使用一個數組來存放所有的邊,這樣的話數組的大小僅有E。但是因爲我們的操作總是需要訪問某個頂點的相鄰節點,對於這種數據類型,要訪問相鄰節點的話必須遍歷整個數組,造成效率的低下,所以我們在這裏也不使用這個數據結構。


3.鄰接表數組

我們使用一個鏈表數組來表示,數組中每個元素都是鏈表表頭,鏈表中存放對應下標的節點所連接的邊。

這種數據結構使用的空間爲V+E。並且可以相當方便的獲取相鄰節點。

如圖:




實現如下:

import java.util.ArrayList;
import java.util.List;

public class Graph {
	private List<Integer>[] adj; // 鄰接表
	private int V;	// 頂點數目
	private int E;	// 邊的數目
	
	public Graph(int V) {
		this.V = V;
		adj = (List<Integer>[])new List[V];
		E = 0;
		for (int i = 0; i < V; i++) {
			adj[i] = new ArrayList<Integer>();
		}
	}
	
	public void addEdge(int v, int w) {
		adj[v].add(adj[v].size(), w);
		adj[w].add(adj[w].size(), v);
		E++;
	}
	
	public List<Integer> adj(int v) {
		return adj[v];
	}
	
	public int V() {
		return V;
	}
	
	public int E() {
		return E;
	}
	
	public String toString() {
		String s = V + " 個頂點, " + E + " 條邊\n";
		for (int i = 0; i < V; i++) {
			s += i + ": ";
			for (Integer node : adj(i)) {
				s += node + " ";
			}
			s += "\n";
		}
		return s;
	}

}

到此爲止,我們已經完成了圖的表示。


有了表示,我們就需要使用圖完成一些簡單的應用。

例如,圖的搜索,圖的連通分量,圖是否有環等等。


首先我們來實現圖的搜索。

我們在這裏實現一個模板。並不實際進行搜索。

目標:給定一個起點,在圖中進行搜索。

方案:1.深度優先搜索 2.廣度優先搜索。


深搜

原理:

這裏打個比喻,搜索圖中所有的節點,就像走迷宮一樣,需要探索迷宮中所有的通道。探索迷宮所有的通道,我們需要什麼呢?

1.我們需要選擇一條沒有標記的路,並且一邊走一遍鋪上一條繩子。

2.標記走過的路。

3.當走到一個標記的地方時,我們需要回退,根據繩子回退到上一個地方。

4.當回退的地方沒有可以走的路了,就要繼續回退。

也就是說,首先我們需要一直走下去,但是我們一邊走就要一邊做標記,如果走不下去了,就回退,回退到沒有被標記的的路。循環往復,我們就能探索整個圖了。


實現:

import java.util.Stack;

public class DepthFirstSearch {
	private boolean[] isMarked;
	private int begin;
	private int count;
	private Integer[] edgeTo; 
	
	public DepthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		count = 0;
		this.begin = begin;
		dfs(g, begin);
	}
	
	public void dfs(Graph g, int begin) {
		isMarked[begin] = true;
		for (Integer i : g.adj(begin)) {
			if (!isMarked[i]) {
				edgeTo[i] = begin;
				count++;
				dfs(g, i);
			}
		}
	}
	
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}
	
	
}
我們需要一個數組來標記某個節點是否已經走過了,如果走過了,我們就不會再走了。
並且我們有一個數組去保存是從哪個節點到達當前節點。這樣,我們往回追朔的時候,就可以找到一條路徑了。

這是一個模板,並沒有具體的搜索某個節點,而是將所有節點都搜索了一遍,在實際過程中,我們可以判斷節點是否找到,找到就停止了。


對於無向圖來說,深搜雖然可以找到一條從v到w的路徑,但是這條路徑是否是最優的並不是可靠的,往往都不是。

如果我們希望找到一條最短的路徑,我們就應該使用廣搜。

廣搜

原理:

廣搜並不是先一條路走到黑,而是慢慢的根據距離進行搜索。例如,一開始先根據距離是1進行搜索,先搜索所有距離爲1的地方。如果沒找到,再搜索距離爲2的地方。以此類推。

如果說深搜是一個人在迷宮中搜索,那麼廣搜就是一組人在朝着各個方向進行搜索。當然不是效率比較高的意思,只是比喻而已。


實現:

import java.util.LinkedList;
import java.util.Queue;
import java.util.Stack;

public class BreadthFirstSearch {
	
	private boolean[] isMarked;
	private Integer[] edgeTo;
	private int begin;
	private int count; // 多少個點連通
	
	public BreadthFirstSearch(Graph g, int begin) {
		isMarked = new boolean[g.V()];
		edgeTo = new Integer[g.V()];
		this.begin = begin;
		count = 0;
		bfs(g, begin);
	}
	
	private void bfs(Graph g, int begin) {
		Queue<Integer> queue = new LinkedList<>();
		isMarked[begin] = true;
		queue.offer(begin);
		while (!queue.isEmpty()) {
			Integer temp = queue.poll();
			for (Integer i : g.adj(temp)) {
				if (!isMarked[i]) {
					isMarked[i] = true;
					count++;
					edgeTo[i] = temp;
					queue.offer(i);
				}
			}
		}
	}
	
	public boolean hasPath(int v) {
		return isMarked[v];
	}
	
	public int count() {
		return count;
	}
	
	public String pathTo(int v) {
		if (!hasPath(v)) return "";
		Stack<Integer> stack = new Stack<>();
		stack.push(v);
		for (int i = v; i != begin; i = edgeTo[i]) {
			stack.push(edgeTo[i]);
		}
		
		return stack.toString();
	}

}


其實廣搜和深搜的不同就在於搜索規則的不同,深搜使用的是stack的LIFO(後進先出)的思想,總是搜索最新的節點。而廣搜則是使用queue的FIFO(先進先出)的規則。

就如同上面的一樣,節點進入隊列的順序是根據距離的,所以我們就可以實現慢慢範圍的擴大搜索。

同樣的,我們也標記了進入節點的前一個節點,用來追蹤路徑。因爲我們是根據範圍搜索的,所以得到的就是最短路徑。


我們可以使用廣搜和深搜來實現很多應用,例如是否有環,是否是二部圖等等。這裏我們就不展開了。


我們上面的圖的節點都是以數字作爲標記的,而對於實際應用來講,圖的節點一般都不會是數字,而是String類型的字符串等。

要實現這種符號圖,我們只需要將我們的代碼進行一些擴展,使用符號表的方法,將字符串映射到某個整數上就可以了。

例如:


我們只需要在將字符串映射得到一個數字,也就是使用散列表的方式,存儲成鍵值對,就可以繼續使用上面的代碼了。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章