一、普里姆演算法
原始碼:普里姆演算法
1,介紹
普里姆演算法是圖結構中尋找最小生成樹的一種演算法。所謂生成樹,即為連通圖的極小連通子圖,其包含了圖中的n個頂點,和n-1條邊,這n個頂點和n-1條邊所構成的樹即為生成樹。當邊上帶有權值時,使生成樹中的總權值最小的生成樹稱為最小代價生成樹,簡稱最小生成樹。最小生成樹不唯一,且需要滿足一下準則:
- 只能使用圖中的邊構造最小生成樹
- 具有n個頂點和n-1條邊
- 每個頂點僅能連線一次,即不能構成迴路
2,案例
看一個應用場景和問題:
- 有勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在需要修路把7個村莊連通
- 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
- 問:如何修路保證各個村莊都能連通,並且總的修建公路總里程最短?
3,思路
4,實現O(n3)
採用三層for迴圈,第一層控制生成的邊的條數,第二層表示已經被訪問的頂點,第三層為未被訪問的頂點。獲取滿足這三者的最小權重值
//最終會生成vertexNum-1條邊,故而只需要遍歷vertexNum-1次。當然也可以多遍歷因為後面會有最小值的控制 for (int k = 1; k < graph.vertexNum; k++) { min = 10000; //表示被訪問的結點 for (int i = 0; i < graph.vertexNum; i++) { //表示未被訪問的結點 for (int j = 0; j < graph.vertexNum; j++) { //如果當前為i結點被訪問,j結點為未被訪問,當前路徑權重小於最小權重 if (visited[i]==1 && visited[j]==0 && graph.weight[i][j] < min ) { //此時記錄最小權重,記錄i結點和j結點 min = graph.weight[i][j]; x = i; y = j; } } } //如果當前結點全部被訪問,則min==10000,否則就有邊未被訪問。 if (min < 10000) { //記錄當前y為被訪問,並輸出此時訪問的邊 visited[y] = 1; System.out.println(getVertex(x) +" -> " +getVertex(y) +" "+ min); } }
5,優化實現O(n2)
int minIndex: 記錄每輪查詢的最小值對應索引 int[] indexArr:記錄每次訪問的結點對應的索引 int[] visited: 記錄訪問狀態(如果為0表示已訪問)和距離。當查詢到最小值對應的索引minIndex後,如果以minIndex為原點到達各頂點的距離 小於 原始visited的值,就更新visited當前索引值
/** * prim演算法(優化) * * @param data 頂點 * @param weight 鄰接矩陣 * @param start 開始頂點 */ public static void prim(char[] data,int[][] weight,int start) { int length = data.length; //記錄各最小節點的索引 int[] indexArr = new int[length]; //如果當前值為0 表示已經被訪問, 其他情況表示未訪問 int[] visited = new int[length]; //獲取start這一行與其他節點的距離 for (int i = 0; i < length; i++) { visited[i] = weight[start][i]; } //標記start行為已訪問 visited[start] = 0; //記錄第一個結點 int index = 0; indexArr[index++] = start; int min; int minIndex ; //記錄權重 int sum = 0; for (int i = 1; i <length; i++) { //記錄每輪最小值 min = 10000; //記錄每輪的最小索引 minIndex = -1; //獲取visited中最小值(非零) for (int j = 0; j < length; j++) { if (visited[j] != 0 && visited[j] < min ) { min = visited[j]; minIndex = j; } } //證明這一輪沒有最小索引,迴圈結束 if (minIndex == -1) { break; } //記錄(最小的索引) indexArr[index++] = minIndex; sum += min; //更新visited中的資料 visited[minIndex] = 0; for (int j = 0; j < length; j++) { //如果當前visited陣列中j的值 > 鄰接矩陣中從minIndex到j的值 更新visited if (visited[j] != 0 && visited[j] > weight[minIndex][j]) { visited[j] = weight[minIndex][j]; } } } System.out.println("總的權重為: " + sum); for (int i = 0; i < length; i++) { System.out.printf("%s\t",data[indexArr[i]]); } System.out.println(); }
二、克魯斯卡爾演算法
原始碼:克魯斯卡爾演算法
1,介紹
- 克魯斯卡爾(Kruskal)演算法,是用來求加權連通圖的最小生成樹的演算法。
- 基本思想:按照權值從小到大的順序選擇 n-1 條邊,並保證這 n-1 條邊不構成迴路
- 具體做法:首先構造一個只含 n 個頂點的森林,然後依權值從小到大從連通網中選擇邊加入到森林中,並使森林中不產生迴路,直至森林變成一棵樹為止
2,案例
看一個應用場景和問題:
- 某城市新增7個站點(A, B, C, D, E, F, G) ,現在需要修路把7個站點連通
- 各個站點的距離用邊線表示(權) ,比如 A – B 距離 12公里
- 問:如何修路保證各個站點都能連通,並且總的修建公路總里程最短?
3,思路
- 對所有的邊按照權值大小進行排序
- 新增權值最小並且不構成迴路的邊
如果判定新新增的邊是否與原來的邊構成迴路?新新增邊的兩個頂點的終點不相同,下面展示如果求指定索引的終點:
/** * 查詢當前索引對應的終點的索引 * 如果當前索引對應的終點為0,則直接返回自己, * 如果不為0記錄向後找當前的終點 迴圈得到其終點 * 例如: E的終點為F D的終點為E(此時會通過E找到F) 那麼就是D和E的終點均為F * @param end 陣列就是記錄了各個頂點對應的終點是哪個,ends 陣列是在遍歷過程中,逐步形成 * @param i 表示傳入的頂點對應的下標 * @return 返回的就是 下標為i的這個頂點對應的終點的下標 */ public int getEnd(int[] end,int i) { while (end[i] != 0) { i = end[i]; } return i; }
4,程式碼實現
遍歷圖的右半邊,將其構建成一個儲存所有邊的陣列。對邊進行排序(升序),遍歷每條邊獲取其start和end的終點是否相同,不同就加入結果集,並更新終點陣列。
/** * 克魯斯卡爾演算法 */ public void kruskal() { System.out.println("排序前:"+Arrays.toString(edges)); sort(edges); System.out.println("排序後:"+Arrays.toString(edges)); //最終的結果肯定是length-1條 Edge[] res = new Edge[vertexs.length-1]; int index = 0; for (int i = 0; i < edges.length; i++) { int start = getIndexByChar(edges[i].start); int end = getIndexByChar(edges[i].end); //獲取start結點的終點 int startParent = getEnd(ends, start); //獲取end結點的終點 int endParent = getEnd(ends, end); //如果兩個結點的 終點不是同一下標 則記錄 if (startParent != endParent) { //將當前邊加入到結果集中 res[index++] = edges[i]; //同時更新start結點的終點為end結點的終點 ends[startParent] = endParent; } } for (int i = 0; i < res.length; i++) { System.out.println(res[i]); } }
三、迪傑斯特拉演算法
原始碼:迪傑斯特拉演算法
1,介紹
迪傑斯特拉(Dijkstra)演算法是典型最短路徑演算法,用於計算一個結點到其他結點的最短路徑。它的主要特點是以起始點為中心向外層層擴充套件(廣度優先搜尋思想),直到擴充套件到終點為止。
2,案例
看一個應用場景和問題:
- 戰爭時期,勝利鄉有7個村莊(A, B, C, D, E, F, G) ,現在有六個郵差,從G點出發,需要分別把郵件分別送到 A, B, C , D, E, F 六個村莊
- 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
- 問:如何計算出G村莊到 其它各個村莊的最短距離?
- 如果從其它點出發到各個點的最短距離又是多少?
3,思路
- 從指定起點開始,找出所有鄰接節點,更新起點到鄰接節點路徑權值和記錄的前驅節點,從中選出路徑權值最小的一個節點,作為下一輪的起點
- 從次輪起點開始,重複第一輪的操作
- 每一輪更新記錄的路徑權值,是把 “記錄得原始起點到該目標節點的路徑總權值” 與 “記錄中原始起點到本輪起點的路徑權值 + 本輪起點到鄰接節點的權值” 比較,保留小的
- 更新了權值的同時要記得更新路徑終點的前驅節點
- 每一輪都將此輪的起點設定為已訪問,並且尋找鄰接節點時也要跳過那些已訪問的
- 所有節點都"已訪問"時結束
4,程式碼實現
思路:1,構建一個vistedvetex物件(pre陣列記錄到達當前頂點的上一個頂點索引;already陣列記錄當前頂點是否被訪問;dis陣列記錄從初始頂點出發到達當前頂點的距離) 2,初始化根據初始頂點,構建:除初始頂點以外所有dis均為最大值(65535),除初始頂點以外的所有頂點均為被訪問 3,以index(第一次index為初始頂點,之後均為獲取當前dis陣列中未被訪問的最短距離為index,並標記當前index為已訪問) 作為基準點,獲取當前dis中index的值(該值則為從初始結點到index的距離) + index到i(遍歷圖變化) 距離之和 記為len, 用len與dis中i的值進行比較:如果len < dis[i],則說明以index作為基準點 更為合適,此時更新dis[i] = len ; pre[i] = index
/** * 迪傑斯特拉演算法 * * @param index 初始頂點的座標 */ public void dsj(int index) { //初始化visitedVetex vv = new VisitedVetex(index, vertex.length); //以index作為介質,更新visitedvetex中的dis和pre update(index); //記錄當前儲存在dis陣列中 並 未被訪問的最小節點的距離 int i; //如果當前i==-1 ,則表示dis陣列中所有的資料均被訪問過 while ((i = vv.updateArr()) != -1) { //以i作為介質 更新vv update(i); } //列印 vv.show(); } /** * 更新以當前結點為 介質 的visitedvetex的 dis和pre陣列 * * @param index 當前結點的索引 */ private void update(int index) { //遍歷以index作為結點 獲取 與其連線的所有結點 for (int i = 0; i < matrix[index].length; i++) { //len為 從index到i結點的鄰接距離 + 儲存在dis陣列中的起始點到index的距離 int len = matrix[index][i] + vv.dis[index]; //如果len < 儲存在dis陣列中的起始點到i 結點的距離 並且當前i結點未被訪問過(只有i在dis陣列中為最小的數時才會被訪問) if (len < vv.dis[i] && !vv.isVisited(i)) { //更新dis陣列中 起始點到i的距離為len vv.updateDis(i, len); //修改當前i結點的前驅結點為index vv.updatePre(i, index); } } }
四、弗洛伊德演算法
原始碼:弗洛伊德演算法
1,介紹
- 弗洛伊德(Floyd)演算法也是一種用於尋找給定的加權圖中頂點間最短路徑的演算法
- 弗洛伊德演算法(Floyd)計算圖中各個頂點之間的最短路徑
- 迪傑斯特拉演算法用於計算圖中某一個頂點到其他頂點的最短路徑。
- 弗洛伊德演算法 VS 迪傑斯特拉演算法:
- 迪傑斯特拉演算法通過選定的被訪問頂點,求出從出發訪問頂點到其他頂點的最短路徑;
- 弗洛伊德演算法中每一個頂點都是出發訪問點,所以需要將每一個頂點看做被訪問頂點,求出從每一個頂點到其他頂點的最短路徑。
2,案例
- 勝利鄉有7個村莊(A, B, C, D, E, F, G)
- 各個村莊的距離用邊線表示(權) ,比如 A – B 距離 5公里
- 問:如何計算出各村莊到其它各村莊的最短距離?
3,思路
- 設定頂點vi到頂點vk的最短路徑已知為Lik,頂點vk到vj的最短路徑已知為Lkj,頂點vi到vj的路徑為Lij,則vi到vj的最短路徑為:min((Lik+Lkj),Lij),vk的取值為圖中所有頂點,則可獲得vi到vj的最短路徑
- 至於vi到vk的最短路徑Lik或者vk到vj的最短路徑Lkj,也是以同樣的方式獲得
4,程式碼實現
/** * 弗洛伊德演算法 : 以i為中間點 比較j->i->k 的距離與 j->k的距離 * 如果採用i作為中間頂點距離短則記錄i為前驅頂點,並更新距離 * 如果是j->k 距離更短 則直接記錄j * @param vetex 頂點集合 * @param w 鄰接矩陣 */ public static void floyd(char[] vetex, int[][] w) { //儲存前驅結點 char[][] pre = new char[vetex.length][vetex.length]; // 對pre陣列初始化, 注意存放的是前驅頂點的下標 for (int i = 0; i < vetex.length; i++) { Arrays.fill(pre[i], vetex[i]); } //作為中間結點 for (int i = 0; i < vetex.length; i++) { //初始結點 for (int j = 0; j < vetex.length; j++) { //終點 for (int k = 0; k < vetex.length; k++) { //j->i->k的距離 int len = w[j][i] + w[i][k]; if (w[j][k] > len) { w[j][k] = len; //記錄前驅結點為中間結點 pre[j][k] = pre[i][k]; } } } } }
五、馬踏棋盤演算法(騎士周遊問題)
原始碼:馬踏棋盤演算法
1,介紹
將馬隨機放在國際象棋的8×8棋盤Board[0~7][0~7]
的某個方格中,馬按走棋規則(馬走日字)進行移動。要求每個方格只進入一次,走遍棋盤上全部64個方格
2,思路
- 馬踏棋盤問題(騎士周遊問題)實際上是圖的深度優先搜尋(DFS)的應用。
- 編碼思路
- 建立棋盤 chessBoard,是一個二維陣列
- 將當前位置設定為已經訪問,然後根據當前位置,計算馬兒還能走哪些位置,並放入到一個集合中(ArrayList),下一步可選位置最多有8個位置, 每走一步,就使用 step+1
- 遍歷ArrayList中存放的所有位置,看看哪個可以走通,如果走通,就繼續,走不通,就回溯
- 判斷馬兒是否完成了任務,使用 step 和應該走的步數比較 , 如果沒有達到數量,則表示沒有完成任務,將整個棋盤置 0
- 注意:馬兒不同的走法(策略),會得到不同的結果,效率也會有影響(優化)
3,程式碼實現
/** * 馬踏棋盤演算法核心 * * @param x 初始棋子在棋盤的哪一行 * @param y 初始棋子在棋盤的哪一列 * @param step 當前是第幾步 */ public void start(int x, int y, int step) { //首先儲存對應的步數 chess[x][y] = step; //獲取當前x,y對應的 visited陣列中的索引位置 int index = x * Y + y; //標記為已訪問 visited[index] = true; //構建當前point Point currentPoint = new Point(x, y); List<Point> ps = next(currentPoint); //對ps進行排序,排序的規則就是對ps的所有的Point物件的下一步的位置的數目,進行非遞減排序 sort(ps); while (!ps.isEmpty()) { //將第一個元素移除 Point removePoint = ps.remove(0); //未被訪問過,則呼叫當前 if (!visited[(removePoint.x) * Y + removePoint.y]) { start(removePoint.x, removePoint.y, step + 1); } } //回溯 if (step < X * Y && !finished) { chess[x][y] = 0; visited[index] = false; } else { finished = true; } } /** * 對ps集合進行非遞減排序 * @param ps 集合 */ private void sort(List<Point> ps) { ps.sort((p1, p2) -> { //獲取當前p1和p2 各自對應的 List<Point> next1 = next(p1); List<Point> next2 = next(p2); return next1.size() - next2.size(); }); } private List<Point> next(Point currentPoint) { List<Point> ps = new ArrayList<>(); int x, y; //0 if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y + 2) < Y) { ps.add(new Point(x, y)); } //1 if ((x = currentPoint.x + 1) < X && (y = currentPoint.y + 2) < Y) { ps.add(new Point(x, y)); } //2 if ((x = currentPoint.x + 2) < X && (y = currentPoint.y + 1) < Y) { ps.add(new Point(x, y)); } //3 if ((x = currentPoint.x + 2) < X && (y = currentPoint.y - 1) >= 0) { ps.add(new Point(x, y)); } //4 if ((x = currentPoint.x + 1) < X && (y = currentPoint.y - 2) >= 0) { ps.add(new Point(x, y)); } //5 if ((x = currentPoint.x - 1) >= 0 && (y = currentPoint.y - 2) >= 0) { ps.add(new Point(x, y)); } //6 if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y - 1) >= 0) { ps.add(new Point(x, y)); } //7 if ((x = currentPoint.x - 2) >= 0 && (y = currentPoint.y + 1) < Y) { ps.add(new Point(x, y)); } return ps; }