一,尋路問題介紹
正如組合問題與動態規划的聯系之應用提到的從起點(0,0)到終點(X,Y)一共有多少種走法。與之相似的另一個問題是如何找到從(0,0)到(X,Y)的路徑?
首先對問題建模。使用一個矩陣(二維數組)的下標 表示 各個點的坐標。矩陣元素只取 0 或者 1,0 表示此坐標是一個可達的正常頂點;而 1 則表示這是一個不可達的障礙頂點。比如 如下矩陣:
{0,0,1,1,0}
{1,0,0,0,0}
{0,1,0,0,0}
{0,0,1,1,0}
{0,1,0,0,0}
從最右上角的頂點(坐標是(0,4)) 到左下的頂點(坐標是(4,0))是 沒有 路徑的,因為路徑不能穿過障礙頂點(4個)
而從最左上角的頂點(坐標是(0,0))到最右下的頂點(坐標是(4,4))是可達的。比如,如下就是頂點坐標就構成了一條可達的路徑:
<0,0> <0,1> <1,1> <1,2> <2,2> <2,3> <2,4> <3,4> <4,4>
而本文討論的是:如何 尋找一條從起點(0,0)到終點(X,Y)的可達路徑?為了簡便起見,每步只允許向右走,或者向下走。
在這里,默認(X,Y)是可達頂點,因為若(X,Y)是不可達頂點(坐標(x,y)處元素值為1),就沒有太大的討論意義了。
此外,看到了上面的矩陣,是不是想到了圖的鄰接矩陣表示法?雖然二者的表達的意思有點不一樣,但圖的基本操作如判斷一個頂點到另一個頂點的最短路徑問題 與本文中的問題還是很相似的。
二,思路分析
提到尋路算法,不得不提到A*算法。由於對A*算法不是太了解,就不詳細介紹了。但是A*算法肯定也是可以解決本文的尋路問題的。
在本文中,起點是(0,0);終點是(X,Y)。
①由於每步只能向右走或者向下走,對於(X,Y)而言,只有兩種情況到達它。一種是從(X-1,Y)向右走一步到達;另一種是(X,Y-1)向下走一步到達。
這個分析,和組合問題與動態規划的聯系之應用的分析一樣。唯一的不同是,我們需要記錄已經走過的頂點。
因此,類似地也有兩種編程實現方式:遞歸方式和動態規划。
對於動態規划而言,其實就是把已經走過的頂點的“狀態”保存起來,這里的頂點的“狀態”表示的是:是否存在一條路徑,能夠從該頂點到達終點。
這也是為什么動態規划要比遞歸解運行得快 的本質原因。因為,遞歸求解該問題時,會有大量的重疊子問題。但是遞歸還是一 一地計算這些重疊的子問題,也就是說遞歸沒有“記憶性”,對於同一個問題出現了若干次,遞歸解法是每出現一次,就計算一次。而動態規划則是只計算一次,並把計算的結果保存起來,后面再碰到該問題時,直接“查表”找出上次計算保存的結果即可。另外可參考:從 活動選擇問題 看動態規划和貪心算法的區別與聯系
首先來看遞歸解:
1 //尋找起點(0,0)到終點(x,y)的一條路徑 2 public boolean findPath(int x, int y, ArrayList<Point> paths) 3 { 4 Point p = new Point(x, y); 5 paths.add(p);//默認終點p的坐標對應的 數組值不是1 6 7 //base condition 8 if(x == 0 && y == 0) 9 return true; 10 11 boolean isSuccess = false; 12 if(y >= 1 && checkFree(x, y - 1)) 13 isSuccess = findPath(x, y - 1, paths); 14 if(!isSuccess && x >= 1 && checkFree(x - 1, y)) 15 isSuccess = findPath(x - 1, y, paths); 16 17 if(!isSuccess) 18 paths.remove(p);//O(N) 19 return isSuccess; 20 }
Point類封裝了點的坐標(橫坐標和縱坐標),ArrayList<Point> paths 用來 保存走過的各個頂點的坐標,從而將整個路徑記錄下來。
第5行首先就把終點(x,y)添加到路徑中去--這里默認了終點是可達的,即終點坐標的矩陣值為0
第8-9行是遞歸的基准條件。也就是說:到了起點(0,0)時,遞歸就結束了。
第12-13行是判斷是否可以從(x,y-1)向下走一步到達(x,y)。if 條件中 y>=1,因為若 y < 1,說明已經不能再往下走了,再往下走,y坐標(縱坐標)就小於0了。
如果12-13行遞歸返回false,說明:最終未找到一條路徑到達終點。故在第14-15行,變換尋找方向:判斷是否可以從(x-1,y)到達(x,y)
第17行,表示:不存在路徑使得:當前頂點p 到終點(X,Y)是可達的。因此,需要將 p 從ArrayList中刪除。(理解遞歸)
再來看看動態規划的實現:
1 //另一種動態規划解決方案,它用HashMap緩存已經檢查過的頂點是否可達終點 2 public boolean findPath_dp2(int x, int y, ArrayList<Point> paths, HashMap<Point, Boolean> cache) 3 { 4 Point p = new Point(x, y); 5 6 //先查表. 7 if(cache.containsKey(p)) 8 return cache.get(p); 9 10 paths.add(p); 11 12 if(x == 0 && y == 0) 13 return true; 14 boolean isSuccess = false; 15 if(x >= 1 && checkFree(x - 1, y)) 16 isSuccess = findPath_dp2(x - 1, y, paths, cache); 17 if(!isSuccess && y >= 1 && checkFree(x, y - 1)) 18 isSuccess = findPath_dp2(x, y - 1, paths, cache); 19 20 if(!isSuccess) 21 paths.remove(p); 22 cache.put(p, isSuccess); 23 return isSuccess; 24 }
①使用HashMap<Point,Boolean>保存頂點Point是否至少存在一條路徑可以到達終點。假設頂點<p1,true>,則表示頂點p1到終點(X,Y)是可達的。
②這里的動態規划實現,也是方法的遞歸調用。但是,與上面的遞歸實現中的遞歸調用有本質不同。
這里在第7-8行,如果cache已經保存某個頂點,則直接返回結果。這就是動態規划中的“查表”。其它代碼的實現與遞歸差不多。需要注意的是:
在第22行,不管isSuccess為true還是False,只要訪問了頂點p,就把該頂點 p的結果保存起來。從而使得下一次碰到頂點p時,可直接查表。
比如說:要找到一條到達 (x,y)的路徑,就要找出到它的相鄰點(x-1,y) 和 (x,y-1)的路徑。再看看與(x-1,y) 和 (x,y-1) 相鄰的頂點坐標是:
(x-2,y)、(x-1,y-1)、(x-1,y-1)、(x,y-2)。(x-1,y-1)出現了兩次,這就是重疊子問題。
當第一次訪問(x-1,y-1)時,計算出了該頂點是否可達(x,y),當下次再訪問 (x-1,y-1)時,動態規划就直接查表獲得結果了,而遞歸實現則是又執行了一次遞歸調用。
另外,還有一種動態規划的實現方式。它不是用HashMap來保存已經訪問過的頂點的結果,而是使用一個二維數組來保存某個頂點是否可達終點(x,y)
並根據狀態方程: path(X,Y)=hasPath{path(X-1,Y) , path(X,Y-1)} 判斷某頂點是否可到達(x,y)
1 public boolean findPath_DP(int x, int y, ArrayList<Point> paths){ 2 //if dp[i][j]=true, exist at least one path from <i,j> to <x,y>(destination) 3 boolean[][] dp = new boolean[x+1][y+1];//"狀態矩陣"保存 各點的可達情況 4 // //init 5 // for(int i = x - 1; i >= 0; i--){ 6 // for(int j = y - 1; j >= 0; j--){ 7 // dp[i][j] = false; 8 // } 9 // } 10 dp[x][y] = true;//init,初始時終點坐標肯定是可達的.因為 martix[x][y]==0 11 12 for(int i = x; i >= 0; i--){ 13 for(int j = y; j >= 0; j--){ 14 if(dp[i][j])//只有 從可達的點開始(初始時為終點)判斷前面一個頂點是否可以到達本頂點 15 { 16 if(i >= 1 && checkFree(i-1, j)) 17 dp[i-1][j] = true; 18 if(j >= 1 && checkFree(i, j-1)) 19 dp[i][j-1] = true; 20 } 21 } 22 } 23 24 /* 25 * findPath recursively using dp "state martix" 26 * 它是通過查表 而不是 遞歸調用 判斷 從某個頂點到終點是否可達 27 */ 28 return getPath(x, y, dp, paths); 29 } 30 private boolean getPath(int x, int y, boolean[][] dp, ArrayList<Point> paths){ 31 Point p = new Point(x, y); 32 paths.add(p); 33 34 if(dp[x][y] == false)//查表 判斷 從<x,y>是否可達終點 35 return false; 36 37 //base condition 38 if(x == 0 && y == 0) 39 return true; 40 41 boolean isSuccess = false; 42 if(x >= 1 && (dp[x - 1][y] == true)) 43 isSuccess = getPath(x-1, y, dp, paths); 44 if(!isSuccess && y >= 1 && dp[x][y-1] == true) 45 isSuccess = getPath(x, y-1, dp, paths); 46 if(!isSuccess) 47 paths.remove(p); 48 return isSuccess; 49 }
①狀態矩陣dp[][]對應每個頂點的坐標,第12行-22行檢查每個頂點是否存在路徑可以到達終點。
②檢查完后,在第28行,調用getPath()來找出一條從起點到達終點的路徑。可以看出,getPath()尋找路徑時,是直接“查表”判斷出該頂點是否可達終點的(第34-35行)
而且可以看出,第12-22行的時間復雜度為O(N^2),而遞歸調用的時間復雜度為O(2^N)
三,整個完整代碼實現如下:

import java.util.ArrayList; import java.util.HashMap; public class FinaPath { private int[][] martix; private class Point{ int x;//橫坐標 int y;//縱坐標 public Point(int x, int y) { this.x = x; this.y = y; } } public FinaPath(int[][] martix) { this.martix = martix; } //尋找起點(0,0)到終點(x,y)的一條路徑 public boolean findPath(int x, int y, ArrayList<Point> paths) { Point p = new Point(x, y); paths.add(p);//默認終點p的坐標對應的 數組值不是1 //base condition if(x == 0 && y == 0) return true; boolean isSuccess = false; if(y >= 1 && checkFree(x, y - 1)) isSuccess = findPath(x, y - 1, paths); if(!isSuccess && x >= 1 && checkFree(x - 1, y)) isSuccess = findPath(x - 1, y, paths); if(!isSuccess) paths.remove(p);//O(N) return isSuccess; } private boolean checkFree(int x, int y){ return martix[x][y] == 0;//0 表示有路 } public boolean findPath_DP(int x, int y, ArrayList<Point> paths){ //if dp[i][j]=true, exist at least one path from <i,j> to <x,y>(destination) boolean[][] dp = new boolean[x+1][y+1];//"狀態矩陣"保存 各點的可達情況 // //init // for(int i = x - 1; i >= 0; i--){ // for(int j = y - 1; j >= 0; j--){ // dp[i][j] = false; // } // } dp[x][y] = true;//init for(int i = x; i >= 0; i--){ for(int j = y; j >= 0; j--){ if(dp[i][j])//只有 從可達的點開始(初始時為終點)判斷前面一個頂點是否可以到達本頂點 { if(i >= 1 && checkFree(i-1, j)) dp[i-1][j] = true; if(j >= 1 && checkFree(i, j-1)) dp[i][j-1] = true; } } } /* * findPath recursively using dp "state martix" * 它是通過查表 而不是 遞歸調用 判斷 從某個頂點到終點是否可達 */ return getPath(x, y, dp, paths); } private boolean getPath(int x, int y, boolean[][] dp, ArrayList<Point> paths){ Point p = new Point(x, y); paths.add(p); if(dp[x][y] == false)//查表 判斷 從<x,y>是否可達終點 return false; //base condition if(x == 0 && y == 0) return true; boolean isSuccess = false; if(x >= 1 && (dp[x - 1][y] == true)) isSuccess = getPath(x-1, y, dp, paths); if(!isSuccess && y >= 1 && dp[x][y-1] == true) isSuccess = getPath(x, y-1, dp, paths); if(!isSuccess) paths.remove(p); return isSuccess; } //另一種動態規划解決方案,它用HashMap緩存已經檢查過的頂點 public boolean findPath_dp2(int x, int y, ArrayList<Point> paths, HashMap<Point, Boolean> cache) { Point p = new Point(x, y); //先查表. if(cache.containsKey(p)) return cache.get(p); paths.add(p); if(x == 0 && y == 0) return true; boolean isSuccess = false; if(x >= 1 && checkFree(x - 1, y)) isSuccess = findPath_dp2(x - 1, y, paths, cache); if(!isSuccess && y >= 1 && checkFree(x, y - 1)) isSuccess = findPath_dp2(x, y - 1, paths, cache); if(!isSuccess) paths.remove(p); cache.put(p, isSuccess); return isSuccess; } //test public static void main(String[] args) { //0表示有路,1表示障礙 int[][] martix = { {0,0,1,1,0}, {1,0,0,0,0}, {0,1,0,0,0}, {0,0,1,1,0}, {0,1,0,0,0} }; FinaPath fp = new FinaPath(martix); ArrayList<Point> paths = new ArrayList<FinaPath.Point>(martix.length + martix[0].length); int endPivot_x = 4; int endPivot_y = 4; boolean hasPath = fp.findPath(endPivot_x, endPivot_y, paths); if(hasPath) printPath(paths); else System.out.println("recursive finds no path"); System.out.println(); ArrayList<Point> paths_dp = new ArrayList<FinaPath.Point>(); boolean hasPath_DP = fp.findPath_DP(endPivot_x, endPivot_y, paths_dp); if(hasPath_DP) printPath(paths_dp); else System.out.println("dp finds no path"); System.out.println(); ArrayList<Point> paths_dp2 = new ArrayList<FinaPath.Point>(); HashMap<Point, Boolean> cache = new HashMap<FinaPath.Point, Boolean>(endPivot_y + endPivot_x); boolean hasPath_DP2 = fp.findPath_dp2(endPivot_x, endPivot_y, paths_dp2, cache); if(hasPath_DP2) printPath(paths_dp2); else System.out.println("dp2 finds no path"); } private static void printPath(ArrayList<Point> paths){ for(int i = paths.size() - 1; i >= 0; i--) { System.out.print("<" + paths.get(i).x + "," + paths.get(i).y + ">"); System.out.print(" "); } } }