【LeetCode学习计划】《算法-入门-C++》第7天 广度优先搜索 / 深度优先搜索



733. 图像渲染

LeetCode

简 单 \color{#00AF9B}{简单}

有一幅以二维整数数组表示的图画,每一个整数表示该图画的像素值大小,数值在 0 到 65535 之间。
给你一个坐标 (sr, sc) 表示图像渲染开始的像素值(行 ,列)和一个新的颜色值 newColor,让你重新上色这幅图像。
为了完成上色工作,从初始坐标开始,记录初始坐标的上下左右四个方向上像素值与初始坐标相同的相连像素点,接着再记录这四个方向上符合条件的像素点与他们对应四个方向上像素值与初始坐标相同的相连像素点,……,重复该过程。将所有有记录的像素点的颜色值改为新的颜色值。
最后返回经过上色渲染后的图像。

示例 1:

输入: 
image = [[1,1,1],[1,1,0],[1,0,1]]
sr = 1, sc = 1, newColor = 2
输出: [[2,2,2],[2,2,0],[2,0,1]]
解析: 
在图像的正中间,(坐标(sr,sc)=(1,1)),
在路径上所有符合条件的像素点的颜色都被更改成2。
注意,右下角的像素没有更改为2,
因为它不是在上下左右四个方向上与初始点相连的像素点。

注意:

  • imageimage[0] 的长度在范围 [1, 50] 内。
  • 给出的初始点将满足 0 <= sr < image.length0 <= sc < image[0].length
  • image[i][j]newColor 表示的颜色值在范围 [0, 65535] 内。

前言

我们可以发现,我们的目标就是一个遍历同色岛屿,然后将岛屿内每一格的值改为新的值。本题中的岛屿即为直接相邻或间接相邻的同色方块构成的整体,我们从岛屿的任意一个位置开始,使用广度优先搜索或深度优先搜索就可以遍历整个岛屿。


方法1:广度优先搜索

我们需要记录起点的颜色color,用于判断下一个结点是否属于同岛屿,如果不属于则跳过该结点。

我们先将起点的值改为newColor,随后从起点开始进行广度优先搜索。假设遍历的顺序为“上下左右”(顺序不重要,因为最后一定能遍历完整个岛屿),即按序对起点的“上下左右”4个方格进行查找,如果某一格的颜色与起点颜色color相同,那么就把这一格的颜色修改,然后将它加入到队列中。以此类推。

这里需要用到队列的原因是我们在修改完中间格之后,需要记录相邻的同色格子,之后要将这些格子作为中间格重复“上下左右”的查找操作。这样我们就能从岛屿的任意一个位置开始遍历完整个岛屿。

我们还需要一个方向数组,方便我们进行“上下左右”的遍历:

  1. 一个数组存x方向上的变化情况,一个存y方向上的。

    /* C++ */
    // 左,上,右,下
    const int dy[4] = {-1, 0, 1, 0};
    const int dx[4] = {0, -1, 0, 1};
    ...
    for (int i=0;i<4;i++)
    {
    	int ny = y + dy[i];
    	int nx = x + dx[i];
    }
    ...
    
  2. 仅用一个数组,里面的元素是一对值。

    /* C++ */
    // 左,上,右,下
    const pair<int, int> D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    ...
    for (const direction : D)
    {
    	int ny = y + direction.first;
    	int nx = x + direction.second;
    }
    

  1. imageimage[0]的长度均在范围[1, 50]内,如果想要在变量上省空间的话,我们可以使用无符号字符型(unsigned char,1字节)。
  2. 颜色值在范围[0, 65535]内,而这正好是无符号短整型能表达的范围(unsigned short int,2字节)。我们也可以用它来替换int类型。
  3. 只用int也可以。
  4. 我们需要注意一下平时遍历二维数组的过程:先找到第i行,然后在这一行中找到第j列的值。请思考一下,这里的i是坐标中的yj是坐标中的x。也就是说,我们在找平面坐标系中(x,y)的点,我们应该要用image[y][x]的方式去定位,因为它是第y行第x列的值。同样的,题目中给出的sr也是y坐标,sc则是x坐标。
  5. 我们上文中的方向数组同理,结构为{y坐标变化,x坐标变化}

过程演示

请添加图片描述

#include <vector>
#include <queue>
using namespace std;

typedef pair<int, int> dpoint;
typedef unsigned char uc;
typedef unsigned short int usi;

class Solution
{
private:
    // 左,上,右,下
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};

public:
    vector<vector<int>> floodFill(vector<vector<int>> &image, uc sr, uc sc, usi newColor)
    {
        uc m = image.size(), n = image[0].size();
        usi color = usi(image[sr][sc]);
        if (color == newColor)
            return image;
        image[sr][sc] = newColor;

        queue<dpoint> que;
        que.emplace(sr, sc);
        while (!que.empty())
        {
            uc y = que.front().first, x = que.front().second;
            que.pop();

            for (const auto &direction : D)
            {
                uc ny = y + direction.first, nx = x + direction.second;
                if (0 <= ny && ny < m && 0 <= nx && nx < n && image[ny][nx] == color)
                {
                    image[ny][nx] = newColor;
                    que.emplace(ny, nx);
                }
            }
        }

        return image;
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

  • 空间复杂度:O(mn)。空间上的资源占用主要是队列的开销。

参考结果

Accepted
277/277 cases passed (8 ms)
Your runtime beats 80.32 % of cpp submissions
Your memory usage beats 62.44 % of cpp submissions (13.7 MB)

方法2:深度优先搜索(递归)

由上文广度优先搜索我们可以发现其特征:遍历完当前结点的所有可遍历结点后,转向下一个结点继续遍历。

深度优先搜索的特征:遍历到当前结点的某个可遍历结点后,马上跳转到该结点。跳转到下一个结点后,也马上跳转到下个结点的某个可遍历结点,以此类推,所以称为深度。

而上述特征就是广度和深度的意思。广度:先把当前结点完全扩展开。深度:先跳到下一个结点。

递归的深度优先算法在思路上稍微容易一些。当我们遍历到某个结点时,马上就要跳转到下一个结点(例如去左侧结点);当我们左侧遍历完后,回到该结点时,马上要去上方的结点。我们可以将这个过程看做是在4次循环内调用了同一个函数dfs。我们对每个“下一个结点”都调用4次dfs函数,每次调用都有可能跳到再下一个结点,跳转之后马上就是4次dfs函数。这样一来,就是对所有结点进行递归的深度遍历。

大体结构如下:

const D[4][2] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};

int main()
{
	...
	dfs(image, sr, sc);
	...
}

void dfs(vector<vector<int>> &image, int y, int x)
{
	// maybe do something
	for (const auto direction : D)
	{
		// maybe do something
		dfs(image, y+direction[0], x+direction[1]);
	}
}

与方法1同理,我们需要记录起点的颜色,用于判断下一个结点是否属于同岛屿,如果不属于则跳过该结点。

#include <vector>
using namespace std;

typedef pair<int, int> dpoint;
typedef unsigned char uc;
typedef unsigned short int usi;

class Solution
{
private:
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    uc m = 0xFF;
    uc n = 0xFF;
    usi color = 0xFFFF;
    usi newColor = 0xFFFF;

public:
    vector<vector<int>> floodFill(vector<vector<int>> &image, int sr, int sc, int newColor)
    {
        this->m = image.size();
        this->n = image[0].size();

        this->newColor = usi(newColor);
        this->color = image[sr][sc];

        if (this->color != this->newColor)
        {
            dfs(image, sr, sc);
        }
        return image;
    }
    void dfs(vector<vector<int>> &image, int y, int x)
    {
        if (image[y][x] == this->color)
        {
            image[y][x] = this->newColor;
            for (const auto &direction : D)
            {
                uc ny = y + direction.first, nx = x + direction.second;
                if (0 <= ny && ny < this->m && 0 <= nx && nx < this->n && image[ny][nx] == color)
                {
                    dfs(image, ny, nx);
                }
            }
        }
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

  • 空间复杂度:O(mn)。主要是递归所用的栈开销。

参考结果

Accepted
277/277 cases passed (4 ms)
Your runtime beats 97.87 % of cpp submissions
Your memory usage beats 71.56 % of cpp submissions (13.6 MB)

方法3:深度优先搜索(非递归)

与广度优先的非递归方法不同,深度优先的非递归需要用到栈。我们来分别思考一下为什么是这么安排的。

我们设i轮加入的结点值都是i,即结点值代表着它是第几轮加进去的,则第一轮结束后:

  • 队列:[1,1,1,1] <-队尾
  • 栈:[1,1,1,1] <-栈底

第2轮开始,由于栈顶和队首被选中,然后加入新的元素:

  • 队列:[1,1,1,2,2,2,2] <-队尾
  • 栈:[2,2,2,2,1,1,1] <-栈底

从这里便可以发现,由于队列先进先出的特点,我们前4轮遍历的都会是1,也就是最初结点的4个相邻结点。这符合广度优先先完全扩展结点”的概念。我们将会拿到1,1,1,1、2,2,2,2、3,3,3,3、…。

由于栈先进后出的特点,我们第2轮对1遍历完成后,加入了4个2。由于加在栈顶,我们第3轮拿也是从栈顶拿,于是我们拿到了2。这样就符合深度优先先往外找”的概念,我们将会拿到2、3、4、…。

我们将方法1的代码直接拿过来,然后将队列替换成栈即可,其它代码一模一样。

(选择性理解)我们还可以关注一下更细致的地方。每个元素入栈就会成为新的栈顶,那么对于我们设定的“左上右下”顺序遍历到的结点,在栈的排列如下:

  • [下,右,上,左] <-栈底

我们每次从栈顶拿元素,那么我们拿到的顺序就是“下右上左”。我们加入结点的顺序是“左上右下”,而遍历的顺序变成了“下右上左”,这也是栈的特点。
这只是一个细节,对于本题来说无关紧要。

#include <vector>
#include <stack>
using namespace std;

typedef pair<int, int> dpoint;
typedef unsigned char uc;
typedef unsigned short int usi;

class Solution
{
private:
    // 左,上,右,下
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};

public:
    vector<vector<int>> floodFill(vector<vector<int>> &image, uc sr, uc sc, usi newColor)
    {
        uc m = image.size(), n = image[0].size();
        usi color = usi(image[sr][sc]);
        if (color == newColor)
            return image;
        image[sr][sc] = newColor;

        stack<dpoint> stk;
        stk.emplace(sr, sc);
        while (!stk.empty())
        {
            uc y = stk.top().first, x = stk.top().second;
            stk.pop();

            for (const auto &direction : D)
            {
                uc ny = y + direction.first, nx = x + direction.second;
                if (0 <= ny && ny < m && 0 <= nx && nx < n && image[ny][nx] == color)
                {
                    image[ny][nx] = newColor;
                    stk.emplace(ny, nx);
                }
            }
        }

        return image;
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

  • 空间复杂度:O(mn)。栈中最多会存放所有的土地。

参考结果

Accepted
277/277 cases passed (4 ms)
Your runtime beats 97.87 % of cpp submissions
Your memory usage beats 47.27 % of cpp submissions (13.7 MB)


695. 岛屿的最大面积

LeetCode

中 等 \color{#FFB800}{中等}

给你一个大小为 m x n 的二进制矩阵 grid
岛屿 是由一些相邻的 1 (代表土地) 构成的组合,这里的「相邻」要求两个 1 必须在 水平或者竖直的四个方向上 相邻。你可以假设 grid 的四个边缘都被 0(代表水)包围着。
岛屿的面积是岛上值为 1 的单元格的数目。
计算并返回 grid 中最大的岛屿面积。如果没有岛屿,则返回面积为 0

示例 1:
在这里插入图片描述

输入:grid = [[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:

输入:grid = [[0,0,0,0,0,0,0,0]]
输出:0

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 50
  • grid[i][j]01

前言

这道题图中会有多个岛屿,为了找到每一个岛屿,两重循环是必须的。要把整个图遍历一遍才能确保找到每个岛屿。

两重循环中,我们需要找到值为1的方格。一旦找到了值为1的方格也就代表我们找到了一个岛屿。而由上题我们可知,使用广度/深度优先,我们从岛屿的任一位置开始能够遍历完整个岛屿。而遍历岛屿的过程中我们需要将已到达的方格改为0,以免重复入栈/队列。

至此,代码的结构也清晰了,两重循环内部嵌着一个针对栈/队列的循环,或者是递归的深度优先

而且,我们可以发现,这一题和上题几乎一模一样。上一题是将整个岛屿内方格的值更改为newColor,这一题相当于把岛屿的颜色改为0

剩下的问题就是求岛屿大小了,这个也很简单。找到值为1的方格后,岛屿大小area的初值即为1。然后开始广度/深度优先搜索,每次找到值为1的方格后,area自增。


方法1:广度优先搜索

#include <vector>
#include <queue>
using namespace std;

typedef pair<int, int> dpoint;
typedef unsigned char uc;

class Solution
{
private:
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};

public:
    int maxAreaOfIsland(vector<vector<int>> &grid)
    {
        const uc m = grid.size(), n = grid[0].size();
        int ans = 0;
        for (uc _y = 0; _y < m; _y++)
        {
            for (uc _x = 0; _x < n; _x++)
            {
                if (grid[_y][_x] == 0)
                    continue;
                queue<dpoint> que;
                que.emplace(_y, _x);
                grid[_y][_x] = 0;

                int area = 1;

                while (!que.empty())
                {
                    uc y = que.front().first, x = que.front().second;
                    que.pop();
                    for (const auto &direction : D)
                    {
                        uc ny = y + direction.first, nx = x + direction.second;
                        if (0 <= ny && ny < m && 0 <= nx && nx < n && grid[ny][nx] == 1)
                        {
                            que.emplace(ny, nx);
                            grid[ny][nx] = 0;
                            area++;
                        }
                    }
                }
                ans = max(ans, area);
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下,所有的土地为一整块岛屿。内层循环将所有土地置0时遍历一次,外层循环判断所有岛屿遍历完(所有土地确保为0)时遍历一次。一块土地最多遍历两次,所以时间复杂度为O(2mn)=O(mn)。

  • 空间复杂度:O(mn)。主要是队列的开销。

参考结果

Accepted
728/728 cases passed (16 ms)
Your runtime beats 77.64 % of cpp submissions
Your memory usage beats 19.24 % of cpp submissions (26.1 MB)

方法2:深度优先搜索(非递归)

#include <vector>
#include <stack>
using namespace std;

typedef pair<size_t, size_t> dpoint;

class Solution
{
private:
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};

public:
    int maxAreaOfIsland(vector<vector<int>> &grid)
    {
        const size_t m = grid.size(), n = grid[0].size();
        int ans = 0;
        for (size_t _y = 0; _y < m; _y++)
        {
            for (size_t _x = 0; _x < n; _x++)
            {
                if (grid[_y][_x] == 0)
                    continue;

                stack<dpoint> stk;
                stk.emplace(_y, _x);
                grid[_y][_x] = 0;

                int area = 1;

                while (!stk.empty())
                {
                    size_t y = stk.top().first, x = stk.top().second;
                    stk.pop();
                    for (const auto &direction : D)
                    {
                        size_t ny = y + direction.first, nx = x + direction.second;
                        if (0 <= ny && ny < m && 0 <= nx && nx < n && grid[ny][nx] == 1)
                        {
                            stk.emplace(ny, nx);
                            grid[ny][nx] = 0;
                            area++;
                        }
                    }
                }
                ans = max(ans, area);
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

  • 空间复杂度:O(mn)。栈中最多会存放所有的土地。

参考结果

Accepted
728/728 cases passed (20 ms)
Your runtime beats 53.5 % of cpp submissions
Your memory usage beats 15.26 % of cpp submissions (26.2 MB)

方法3:深度优先搜索(递归)

#include <vector>
using namespace std;

typedef pair<size_t, size_t> dpoint;

class Solution
{
private:
    const dpoint D[4] = {{-1, 0}, {0, -1}, {1, 0}, {0, 1}};
    size_t m = 0, n = 0;

public:
    int maxAreaOfIsland(vector<vector<int>> &grid)
    {
        this->m = grid.size(), this->n = grid[0].size();
        int ans = 0;
        for (size_t y = 0; y < m; y++)
        {
            for (size_t x = 0; x < n; x++)
            {
                if (grid[y][x] == 1)
                    ans = max(ans, dfs(grid, y, x));
            }
        }
        return ans;
    }
    int dfs(vector<vector<int>> &grid, size_t y, size_t x)
    {
        grid[y][x] = 0;
        int ans = 1;
        for (const auto &direction : D)
        {
            size_t ny = y + direction.first, nx = x + direction.second;
            if (0 <= ny && ny < m && 0 <= nx && nx < n && grid[ny][nx] == 1)
            {
                ans += dfs(grid, ny, nx);
            }
        }
        return ans;
    }
};

复杂度分析

  • 时间复杂度:O(mn),mn分别是二维数组的行数和列数。最坏情况下需要遍历所有的方格一次。

  • 空间复杂度:O(mn)。主要是递归所用的栈开销。

参考结果

Accepted
728/728 cases passed (16 ms)
Your runtime beats 77.67 % of cpp submissions
Your memory usage beats 45.27 % of cpp submissions (22.7 MB)

Animation powered by ManimCommunity/manim

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

亡心灵

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值