1. 程式人生 > >遞迴演算法深入淺出五:深度搜索尋找圖最短路徑

遞迴演算法深入淺出五:深度搜索尋找圖最短路徑

如果你看到這篇文章並不是在我的CSDN部落格釋出,同時文章裡面的圖片、URL全沒了的,那麼,很有可能你上了一個爬蟲網站!

在此,我建議你馬上關閉該頁面!因為爬蟲或多或少都會出現內容的紕漏,對讀者造成的危害更大,誤人子弟。
同時,轉載本文的請加上本文連結:
對於爬蟲網站隨意爬取以及轉載不加本文連結的,本人保留追究法律責任的權力!
對於不尊重版權的行為,我們也沒必要客氣!

深度優先搜尋

又稱深度搜索、深搜。簡單地說深搜就是一種**【不撞南牆不回頭】** 的 暴力演算法,基本上該演算法常用遞迴作為設計基礎,當然也有使用for迴圈巢狀的,本文是以遞迴為講解方向的。
至於更深一層的理論在這裡就不詳細說明了,詳細可以去搜索更多關於。

簡單的圖搜尋問題

本文講述的是一個基於無向圖為基礎的圖搜尋,用二位陣列組成的圖。
【關於圖的更多的理論也麻煩大家去搜索相關的資料,今天寫這個文章主要針對下面描述的問題,在這裡不過多闡述】

問題描述

描述如下:
在一個n行m列組成的二位陣列中,每個單元格代表空地障礙物
鄰接的單元格距離單位為1,但不包括對角的單元格。
圖中是屬於無向圖,移動的方向不受限制(不能出界)。
現在給定在圖中任意的兩個座標(兩個均座標不屬於障礙物),求出兩個座標之間到達的最短距離
 
如圖:
這是一個6行5列的圖,其中 (1,2)、(3,2)、(3,3) 、(4,1)、(4,2) 為障礙物

 
求A點到B點的最短距離

問題分析

問題中可以知道這是一個由二維陣列組成的圖,每個單元格代表空地或者障礙物。
現在要從A點到達B點或者從B點到達A點,行走的方向可以是(上、下、左、右),同時要避開所有紅色的障礙物(如上圖)。首先要明白每走一步所到達的位置:

  • 當在A點(0,0)時,下一步能到達的點為**(0,1)、(1,0)**
  • 當在點**(0,1)時,下一步能到達的點為(0,0)、(1,1)、(1、2)**
  • 當在點**(1,0)時,下一步能到達的點為(0,0)、(2,0)、(1、1)**
  • 當在點**(1,1)時,由於(1,2)為障礙物**,因此下一步能到達的點為**(0,1)、(1,0)、(2,1)**
  • 每一步都去嘗試下一步可以到達的位置,直到到達終點B。

有個問題就來了,例如上面的,有的位置是已經被走過的,如果程式沒有對走過的位置進行判斷,那麼可能永遠都不能到達B點...
應該怎麼做呢?定義一個結果集來記錄當前訪問過的點。
請慢慢往下看,別急!

遞迴程式設計思路

一、定義

1. 設定地圖

我們可以用一個二維的 int 陣列來表示該圖,我們假設在陣列中:
  • 值為 1 的是障礙物
  • 值為 0 的是為空地
  • 注意:使用二維int陣列的原因是:如果需要,可以用數字表示不同型別的障礙物,本問題中可以用boolean陣列表示空地或障礙物,但為了讓大家更加清晰不和下面的 boolean[][] used 二維標記陣列弄混,還是使用 int[][] map 來定義。

那麼就可以得出下列二維陣列:

0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 1 1 0
0 1 1 0 0
0 0 0 0 0

2. 首先是定義需要用上的變數:

  • int n,m:定義圖的大小。
  • int[][] map:需要搜尋的圖(在這裡用int[][]二維陣列表示)
  • boolean[][] used:大小和圖一樣,用於標記被訪問過的點(訪問過為true),保證每次走的都是沒有被走過的點,這也是解決上面的重複訪問同一個點的問題的方案
  • int p,q:終點的Y軸、X軸座標,由使用者輸入。
  • int count:計算由起點到終點所有的可行路徑。
  • int minStep:記錄最短路徑所需要的步數,因為要考慮起點和終點為同一個點,因此設定初始值為 -1。

3.設定一個 dfsMap(...) 方法,該方法主要用於深度搜索圖。

除了上面設定的變數,dfsMap(...) 需要管理的引數有:
  • int x:當前點所在的X軸座標值
  • int y:當前點所在的Y軸座標值
  • int step:當前點與開始點的距離

二、程式碼編寫的思路

當我們在一個點時,需要做的是要判斷當前所在的點是否為終點,如果是終點,那麼就對歷史記錄進行判斷,程式碼如下:

if (y == p && x == q) {
	System.out.println("找到一條路徑,距離為:" + step);
	count++;
	if (minStep == -1) {
		minStep = step;
	}
	if (step < minStep) {
		minStep = step;
	}
}

        如果不是終點,那麼程式就要去尋找當前點的下一步;同時,我們需要用上boolean[][] used二維陣列,大小和當前的地圖一樣,用於標記當前地圖中哪些點被訪問過,如果被訪問過,那麼就跳過該點。

假設目前所在點的位置為 (x,y),那麼可以得出下一步可到達的點為:
(x+1, y)、(x-1, y)、(x, y+1)、(x, y-1),如下圖:


轉換成程式碼形式就是,該陣列可以定義為全域性靜態變數:

int[][] wayPoint = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};

根據上面四個點,用表格來表示(X,Y)座標的變化量更為直觀:(以行為變化單位)

X
Y
代表
0
1
向下
0
-1
向上
1
0
向右
-1
0
向左

用迴圈就能得到以(X,Y)為中心的周邊四個點。
但得出這些點並不能一下子就進行遞迴尋路操作,要確定這些點是不是能夠“走”得到,需要對其進行邊界障礙物以及該點是否被訪問過判斷。程式碼如下:

for (int i = 0; i < 4; i++) {
	int gX, gY;//新的座標位置
	gX = x + wayPoint[i][0];//獲取每行的第0列,即上面表格中X的變化值
	gY = y + wayPoint[i][1];//獲取每行的第1列,即上面表格中Y 的變化值	

	//判斷越界
	if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
		continue;
	}

	//判斷障礙物,以及該點是否被訪問過
	if (map[gY][gX] == 1 || used[gY][gX] == true) {
		continue;
	}
	
	....
}

三、得到dfs(...)遞迴體程式碼

        在對新的點進行判斷後,就確定該點是能到達的,那麼就可以 將當前的結果集(即當前深度搜索所走過的位置的集合) 進行遞迴,繼續交給 dfsMap(...) 方法進行迭代尋找。
在進入該點之前,我們需要標記該點已經被訪問過,同時在遞迴結束之後要對標記進行消除, dsfMap(int, int ,int) 核心程式碼如下:

/**
 * @param x    當前所處的X軸座標
 * @param y    當前所處的Y軸座標
 * @param step 距離
 */
static void dfsMap(int x, int y, int step) {
	if (y == p && x == q) {
		//System.out.println("找到一條路徑,距離為:" + step);
		count++;
		if (minStep == -1) {
			minStep = step;
		}
		if (step < minStep) {
			minStep = step;
		}
	} else {
		for (int i = 0; i < 4; i++) {
			int gX, gY;//新的座標位置
			gX = x + wayPoint[i][0];//獲取每行的第0列,即上面表格中X的變化值
			gY = y + wayPoint[i][1];//獲取每行的第1列,即上面表格中Y 的變化值

			//判斷越界
			if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
				continue;
			}

			//判斷障礙物,以及該點是否被訪問過
			if (map[gY][gX] == 1 || used[gY][gX] == true) {
				continue;
			}

			used[gY][gX] = true;
			dfsMap(gX, gY, step + 1);
			used[gY][gX] = false;
		}
	}
}

測試

最終完整的程式碼如下:

//此文老貓原創,轉載請加本文連線:
//http://blog.csdn.net/nthack5730/article/details/71774434
//更多有關老貓的文章:http://blog.csdn.net/nthack5730
public class SearchMap {
    static int[][] map;
    static boolean[][] used;

    //圖面積設定
    static int n;
    static int m;

    //需要尋找的點
    static int p;
    static int q;

    //最小位置
    static int minStep = -1;

    //次數統計
    static int count = 0;

    static int[][] wayPoint = {{0, 1}, {0, -1}, {1, 0}, {-1, 0}};


    public static void main(String[] args) {
        Scanner scan = new Scanner(System.in);
        System.out.println("輸入圖的行、列:");
        n =scan.nextInt();
        m =scan.nextInt();

        System.out.println("輸入開始點的座標:");
        int startX = scan.nextInt();
        int startY = scan.nextInt();

        System.out.println("輸入終點的座標:");
        p =scan.nextInt();
        q =scan.nextInt();

        map = new int[n][m];
        used = new boolean[n][m];

        System.out.println("輸入圖資料:");
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                map[i][j] = scan.nextInt();
            }
        }

		//遞迴呼叫開始
        used[startY][startX] = true;//初始化開始點被訪問過,注意Y值代表行,X值代表列
        dfsMap(startX, startY, 0);

		//輸出結果
        System.out.println("\n//=============================");
        System.out.println("// 找到的總路徑數為:" + count);
        if (minStep == -1) {
            System.out.println("// 沒有找到結果");
        } else {
            System.out.println("// 最小距離為:" + minStep);
        }
		System.out.println("\n//=============================");
    }


    /**
     * 深搜暴力尋圖
     *
     * @param x    當前所處的X軸座標
     * @param y    當前所處的Y軸座標
     * @param step 距離
     */
    static void dfsMap(int x, int y, int step) {
        if (y == p && x == q) {
//            System.out.println("找到一條路徑,距離為:" + step);
            count++;
            if (minStep == -1) {
                minStep = step;
            }
            if (step < minStep) {
                minStep = step;
            }
        } else {
            for (int i = 0; i < 4; i++) {
                int gX, gY;//新的座標位置
                gX = x + wayPoint[i][0];//獲取每行的第0列,即上面表格中X的變化值
                gY = y + wayPoint[i][1];//獲取每行的第1列,即上面表格中Y 的變化值

                //判斷越界
                if (gX < 0 || gX >= m || gY < 0 || gY >= n) {
                    continue;
                }

                //判斷障礙物,以及該點是否被訪問過
                if (map[gY][gX] == 1 || used[gY][gX] == true) {
                    continue;
                }

                used[gY][gX] = true;
                dfsMap(gX, gY, step + 1);
                used[gY][gX] = false;
            }
        }
    }
}

測試輸入如下:

輸入圖的行、列:
6 5
輸入開始點的座標:
0 0
輸入終點的座標:
4 3
輸入圖資料:
0 0 0 0 0
0 0 1 0 0
0 0 0 0 0
0 0 1 1 0
0 1 1 0 0
0 0 0 0 0

程式輸出:

//=============================
// 找到的總路徑數為:124
// 最小距離為:9
//=============================

總結

難點所在

至此,深度搜索圖的最短路徑已經完成。其中最難理解的應該就是每次遞迴前標記位置已經被訪問,並且在遞迴結束(相當於當前層)後對標記進行撤銷:

....
used[gY][gX] = true;
dfsMap(gX, gY, step + 1);
used[gY][gX] = false;
....

回到圖搜尋

我們將圖分為兩類結果集:

  • 一類是“已經被訪問過的”結果集
  • 剩下的就是“沒有被訪問過的”結果集
  • 每次進行下一步都是以 當前“已經被訪問過的”集合 為基礎,將當前的結果集繼續迭代
  • 當 “當前的結果集” 所有的可能性都被嘗試完時,就要將當前結果集的最後一步還原為上一個結果集的狀態。

在這裡,我簡單地用數學集合表示法描述下:



按照上圖,假設程式在前面訪問了2個點:(0,0)、(0,1),其中(0,1)是目前遊標所在(最後一個訪問的)。
設U為全圖所有點的集合,設A為“已經被訪問過的”結果集,當**A ={(0,0)、(0,1)}**時,剩下的 {U - A} 都是沒有被訪問的集合。
按照【每次只能走一步】的約定,當我們要走下一步時只能走(0,2)、(1,1)兩個中的一個:

  1. 按照順時針訪問順序,我們先走(0,2)這個點,對應程式碼中標記:
used[gY][gX] = true;
  1. 當走到(0,2)時,集合A就變為{(0,0),(0,1),(0,2)},設為A1,如果還要繼續往下走,那麼就要在A1的基礎上繼續擴充套件,對應程式碼中遞迴呼叫,表示繼續從(0,2)這個點繼續擴充套件其所有的結果:
dfsMap(gX, gY, step + 1);
  1. 當A1所有的情況都嘗試完的時候,A1就要返回A的集合狀態,這時就要從集合A1中移除(0,2),在程式碼中也就是取消(0,2)的標記:
used[gY][gX] = false;
  1. 當返回到集合A的資料時,就要去訪問(1,1)這個點,繼續重複上面的1,2,3步。

至於擴充套件的順序,就是根據上面定義的方向陣列waypoint陣列,用for迴圈獲取所有的(上、下、左、右)可能,然後進行1,2,3步
當然,在進行遞迴迭代之前,要對新的點進行邊界、障礙物判斷。

這個過程與全排列生成的解答樹相似

圖片參考《演算法競賽:入門經典》中P119頁的圖。
裡面通過描述全排列生成的解答樹,和本題的思維非常相似,如圖:


和全排列相似地,整個過程就如同生成一棵解答樹【如圖】

  • 每到達一個結點,所有已知的(走過的)都是一個結果集;
  • 同時當前結果集與下一個可行的結點又會形成一個新的結果集(可行的結點越多,新的結果集越多);
  • 如此下去,直到當前結果集的所有可行結點被列舉,返回當前結果集的上一個結果集;
  • 當所有的結果集都被列舉,那麼就能得出所有可行性的遍歷。

寫在最後

雖然本人技術和文筆和很多大牛相比都是一般般的,但我樂於和大家分享技術、交流。
撰寫本文差不多花了一個多星期的時間去收集整理資料、寫稿。程式碼也前後修改了很多次,不修改分塊發出來很多人根本看不懂(變數比較多),也不想一次過全部程式碼“無腦推送”。如果你喜歡本文,請在下面給我點個贊吧!
(^ _ ^)

如果你看到這篇文章並不是在我的CSDN部落格釋出,同時文章裡面的圖片、URL全沒了的話,那麼,很有可能你上了一個爬蟲網站!
在此,我建議你馬上關閉該頁面!因為爬蟲或多或少都會出現內容的紕漏,對讀者造成的危害更大,誤人子弟。
同時,轉載本文的請加上本文連結:http://blog.csdn.net/nthack5730/article/details/71774434
對於爬蟲網站隨意爬取以及轉載不加本文連結的,本人保留追究法律責任的權力!
對於不尊重版權的行為,我們也沒必要客氣!