深度遞歸之島嶼問題

網格類問題的DFS遍歷方法

我們所熟悉的DFS(深度優先搜索)問題通常是在樹上或者圖結構上進行的。現在要討論的是在一種網格結構中進行

DFS的基本結構

網格結構要比二叉樹結構稍微複雜一些,它其實是一種簡化版的的結構。要寫好網格上的dfs遍歷,首先我們要理解二叉樹上的DFS遍歷方法,再類比寫出網格結構上的DFS遍歷。我們寫的二叉樹DFS遍歷一般是這樣的:

void traverse(TreeNode root) {
    // 判斷 base case
    if (root == null) {
        return;
    }
    // 訪問兩個相鄰結點:左子結點、右子結點
    traverse(root.left);
    traverse(root.right);
}

可以看到:二叉樹的DFS有兩個要素:訪問相鄰節點判斷base case

  • 第一個要素是訪問相鄰節點:二叉樹的相鄰節點非常簡單,只有左子節點和右子節點兩個,二叉樹本身就是一個遞歸定義的結構:一顆二叉樹,它的左子樹和右子樹也是一顆二叉樹,那麼我們的DFS遍歷只需要遞歸調用左子樹右子樹即可
  • 第二個要素是判斷base case,一般來說,二叉樹遍歷的base caseroot==null。這樣一個條件判斷其實有兩個含義:一方面,這表示root指向的子樹爲空,不需要再往下遍歷。另一方面,在root-==null的時候及時返回,可以讓後面的root.leftroot.right操作不出現空指針異常

對於網格上的dfs,我們完全可以參考二叉樹的dfs,寫出網格dfs的兩個要素:

  • 首先,網格結構中的格子有多少相鄰的節點?答案是上下左右四個,對於格子(r,c)來說(r和c分別代表行座標和列座標),四個相鄰的格子分別是(r-1,c)(r+1,c)(r,c-1)(r,c+1)。換句話說,網格結構是[死叉的
    在這裏插入圖片描述

  • 其次,網格 DFS 中的 base case 是什麼?從二叉樹的base case對應過來,應該是網格中不需要繼續遍歷、grid[r][c] 會出現數組下標越界異常的格子,也就是那些超出網格範圍的格子。
    在這裏插入圖片描述

這樣,我們得到了網格上DFS遍歷的框架的代碼:

void dfs(int[][] grid, int r, int c) {
    // 判斷 base case
    // 如果座標 (r, c) 超出了網格範圍,直接返回
    if (!inArea(grid, r, c)) {
        return;
    }
    // 訪問上、下、左、右四個相鄰結點
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判斷座標 (r, c) 是否在網格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

如何避免重複遍歷

網格結構的 DFS 與二叉樹的 DFS 最大的不同之處在於,遍歷中可能遇到遍歷過的結點。這是因爲,網格結構本質上是一個「圖」,我們可以把每個格子看成圖中的結點,每個結點有向上下左右的四條邊。在圖中遍歷時,自然可能遇到重複遍歷結點

這時候,DFS 可能會不停地「兜圈子」,永遠停不下來
如何避免這樣的重複遍歷呢?答案是標記已經遍歷過的格子。以島嶼問題爲例,我們需要在所有值爲 1 的陸地格子上做 DFS 遍歷。每走過一個陸地格子,就把格子的值改爲 2,這樣當我們遇到 2 的時候,就知道這是遍歷過的格子了。也就是說,每個格子可能取三個值:

  • 0 —— 海洋格子
  • 1 —— 陸地格子(未遍歷過)
  • 2 —— 陸地格子(已遍歷過)

void dfs(int[][] grid, int r, int c) {
    // 判斷 base case
    if (!inArea(grid, r, c)) {
        return;
    }
    // 如果這個格子不是島嶼,直接返回
    if (grid[r][c] != 1) {
        return;
    }
    grid[r][c] = 2; // 將格子標記爲「已遍歷過」
    
    // 訪問上、下、左、右四個相鄰結點
    dfs(grid, r - 1, c);
    dfs(grid, r + 1, c);
    dfs(grid, r, c - 1);
    dfs(grid, r, c + 1);
}

// 判斷座標 (r, c) 是否在網格中
boolean inArea(int[][] grid, int r, int c) {
    return 0 <= r && r < grid.length 
        	&& 0 <= c && c < grid[0].length;
}

作者:nettee
鏈接:https://leetcode-cn.com/problems/number-of-islands/solution/dao-yu-lei-wen-ti-de-tong-yong-jie-fa-dfs-bian-li-/
來源:力扣(LeetCode)
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

題目

LeetCode200島嶼數量

島嶼數量

在這裏插入圖片描述

package JDFS;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/2 0002  22:22
 */
public class Problem200 {

    //每座島嶼只能由水平和/或豎直方向上相鄰的陸地連接而成。
    //遍歷到這二維數組

    /**
     * 這道題是可以使用一個經典的算法來解決的,那就是Flood Fill
     * Flood Fill算法是從一個區域中提取出若干個連通的點與其他相鄰區域分開(或分別染成不同的顏色)的經典算法
     * 因爲其思路類似洪水從一個區域擴散到所有能到大的區域而得名,在Gun Go或掃雷中,Flood Fill算法被用來就算需要被
     * 清除的區域
     * 從一個點擴散開,找到與其連通的點,這不是什麼高深的算法,其實就是從一個點開始,進行一次“深度優先遍歷”或者“廣度優先遍歷”
     * 通過“深度優先遍歷”或者:“廣度優先遍歷”發現一片連着的區域,對於這道題來說就是從一個“陸地"的格子開始進行一次”深度優先遍歷“
     * 或者“廣度優先遍歷”,把與值相鄰的所有格子都被標記上,視爲發現了一個“島嶼“
     * 說明:這裏做 “標記” 的意思是,通過 “深度優先遍歷” 或者 “廣度優先遍歷” 操作,我發現了一個新的格子,與起始點的那個格子是連通的,我們視爲 “標記” 過,也可以說 “被訪問過”。
     那麼每一次進行 “深度優先遍歷” 或者 “廣度優先遍歷” 的條件就是:

     1、這個格子是陸地 1,如果是水域 0 就無從談論 “島嶼”;

     2、這個格子不能是之前發現 “島嶼” 的過程中執行了 “深度優先遍歷” 或者 “廣度優先遍歷” 操作,而被標記的格子(這句話說得太拗口了,大家意會即可,意會不了不是您的問題,是我表達的問題,直接看代碼會清楚很多)。

     * @param grid
     * @return
     */
    public int numIslands(char[][] grid) {

        int count = 0;
        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                //發現一個1
                if(grid[i][j]=='1'){
                    //使用感染函數將其周圍的1都標記爲2,防止以後重複計算島嶼的數量
                    dfs(grid,i,j);
                    count++;
                }
            }

        }
        return count;


    }

    //相鄰的1只能算1個島嶼,只要發現一個一將其相鄰的所有1都標記爲2,
    //防止重複標記,重複算島嶼的數量
    public void dfs(char[][]grid,int x,int y){

        //當前爲1則需要向上下左右則把周圍的所有1都訪問完,
        //標記爲visited
       if(x<0||y<0||x>grid.length-1||y>grid[0].length-1||grid[x][y]!='1') return;

       //標記防止重複遍歷
       grid[x][y]=2;
       
       dfs(grid,x+1,y);
       dfs(grid,x-1,y);
       dfs(grid,x,y+1);
       dfs(grid,x,y-1);
       
    }


//
//    //採用BFS
//    //
//    public int numIslands1(char[][] grid) {
//
//        int count = 0;
//        Queue<>
//
//    }
}


LeetCode695 島嶼的最大面積

LeetCode695島嶼的最大面積
在這裏插入圖片描述

package JDFS;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/2 0002  23:09
 */
public class Problem695 {

    //這道題只需要對每個島嶼左DFS遍歷,求出每個島嶼的面積就可以了,求島嶼面對
    public int maxAreaOfIsland(int[][] grid) {

        int maxArea = 0;

        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                //假如發現一個島嶼的一部分呢則不斷遞歸島嶼的其餘部分
                if(grid[i][j]==1){
                    maxArea=Math.max(dfs(grid,i,j),maxArea);
                }

            }
        }
        return maxArea;

    }


    //求島嶼的面積:注意島嶼的定義是相連的1所構成的區域
    public int dfs(int[][] grid,int i,int j){
        //越界
        if(!inArea(grid,i,j)) return 0;

        //dfs:若爲0或者之前已經訪問過了則返回0
        if(grid[i][j]==0||grid[i][j]==2) return 0;

        //上下左右相鄰的1的數量.將當前訪問的位置標記爲2,這樣不會造成反覆的橫條
        grid[i][j]=2;

        //訪問上下左右相鄰的島嶼
        return 1+dfs(grid,i-1,j)+dfs(grid,i+1,j)+dfs(grid,i,j-1)+dfs(grid,i,j+1);

    }

    //數組越界檢查
    public  boolean inArea(int[][] grid,int x,int y){
        if(x<0||y<0||x>grid.length-1||y>grid[0].length-1) return false;
        return true;
    }

}



LeetCode463島嶼的周長

在這裏插入圖片描述


package JDFS;

/**
 * @Author Zhou  jian
 * @Date 2020 ${month}  2020/5/2 0002  23:32
 */
public class Problem463 {
    //島嶼的周長
    public int islandPerimeter(int[][] grid) {

        for(int i=0;i<grid.length;i++){
            for(int j=0;j<grid[0].length;j++){
                if(grid[i][j]==1){
                    //題目限制只有一個島嶼,計算一個即可
                    return dfs(grid,i,j);
                }
            }
        }
        return 0;

    }

    /**
     * 那麼這些和我們島嶼的周長有什麼關係呢?
     * 實際上,島嶼的周長是計算島嶼全部的「邊緣」,而這些邊緣就是我們在 DFS 遍歷中,dfs 函數返回的位置。
     * 觀察題目示例,我們可以將島嶼的周長中的邊分爲兩類,如下圖所示。
     * 黃色的邊是與網格邊界相鄰的周長,而藍色的邊是與海洋格子相鄰的周長。
     * @param grid
     * @param x
     * @param y
     * @return
     */
    public int dfs(int[][] grid,int x,int y){

        //函數因爲座標[r,c]超出網格範圍返回 對應一條黃色的邊
        if(!inArea(grid,x,y)) return 1;

        //當前格子是海洋格子返回,對應一條藍色的邊
        if(grid[x][y]==0) return 1;

        //擋圈格子是已經遍歷的陸地格子返回和周長沒關係
        if(grid[x][y]!=1) return 0;

        grid[x][y]=2;

        return dfs(grid,x-1,y)+dfs(grid,x+1,y)+dfs(grid,x,y-1)+dfs(grid,x,y+1);
    }
    public  boolean inArea(int[][] grid,int x,int y){
        if(x<0||y<0||x>=grid.length||y>=grid[0].length) return false;
        return true;

    }

}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章