关于岛屿问题的总结
网格结构要比二叉树结构稍微复杂一些,它其实是一种简化版的图结构。要写好网格上的 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] 会出现数组下标越界异常的格子,也就是那些超出网格范围的格子。
这一点稍微有些反直觉,坐标竟然可以临时超出网格的范围?这种方法我称为「先污染后治理」—— 甭管当前是在哪个格子,先往四个方向走一步再说,如果发现走出了网格范围再赶紧返回。这跟二叉树的遍历方法是一样的,先递归调用,发现 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 遍历框架上稍加修改而已
LeetCode 695. Max Area of Island (Medium)
这道题目只需要对每个岛屿做 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();
}
};
java没有指针的概念,c++中dfs
(grid是左值,是引用,全局只有此一份,因为我们对遍历过的岛屿不希望其他递归树再去遍历,所以从全局的角度修改grid的值,在这里并不同于之前的解数独、N皇后问题需要回溯撤销值的修改)
基本类型r,c的在dfs中是复制值,每一次搜索都创建新值,因此不必回溯/(撤销选择),因为r,c只是在这个递归子树上的这个结点是这个值,在其他递归子树/其他结点并不是这个值
主要思路:
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;
}
LeetCode 827. Making A Large Island (Hard)
基本的思路文章说:
这道题是岛屿最大面积问题的升级版。现在我们有填海造陆的能力,可以把一个海洋格子变成陆地格子,进而让两块岛屿连成一块。那么填海造陆之后,最大可能构造出多大的岛屿呢?
大致的思路我们不难想到,我们先计算出所有岛屿的面积,在所有的格子上标记出岛屿的面积。然后搜索哪个海洋格子相邻的两个岛屿面积最大。例如下图中红色方框内的海洋格子,上边、左边都与岛屿相邻,我们可以计算出它变成陆地之后可以连接成的岛屿面积为7+1+2=10
然而,这种做法可能遇到一个问题。如下图中红色方框内的海洋格子,它的上边、左边都与岛屿相邻,这时候连接成的岛屿面积难道是7+7+1?显然不是。这两个7来自同一个岛屿,所以填海造陆之后得到的岛屿面积应该只有7+1=8
可以看到,要让算法正确,我们得能区分一个海洋格子相邻的两个 7 是不是来自同一个岛屿。那么,我们不能在方格中标记岛屿的面积,而应该标记岛屿的索引(下标),另外用一个数组记录每个岛屿的面积,如下图所示。这样我们就可以发现红色方框内的海洋格子,它的「两个」相邻的岛屿实际上是同一个。
可以看到,这道题实际上是对网格做了两遍 DFS:第一遍 DFS 遍历陆地格子,计算每个岛屿的面积并标记岛屿;第二遍 DFS 遍历海洋格子,观察每个海洋格子相邻的陆地格子。
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();
}
LeetCode 463. Island Perimeter (Easy)
我们先回顾一下网格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 函数直接返回有这几种情况:
那么这些和我们岛屿的周长有什么关系呢?实际上,岛屿的周长是计算岛屿全部的「边缘」,而这些边缘就是我们在 DFS 遍历中,dfs 函数返回的位置。观察题目示例,我们可以将岛屿的周长中的边分为两类,如下图所示。黄色的边是与网格边界相邻的周长,而蓝色的边是与海洋格子相邻的周长。
当我们的 dfs 函数因为「坐标 (r, c) 超出网格范围」返回的时候,实际上就经过了一条黄色的边;而当函数因为「当前格子是海洋格子」返回的时候,实际上就经过了一条蓝色的边。这样,我们就把岛屿的周长跟 DFS 遍历联系起来了,我们的题解代码也呼之欲出:
主要思想:
// 看四个方向 边界或者 邻居是水 周长 + 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();
}
你从[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长度的区别