leecode岛屿问题总结DFS

关于岛屿问题的总结

文章目录

        • DFS 的基本结构
        • 例题 1:695.岛屿的最大面积
        • 例题2:200.岛屿的数量
          • 关于DFS的状态重置:(c++的右值和左值)
        • 例题 3:填海造陆问题 827.最大人工岛
        • 例题 4:岛屿的周长
        • 与岛屿问题不同,DFS某一个节点时,需要考虑当前节点与相邻节点的关系
        • 二级指针与二维数组

flood fill algorithm这个算法是否与这个问题有关

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 的两个要素:

  1. 首先,网格结构中的格子有多少相邻结点?答案是上下左右四个。对于格子 (r, c) 来说(r 和 c 分别代表行坐标和列坐标),四个相邻的格子分别是 (r-1, c)、(r+1, c)、(r, c-1)、(r, c+1)。换句话说,网格结构是「四叉」的。

  2. 其次,网格 DFS 中的 base case 是什么?从二叉树的 base case 对应过来,应该是网格中不需要继续遍历、grid[r][c] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。

这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?这种方法我称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 root == null 再返回

这样,我们得到了网格 DFS 遍历的框架代码:

void dfs(vector>& grid, int x, int y)
{
  // 判断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)是否在网格中
bool inArea(vector>& grid, int x, int y)
{
  return 0<=r && r

如何避免重复遍历
网格结构的 DFS 与二叉树的 DFS 最大的不同之处在于,遍历中可能遇到遍历过的结点。这是因为,网格结构本质上是一个「图」,我们可以把每个格子看成图中的结点,每个结点有向上下左右的四条边。在图中遍历时,自然可能遇到重复遍历结点。

这时候,DFS 可能会不停地「兜圈子」,永远停不下来

如何避免这样的重复遍历呢?答案是标记已经遍历过的格子。以岛屿问题为例,我们需要在所有值为 1 的陆地格子上做 DFS 遍历。每走过一个陆地格子,就把格子的值改为 2,这样当我们遇到 2 的时候,就知道这是遍历过的格子了。也就是说,每个格子可能取三个值:

0 —— 海洋格子
1 —— 陆地格子(未遍历过)
2 —— 陆地格子(已遍历过)
我们在框架代码中加入避免重复遍历的语句:

void dfs(vector>& grid, int x, int y)
{
  // 判断base case
  // 如果坐标(r, c)超出了网格范围,直接返回
  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)是否在网格中
bool inArea(vector>& grid, int x, int y)
{
  return 0<=r && r

这样,我们就得到了一个岛屿问题、乃至各种网格问题的通用 DFS 遍历方法。以下所讲的几个例题,其实都只需要在 DFS 遍历框架上稍加修改而已

例题 1:695.岛屿的最大面积

LeetCode 695. Max Area of Island (Medium)
leecode岛屿问题总结DFS_第1张图片
这道题目只需要对每个岛屿做 DFS 遍历,求出每个岛屿的面积就可以了。求岛屿面积的方法也很简单,代码如下,每遍历到一个格子,就把面积加一。

int area(vector<vector<int>>& grid, int r, int c)
{
  return 1+area(grid, r-1, c)+area(grid, r, c+1)+area(grid, grid, r,c-1)+area(grid, r+1, c);
}

最终得到的完整解题代码如下:

class Solution {
public:
    int maxAreaOfIsland(vector<vector<int>>& grid) {
      int res =0;
      for(int r=0; r<grid.size();r++){
        for(int c=0; c<grid[0].size(); c++){
          if(grid[r][c]==1){
            int a = dfs(grid, r, c);
            res=max(res, a);
          }
        }
      }
      return res;
    }

    int dfs(vector<vector<int>>& grid, int r, int c){
      if(!inArea(grid, r, c)){
        return 0;
      }
      if(grid[r][c]!=1){
        return 0;
      }
      // 表示该岛屿已经遍历过
      grid[r][c]=2;

      return (1 + dfs(grid, r-1, c)+ dfs(grid, r+1, c)+ dfs(grid, r, c-1)+ dfs(grid, r, c+1));
    }

    bool inArea(vector<vector<int>>& grid, int r, int c){
      return 0<=r && r<grid.size() && 0<=c && c < grid[0].size();
    }

};
例题2:200.岛屿的数量

leecode岛屿问题总结DFS_第2张图片

关于DFS的状态重置:(c++的右值和左值)

java没有指针的概念,c++中dfs

(grid是左值,是引用,全局只有此一份,因为我们对遍历过的岛屿不希望其他递归树再去遍历,所以从全局的角度修改grid的值,在这里并不同于之前的解数独、N皇后问题需要回溯撤销值的修改)
基本类型r,c的在dfs中是复制值,每一次搜索都创建新值,因此不必回溯/(撤销选择),因为r,c只是在这个递归子树上的这个结点是这个值,在其他递归子树/其他结点并不是这个值

leecode岛屿问题总结DFS_第3张图片

主要思路:

  1. 遍历grid的行和列
  2. 对于grid[r][c]==1的进行dfs
  3. dfs首先是base case: inArea()判断,超过边界return; 还有 遍历过return即 grid[r][c]==‘0’ return;
  4. 常规套路了
bool inArea(vector<vector<char>>& grid, int r, int c){
      return 0<=r && r<grid.size() && 0<=c && c < grid[0].size();
    }
  
  void dfs(vector<vector<char>>& grid, int r, int c)
  {
    if(!inArea(grid, r, c)) return;
    if(grid[r][c]=='0') return; // 来这治理

    grid[r][c] = '0'; // 将当前格的值设为0,表示已经遍历过
    //visited[r][c] = true;
    // 遍历上下左右四个网格, 在其为连接岛屿的情况下
    # 对这个扩展不应该加一些条件,先污染,后续在dfs的base case中进行return
    dfs(grid, r-1, c); // 不管grid[r-1][c]是‘0’ or ‘1’, 先进去,即先污染后治理
    dfs(grid, r+1, c);
    dfs(grid, r, c-1);
    dfs(grid, r, c+1);

  }
public:

  int numIslands(vector<vector<char>>& grid)
  {
    int nr = grid.size();
    if(!nr) return 0;
    int nc = grid[0].size();
    //vector> visited(nr, vector(grid[0].size()));

    int num_islands = 0;
    for(int r =0; r<nr; r++)
    {
      for(int c =0; c<nc; c++)
      {
        if(grid[r][c]=='1')
        {
          //++num_islands; // 若出现元素值为1,则岛屿数量加一
          dfs(grid, r, c); // 使用深度优先遍历将此岛屿所有元素变为0
          num_islands++;
        }
      }
    }
    return num_islands; 
  }
例题 3:填海造陆问题 827.最大人工岛

LeetCode 827. Making A Large Island (Hard)
leecode岛屿问题总结DFS_第4张图片

基本的思路文章说:
这道题是岛屿最大面积问题的升级版。现在我们有填海造陆的能力,可以把一个海洋格子变成陆地格子,进而让两块岛屿连成一块。那么填海造陆之后,最大可能构造出多大的岛屿呢?

大致的思路我们不难想到,我们先计算出所有岛屿的面积,在所有的格子上标记出岛屿的面积。然后搜索哪个海洋格子相邻的两个岛屿面积最大。例如下图中红色方框内的海洋格子,上边、左边都与岛屿相邻,我们可以计算出它变成陆地之后可以连接成的岛屿面积为7+1+2=10

然而,这种做法可能遇到一个问题。如下图中红色方框内的海洋格子,它的上边、左边都与岛屿相邻,这时候连接成的岛屿面积难道是7+7+1?显然不是。这两个7来自同一个岛屿,所以填海造陆之后得到的岛屿面积应该只有7+1=8

可以看到,要让算法正确,我们得能区分一个海洋格子相邻的两个 7 是不是来自同一个岛屿。那么,我们不能在方格中标记岛屿的面积,而应该标记岛屿的索引(下标),另外用一个数组记录每个岛屿的面积,如下图所示。这样我们就可以发现红色方框内的海洋格子,它的「两个」相邻的岛屿实际上是同一个。

可以看到,这道题实际上是对网格做了两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子。

  • 主要思路:
  1. 先给每个grid[r][c]=color染色,处于相同岛屿的格子颜色一样,其中海洋格子依然为0
  2. 再合并岛屿,对每一个grid[r][c]而言,递归其上下左右即对颜色不同的岛屿面积进行相加,而颜色相同的岛屿则不用相加因为是同一个连通域
	int largestIsland(vector<vector<int>>& grid) {
      int rows = grid.size(), cols = grid[0].size();
      if(0==rows||0==cols) return 0;

      int color = 1; // 每个连通分量的颜色
      int max_area = 0; //

      unordered_map<int, int> areas{{0, 0}}; // 记录一个连通分量的面积,海洋(0)的面积记为0

      // 找所有连通分量, 每个连通分量标记为 > 1的颜色,因为是陆地,所以color大于1
      for(int i=0; i<rows; i++){
        for(int j=0;j<cols;j++){
          if(grid[i][j]==1){
            color++;
            // 某个颜色的areas/面积为:
            // 将当前连通分量设为一种颜色
            areas[color]=getArea(grid, color, i, j);
            # 统计岛屿染色时的最大面积
            max_area = max(max_area, areas[color]);
          }
        }
      }

      if(max_area==rows*cols) return max_area;

      // 对每个海洋格子,找四周的不同颜色的陆地,并连成一起
      // 如果颜色相同就不能重复相加了
      // 此时用set这个来实现能够去重
      for(int i=0; i<rows; i++){
        for(int j=0;j<cols;j++){
          if(grid[i][j]==0){
            set<int> unique_color{getColor(grid, i+1, j), getColor(grid, i-1, j), getColor(grid, i, j+1), getColor(grid, i, j-1)};

            int cur_max_area=1; // 当前海洋换成陆地格子,面积算1
            for(auto c:unique_color) cur_max_area+=areas[c];
            # 与岛屿之前染色时最大的岛屿面积作比较
            max_area=max(max_area, cur_max_area);
          }
        }
      }
      return max_area;
      
    }

    int getArea(vector<vector<int>>& grid, const int color, int i, int j){
      // base case
      if(!inArea(grid, i,j)) return 0;
      if(grid[i][j]!=1) return 0; // 这种情况为0或者其他颜色,初始颜色设置为1

      grid[i][j]=color; // 将当前连通分量设为一种颜色
      return 1+getArea(grid, color, i+1, j)+getArea(grid, color,i-1, j)+getArea(grid, color,i, j-1)+getArea(grid, color,i, j+1);
    }

    int getColor(vector<vector<int>>& grid, int i, int j){
      if(!inArea(grid, i,j)) return 0;
      return grid[i][j];
    }

    bool inArea(vector<vector<int>>& grid, int r, int c){
      return 0<=r && r<grid.size() && 0 <=c && c<grid[0].size();
    }
例题 4:岛屿的周长

LeetCode 463. Island Perimeter (Easy)
leecode岛屿问题总结DFS_第5张图片

我们先回顾一下网格DFS遍历的基本框架:

void dfs(vector<vector<int>>& grid, int r , int c)
{
  // 判断 base cade, 如何理解先污染后治理
  // 先污染就是对[r, c]的领域进行DFS
  // 后治理就是在DFS中再去做判断是否搜索超过领域
  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)是否在网格中
bool inArea(vector<vector<int>>& grid, int r, int c)
{
  return 0<=r && r < grid.size() &&  0<=c && c < grid[0].size();
}

可以看到,dfs 函数直接返回有这几种情况:

  • !inArea(grid, r, c),即坐标 (r, c) 超出了网格的范围,也就是我所说的「先污染后治理」的情况
  • grid[r][c] != 1,即当前格子不是岛屿格子,这又分为两种情况:
    • grid[r][c] == 0,当前格子是海洋格子
    • grid[r][c] == 2,当前格子是已遍历的陆地格子

那么这些和我们岛屿的周长有什么关系呢?实际上,岛屿的周长是计算岛屿全部的「边缘」,而这些边缘就是我们在 DFS 遍历中,dfs 函数返回的位置。观察题目示例,我们可以将岛屿的周长中的边分为两类,如下图所示。黄色的边是与网格边界相邻的周长,而蓝色的边是与海洋格子相邻的周长。

当我们的 dfs 函数因为「坐标 (r, c) 超出网格范围」返回的时候,实际上就经过了一条黄色的边;而当函数因为「当前格子是海洋格子」返回的时候,实际上就经过了一条蓝色的边。这样,我们就把岛屿的周长跟 DFS 遍历联系起来了,我们的题解代码也呼之欲出:

主要思想:

  1. 看四个方向 边界或者 邻居是水 周长 + 1
  2. 每个格子都统计四个方向
// 看四个方向 边界或者 邻居是水 周长 + 1
    int islandPerimeter(vector<vector<int>>& grid) {
      int res=0;
      if(grid.size()==0||grid[0].size()==0) return res;
      for(int i =0;i<grid.size();i++){
        for(int j=0;j<grid[0].size();j++){
          if(1==grid[i][j]){
            res+=dfs(grid, i, j+1)+dfs(grid, i, j-1)+dfs(grid, i-1, j)+dfs(grid, i+1, j);
          }
        }
      }
      return res;
    }

    int dfs(vector<vector<int>>& grid, int row, int col){
      if(!inArea(grid, row, col)) return 1;
      if(0==grid[row][col]) return 1;
      //if(1==grid[row][col]) return 0;
      return 0;
      //return dfs(grid, row, col+1)+dfs(grid, row, col-1)+dfs(grid, row-1, col)+dfs(grid, row+1, col);
    }

    bool inArea(vector<vector<int>>& grid, int r, int c){
      return 0<=r && r<grid.size() && 0 <=c && c<grid[0].size();
    }
void girth(vector<vector<int>>& grid, int r, int c)
{
  for(int r = 0; r < grid,size() ; r++)
  {
    for(int c = 0 ; c< grid[0].size(); c++)
    {
      // 只有一个岛屿从这里开始DFS
      if(gird[r][c]==1)
        dfs(grid, r , c);
    }
  }
  
}

void dfs(vector<vector<int>>& grid, int r, int c)
{
  if(!inArea(grid, r, c))
  {
    // 越界黄色周长+1
    return 1;
  }

  if(grid[r][c]==0)
  {
    // 岛屿越过海洋蓝色周长+1
    return 1;
  }

  // 函数因为「当前格子是已遍历的陆地格子」返回,和周长没关系
  if(grid[r][c]==2)
  {
    return 0;
  }
  grid[r][c]=2; // dfs遍历过后要标记,不然会死循环

  return dfs(grid, r, c-1)+dfs(grid, r, c+1)+dfs(grid, r-1, c)
  + dfs(grid, r+1, c);
}

bool inArea(vector<vector<int>>& grid, int r, int c)
{
  return 0<=r && r < grid.size() && 0<=c && c < grid.size();
}
与岛屿问题不同,DFS某一个节点时,需要考虑当前节点与相邻节点的关系

leecode岛屿问题总结DFS_第6张图片
主要思想:
求二维矩阵,递减子串的最大长度
这题特殊在:

你从[row,col]访问[row+1,col+1]时需要考虑他俩的大小关系,所以不能想岛屿问题先污染再治理只需要考虑是否越界,需要在遍历[row,col]时就比较与[row+1,col+1]的大小,看是否进入下一步深搜

采用暴力搜索+memo的方式,此外特殊的是不需要加visited,因为data[row][col]>data[row+1][col+1]保证了不会重复访问同一个节点

#include 
#include 
#include 
#include 

using namespace std;

int dx[4] = {0, 0, 1, -1}; // 对应下、上、右。左
int dy[4] = {1, -1, 0, 0};

//int maxSize = 0;
bool inArea(vector<vector<int>> &data, int i, int j)
{
  return (i >= 0 && i < data.size()) && (j >= 0 && j < data[0].size());
}

// 因为要相邻点之间的递减关系,因此这个dfs的结构和岛屿问题不同
// 需要dx[4]与dy[4],这样便能在一个dfs()中探究data[row][col]与相邻点递减的关系
int dfs(vector<vector<int>> &data, int row, int col, vector<vector<int>> &memo)
{
  // base case
  int t = 1; // 就算只有遍历[x,y],路径也为1

  if (memo[row][col] > 0) // 有答案
  {
    return memo[row][col];
  }

  // 状态转移
  for (int i = 0; i < 4; i++)
  {
    int xx = dx[i] + row;
    int yy = dy[i] + col;
    // 这题这个条件相当于一个visited数组:data[row][col] > data[xx][yy]
    // 因为若你访问过这个数组data[row][col], 后续你dfs()过程中,若又搜索到[row,col]
    // 此时[row,col]一定比当前访问的节点大,因此是不会重复访问[row,col]节点的
    if (inArea(data, xx, yy) && data[row][col] > data[xx][yy])
    {
      int tmp = dfs(data, xx, yy, memo) + 1;
      if (tmp > t)
      {
        t = tmp;
      }
    }
  }
  memo[row][col] = t; // 保存到备忘录
  return t;
}

int main()
{
  int rows, cols;
  cin >> rows >> cols;
  if (rows == 0 || cols == 0)
  {
    return 0;
  }
  vector<vector<int>> data(rows, vector<int>(cols));
  for (int i = 0; i < rows; i++)
  {
    for (int j = 0; j < cols; j++)
    {
      cin >> data[i][j];
    }
  }

  vector<vector<int>> memo(rows, vector<int>(cols));

  int ans = INT_MIN;
  // 开始dfs找最长水沟
  for (int i = 0; i < rows; i++)
  {
    for (int j = 0; j < cols; j++)
    {
      // 这题每个点出发有可能,所以我们每个点都要开始dfs,最后取他们的最大值
      // 初始化从(0,0)开始出发
      ans = max(ans, dfs(data, i, j, memo));
    }  }

  cout << ans << endl;

  return 0;
}
二级指针与二维数组

中智行第一次岛屿的数量,第二次最大岛屿的面积

#include 
#include 

using namespace std;

void dfs(int **data, int row, int col, int x, int y)
{
  // 二维数组的访问
  cout << *(data[0] + col * x + y) << endl; // 看成一维数组来访问
}

int main()
{
  int data[3][4] = {{1, 2, 3, 11}, {4, 5, 6, 12}, {7, 8, 9, 13}};
  int *p = data[0];
  int **pointer = &p;
  //int **pointer2 = data[0][0];
  dfs(pointer, 3, 4, 2, 2);

  return 0;
}

堆排序通过什么实现:二叉树
我选了个forward_list

还有就是最坏条件下,哪个排序最慢
快排,merge, 堆排序

指针与数组的关系

32位系统与64位系统int长度的区别

你可能感兴趣的:(DFS回溯,leetcode,dfs)