深度優先搜索原理與實踐

概論

深度優先搜索屬於圖算法的一種,是一個針對圖和樹的遍歷算法,英文縮寫爲 DFS 即 Depth First Search。深度優先搜索是圖論中的經典算法,利用深度優先搜索算法能夠產生目標圖的相應拓撲排序表,利用拓撲排序表能夠方便的解決不少相關的圖論問題,如最大路徑問題等等。通常用堆數據結構來輔助實現 DFS 算法。其過程簡要來講是對每個可能的分支路徑深刻到不能再深刻爲止,並且每一個節點只能訪問一次。html

基本步奏

(1)對於下面的樹而言,DFS 方法首先從根節點1開始,其搜索節點順序是 1,2,3,4,5,6,7,8(假定左分枝和右分枝中優先選擇左分枝)。算法

 
 

(2)從 stack 中訪問棧頂的點;數組

(3)找出與此點鄰接的且還沒有遍歷的點,進行標記,而後放入 stack 中,依次進行;數據結構

 

(4)若是此點沒有還沒有遍歷的鄰接點,則將此點從 stack 中彈出,再按照(3)依次進行;框架

(5) 因爲與節點 5 相連的的節點都被訪問過了,因而5被彈出,查找與 4 相鄰但沒有被訪問過的節點:oop

(6)直到遍歷完整個樹,stack 裏的元素都將彈出,最後棧爲空,DFS 遍歷完成。優化

 
(7)
針對上面的過程,能夠用代碼表示以下:
 // 用於記錄某個節點是否訪問過
   private
Map<String, Boolean> status = new HashMap<String, Boolean>();
  // 用於保存訪問過程當中的節點
private Stack<String> stack = new Stack<String>();
  // 入口,這裏選擇 1 爲入口
public void DFSSearch(String startPoint) { stack.push(startPoint); status.put(startPoint, true); dfsLoop(); } private void dfsLoop() {
     // 到達終點,結束循環
if(stack.empty()){ return; } // 查看棧頂元素,但並不出棧 String stackTopPoint = stack.peek(); // 找出與此點鄰接的且還沒有遍歷的點,進行標記,而後所有放入list中。 List<String> neighborPoints = graph.get(stackTopPoint); for (String point : neighborPoints) { if (!status.getOrDefault(point, false)) { //未被遍歷 stack.push(point);
          // 加上已訪問標記 status.put(point,
true); dfsLoop(); } }
     // 若是鄰接點都被訪問了,那麼就彈出,至關因而恢復操做,也就是在遞歸後面作的。 String popPoint
= stack.pop(); System.out.println(popPoint); }

經過上面的示例,基本瞭解 dfs 使用。spa

通用框架

其通常框架原理以下:指針

void dfs()
{
    if(到達終點狀態)
    {
        ... //根據題意添加 
        return; 
    }
    if(越界或不合法狀態) return; 
    if(特殊狀態) // 剪枝
         return;
    for(擴展方式)
    {
        if(擴張方式所到達狀態合法)
        {
            修改操做; // 根據題意添加
            標記;
            dfs();
            (還原標記);
            //是否加上還原標記根據題意
            //若是加上還原標記就是回溯法 
        }
    }
}

經過這個 dfs 框架能夠看出該方法主要有如下幾個規律:code

  1. 訪問路徑的肯定。根據不一樣的題目思考怎麼纔算是一條訪問路徑,如何去實現遍歷。

  2. 起點條件。從哪一個點開始訪問?是否每一個點都須要看成起點?第一次 dfs 調用相當重要。

  3. 遞歸參數。也就是 dfs 遞歸怎麼在上一個節點的基礎上繼續遞歸,實現遞歸依賴什麼參數?須要知道一條路徑上各個節點之間的關係,當前訪問節點。

  4. 終結條件。訪問的終結條件是什麼?好比到達邊界點,全部點已經都訪問過了。終結條件須要在下一次遞歸前進行判斷。

  5. 訪問標誌。當一條路走不通的時候,會返回上一個節點,嘗試另外一個節點。爲了不重複訪問,須要對已經訪問過的節點加上標記,避免重複訪問。

  6. 剪枝。屬於算法優化。好比已經知道沿着當前路徑再走下去也不會知足條件的時候,提早終止遞歸。

下面將結合幾道算法題來加深對深度優先搜索算法的理解。

一、全排列

問題:給定大於0的數字n,輸出數字 1 ~ n 之間的全排列。

對於這道題目,有些人可能會好奇爲啥這到題目可使用 dfs 算法。對於全排列,其實能夠經過樹的形式來進行理解:

 能夠發現就是一個 n 叉樹,總共是 n 層,下面採用前面總結的規律來看看算法實現原理:

  1. 訪問路徑:從起始位置到葉節點就是一個排列,也就是一條路徑

  2. 起點條件:start 下面有 n 個節點,每一個點均可以被看成起始點,說明須要採用 for 循環方式,。

  3. 遞歸參數:當前訪問的節點位置,定位下一個遞歸節點。須要一個變量記錄數字的排列,須要輸出。節點總數 n,便於知道什麼時候遞歸結束。

  4. 終結條件:遞歸訪問到節點數到達 n 層的時候中止遞歸。

  5. 訪問標誌:不須要,可重複訪問;

  6. 剪枝:不須要,沒有其餘須要提早終止遞歸的條件。

下面就是算法實現:

     // 調用入口,起始點
   dfs(total, 0, "");
   // 遞歸參數:tatal 表示數字n, index 當前訪問節點,s 記錄排列方式
public void dfs(int total, int index, String s) {
     // 終結條件
if (index == total) { System.out.println(s); return; }
     // 對於每一個節點,當前有 total 種選擇
for (int i= 1;i<=total;i++) { dfs(total, index+1, s+i); } }

能夠發現,代碼仍是很簡單的。


 

695. 島嶼的最大面積

給定一個包含了一些 0 和 1 的非空二維數組 grid 。

一個 島嶼 是由一些相鄰的 1 (表明土地) 構成的組合,這裏的「相鄰」要求兩個 1 必須在水平或者豎直方向上相鄰。你能夠假設 grid 的四個邊緣都被 0(表明水)包圍着。

找到給定的二維數組中最大的島嶼面積。(若是沒有島嶼,則返回面積爲 0 。)

示例 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。注意答案不該該是 11 ,由於島嶼只能包含水平或垂直的四個方向的 1 。

示例 2:

[[0,0,0,0,0,0,0,0]]

對於上面這個給定的矩陣, 返回 0。

注意: 給定的矩陣grid 的長度和寬度都不超過 50。


 對於這道題目仍是採用以前的分析方式:

  1. 訪問路徑:節點中相鄰的1構成一條路徑。0 直接無視。

  2. 起點條件:二維數組的每一個點均可以看成起點。因此兩個 for 循環來進行調用。

  3. 遞歸參數:當前訪問的節點位置(x,y),二維數組表,從表中查找下一個節點

  4. 終結條件:到達二維數組的邊界,節點爲0

  5. 訪問標誌:須要,不可重複訪問;能夠將訪問過的節點置爲0,避免再次訪問,重複計算。

  6. 剪枝:只有在節點等於1的時候,才調用dfs。這樣能夠減小調用次數。

題目解答以下:

class Solution {
    public int maxAreaOfIsland(int[][] grid) {
        if (grid == null || grid.length <1 || grid[0].length<1) {
            return 0;
        }
        int rx = grid.length;
        int cy = grid[0].length;
        int max = 0;
        for (int x =0; x< rx; x++) {
            for (int y= 0;y<cy; y++) {
                if (grid[x][y]==1) { //只有節點等於1才調用,這裏就能夠算做是剪枝,算法的優化 int num = dfs(grid,x,y);
                   max = Math.max(max, num);
                }
            }
        }
        return max;

    }
   // 遞歸參數:節點位置x,y, 二維數組 private int  dfs (int[][] grid, int x, int y){
        int rx = grid.length;
        int cy = grid[0].length;
     // 邊界條件,節點爲0
if (x >= rx || x < 0 || y>=cy || y<0 || grid[x][y]==0 ) { return 0; }
     // 直接修改原數組來標記已訪問 grid[x][y]
=0;
     // 每次遞歸就表示面積多了一塊
int num = 1;
     // 每一個節點有四種不一樣的選擇方向 num
+= dfs(grid, x-1, y); num += dfs(grid, x, y-1); num += dfs(grid, x+1, y); num += dfs(grid, x, y+1); return num; } }

200. 島嶼數量

給你一個由 '1'(陸地)和 '0'(水)組成的的二維網格,請你計算網格中島嶼的數量。

島嶼老是被水包圍,而且每座島嶼只能由水平方向或豎直方向上相鄰的陸地鏈接造成。

此外,你能夠假設該網格的四條邊均被水包圍。

示例 1:

// 輸入:
11110
11010
11000
00000
// 輸出: 1

示例 2:

// 輸入:
11000
11000
00100
00011
// 輸出: 3

解釋: 每座島嶼只能由水平和/或豎直方向上相鄰的陸地鏈接而成。


能夠發現,這道題目與前面的題目很相似,關於 dfs 規則這裏就不在分析了,留給你們本身去分析。

題目解答以下:

class Solution {
    public int numIslands(char[][] grid) {
        if (grid == null || grid.length < 1 || grid[0].length<1) {
            return 0;
        }
        int num = 0;
        int rx = grid.length;
        int cy = grid[0].length;
     // 起始點
for (int x =0;x<rx;x++) { for (int y =0;y<cy;y++) {
          // 題目要求,'0'不符合路徑條件
if (grid[x][y]=='1') { dfs(grid,x,y); num++; } } } return num; }    // 遞歸條件 private void dfs(char[][] grid, int x, int y) { int rx = grid.length; int cy = grid[0].length;
     // 終結條件
if (x<0 || x>=rx || y<0 || y>= cy || grid[x][y] == '0') { return; }
     // 訪問方向實質是由訪問路徑來決定的,就是你得想清楚怎麼纔算一條路徑 grid[x][y]
='0'; dfs(grid,x-1,y); dfs(grid,x,y-1); dfs(grid,x+1,y); dfs(grid,x,y+1); return ; } }

到這裏,深度優先搜索的理論和實踐就講完了,相信看到這裏的小夥伴應該也掌握了其算法的原理,以及如何去書寫。

 

算法系列文章

滑動窗口算法基本原理與實踐

二分查找法基本原理和實踐

廣度優先搜索原理與實踐

深度優先搜索原理與實踐

雙指針算法基本原理和實踐

分治算法基本原理和實踐

動態規劃算法原理與實踐

算法筆記

 

參考文章 

基本算法——深度優先搜索(DFS)和廣度優先搜索(BFS)

相關文章
相關標籤/搜索