四種重要的圖模型:java
無向圖(簡單鏈接)算法
有向圖(鏈接有方向性)數據庫
加權圖(鏈接帶有權值)設計模式
加權有向圖(鏈接既有方向性又帶有權值)數組
定義:由一組頂點和一組可以將兩個頂點相連的邊組成。網絡
特殊:自環(一條鏈接一個頂點和其自身的邊);平行邊(鏈接同一對頂點的兩條邊) 數據結構
數學家將含有平行邊的圖稱爲多重圖;將沒有平行邊或自環的圖稱爲簡單圖。現實當中,兩點就能夠指代一條邊。app
兩個頂點經過一條邊相連,稱這兩頂點相鄰,並稱該鏈接依附於這兩個頂點。函數
某個頂點的度數即爲依附於它的邊的總數。工具
子圖是由一幅圖的全部邊的一個子集(以及它們所依附的全部頂點)組成的圖。
路徑是由邊順序鏈接的一系列頂點。
簡單路徑是一條沒有重複頂點的路徑。
環是一條至少含有一條邊且起點和終點相同的路徑。
簡單環是一條(除了起點和終點必須相同以外)不含有重複頂點和邊的環。
路徑或者環的長度爲其中所包含的邊數。
大多狀況研究簡單環和簡單路徑,並會省略簡單二字。當容許重複的頂點時,指的都是通常的路徑和環。
當兩個頂點之間存在一條鏈接雙方的路徑時,稱一個頂點和另外一個頂點是連通的。
U-V-W-X記爲U到X的一條路徑;U-V-W-X-U記爲U到V到W到X再回到U的一條環。
從任意一個頂點都存在一條路徑到達另外一個任意頂點,稱這幅圖是連通圖。
一副非連通的圖由若干連通的部分組成,它們都是其極大連通子圖。
直觀上:若是頂點是念珠,邊是鏈接念珠的線,它們都是物理存在的對象,那麼將任意頂點提起來,連通圖都將是一個總體,而非連通圖則會變成兩個或多個部分。
通常來講:要處理一張圖就要一個個地處理它的連通份量(子圖)。
無環圖:不包含環的圖。
樹是一副無環連通圖。互不相連的樹組成的集合稱爲森林。連通圖的生成樹是它的一副子圖,它含有圖中的全部頂點且是一棵樹。圖的生成樹森林是它的全部連通子圖的生成樹的集合。
樹的定義很是通用,稍做改動就能夠變成用來描述程序行爲的(函數調用層次)模型和數據結構(二叉查找樹、2-3樹等)。
當且僅當一幅含有V個節點的圖G知足下列5個條件之一時,它就是一棵樹:
G有V-1條邊且不含有環;
G有V-1條邊且是連通的;
G是連通的,但刪除任意一條邊都會使之再也不連通;
G是無環圖,但添加任意一條邊都會產生一條環;
G中的任意一對頂點之間僅存在一條簡單路徑。
圖的密度是指已經鏈接的頂點對佔全部可能被鏈接的的頂點對的比例。通常來講,若是一幅圖中不一樣的邊的數量只佔頂點總數V的一小部分,那麼就認爲這幅圖是稀疏的,不然是稠密的。
二分圖是一種可以將全部節點分爲兩部分的圖,其中圖的每條邊所鏈接的兩個頂點都分別屬於不一樣的部分。二分圖會出如今許多場景中。
圖的基本操做的API:
兩個構造,獲得頂點數V( )和邊數E( ),增長一條邊addEdge( int v, int w )。本節全部算法都基於adj( )方法所抽象的基本操做。第二個構造函數接受的輸入由2E+2個整數組成:首先是V, 而後是E, 在而後是 E 對 0到V-1之間的整數,每一個整數對都表示一條邊。
要面對的下一個圖處理問題就是用哪一種數據結構來表示並實現這份API,這包含兩個要求:
必須爲可能在應用中碰到的各類類型的圖預留出足夠的空間;
實例方法的實現必定要快—它們是開發處理圖的各類用例的基礎。
要求比較模糊,可是仍然能幫咱們在三種圖的表示方法中進行選擇。
鄰接矩陣。用V*V的布爾矩陣,當V和W有邊時,定義V行W列元素爲TRUE,不然爲FALSE。該方法不符合第一個條件,上百萬個頂點的圖是很常見的.V^2空間不知足。
邊的數組。可使用一個Edge類,含有兩個int實例變量。表示方法簡單可是不知足第二個條件—要實現adj( )須要檢查全部邊。
鄰接表數組。可使用一個以頂點爲索引的列表數組,其中每一個元素都是和該頂點相鄰的頂點列表。該結構同時知足兩個條件。本章一直用它。
除了性能目標,還發現:容許存在平行邊至關於排除了鄰接矩陣,由於鄰接矩陣沒法表示它們。
非稠密圖的標準表示稱爲鄰接表的數據結構,它將每一個頂點的全部相鄰頂點都保存在該頂點對於的元素所指向的一張鏈表中。使用這個數組就是爲了快速訪問給定頂點的鄰接頂點列表。
使用Bag抽象數據類型(也可用Java中的<LinkedList>)來實現這個鏈表,這樣就能夠在常數時間內添加新的邊或遍歷任意頂點的全部相鄰頂點。
這種Graph的實現的性能:
使用的空間和V+E成正比;
添加一條邊所須要的時間爲常數;
遍歷頂點V的全部相鄰頂點所須要的時間和V的度數成正比。
對於這樣的操做,這樣的特性已是最優,能夠知足圖處理應用的須要,而且支持平行邊和自環。邊的插入順序決定了Graph得鄰接表中頂點的出現順序。使用構造函數從標準輸入中讀入一副圖時,就意味着輸入的格式和邊的順序決定了Graph的鄰接表數組中頂點的出現順序。
/** * 無向圖 */ public class Graph { private int vertexCount; // 頂點數 private int edgeCount; // 邊數 private LinkedList<Integer>[] adj; // 鄰接表數組 public Graph(int v){ this.adj = new LinkedList[v]; for(int i = 0; i<v; i++) adj[i] = new LinkedList<>();// 初始化鄰接表數組 this.vertexCount = v; } public Graph(In in) { this(in.readInt()); int e = in.readInt();//獲得邊數 // 讀取每條邊,進行圖的初始化操做 for(int i = 0; i<e;i++){ int v = in.readInt(); // 起點 int w = in.readInt(); // 終點 addEdge(v, w); } } /*** 增長一條邊*/ public void addEdge(int start, int end) { adj[start].add(end); adj[end].add(start); this.edgeCount++; } public int getEdgeCount() { return edgeCount; } public int getVertexCount() { return vertexCount; } /** 返回頂點v的鄰接表*/ public LinkedList<Integer> adj(int v){return adj[v];} /** 把圖轉化成標準字符串形式*/ public String toString(){ String NEWLINE = System.getProperty("line.separator"); StringBuilder sb = new StringBuilder(); sb.append("vertex count: ").append(getVertexCount()) .append(" edge count: ").append(getEdgeCount()) .append(Config.NEWLINE); for (int v = 0; v < getVertexCount();v++){ LinkedList<Integer> list = adj(v); sb.append(v).append(":\t").append("["); for (int i=0; i < list.size();i++){ sb.append(list.get(i)).append(","); } sb.deleteCharAt(sb.length() - 1); sb.append("]").append(NEWLINE); } return sb.toString(); } public static void main(String[] args) { String dir = Graph.class.getPackage().getName().replace(".", "/"); String path = Graph.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); System.out.println(g.toString()); } }
/** * 圖的基本經常使用操做工具類 */ public class GraphUtils { /** 計算頂點v的度數*/ public static int degree(Graph graph, int v){return graph.adj(v).size();} /** 計算圖中最大的度*/ public static int maxDegree(Graph graph){ int max = 0; for(int i = 0;i<graph.getVertexCount();i++){ int currentDegree = degree(graph, i); max = currentDegree > max ? currentDegree : max; } return max; } /** 計算圖的平均度數*/ public static int avgDegree(Graph g){ return 2 * g.getEdgeCount() / g.getVertexCount(); } /** 計算自環的個數*/ public static int numberOfSelfLoops(Graph g){ int count = 0; for(int v = 0; v < g.getVertexCount(); v++) for(int w: g.adj(v)) if(v == w) count++; return count / 2; // 每條邊計算了兩次 } public static void main(String[] args) { String dir = GraphUtils.class.getPackage().getName().replace(".", "/"); String path = GraphUtils.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); for (int i = 0; i < g.getVertexCount(); i++) { System.out.println(i+" degree : "+GraphUtils.degree(g, i)); } System.out.println("the max degree is : " + GraphUtils.maxDegree(g)); System.out.println(g.toString()); System.out.println("avg degree: "+GraphUtils.avgDegree(g)); System.out.println("count of self loop: "+GraphUtils.numberOfSelfLoops(g)); } }
0 degree : 4 1 degree : 1 2 degree : 1 3 degree : 2 4 degree : 3 5 degree : 3 6 degree : 2 7 degree : 1 8 degree : 1 9 degree : 3 10 degree : 1 11 degree : 2 12 degree : 2 the max degree is : 4 vertex count: 13 edge count: 13 0: [5,1,2,6] 1: [0] 2: [0] 3: [4,5] 4: [3,6,5] 5: [0,4,3] 6: [4,0] 7: [8] 8: [7] 9: [12,10,11] 10: [9] 11: [12,9] 12: [9,11] avg degree: 2 count of self loop: 0
將圖的表示和實現分離開。爲每一個任務建立一個相應的類,用例能夠建立相應的對象來完成任務。
探索迷宮方法:tremaux搜索:
選擇一條沒有標記過的通道,在走過的路上鋪一條繩子;
標記全部你第一次路過的路口和通道;
當來到一個標記過的路口時(用繩子)回退到上一個路口;
當回退到得路口已經沒有可走的通道時繼續回退。
繩子可保證總能找到一條出路,標記則能保證不會兩次通過同一條通道或同一個路口。
看Java代碼實現:
/** * 圖的深度優先搜索算法 */ public class DepthFirstSearch { private int count; private boolean[] marked; // 數組存儲每一個頂點是否被遍歷過 /** * 從頂點s開始對g進行深搜 * @param g * @param s */ public DepthFirstSearch(Graph g, int s) { marked = new boolean[g.getVertexCount()]; dfs(g, s); } /** 深搜*/ private void dfs(Graph g, int s) { marked[s] = true; // 1.標記頂點s count++; // 2.count數加一 LinkedList<Integer> list = g.adj(s);// 3.獲取s的鄰接表 for(int w: list) // 4.對鄰接表進行遍歷 if(!isMarked(w)) dfs(g,w); // 5.若是遍歷到的頂點沒有被標記過,對該頂點繼續遞歸深搜 } /** 頂點w是否和起點s相連通*/ public boolean isMarked(int w){return marked[w];} /** 與起點s連通的頂點數量*/ public int count(){return count;} public static void main(String[] args) { String dir = DepthFirstSearch.class.getPackage().getName().replace(".", "/"); String path = DepthFirstSearch.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstSearch search = new DepthFirstSearch(g, start); System.out.print("start vertex: "+ start+". "); StringBuilder sb = new StringBuilder(); for(int i = 0; i< g.getVertexCount(); i++) if(search.isMarked(i)) sb.append(" "+ i); System.out.println("Connected " + sb.toString()); // 若是和s連通的頂點數量和圖的頂點數量相同,說明是連通圖 if(search.count() == g.getVertexCount()) System.out.println("g is a connected graph."); else System.out.println("g is not a connected graph."); } }
start vertex: 0. Connected 0 1 2 3 4 5 6 g is not a connected graph.
「兩個給定頂點是否連通?」等價於「兩個給定的頂點之間是否存在一條路徑」,也叫路徑檢測問題。
union-find算法的數據結構並不能解決找出這樣一條路徑問題,DFS是已經學習過的方法中第一個可以解決該問題的算法
能解決的另外一問題:單點路徑----給定一幅圖和一個起點s,「從S到給定的頂點V是否存在一條路徑,若是有,找出」
構造函數接受一個起點S做爲參數,計算S到與S連通的每一個頂點之間的路徑。在爲S建立了Paths對象後,用例能夠調用pathTo()實例方法來遍歷從S到任意和S連通的頂點的路徑上的全部頂點。之後會實現只查找具備某些屬性的路徑。
Java實現
/** * 深搜尋找路徑問題 */ public class DepthFirstPaths { private boolean[] marked; private int[] edgeTo; // 路徑 private int start; // 起點 public DepthFirstPaths(Graph g, int s){ marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; this.start = s; dfs(g, s); } private void dfs(Graph g, int s) { marked[s] = true; for(int w: g.adj(s)){ if(!marked[w]){ // 若是w沒有被標記過,把路徑數組中的w處置爲s,意思:從s到達了w。此處記錄了每一次深搜的路徑節點 edgeTo[w] = s; dfs(g, w); } } } /** 從起點s到頂點v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} public Stack<Integer> pathTo(int v){ if(!hasPathTo(v)) return null; Stack<Integer> stack = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) // 從終點開始,倒着找起點,依次push入棧 stack.push(x); stack.push(start);// for循環到起點處終止,因此在循環結束後要把起點入棧,至此 一條完整的路徑依次入棧 return stack; } public static void main(String[] args) { String dir = DepthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = DepthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 0; DepthFirstPaths pathSearch = new DepthFirstPaths(g, start); StringBuilder sb = new StringBuilder(); for(int i = 0; i<g.getVertexCount(); i++){ if(i == start) continue; if(!pathSearch.hasPathTo(i)){ System.out.println(start+" to "+ i +" : not connected."); continue; } sb.setLength(0); sb.append(start).append(" to ").append(i).append(": "); Stack<Integer> p = pathSearch.pathTo(i); while(!p.isEmpty()) sb.append(p.pop()).append("->"); sb.deleteCharAt(sb.length()-1); sb.deleteCharAt(sb.length()-1); System.out.println(sb.toString()); } } }
0 to 1: 0->1 0 to 2: 0->2 0 to 3: 0->5->4->3 0 to 4: 0->5->4 0 to 5: 0->5 0 to 6: 0->5->4->6 0 to 7 : not connected. 0 to 8 : not connected. 0 to 9 : not connected. 0 to 10 : not connected. 0 to 11 : not connected. 0 to 12 : not connected.
深搜獲得的路徑不只取決於圖的結構,還取決於圖的表示和遞歸調用的性質。咱們天然對最短路徑感興趣:
單點最短路徑。給定一幅圖和一個起點S,從S到給定頂點V是否存在一條路徑?若是有,請找出其中最短的那條(所含邊數最少)。
DFS遍歷圖的順序和找出最短路徑的目標無關。
BFS爲了這個目標而出現。要找到從S到V得最短路徑,從S開始,在全部由一條邊就能夠到達的頂點中查找V, 若是找不到就繼續在與S距離兩條邊的全部頂點中查找,如此一直執行。
DFS好像是一我的在走迷宮,BFS則像一組人在一塊兒朝各個方向走這個迷宮,每一個人都有本身的繩子,當出現新的叉路時,能夠假設一個探索者能夠分裂爲更多的人來搜索。當來個那個探索者相遇的時候,合二爲一,並繼續使用先到達者的繩子。
在程序中,搜索一幅圖時遇到有多條邊須要遍歷的狀況,咱們會選擇其中一條並將其餘通道留到之後再繼續搜索。在DFS中,用了一個能夠下壓的棧,以支持遞歸搜索。使用LIFO的規則來描述壓棧和走迷宮時先探索相鄰的通道相似。從有待搜索的通道中選擇最晚遇到過的那條。
在BFS中但願按照與起點的距離的順序來遍歷全部的頂點:使用FIFO先進先出隊列來代替棧LIFO後進先出 便可。將從有待搜索的通道中選擇最先遇到的那條。
實現:
算法4.2實現了BFS。使用隊列保存全部已經被標記過但其鄰接表還未被檢查過的頂點。先將起點加入隊列,而後重複下面步驟直到隊列爲空:
取隊列中的下一個頂點V並標記它;
將與V相鄰的全部未被標記過的頂點加入隊列。
算法4.2中的方法不是遞歸的,不像遞歸中隱式使用的棧,而是顯式地使用了一個隊列。
從隊列中刪除0,將相鄰頂點2 1 5加入隊列,標記它們並分別將它們在edgeTo[ ]中的值置爲0;隊列: 0 2 1 5
從隊列中刪除2,並檢查相鄰頂點0 1 3 4, 0和1已經被標記,將3和4這兩個沒被標記的加入隊列,標記它們,並分別將它們在edgeTo[ ] 中的值設爲2;隊列: 0 2 1 5 3 4
刪除1,檢查相鄰點0 2,發現都已經被標記;隊列: 0 2 1 5 3 4
刪除5, 檢查相鄰點 0 3, 發現都已經被標記;隊列: 0 2 1 5 3 4
刪除3, 檢查相鄰點 2 4 5, 發現都已經被標記;隊列: 0 2 1 5 3 4
刪除4, 檢查相鄰點 2 3, 發現都已經被標記;隊列: 0 2 1 5 3 4
/** * 廣搜找到最短路徑 * 對於從s可達的任意頂點v,廣搜都能找到一條從s到v的最短路徑 * (沒有其餘從s到v的路徑所含邊比這條路徑更少) * 廣搜所需時間在最壞狀況下和(v + e)成正比。 */ public class BreadthFirstPaths { private boolean[] marked; private int[] edgeTo; private int start; public BreadthFirstPaths(Graph g, int s){ this.start = s; marked = new boolean[g.getVertexCount()]; edgeTo = new int[g.getVertexCount()]; bfs(g, s); } private void bfs(Graph g, int s) { Queue<Integer> queue = new Queue<>(); marked[s] = true; // 標記起點 queue.enqueue(s); // 起點入隊 while(!queue.isEmpty()){ int head = queue.dequeue(); // 從隊列中取出隊首 LinkedList<Integer> list = g.adj(head); // 獲得隊首的鄰接表 for(int w: list){ //遍歷鄰接表 if(!marked[w]){ // 若當前節點沒有被標記過 edgeTo[w] = head; // 1.存入路徑 marked[w] = true; // 2.進行標記 queue.enqueue(w); // 3.節點入隊 } } } } /** 從起點s到頂點v是否存在通路*/ public boolean hasPathTo(int v){return marked[v];} /** 返回從起點s到頂點v的一條最短路徑*/ public Stack<Integer> pathTo(int v){ if(!hasPathTo(v)) return null; // 若不存在到v的路徑,返回Null Stack<Integer> path = new Stack<>(); for(int x = v; x!=start; x=edgeTo[x]) path.push(x); path.push(start); return path; } public static void main(String[] args) { String dir = BreadthFirstPaths.class.getPackage().getName().replace(".", "/"); String path = BreadthFirstPaths.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); int start = 5; BreadthFirstPaths bfPath = new BreadthFirstPaths(g, start); for(int i = 0; i<g.getVertexCount();i++){ if(i == start) continue; if(!bfPath.hasPathTo(i)){ System.out.println(start + " to "+ i + " : not connected."); continue; } StringBuilder sb = new StringBuilder(); sb.append(start + " to "+ i + " : "); Stack<Integer> p = bfPath.pathTo(i); while(!p.isEmpty()){ sb.append(p.pop() + "->"); } sb.deleteCharAt(sb.length() - 1); sb.deleteCharAt(sb.length() - 1); System.out.println(sb.toString()); } } }
5 to 0 : 5->0 5 to 1 : 5->0->1 5 to 2 : 5->0->2 5 to 3 : 5->3 5 to 4 : 5->4 5 to 6 : 5->0->6 5 to 7 : not connected. 5 to 8 : not connected. 5 to 9 : not connected. 5 to 10 : not connected. 5 to 11 : not connected. 5 to 12 : not connected.
對於這個例子,edgeTo[]數組在第二步以後就已經完成了。和深搜同樣,一點全部頂點都已經被標記,餘下的計算工做就只是在檢查鏈接到各個已被標記的頂點的邊而已。
命題:對於從S可達到的任意頂點V, 廣搜都能找到一條從S到V的最短路徑(沒有其餘從S到V得路徑所含的邊比這條路徑更少)
續: 廣搜所需的時間在最壞狀況下和V+E成正比。
DFS和BFS都會先將起點存入數據結構中,而後重複如下步驟知道數據結構被清空:
取其中的下一個頂點並標記它;
將V的全部相鄰而又未被標記的頂點加入數據結構。
不一樣之處在於從數據結構中獲取下一個頂點的規則:廣搜是最先加入的頂點;深搜是最晚加入的頂點。這種差別獲得了處理圖的兩種徹底不一樣的視角,不管哪一種,全部與起點連通的頂點和邊都會被檢查到。
深搜下一個直接應用就是找出一幅圖的全部連通份量。API:
CC的實現使用了marked[ ]數組來尋找一個頂點做爲每一個連通份量中深度優先搜索的起點。遞歸的深搜第一次調用的參數是頂點0,會標記全部與0連通的頂點。而後構造函數中的for循環會查找每一個沒有被標記的頂點並遞歸調用dfs來標記和它相鄰的全部頂點。另外,它還使用了一個以頂點做爲索引的數組id[ ],將同一個連通份量中的頂點和連通份量的標識符關聯起來。這個數組使得connected( )方法的實現變得十分簡單。
/** * 強連通份量 */ public class CC { private boolean[] marked; private int[] id; private int count; public CC(Graph g){ marked = new boolean[g.getVertexCount()]; id = new int[g.getVertexCount()]; for(int s = 0; s < g.getVertexCount(); 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); } /** v和w連通嗎*/ public boolean connected(int v, int w) { return id[v] == id[w]; } /** v所在的連通份量的標識符*/ public int id(int v) { return id[v]; } /** 連通份量數*/ public int count() {return count;} public static void main(String[] args) { String dir = CC.class.getPackage().getName().replace(".", "/"); String path = CC.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); CC cc = new CC(g); int m = cc.count(); System.out.println("number of components: "+ m); LinkedList<Integer>[] components = new LinkedList[m]; for(int i =0;i<m;i++) components[i] = new LinkedList<>(); for(int v = 0; v< g.getVertexCount(); v++) components[cc.id(v)].add(v); for(int i=0;i<m;i++){ for(int v: components[i]) System.out.print(v + " "); System.out.println(); } } }
number of components: 3 0 1 2 3 4 5 6 7 8 9 10 11 12
其實現基於一個由頂點索引的數組id[ ].若V屬於第i個連通份量,則id[v]的值爲i。構造函數會找出一個未被標記的頂點並調用遞歸函數dfs( )來標記並區分出全部和它連通的頂點,如此重複直到全部的頂點都被標記並區分。
命題C:深搜的預處理使用的時間和空間與V+E成正比且能夠在常數時間內處理關於圖的連通性查詢。
和union-find算法對比:理論上深搜比union-find快,由於能保證所需時間是常數,而union-find不行;但在實際中,該差別微不足道。union-find更快,由於它不須要完整的構造並表示一幅圖。更重要的是:union-find算法是一種動態算法(在任什麼時候候都能用接近常數的時間檢查兩個頂點是否連通,甚至是在添加一條邊的時候),但深搜就必須對圖進行預處理。
所以,在完成只須要判斷連通性或是須要完成有大量連通性查詢和插入操做混合等相似的任務時,更傾向使用union-find,而深搜更適合實現圖的抽象數據類型,由於可以更有效的利用已有數據結構。
DFS已經解決了幾個基礎問題。該方法很簡單,遞歸實現使得咱們可以進行復雜的運算併爲一些圖的處理問題給出簡潔的解決方法。
下面對兩個問題進行解答:
檢測環:給定的圖是無環圖嗎?
雙色問題:可以用兩種顏色將圖的全部頂點着色,使得任意一條邊上的兩個端點的顏色都不一樣嗎?這個問題等價於:這是一幅二分圖嗎?
/** * 給定的圖是無環圖嗎 * 檢測自環:假設沒有自環,沒有平行邊 */ public class Cycle { private boolean[] marked; private boolean hasCycle; public Cycle(Graph g){ marked = new boolean[g.getVertexCount()]; for(int i = 0;i<g.getVertexCount();i++) if(!marked[i]) dfs(g, i, i); } 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); // 若w沒被標記過,那麼從w繼續遞歸深搜,把w的父節點做爲第二參數 else if(w != u) hasCycle = true; // 若w被標記過,那麼若無環,w必然和父節點相同,不然就是有環 } /** 是否含有環*/ public boolean hasCycle(){return hasCycle;} public static void main(String[] args) { String dir = Cycle.class.getPackage().getName().replace(".", "/"); String pathCycle = Cycle.class.getClassLoader().getResource(dir+"/tinyG.txt").getPath(); String pathNoCycle = Cycle.class.getClassLoader().getResource(dir+"/cycle_test.txt").getPath(); In in = new In(new File(pathCycle)); Graph g = new Graph(in); Cycle c = new Cycle(g); System.out.println(c.hasCycle()); In in2 = new In(new File(pathNoCycle)); Graph g2 = new Graph(in2); Cycle c2 = new Cycle(g2); System.out.println(c2.hasCycle()); } }
true false
/** * 雙色問題:可以用兩種顏色將圖的全部頂點着色,使得任意一條邊上的兩個端點的顏色都不一樣嗎? * 等價於:判斷是不是二分圖的問題 */ public class TwoColor { private boolean[] marked; private boolean[] color; private boolean isColorable; public TwoColor(Graph g){ isColorable = true; marked = new boolean[g.getVertexCount()]; color = new boolean[g.getVertexCount()]; for(int i = 0; i<g.getVertexCount(); i++)//遍歷全部頂點 if(!marked[i]) dfs(g, i);//沒有mark就進行深搜 } private void dfs(Graph g, int v) { marked[v] = true; // 標記 for(int w: g.adj(v)) // 對鄰接表進行遍歷 if(!marked[w]){ // 若是沒有被標記 color[w] = !color[v]; // 當前w節點顏色置爲和父節點不一樣的顏色 dfs(g, w); // 對當前節點繼續深搜 }else if(color[w] == color[v]){ // 若是已經被標記,看是否顏色和父節點相同 isColorable = false; // 若相同則不是二分圖 } } /** 是不是二分圖*/ public boolean isBipartite(){return isColorable;} public static void main(String[] args) { String dir = TwoColor.class.getPackage().getName().replace(".", "/"); String path = TwoColor.class.getClassLoader().getResource(dir+"/color_test.txt").getPath(); String path2 = TwoColor.class.getClassLoader().getResource(dir+"/color_test2.txt").getPath(); In in = new In(new File(path)); Graph g = new Graph(in); TwoColor t = new TwoColor(g); System.out.println(t.isBipartite()); In in2 = new In(new File(path2)); Graph g2 = new Graph(in2); TwoColor t2 = new TwoColor(g2); System.out.println(t2.isBipartite()); } }
true false
典型應用中,圖都是經過文件或者網頁定義的,使用的是字符串而非整數來表示和指代頂點。爲了適應這樣的應用,定義擁有如下性質的輸入格式:
頂點名爲字符串
用指定的分隔符來隔開頂點名(容許頂點名中含有空格)
每一行都表示一組邊的集合,每條邊都鏈接着這一行的第一個名稱表示的頂點和其餘名稱所表示的頂點
頂點總數V和邊的總數E都是隱式定義的。
例子:
API
定義了一個構造來讀取並構造圖,用name( )方法和index( )方法將輸入流中的頂點名和圖算法使用的頂點索引對應起來。
例子:飛機場routes.txt--輸入機場代碼查找從該機場起飛到達的城市,但這些信息並非直接從文件中能獲得的。
例子:電影movies.txt--輸入一部電影名字獲得演員列表。這不過是在照搬文件中對應的行數據,
但輸入演員名字 查看其出演的電影列表,至關於反向索引。
儘管數據庫的構造是爲了將電影名鏈接到演員,二分圖模型同時也意味着將演員鏈接到電影名。
二分圖的性質自動完成了反向索引。這將成爲處理更復雜的和圖有關的問題的基礎。
SymbolGraph用到了3種數據結構:
一個符號表st,鍵的類型爲String(頂點名),值得類型爲int(索引);
一個數組keys[ ],用做反向索引,保存每一個頂點索引對應的頂點名;
一個Graph對象G,使用索引來引用圖中的頂點。
SymbolGraph會遍歷兩遍數據結構來構造以上數據結構,主要是由於構造Graph對象須要頂點總數V。在典型的實際應用中,在定義圖的文件中指明V和E可能會不方便,從而有了SymbolGraph,這樣就能夠方便地在routes.txt或者movies.txt中添加或者刪除條目而不用但系須要維護邊或者頂點的總數。
Java實現
/** * 符號圖 */ public class SymbolGraph { private HashMap<String, Integer> map; // key:頂點名 value:索引 private String[] keys; // 反向索引,保存每一個頂點索引對應的頂點名 private Graph g; // 使用索引來引用圖中的頂點 public SymbolGraph(String path, String sp){ map = new HashMap<>(); BufferedReader reader; String line; try { reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){//第一遍,構造索引 String [] vertexs = line.split(sp); for(String s : vertexs) if(!map.containsKey(s)) map.put(s, map.size()); } reader.close(); keys = new String[map.size()]; for(String name: map.keySet()){ // 遍歷map的key,構造頂點名的反向索引 keys[map.get(name)] = name; } g = new Graph(map.size()); line = ""; reader = new BufferedReader(new FileReader(new File(path))); while((line = reader.readLine()) != null){ // 第二遍,構造圖,將每一行的頂點和該行其餘點相連 String[] strs = line.split(sp); int start = map.get(strs[0]);//獲取起點 for(int i = 1; i< strs.length; i++) g.addEdge(start, map.get(strs[i])); } reader.close(); } catch (IOException e) { e.printStackTrace(); } } /** key是一個頂點嗎*/ public boolean contains(String key){return map.containsKey(key);} /** key的索引*/ public int index(String key){return map.get(key);} /** 索引v的頂點名*/ public String name(int v){return keys[v];} /** 隱藏的Graph對象*/ public Graph graph(){return g;} public static void main(String[] args) { String dir = Cycle.class.getPackage().getName().replace(".", "/"); String path = Cycle.class.getClassLoader().getResource(dir+"/routes.txt").getPath(); SymbolGraph sg = new SymbolGraph(path, " "); Graph g = sg.graph(); HashMap<String, Integer> map = sg.map; for(Entry<String, Integer> s: map.entrySet()){ System.out.println(s.getKey() + "-" +s.getValue()); } System.out.println(g.toString()); String start = "JFK"; if(!sg.contains(start)){ System.out.println("起點"+start + " 不在數據庫."); return; } int s = sg.index(start); BreadthFirstPaths bfs = new BreadthFirstPaths(g, s); String end = "LAS"; if(!sg.contains(end)){ System.out.println("終點"+end + " 不在數據庫."); }else{ int t = sg.index(end); if(!bfs.hasPathTo(t)){ System.out.println(start +" 和 " + end + " 沒有路徑相同."); return; } Stack<Integer> stack = bfs.pathTo(t); StringBuilder sb = new StringBuilder(); while(!stack.isEmpty()){ sb.append(sg.name(stack.pop())).append(" "); } System.out.println("起點"+start+"到終點"+end+"的路徑爲:"); System.out.println(sb.toString()); } } }
LAS-9 LAX-8 DFW-5 ORD-2 JFK-0 HOU-4 ATL-7 DEN-3 PHX-6 MCO-1 vertex count: 10 edge count: 18 0: [1,7,2] 1: [0,7,4] 2: [3,4,5,6,0,7] 3: [2,6,9] 4: [2,7,5,1] 5: [6,2,4] 6: [5,2,3,8,9] 7: [0,4,2,1] 8: [6,9] 9: [3,8,6] 起點JFK到終點LAS的路徑爲: JFK ORD DEN LAS
一樣能夠把電影-演員做爲例子輸入:
這個Graph實現容許用例用字符串代替數字索引來表示圖中的頂點。
它維護了
實例變量st(符號表用來映射頂點名和索引)
keys(數組用來映射索引和頂點名)
g(使用索引表示頂點的圖)
爲了構造這些數據結構,代碼會將圖的定義處理兩遍(定義的每一行都包含一個頂點以及它的相鄰頂點列表,用分隔符sp隔開)
圖處理的一個經典問題就是,找到一個社交網絡之中兩我的間隔的度數。
演員K演過不少電影,爲圖中每一個演員附一個K數:
K本人爲0,
全部和K演過同一部電影的人的值爲1,
全部(除K外)和K數爲1的演員出演過同一部電影的人的值爲2,
以此類推。
能夠看到K數必須爲最短電影鏈的長度,所以不用計算機,很難知道。
用例DegreesOfSeparation所示,BreadthFirstPaths纔是咱們所須要的程序,經過最短路徑來找出movies.txt中任意演員的K數。
幾個基本概念:
圖的術語;
一種圖的表示方法,可以處理大型而稀疏的圖;
和圖處理相關的類的設計模式,其實現算法經過在相關的類的構造函數中對圖進行預處理,構造所需的數據結構來高效支持用例對圖的查詢;
DFS&BFS
支持使用符號做爲圖的頂點名的類。
上表總結了本節全部圖算法的實現。適合做爲圖處理的入門學習。隨後學習複雜類型圖以及更加困難的問題時,會用到這些代碼的變種。
考慮了邊的方向以及權重以後,一樣地問題會變得困可貴多,但一樣地算法仍然湊效並將成爲解決更復雜問題的起點。