鄰接矩陣
一維數組存儲頂點信息(數據元素),二維數組存儲邊或弧的信息(數據元素之間的關係)java
int[][] map = new int[n][n];// 頂點 0 ~ n-1, map[i][j] 表示頂點 i 到頂點 j 的邊或邊的權值
<E, V> 最短路徑 Dijkstra 算法
邊集 E, 默認點集 V={0,1,...,n-1}node
struct Edge { int i, j; // 表示頂點 i 到頂點 j 的邊 int w; // 邊的權值 boolean visited; // 訪問標誌位 };
BFS
廣度優先遍歷 (Breadth-First Search): 從圖中某個頂點 v 出發,在訪問該頂點後依次訪問 v 的各個未被訪問的鄰接點,而後分別從這些鄰接點出發依次訪問它們的鄰接點,並使「先被訪問的頂點的鄰接點」先於「後被訪問的頂點的鄰接點」被訪問,直至圖中全部已被訪問的頂點的鄰接點都被訪問到;若此時圖中還有頂點未被訪問到,則另選圖中一個未被訪問的頂點做爲起始點,重複上述過程,直至圖中全部頂點都被訪問到。
類比:樹是無向無環圖,樹的層次遍歷是 DFS 的特例
思想:從圖中某個頂點 v 出發,由近至遠依次訪問和 v 有路徑相同且路徑長度爲 1,2,... 的頂點,每一輪遍歷的頂點都與頂點 v 的距離相同。設 dk 爲第 k 個頂點與起始點 v 的距離,容易推導出:BFS 中依序遍歷的頂點 i和 j 有 di <= dj. 利用這個結論能夠求解最短路徑等「最優解」的問題:第一次遍歷到目標頂點所通過的路徑爲最短路徑。
適宜求解:尋找最優解,求解無權圖的最短路徑
優勢:不需回溯。
缺點:在樹的層次較深或頂點的鄰接點較多時,消耗內存十分嚴重。
程序實現 BFS: 用隊列保存每一輪遍歷獲得的頂點;標記已遍歷過的頂點。
走迷宮:最短路徑算法
計算網格中從原點 (0,0) 到指定點 (tr,tc) 的最短路徑長度,1表示該位置能夠通過 {{1,1,0,1}, {1,0,1,0}, {1,1,1,1}, {1,0,1,1}}; class Point { int x, y; public Point(int x, int y) { this.x = x; this.y = y; } } int minPathLength(int[][] grids, int tr, int tc) { if(grids==null || grids.length==0 || grids[0].length==0 || tr>=grids.length || tc>=grids[0].length) { return -1; } // 方向數組,表示 →東E ←西W ↓南S ↑北N 四個方向 final int[][] directions = {{1,0}, {-1,0}, {0,1}, {0,-1}}; final int rows = grids.length, cols = grids[0].length; int result = 0; Queue<Point> queue = new LinkedList<Point>(); queue.add(new Point(0,0)); // 原點 grids[0][0] = 0; // 進隊列時標記,設爲 0 表示已被訪問過 while(!queue.isEmpty()) { result++; int size = queue.size(); while(size-- > 0) { Point cur = queue.poll(); for(int[] dir: directions) { Point next = new Point(cur.x+dir[0], cur.y+dir[1]); if(next.x<0 || next.x>=rows || next.y>=cols || next.y<0 || grids[next.x][next.y]==0) { continue; } if(next.x==tr && next.y==tc) { // 找到位置 return result; } queue.add(next); grids[cur.x][cur.y]=0;// 進隊列時標記,設爲 0 表示已被訪問過 } } } return -1; }
成語接龍的最短單詞路徑 輸入: beginWord = "hit", endWord = "cog", dict = ["hot","dot","dog","lot","log","cog"] 輸出: 5 解釋: 最短的單詞路徑爲 "hit" -> "hot" -> "dot" -> "dog" -> "cog", 故路徑長度爲 5. int ladderLength(String start, String end, Set<String> dict) { if(start==null || end==null || dict==null || dict.size()==0) { return 0; } if(start.equals(end)) return 1; // 構造詞典,包含 start 和 end. List<String> words = new ArrayList<String>(dict); int startIndx = findWord(words, start); int endIndx = findWord(words, end); // 建立以鄰接表表示的圖 List<Integer>[] graph = buildGraph(words); return calculateLength(graph, startIndx, endIndx); } // 計算最短單詞路徑 int calculateLength(List<Integer>[] graph, int startIndx, int endIndx) { boolean[] visited = new boolean[graph.length]; Queue<Integer> queue = new LinkedList<Integer>(); queue.add(startIndx); visited[startIndx] = true; int result = 1; while(!queue.isEmpty()) { result++; int size = queue.size(); while(size-- > 0) { int cur = queue.poll(); for(int next: graph[cur]) { if(next==endIndx) return result; if(!visited[next]) { queue.add(next); visited[next]=true; } } } } return result; } // 建立以鄰接表表示的圖 List<Integer>[] buildGraph(List<String> words) { List<Integer>[] result = new List[words.size()]; for(int i=0; i<words.size(); i++) { result[i] = new ArrayList<Integer>(); for(int j=0; j<words.size(); j++) { if(isConnective(words.get(i), words.get(j))) { result[i].add(j); } } } return result; } // 判斷單詞之間是否能經過改變一個字符而相互轉換 boolean isConnective(String word1, String word2) { if(word1.length()!=word2.length()) return false; int diffCnt = 0; for(int i=0; i<word1.length(); i++) { if(word1.charAt(i)!=word2.charAt(i)) diffCnt++; } return diffCnt==1; } // 在單詞列表中查找單詞 int findWord(List<String> words, String word) { if(words.contains(word)) { int i=0; for(; i<words.size(); i++) { if(word.equals(words.get(i))) break; } return i; } else { words.add(word); return words.size()-1; } }
DFS
深度優先遍歷 (Depth-First Search): 從圖中某個頂點 v 出發,訪問該頂點,而後依次從 v 的未被訪問的的鄰接點出發深度優先遍歷圖,直至圖中全部和 v 有路徑相通的頂點都被訪問到;若此時圖中還有頂點未被訪問到,則另選圖中一個未被訪問的頂點做爲起始點,重複以上過程,直至圖中全部頂點都被訪問到。
類比:樹是無向無環圖,樹的先序優先遍歷是 DFS 的特例
思想:從圖中某個頂點 v 出發,沿着一條通路走到底,若不能達到目標解(沒有未被訪問的頂點),回溯到上一個頂點,沿另外一條通路走到底。
適宜求解 「可達性」 問題:給定初始狀態跟目標狀態,要求判斷從初始狀態到目標狀態是否有解。
優勢:內存消耗小
缺點:難以尋找最優解,僅僅只能尋找是否有解。
程序實現 DFS: 用棧保存當前頂點信息,當遍歷其鄰接點返回後可以繼續遍歷當前頂點(可以使用遞歸棧);標記已遍歷過的頂點。
求 n 個物品的組合狀況:複雜度爲 2^n數組
給定一個包含了一些 0 和 1的非空二維數組 grid, 一個島嶼是由四個方向 (水平或垂直) 的 1 (表明土地) 構成的組合, 你能夠假設二維矩陣的四個邊緣都被水包圍着。找到給定的二維數組中最大的島嶼面積。 輸入: [[0,0,1,0,0,0,0,1,0,0,0,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,1,1,0,1,0,0,0,0,0,0,0,0], [0,1,0,0,1,1,0,0,1,0,1,0,0], [0,1,0,0,1,1,0,0,1,1,1,0,0], [0,0,0,0,0,0,0,0,0,0,1,0,0], [0,0,0,0,0,0,0,1,1,1,0,0,0], [0,0,0,0,0,0,0,1,1,0,0,0,0]] 輸出: 6 int maxAreaOfIsland(int[][] grid) { if(grid==null || grid.length==0 || grid[0].length==0) { return 0; } int maxArea = 0; for(int i=0; i<grid.length; i++) { for(int j=0; j<grid[0].length; j++) { maxArea = Math.max(maxArea , dfs(grid, i, j)); } } } int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}}; // 東南西北 int dfs(int[][] grid, int row, int col) { if(row<0 || row>=grid.length || col<0 || col>=grid[0].length || grid[row][col]==0) { return 0; } grid[row][col]=0; int area = 1; for(int[] dir: dirs) { area += dfs(grid, row+dir[0], col+dir[1]); } return area; }
最小生成樹
在n個城市之間創建通訊網絡, 連通這n個城市只需n-1條線路, 如何使總代價最小.
最小生成樹: 連通網中權值最小的邊必定在最小生成樹中.網絡
Prim算法: 連通網 N={V,{E}}. 初始化頂點集U={u0}, 最小生成樹中邊的集合TE={}; 重複如下步驟: 在全部邊<u,v> ∈ E (u∈U, v∈V-U) 中找一條權值最小的邊 <u0,v0> 併入集合 TE, 同時將 v0 併入集合 U, 直到 U=V 爲止. 最終 TE 中必有 n-1 條邊, 最小生成樹 MST={U,{TE}}.
int prim(int[][] g, int n) { // 鄰接矩陣 g[n][n] int res = 0; int[] dis=new int[n]; // 頂點到最小生成樹的距離 int[] par=new int[n]; // 頂點 i 的父節點 par[i] boolean[] vt=new boolean[n]; // 頂點訪問標誌 vt[0]=true; for(int i=1; i<n; i++) { dis[i]=g[0][i]; } for(int i=1; i<n; i++) { // 在未被訪問的頂點中找到與最小生成樹頂點集合中距離最短的頂點 int k=-1, minDis=Integer.MAX_VALUE; for(int j=0; j<n; j++) if(!vt[j] && minDis>dis[j]){ minDis=dis[j]; k=j; } if(k==-1) return -1; // 加入到最小生成樹的頂點集合裏 vt[k]=true; res+=minDis; // 加入頂點更新各個頂點到最小生成樹的距離 for(int j=0; j<n; j++) if(!vt[j] && dis[j]>g[k][j]) { par[j]=k; dis[j]=g[k][j]; } } return res; }
Kruskal算法:連通網 N={V,{E}}.初始化最小生成樹爲只有 n 個頂點而無邊的非連通圖 T={V, {}}, 圖中 每一個頂點自成一個連通份量; 在 E 中選擇權值最小的邊, 若該邊的兩個頂點落在 T 中不一樣的 連通份量上, 則將此邊加入到 T 中, 不然去掉此邊而選擇權值次小的邊; 依次類推, 直至 T 中全部頂點都在同一連通份量上.
class Edge{ int i, j, cost; // 邊的起點、終點、權值 } public List<Edge> primMST(List<Edge> edges) { if(edges==null || edges.size()<=1) return edges; List<Edge> res = new ArrayList<Edge>(); Collections.sort(edges, new Comparator<Edge>(){ // 將邊按權值的升序排列 public int compareTo(Edge e1, Edge e2) { if(e1.cost!=e2.cost) return e1.cost-e2.cost; if(e1.i==e2.i) return e1.j-e2.j; return e1.i-e2.i; } }); Set<Integer> nodes = new HashSet<Integer>(); for(Edge e: Edges) { nodes.add(e.i); nodes.add(e.j); } int[] id = new int[nodes.size()+1]; for(Edge e: Edges) { int idI = find(id, e.i); int idJ = find(id, e.j); if(idI!=idJ) { id[idI] = idJ; result.add(e); } } if(result.size()==nodes.size()-1) return result; return new ArrayList<Edge>(); } public int find(int[] id, int i) { while(id[i]!=0) i = id[i]; return i; }
拓撲排序
AOV-網:頂點表示活動、弧表示活動間優先關係的有向無環圖;弧 <i, j> 表示從頂點 i 到 j 的有向路徑。
算法:在AOV-網中, 1) 選一個沒有前驅(入度爲0)的頂點且輸出; 2) 在圖中刪除該頂點以及以它爲起點的弧.
重複步驟1)、2) 直至全部頂點都輸出.ui
List<Integer> topologicalSort(List<Integer>[] g) { // 計算每一個頂點的入度 int[] in = new int[g.length]; for(List<Integer> nds: g) for(int nd: nds) in[nd]++; Queue<Integer> s = new LinkedList<Integer>(); // 入度爲 0 的頂點入隊 for(int i=0; i<in.length; i++) if(in[i]==0) s.add(i); List<Integer> res = new ArrayList<Integer>(); while(s.size()>0) { // 每次從隊列中彈出入度爲 0 的頂點 int nd = s.remove(); res.add(nd); // 將該頂點的鄰接點的入度減 1, 若結果爲 0 則將該鄰接點入隊 for(int i: g[nd]) if(--in[i]==0) s.add(i); } if(res.size()==g.length) return res; // 拓撲排序輸出的頂點數少於頂點總數, 則說明 AOE-網中有環 throw new RuntimeException("AOV-Net has rings"); }
關鍵路徑
AOE-網:頂點表示事件、弧表示活動、弧的權值表示活動持續時間的有向無環圖;只有一個起點(入度爲 0 的頂點)和一個終點(出度爲 0 的頂點);定義活動實施的前後順序,決定工程完成的最短期。this
ai 是弧 <j,k> 上的活動: 活動 ai 的持續時間:弧 <j,k> 的權值 dut<j,k>; 事件最先發生時間:ve[j] = max{ve[i]+dut<i,j>} 事件最遲發生時間:vl[i] = min{ve[j]-dut<i,j>} 活動最先發生時間:從起點 v0 到頂點 vj 的最長路徑長度 e[j]=ve[j] 活動最遲發生時間:不推遲工期的最晚開工時間 l[j]=vl[k]-dut<j,k> 關鍵路徑:AOE-網中從起點到終點最長路徑的長度(邊的權值之和) 關鍵活動:關鍵路徑上的活動
算法:code
初始化 AOE-網; 1) 令起點處 ve[0]=0, 按拓撲順序計算每一個頂點的最先發生時間 ve[j]=max{ve[i]+dut<i,j>} 2) 令終點處 vl[n-1]=ve[n-1], 按拓撲逆序計算每一個頂點的最遲發生時間 vl[i]=min{ve[j]-dut<i,j>} 3) 計算每一個弧上活動的最先發生時間 e[i]=ve[i] 和最遲發生時間 l[i]=vl[j]-dut<i,j>, 若 e[i]=l[i] 則將該活動加入到關鍵路徑中。
class Edge{ int end, cost; } List<int[]> criticalPath(List<Edge>[] g) { Stack<Integer> s = new Stack<Integer>(); int[] ve = topologic(g, s); int[] vl = reverseTop(g, s, ve); List<int[]> res = new ArrayList<int[]>(); for(int i=0; i<g.length; i++) for(Edge e: g[i]) if(ve[i]==vl[e.end]-e.cost) res.add(new int[]{i, e.end, e.cost}); return res; } int[] reverseTop(List<Edge>[] g, Stack<Integer> s, int[] ve) { int[] vl = new int[g.length]; Arrays.fill(vl, Integer.MAX_VALUE); vl[g.length-1]=ve[g.length-1]; while(s.size()>0) { int nd = s.pop(); for(Edge e: g[nd]) vl[nd] = Math.min(vl[nd], vl[e.end]-e.cost); } return vl; } int[] topologic(List<Edge>[] g, Stack<Integer> revs) { int[] in=new int[g.length]; for(List<Edge> es: g) for(Edge e: es) in[e.end]++; Stack<Integer> s = new Stack<Integer>(); for(int i=0; i<in.length; i++) if(in[i]==0) s.push(i); int[] ve = new int[g.length]; while(s.size()>0) { int nd = s.pop(); revs.add(nd); for(Edge e: g[nd]) { if(--in[e.end]==0) s.push(e.end); ve[e.end] = Math.max(ve[e.end], ve[nd]+e.cost); } } if(revs.size()<g.length()) throw new RuntimeException("AOE-Net has rings"); return ve; }
最短路徑 Dijkstra 算法排序
1) 鄰接表表示的有向圖中,集合 S 表示源點 v 可達的且找到最短路徑的頂點集合,初始化爲空, 數組元素 dis[i] 表示當前從源點 v 到頂點 vi 的最短路徑長度,初始化爲 dis[i] = g[v][i]. 2) 選擇 vj 使得 dis[j] = min{dis[i]|v∈V-S}, 將頂點 vj 加入到集合 S 中。 3) 修改從 v 出發到 V-S 中任一頂點 vk 可達的最短路徑長度: dis[k]=max{dis[k], dis[j]+g[j][k]}. 4) 重複步驟 2) 和 3) n-1 次。
Map<Integer, String> dijkstraShortestPath(int[][] g, int v) { int[] dis=new int[g.length]; Map<Integer, String> idToPath = new HashMap<Integer, String>(); for(int j=0; j<g[v].length; j++) { path.put(j, ""+j); if(v==j) dis[j]=0; else if(g[v][j]==-1) // j 不可達表示爲 g[v][j]=-1; dis[j]=Integer.MAX_VALUE; else {dis[j]=g[v][j]; idToPath.put(j, v+"-->"+j);} } Queue<Integer> visited = new LinkedList<Integer>(); // v 可達且找到最短路徑的頂點集合 visited.add(v); while(visited.size()<g.length) { int k = getClosestV(dis, visited); if(k==-1) break; visited.add(k); for(int j=0; j<g[k].length; j++) { if(g[k][j]!=-1) { if(dis[j]>dis[k]+g[k][j]) { dis[j]=dis[k]+g[k][j]; idToPath.put(j, idToPath.get(k)+"-->"+j); } } } } for(int j=0; j<dis.length; j++) if(dis[j]==0 || dis[j]==Integer.MAX_VALUE) idToPath.remove(j); else idToPath.put(j, idToPath.get(j)+"("+dis[j]+")"); return idToPath; } int getClosest(int[] dis, Queue<Integer> visited) { int min=Integer.MAX_VALUE, minIndex=-1; for(int i=0; i<dis.length; i++) { if(!visited.contains(i)) { if(dis[i]<min) { min=dis[i]; minIndex=-1; } } } return minIndex; }
最短路徑 Floyd 算法遞歸
1) 初始化最短路徑矩陣 p, 使得 p[i][j]=g[i][j]; 2) vi->vk 表示從 vi 到 vk 的中間頂點序號不大於 k-1 的最短路徑, vk->vj 表示從 vk 到 vj 的中間頂點序號不大於 k-1 的最短路徑, 將已知的 vi->vj 即從 vi 到 vj 的中間頂點序號不大於 k-1 的最短路徑與 vi->vk和vk->vj 比較, 長度較短者是從 vi 到 vj 的中間頂點序號不大於 k 的最短路徑 p(k)[i][j] = min{p(k-1)[i][j], p(k-1)[i][k]+p(k-1)[k][j]} 3) 通過 n 次比較後最終求得 vi->vj 的最短路徑 4) 重複2) 3) 最終求得每對頂點之間的最短路徑
class DisPath { int dis; String path; public DisPath(int dis, String path) { this.dis = dis; this.path = path; } } void floyd(int[][] g) { DisPath[][] p = new DisPath[g.length][g[0].length]; for(int k=0; k<g.length; k++) for(int i=0; i<g.length; i++) for(int j=0; j<g.length; j++) { // 從 i 通過 k 到 j 的一條路徑更短 int temp = (p[i][k].dis==Integer.MAX_VALUE || p[k][j].dis==Integer.MAX_VALUE) ? Integer.MAX_VALUE: p[i][k].dis + p[k][j].dis; if(temp<p[i][j].dis) { p[i][j].dis = temp; p[i][j].path = p[i][k].path + p[k][j].path.substring(1); } } }