FloodFill算法及其应用

floodfill算法+题目

简介

给定一个地形图,浅色表示洼地,深色表示高地。向这个图中的一个格子注水,随后询问这个格子的四个方向上的格子,显然,只有浅色的格子才可以被洪水覆盖,高低不会被覆盖。

这个算法可以在线性复杂度内找到某个点所在的连通块。

image-20240511202156760

题目

1. 200. 岛屿数量 - 力扣(LeetCode)

给定一个图。其中1表示陆地,0表示水。求由1构成的连通块的数量,显然是floodfill算法。

此为最经典的模板。以下题目都要用到类似的写法。

image-20240511203516078

class Solution {
public:
    vector<vector<char>> g;
    int dx[4] = {0, 1, 0, -1};
    int dy[4] = {1, 0, -1, 0};
    int numIslands(vector<vector<char>>& grid) {
        g = grid;
        int cnt = 0;
        for(int i = 0; i < g.size(); i++){
            for(int j = 0; j < g[i].size(); j++){
                if(g[i][j] == '1'){
                    dfs(i, j);
                    cnt++;
                }
            }
        }
        return cnt;
    }
    void dfs(int x, int y){
        g[x][y] = '0';
        for(int i = 0; i < 4; i++){
            int a = x + dx[i];
            int b = y + dy[i];
            if(a >= 0 && a < g.size() && b >= 0 && b < g[a].size() && g[a][b] == '1'){
                dfs(a, b);
            }
        }
    }
};

遍历整个图,遇到1就遍历这个块的上下左右四个方向,并统计cnt。

在遍历上下左右四个方向时,可以用向量来表示。即:

int dx[4] = {0, 1, 0, -1};
int dy[4] = {1, 0, -1, 0};

在dfs函数中,需要将已经询问过的格子标记,以免再被询问。即:

g[x][y] = '0';

之后则是遍历四个方向,并进行边界判断。若不超出边界,则接着询问它的四个方向的格子。即:

 for(int i = 0; i < 4; i++){
            int a = x + dx[i];
            int b = y + dy[i];
            if(a >= 0 && a < g.size() && b >= 0 && b < g[a].size() && g[a][b] == '1'){
                dfs(a, b);
            }
        }

整个算法模板较为清晰。

2. 695. 岛屿的最大面积 - 力扣(LeetCode)

给定一个图,1表示陆地,0表示水。求1所形成的连通块的最大值。显然为floodfill算法。上题为统计所有连通块的数目,此题为统计连通块的面积,较为类似。

image-20240511204403853

class Solution {
public:
    int dx[4] = {0, 1, 0, -1};
    int dy[4] = {-1, 0, 1, 0};
    int maxAreaOfIsland(vector<vector<int>>& grid) {
        int row = grid.size();
        int col = grid[0].size();
        int res = 0;
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(grid[i][j] == 1){
                    res = max(res, dfs(grid, i, j));
                }
            }
        }
        return res;
    }
    
    int dfs(vector<vector<int>>& grid, int x, int y){
        grid[x][y] = 0;
        int res = 1;
        int row = grid.size(), col = grid[0].size();
        for(int i = 0; i < 4; i++){
            int a = x + dx[i];
            int b = y + dy[i];
            if(a >= 0 && a < row && b >= 0 && b < col && grid[a][b] == 1){
                res += dfs(grid, a, b);
            }
        }
        return res;
    }
};

大体模板较为相似。即:还是定义向量,遍历四个方向。在dfs函数中标记已经遍历过的格子,并在边界判断下搜索其余格子。

不同之处在于这里统计连通块的面积。做法如下:

if(a >= 0 && a < row && b >= 0 && b < col && grid[a][b] == 1){
                res += dfs(grid, a, b);
            }
3. 130. 被围绕的区域 - 力扣(LeetCode)

给定一个图,求被X围绕的区域,并将其用0填充。如果0的连通块能到达图的边界,则其不被围绕。所以我们从矩阵最外侧的四个边开始向内搜索。

image-20240511205559829

class Solution {
public:
    int dx[4] = {-1, 0, 1, 0};
    int dy[4] = {0, 1, 0, -1};
    vector<vector<char>> g;
    int row, col;
    void solve(vector<vector<char>>& board) {
        g = board;
        row = board.size(), col = board[0].size();
        for(int i = 0; i < row; i++){
            if(g[i][0] == 'O'){
                dfs(i, 0);
            }
            if(g[i][col - 1] == 'O'){
                dfs(i, col - 1);
            }
        }
        for(int j = 0; j < col; j++){
            if(g[0][j] == 'O'){
                dfs(0, j);
            }
            if(g[row - 1][j] == 'O'){
                dfs(row - 1, j);
            }
        }
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(g[i][j] == '#'){
                    g[i][j] = 'O';
                }else{
                    g[i][j] = 'X';
                }
            }
        }
        board = g;
    }
    void dfs(int x, int y){
            g[x][y] = '#';
            for(int i = 0; i < 4; i++){
                int a = x + dx[i];
                int b = y + dy[i];
                if(a >= 0 && a < row && b >= 0 && b < col && g[a][b] == 'O'){
                    dfs(a, b);
                }
            }
        }
};

大致写法类似,此题为从矩阵的最外侧四条边开始向内扩展搜索,将遇到的0全部标记为#。矩阵中所有被标记为#的方格即为不被包围的方格,将其更改为0。随后将剩下的填为X。

4. P1506 拯救oibh总部 - 洛谷 | 计算机科学教育新生态 (luogu.com.cn)

这道题需要求被围墙*包围的0的连通块的个数。这些连通块无法被洪水覆盖。

这道题也用到了类似的思想,也是从最外侧四条边向内搜索。

image-20240511210442000

#include <iostream>
#include <vector>

using namespace std;

int dx[4] = {0, 1, 0, -1}, dy[4] = {1, 0, -1, 0};
int row, col;
vector<vector<char>> map;

void dfs(int x, int y){
	map[x][y] = '*';
	for(int k = 0; k < 4; k++){
		int a = x + dx[k], b = y + dy[k];
		if(a >= 0 && a < row && b >= 0 && b < col && map[a][b] == '0'){
			dfs(a, b);
		}
	}
}

int main()
{
	cin >> row >> col;
	map = vector<vector<char>>(row, vector<char>(col));
	for(int i = 0; i < row; i++){
		for(int j = 0; j < col; j++){
			cin >> map[i][j];
		}
	}
	for(int i = 0; i < row; i++){
		if(map[i][0] == '0')dfs(i, 0);
	}
	for(int j = 0; j < col; j++){
		if(map[0][j] == '0')dfs(0, j);
	}
	for(int i = 0; i < row; i++){
		if(map[i][col - 1] == '0')dfs(i, col - 1);
	}
	for(int j = 0; j < col; j++){
		if(map[row - 1][j] == '0')dfs(row - 1, j);
	}
	int res = 0;
	for(int i = 0; i < row; i++){
		for(int j = 0; j < col; j++){
			if(map[i][j] == '0'){
				res++;
			}
		}
	}
	cout << res << endl;
	return 0;
}
5. 417. 太平洋大西洋水流问题 - 力扣(LeetCode)

给定一个岛屿,这个岛屿的上面和左面是太平洋。岛屿的右面和下面是大西洋。

方格中的数字代表高度。水只能从高处往低处流,即:只能从数字大的格子向数字小的格子流。现在要求哪些格子的水既能流到太平洋,也能流到大西洋。

此题用到了类似的思想,即从外围向内搜索。将太平洋能达到的格子标记,将大西洋能到达的格子标记(这里的到达应为反过来,即:数字递增)。一个格子中同时标记了两个的即为答案。

image-20240511210940416

class Solution {
public:
    int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};
    int row, col;
    vector<vector<int>> g;
    vector<vector<int>> st;
    vector<vector<int>> pacificAtlantic(vector<vector<int>>& heights) {
        g = heights;
        row = heights.size();
        col = heights[0].size();
        st = vector<vector<int>>(row, vector<int>(col));
        for(int i = 0; i < row; i++){
            dfs(i, 0, 1);
        }
        for(int j = 0; j < col; j++){
            dfs(0, j, 1);
        }
        for(int i = 0; i < row; i++){
            dfs(i, col - 1, 2);
        }
        for(int j = 0; j < col; j++){
            dfs(row - 1, j, 2);
        }
        vector<vector<int>> res;
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(st[i][j] == 3){
                    res.push_back({i, j});
                }
            }
        }
        return res;
    }
    void dfs(int x, int y, int t){
        if(st[x][y] & t){
            return;
        }
        st[x][y] |= t;
        for(int i = 0; i < 4; i++){
            int a = x + dx[i], b = y + dy[i];
            if(a >= 0 && a < row && b >= 0 && b < col && g[a][b] >= g[x][y]){
                dfs(a, b, t);
            }
        }
    }
};

在这里,我用的标记方法是二进制的标记法。假设有一个两位的二进制数。如过这两位为00,则代表太平洋和大西洋都不能到达的格子。01代表太平洋能到达的格子但大西洋不能到达的格子。10代表太平洋不能到达格子但大西洋能到达的格子。11代表太平洋和大西洋同时可以到达的格子,即为答案。

具体实现如下:

void dfs(int x, int y, int t)

这里参数t的作用就是标记。仅考虑两位,0的二进制数为00,1的二进制数为01,2的二进制数为10,3的二进制数为11。

6. 827. 最大人工岛 - 力扣(LeetCode)

给定一个矩阵,最多可将矩阵中的一个格子的0变成1。求经过此变换后最大的岛屿面积。(岛屿是1所形成的连通块)。

此题我是用并查集维护的。关于并查集:并查集的作用、原理与实现-CSDN博客

并查集的作用有:

1. 询问两个元素是否属于同一个集合。
1. 将两个集合合并为一个集合。

将0改为1后使得两个岛屿连通。即并查集的作用:将两个集合合并为一个集合。

而在合并时,我们需要询问这两个岛屿是否属于一个岛屿,如果本来就属于一个岛屿,那么就无需将两个集合合并成一个集合。这里用了并查集的询问。

所以此处使用并查集最为贴切。

image-20240511212316451

class Solution {
public:
    vector<int> p, sz;
    int row, col;
    int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};
    int find(int x){
        if(p[x] != x){
            p[x] = find(p[x]);
        }
        return p[x];
    }
    int get(int x, int y){
        return x * col + y;
    }
    int largestIsland(vector<vector<int>>& grid) {
        row = grid.size(), col = grid[0].size();
        for(int i = 0; i < row * col; i++){
            p.push_back(i);
            sz.push_back(1);
        }
        int res = 0;
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(grid[i][j]){
                    int a = get(i, j);
                    for(int k = 0; k < 4; k++){
                        int x = i + dx[k], y = j + dy[k];
                        if(x >= 0 && x < row && y >= 0 && y < col && grid[x][y]){
                            int b = get(x, y);
                            if(find(a) != find(b)){
                                sz[find(a)] += sz[find(b)];
                                p[find(b)] = find(a);
                            }
                        }
                    }
                    res = max(res, sz[find(a)]);
                }
            }
        }
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(!grid[i][j]){
                    map<int, int> hash;
                    for(int k = 0; k < 4; k++){
                        int x = i + dx[k], y = j + dy[k];
                        if(x >= 0 && x < row && y >= 0 && y < col && grid[x][y]){
                            int a = get(x, y);
                            hash[find(a)] = sz[find(a)];
                        }
                    }
                    int s = 1;
                    for(auto [k, v] : hash){
                        s += v;
                    }
                    res = max(res, s);
                }
            }
        }
        return res;
    }
};

并查集的定义,find函数和初始化:

sz数组用来维护每个集合数量的问题。

vector<int> p, sz;
int find(int x){
        if(p[x] != x){
            p[x] = find(p[x]);
        }
        return p[x];
}
for(int i = 0; i < row * col; i++){
            p.push_back(i);
            sz.push_back(1);
}

将二位下标转为一维下标:

int get(int x, int y){
        return x * col + y;
}

这里就是遍历数组,当格子数字为1时,遍历它四个方向上的格子。如果它的四个方向上的格子有是1的,就先询问两个元素是否属于同一个岛屿(集合),若不属于,就将两个岛屿(集合)合并为一个岛屿(集合),并更新这个集合的大小。最开始初始化并查集时,我们将每个数字为1的方格看成是独立的集合。

res暂时为这些岛屿中最大的。

for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(grid[i][j]){
                    int a = get(i, j);
                    for(int k = 0; k < 4; k++){
                        int x = i + dx[k], y = j + dy[k];
                        if(x >= 0 && x < row && y >= 0 && y < col && grid[x][y]){
                            int b = get(x, y);
                            if(find(a) != find(b)){
                                sz[find(a)] += sz[find(b)];
                                p[find(b)] = find(a);
                            }
                        }
                    }
                    res = max(res, sz[find(a)]);
                }
            }
        }

遍历数字为0的格子,询问它四个方向上的格子。开哈希表去记录上下左右四个方向上的岛屿的面积。这里使用哈希表还可以避免重复操作。

如果不使用哈希表,当多个相邻的1属于同一个岛屿时,我们可能会多次计算同一个岛屿的面积。

哈希表以岛屿的根节点作为键,岛屿的大小作为值。通过检查并更新哈希表,我们可以确保即使多个相邻的1属于同一个岛屿,该岛屿的面积也只被计算并加入总和一次。

随后返回res。

 for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(!grid[i][j]){
                    map<int, int> hash;
                    for(int k = 0; k < 4; k++){
                        int x = i + dx[k], y = j + dy[k];
                        if(x >= 0 && x < row && y >= 0 && y < col && grid[x][y]){
                            int a = get(x, y);
                            hash[find(a)] = sz[find(a)];
                        }
                    }
                    int s = 1;
                    for(auto [k, v] : hash){
                        s += v;
                    }
                    res = max(res, s);
                }
            }
        }
        return res;
7. 463. 岛屿的周长 - 力扣(LeetCode)

给定岛屿,求岛屿的周长。此题只需简单对每个格子四个方向遍历即可解决。

image-20240511215751647

class Solution {
public:
    int dx[4] = {0, 1, 0, -1}, dy[4] = {-1, 0, 1, 0};
    int islandPerimeter(vector<vector<int>>& grid) {
        int row = grid.size(), col = grid[0].size();
        int res = 0;
        for(int i = 0; i < row; i++){
            for(int j = 0; j < col; j++){
                if(grid[i][j]){
                    for(int k = 0; k < 4; k++){
                        int a = i + dx[k], b = j + dy[k];
                        if(a < 0 || a == row || b < 0 || b == col || grid[a][b] == 0){
                            res++;
                        }
                    }
                }
            }
        }
        return res;
    }
};
8. 841. 钥匙和房间 - 力扣(LeetCode)

此题相当于直接给定图的邻接表,直接遍历即可。

image-20240511220026567

class Solution {
public:
    vector<vector<int>> g;
    vector<bool> st;
    int n;
    void dfs(int u){
        st[u] = true;
        for(auto v : g[u]){
            if(st[v] == false){
                dfs(v);
            }
        }
    }
    bool canVisitAllRooms(vector<vector<int>>& rooms) {
        g = rooms;
        n = g.size();
        st = vector<bool>(n);
        dfs(0);
        for(int i = 0; i < n; i++){
            if(st[i] == false){
                return false;
            }
        }
        return true;
    }
};
  • 23
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值