floodfill算法+题目
简介
给定一个地形图,浅色表示洼地,深色表示高地。向这个图中的一个格子注水,随后询问这个格子的四个方向上的格子,显然,只有浅色的格子才可以被洪水覆盖,高低不会被覆盖。
这个算法可以在线性复杂度内找到某个点所在的连通块。
题目
1. 200. 岛屿数量 - 力扣(LeetCode)
给定一个图。其中1表示陆地,0表示水。求由1构成的连通块的数量,显然是floodfill算法。
此为最经典的模板。以下题目都要用到类似的写法。
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算法。上题为统计所有连通块的数目,此题为统计连通块的面积,较为类似。
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的连通块能到达图的边界,则其不被围绕。所以我们从矩阵最外侧的四个边开始向内搜索。
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的连通块的个数。这些连通块无法被洪水覆盖。
这道题也用到了类似的思想,也是从最外侧四条边向内搜索。
#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)
给定一个岛屿,这个岛屿的上面和左面是太平洋。岛屿的右面和下面是大西洋。
方格中的数字代表高度。水只能从高处往低处流,即:只能从数字大的格子向数字小的格子流。现在要求哪些格子的水既能流到太平洋,也能流到大西洋。
此题用到了类似的思想,即从外围向内搜索。将太平洋能达到的格子标记,将大西洋能到达的格子标记(这里的到达应为反过来,即:数字递增)。一个格子中同时标记了两个的即为答案。
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后使得两个岛屿连通。即并查集的作用:将两个集合合并为一个集合。
而在合并时,我们需要询问这两个岛屿是否属于一个岛屿,如果本来就属于一个岛屿,那么就无需将两个集合合并成一个集合。这里用了并查集的询问。
所以此处使用并查集最为贴切。
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)
给定岛屿,求岛屿的周长。此题只需简单对每个格子四个方向遍历即可解决。
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)
此题相当于直接给定图的邻接表,直接遍历即可。
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;
}
};