Coursera Algorithm, Part2 Week1: Undirected Graph & Directed Graph

Algorithm Part1 沒學多久就結課了,真是憂桑,當時開始學這門課的時候離結課也就還剩下一個星期,所以學到的東西也不是很多。


於是,就直接跳到 Algorithm Part2 了,不知道沒有Part1 的基礎直接進入Part2 壓力會不會很大。


中間大概等了一兩個星期吧,開始Part2 的課程。一上來就是圖論,自己心裏當時還真有點打鼓,畢竟圖論是一個看上去很抽象、很深奧的東西。而且當時學習算法的時候對圖論也是抱着一知半解的理解,知道的圖論的算法也就最最基礎的深搜、廣搜、最小生成樹、單源點最短路徑,可即使是這最最基礎的算法玩得也不利索。所以這次真的是抱着認認真真、不打一點馬虎眼、不騙自己的態度來學習的,認真學習理解每一個算法、認真寫每一行靠譜的代碼(每星期的課後作業還是很有意思的,有難度但有心就能做),還有及時作總結,就像現在做的事情一樣。


我希望能夠在這門課結束的時候能夠堅實自己的基礎,也能夠保持對代碼的感覺,代碼量的不同真的會讓程序員有不同的感覺。


好了,進入正題吧、、

1.1 數據結構

構建圖的數據結構不多,典型的就幾種吧:鏈表或者是數組,存放的方式可以是二維矩陣,0/1表示兩個節點是否相連,空間開銷是V*V,V表示圖的節點個數;再有就是直接存放每條邊,空間開銷是E,E表示邊的條數。

顯然,有更好的數據結構方式,那就是鄰接表。V大小的數組存放每個節點,數組的每個元素又由鏈表鏈出,鏈表中的每個元素表示與該數組元素相連的節點。鄰接表的數據結構節省了空間的開銷,也降低了查找的時間代價,是一種常用的圖論的數據結構。

坑爹的csdn,好不容易寫一回blog,竟然不能上傳圖片,只得一行一行敲代碼!!!注:所有的示例代碼以及所需包含的庫都可以在這裏下載。

public class Graph {
	
	private final int V;
	private Bag<Integer>[] adj;  // adjacency lists (using Bag data type)
	
	public Graph(int V) {
		
		this.V = V;
		adj = (Bag<Integer>[]) new Bag[V];  // create empty graph with V vertices
		for (int v = 0; v < V; v++)
			adj[v] = new Bag<Integer>();
	}
	
	public void addEdge(int v, int w) {
		
		// add edge v-w (parallel edges and self-loops allowed)
		adj[v].add(w);
		adj[w].add(v);
	}
	
	// iterator for vertices adjacent to v
	public Iterable<Integer> adj(int v) {
		return adj[v]
	}
}

1.2 深搜(帶路徑記錄)

深搜是圖論中很常見的算法,可能是圖論中很多算法的基礎,也許比廣搜用得還多,話不多說,直接上代碼。

public class DepthFirstPaths {
	
	private boolean[] marked[];  // marked[v]=true if v connected to s
	private int[] edgeTo;        // edge[v] = previous vertex on path from s to v
	private int s;
	
	public DepthFirstPahts(Graph G, int s) {
		
		// ... initialize data structire
		dfs(G, s);               // find vertices connected to s
	}
	
	// recursive DFS does the work
	private void dfs(Graph G, int v) {
		
		marked[v] = true;
		for (int w : G.adj(v)) {
			if (!marked[w]) {
				dfs(G, w);
				edge[w] = v;
 			}
		}
	}
	
	public boolean hasPathTo(int v) {
		return marked[v];
	}
	
	public Iterable<Integer> pathTo(int v) {
		
		if (!hasPathTo(v)) return null;
		Stack<Integer> path = new Stack<Integer>();
		for (int x = v; x != s; x = edgeTo[x])
			path.push(x);
		path.push(s);
		return path;
	}
}


1.3 廣搜

與深搜不同,同樣是圖搜索,但是更廣泛地用於最短路徑的算法中。

public class BreadthFirstPaths {
	
	private boolean[] marked;
	private int[] edgeTo;
	
	private void bfs(Graph G, int s) {
		
		Queue<Integer> q = new Queue<Integer>();
		q.enqueue(s);
		marked[s] = true;
		while(!q.isEmpty()) {
			int v = q.dequeue();
			for (int w : G.adj(v)) {
				if (!marked[w]) {
					q.enqueue(w);
					marked[w] = true;
					edgeTo[w] = v;
				}
			}
		}
	}
}

1.4 連通分量(深搜解決)

public class CC {
	
	private boolean[] marked;
	private int[] id;  // id[v] = id of component containing v
	private int count; // number of components
	
	public CC(Graph G) {
		
		marked = new boolean[G.V()];
		id = new int[G.V()];
		for (int v = 0; v < G.V(); v++) {
			if (!marked[v]) {
				dfs(G, v);  // run DFS from one vertex in each component
				count++;
			}
		}
	}
	
	// number of components
	public int count() {
		return count;
	}
	
	// id of component containing v
	public int id(int v) {
		return id[v];
	}
	
	// all vertices discovered in same call of dfs have same id
	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);
	}
}



下面是有向圖的基本算法,深搜和廣搜就不再贅述了,和無向圖基本上一致,沒什麼好說的。

2.1 拓撲排序(DAG:Directed acyclic graph)

首先,什麼是拓補排序呢。我自己也說不清楚,只能感覺出來,就比如一批工作,每兩個工作有先後次序,什麼先做、什麼後做,需要給出所有這些工作它們之間清楚的序列關係,大概就是拓撲排序了吧。這裏拓撲排序算法是藉助深搜,終於認識到深搜的強大了吧。

public class DepthFirstOrder {
	
	private boolean[] marked;
	private Stack<Integer> reversePost;
	
	public DepthFirstOrder(Digraph G) {
		
		reversePost = new Stack<Integer>();
		marked = new boolean[G.V()];
		for (int v = 0; v < G.V(); v++) 
			if (!marked[v]) dfs(G, v);
	}
	
	private void dfs(Digraph G, int v) {
		
		marked[v] = true;
		for (int w : G.adj(v))
			if (!marked[w]) dfs(G, w);
		reversePost.push(v);
	}
	
	// returns all vertices in "reverse DFS postorder"
	public Iterable<Integer> reversePost() {
		return reversePost;
	}
}

2.2 強連通分量

首先,強連通的定義:節點v和節點w強連通充分必要條件是在有向圖中有v到w的路徑,也有w到v的路徑。

那麼強連通分量的定義:最大化強連通集合的節點個數。

這裏用的是Kosaraju-Sharir算法計算強連通分量,Kosaraju-Sharir算法主要由兩部分組成:

(1)計算圖G'的拓撲排序,其中圖G'是圖G中所有的有向邊調轉指向,即原來由v->w的邊變成w->v

(2)根據圖G'的拓撲排序的順序在圖G中進行深搜,所能訪問到的節點屬於同一個強連通分量

其實代碼和連通分量非常像,就是加了個拓撲排序,深搜的節點順序是拓撲排序的結果。話不多說,上代碼:

public class KosarajuSharirSCC {
	
	private boolean[] marked;
	private int[] id;
	private int count;
	
	public KosarajuSharirSCC(Digraph G) {
		
		marked = new boolean[G.V()];
		id = new int[G.V()];
		DepthFirstOrder dfs = new DepthFirstOrder(G.reverse());
		for (int v : dfs.reversePost()) {
			if (!marked[v]) {
				dfs(G, v);
				count++;
			}
		}
	}
	
	private void dfs(Digraph G, int v) {
		
		marked[v] = true;
		id[v] = count;
		for (int w : G.adj(v))
			if (!marked[w]) dfs(G, w);
	}
	
	public boolean stronglyConnected(int v, int w) {
		return id[v] == id[w];
	}
}


Assigment

最後附上Algorithm Part2 Week1 的作業和測試用例。看這裏



鼕鼕加油!再苦再累路也得走完,想明白自己想要的是什麼,放棄的是什麼。每天進步一點點,我知道過程會很艱苦,會沒有人相信你,但是真的要自己相信自己。一定行行行!!!

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