尋路問題--如何找到一條從起點坐標到終點坐標的路徑?


一,尋路問題介紹

正如組合問題與動態規划的聯系之應用提到的從起點(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("  ");
        }
    }
}
View Code

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM