網格類問題的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 case
是root==null
。這樣一個條件判斷其實有兩個含義:一方面,這表示root
指向的子樹爲空,不需要再往下遍歷。另一方面,在root-==null
的時候及時返回,可以讓後面的root.left
和root.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 島嶼的最大面積
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;
}
}