一、学习任务
- 102. 沉没孤岛代码随想录
- 103. 水流问题
- 104. 建造最大岛屿
- 106. 岛屿的周长
二、具体题目
1.102沉没孤岛102. 沉没孤岛
题目描述:
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
现在你需要将所有孤岛“沉没”,即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。
输入描述:
第一行包含两个整数 N, M,表示矩阵的行数和列数。
之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述
输出将孤岛“沉没”之后的岛屿矩阵。
和孤岛总面积101. 孤岛的总面积类似:
- 孤岛总面积:从四条边开始遍历,标记岛屿并将它们由1->0,剩下的就是孤岛
- 沉没孤岛:只需把上一题换成1->2,就知道了哪些岛不是孤岛,然后遍历图,将2->1(非孤岛的岛); 1->0(孤岛被沉没)即可得到最终的图
这里使用dfs版本二,也就是更接近二叉树/回溯三部曲的代码风格:
#include <iostream>
#include <vector>
using namespace std;
int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 四个方向
void dfs(vector<vector<int>>& grid, int x, int y) {if (grid[x][y] == 0 || grid[x][y] == 2) return;grid[x][y] = 2;for (int i = 0; i < 4; i++) { // 向四个方向遍历int nextx = x + dir[i][0];int nexty = y + dir[i][1];if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 超过边界,不符合条件,不继续遍历dfs (grid, nextx, nexty);}
}int main() {int n, m;cin >> n >> m;vector<vector<int>> grid(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}// 从左侧边,和右侧边 向中间遍历for (int i = 0; i < n; i++) {if (grid[i][0] == 1) dfs(grid, i, 0);if (grid[i][m - 1] == 1) dfs(grid, i, m - 1);}// 从上边和下边 向中间遍历for (int j = 0; j < m; j++) {if (grid[0][j] == 1) dfs(grid, 0, j);if (grid[n - 1][j] == 1) dfs(grid, n - 1, j);}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (grid[i][j] == 1) grid[i][j] = 0;if (grid[i][j] == 2) grid[i][j] = 1;}}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cout << grid[i][j] << " ";}cout << endl;}
}
bfs:
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 四个方向
void bfs(vector<vector<int>>& grid, int x, int y) {queue<pair<int, int>> que;que.push({x, y});grid[x][y] = 2; // 只要加入队列,立刻标记while(!que.empty()) {pair<int ,int> cur = que.front(); que.pop();for (int i = 0; i < 4; i++) {int nextx = cur.first + dir[i][0];int nexty = cur.second + dir[i][1];if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过if (grid[nextx][nexty] == 1) {que.push({nextx, nexty});grid[nextx][nexty] = 2; // 只要加入队列立刻标记}}}
}int main() {int n, m;cin >> n >> m;vector<vector<int>> grid(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}// 从左侧边,和右侧边 向中间遍历for (int i = 0; i < n; i++) {if (grid[i][0] == 1) bfs(grid, i, 0);if (grid[i][m - 1] == 1) bfs(grid, i, m - 1);}// 从上边和下边 向中间遍历for (int j = 0; j < m; j++) {if (grid[0][j] == 1) bfs(grid, 0, j);if (grid[n - 1][j] == 1) bfs(grid, n - 1, j);}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (grid[i][j] == 1) grid[i][j] = 0;if (grid[i][j] == 2) grid[i][j] = 1;}}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cout << grid[i][j] << " ";}cout << endl;}
}
2.103水流问题103. 水流问题
题目描述:
现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。
矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。
输入描述:
第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。
后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。
输出描述:
输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。
从第一组边界上的节点 逆流而上,将遍历过的节点都标记上。
同样从第二组边界的边上节点 逆流而上,将遍历过的节点也标记上。
然后两方都标记过的节点就是既可以流向第一组边界也可以流向第二组边界的节点。
最后如果这个节点,从第一组边界和第二组边界出发都遍历过,就是最后要求的节点。
#include <iostream>
#include <vector>
using namespace std;
int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}}; // 四个方向
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y) {if (visited[x][y] == true) return;visited[x][y] = true;for (int i = 0; i < 4; i++) {int nextx = x + dir[i][0];int nexty = y + dir[i][1];if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue;if (grid[x][y] > grid[nextx][nexty]) continue; // 注意:这里是从低向高遍历,这里还是第二种写法,在这里添加判断条件的原因是:不是全部相邻节点都要访问并标记,只要“逆流而上”的途径,所以不是逆流而上就跳过循环的这一轮dfs (grid, visited, nextx, nexty);}
}
int main() {int n, m;cin >> n >> m;vector<vector<int>> grid(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}// 标记从第一组边界上的节点出发,可以遍历的节点vector<vector<bool>> firstBorder(n, vector<bool>(m, false));// 标记从第一组边界上的节点出发,可以遍历的节点vector<vector<bool>> secondBorder(n, vector<bool>(m, false));// 从最左和最右列的节点出发,向高处遍历for (int i = 0; i < n; i++) {dfs (grid, firstBorder, i, 0); // 遍历最左列,接触第一组边界dfs (grid, secondBorder, i, m - 1); // 遍历最右列,接触第二组边界}// 从最上和最下行的节点出发,向高处遍历for (int j = 0; j < m; j++) {dfs (grid, firstBorder, 0, j); // 遍历最上行,接触第一组边界dfs (grid, secondBorder, n - 1, j); // 遍历最下行,接触第二组边界}for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {// 如果这个节点,从第一组边界和第二组边界出发都遍历过,就是结果if (firstBorder[i][j] == true && secondBorder[i][j] == true) cout << i << " " << j << endl;}}
}
3.104建造最大岛屿104. 建造最大岛屿
题目描述:
给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。
岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。
输入描述:
第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述:
输出一个整数,表示最大的岛屿面积。
只要用一次深搜把每个岛屿的面积记录下来就好。
第一步:一次遍历地图,得出各个岛屿的面积,并做编号记录。可以使用map记录,key为岛屿编号,value为岛屿面积
第二步:再遍历地图,遍历0的方格(因为要将0变成1),并统计该1(由0变成的1)周边岛屿面积,将其相邻面积相加在一起,遍历所有 0 之后,就可以得出 选一个0变成1 之后的最大面积。
拿如下地图的岛屿情况来举例: (1为陆地)
第一步,则遍历地图,并将岛屿的编号和面积都统计好,过程如图所示:
第二步过程如图所示:
也就是遍历每一个0的方格,并统计其相邻岛屿面积,最后取一个最大值。
这个过程的时间复杂度也为 n * n。
所以整个解法的时间复杂度,为 n * n + n * n 也就是 n^2。
当然这里还有一个优化的点,就是 可以不用 visited数组,因为有mark来标记,所以遍历过的grid[i][j]是不等于1的。
不过为了让各个变量各司其事,代码清晰一些,完整代码还是使用visited数组来标记。
#include <iostream>
#include <vector>
#include <unordered_set>
#include <unordered_map>
using namespace std;int dir[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
void dfs(vector<vector<int>>& grid, vector<vector<bool>>& visited, int x, int y, int mark, int& count) {if (visited[x][y] == true || grid[x][y] == 0) return; // 终止条件:遇到访问过的节点 或者 遇到海水visited[x][y] = true; // 没访问过且这是一块陆地,标记访问过grid[x][y] = mark; // 给陆地标记新标签(区分不同的岛屿)count++; // 每个不同的岛屿(不同的mark对应不同的岛屿)面积做累加for (int i = 0; i < 4; i++) {int nextx = x + dir[i][0];int nexty = y + dir[i][1];if (nextx < 0 || nextx >= grid.size() || nexty < 0 || nexty >= grid[0].size()) continue; // 越界了,直接跳过dfs(grid, visited, nextx, nexty, mark, count);}
}
int main() {int n, m;cin >> n >> m;vector<vector<int>> grid(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}vector<vector<bool>> visited(n, vector<bool>(m, false)); // 标记访问过的点unordered_map<int, int> gridNum; // key是岛屿的mark,value是岛屿的面积int mark = 2; // 记录每个岛屿的编号(要与原先的1不同,所以第一个岛屿从2开始)int count = 0;bool isAllGrid = true; // 标记是否整个地图都是陆地for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (grid[i][j] == 0) isAllGrid = false;if (visited[i][j] == false && grid[i][j] == 1) { // 没访问且是陆地1count = 0;dfs(grid, visited, i, j, mark, count); // 将与其链接的陆地都标记上 true,打上每个岛屿的mark,同时累计每个岛屿的面积gridNum[mark] = count; // 记录每一个岛屿的面积mark++; // 记录下一个岛屿编号}}}if (isAllGrid == true) {cout << n * m << endl; // 如果都是陆地,返回全面积return 0; // 结束程序}// 以下逻辑是根据添加陆地的位置,计算周边岛屿面积之和int result = 0; // 记录最后结果unordered_set<int> visitedGrid; // 标记访问过的岛屿for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {count = 1; // 记录连接之后的岛屿数量,本次这个一格水变成一格大陆后面积=1visitedGrid.clear(); // 每次使用时,清空if (grid[i][j] == 0) { // 发现这一块是可变为陆地的海水for (int k = 0; k < 4; k++) {int neari = i + dir[k][0]; // 计算相邻坐标int nearj = j + dir[k][1];if (neari < 0 || neari >= grid.size() || nearj < 0 || nearj >= grid[0].size()) continue;if (grid[neari][nearj] == 0) continue; // 跳过水域if (visitedGrid.count(grid[neari][nearj]) > 0) continue; // 添加过的岛屿不要重复添加// 把相邻四面的岛屿数量加起来count += gridNum[grid[neari][nearj]];visitedGrid.insert(grid[neari][nearj]); // 标记该岛屿已经添加过}}result = max(result, count);}}cout << result << endl;
}
这里需要注意两点:
- count需要以引用的方式传递参数,而不是值传递
- 原因:x, y, mark均为“仅输入参数”,count在dfs里累加的结果需要“输出”,用来给岛屿面积赋值,所以他不能仅仅在dfs函数内改变,他的改变需要传递到函数外面,所以使用int& count。
dfs(grid, visited, i, j, mark, count); // 将与其链接的陆地都标记上 true,打上每个岛屿的mark,同时累计每个岛屿的面积gridNum[mark] = count; // 记录每一个岛屿的面积
- 添加岛屿的面积时,有以下几种情况:
- 情况一:相邻格子越界(不在网格内)
- 情况二:相邻格子是水域(值为0)
- 情况三:相邻格子是陆地,但该岛屿已经被添加到set中
- 情况四:相邻格子是陆地,还未被添加,此时才可以将面积进行累计
// 情况一:越界 if (neari < 0 || neari >= grid.size() || nearj < 0 || nearj >= grid[0].size()) continue; // 情况二:水域 if (grid[neari][nearj] == 0) continue; // 跳过水域// 情况三:是陆地,但该岛屿已经被添加到set中 if (visitedGrid.count(grid[neari][nearj]) > 0) continue; // 添加过的岛屿不要重复添加// 情况四:是陆地,还未被添加,此时才可以将面积进行累计 count += gridNum[grid[neari][nearj]]; // 把相邻四面的岛屿数量加起来 visitedGrid.insert(grid[neari][nearj]); // 标记该岛屿已经添加过
4.106岛屿的周长106. 岛屿的周长
题目描述
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。
你可以假设矩阵外均被水包围。在矩阵中拥有一个或者多个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。
输入描述
第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述
输出一个整数,表示岛屿的周长。
遍历每一个节点,遇到岛屿则计算其上下左右的空格情况。
如果该陆地周围的节点是水域,则说明是挨着水的这条边需要被计算周长,如果该陆地周边的节点有出界,也是找到计算周长的一条边。
这道题和深搜/广搜没关系!!!
#include <iostream>
#include <vector>
using namespace std;
int main() {int n, m;cin >> n >> m;vector<vector<int>> grid(n, vector<int>(m, 0));for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {cin >> grid[i][j];}}int direction[4][2] = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};int result = 0;for (int i = 0; i < n; i++) {for (int j = 0; j < m; j++) {if (grid[i][j] == 1) {for (int k = 0; k < 4; k++) { // 上下左右四个方向int x = i + direction[k][0];int y = j + direction[k][1]; // 计算周边坐标x,yif (x < 0 // x在边界上|| x >= grid.size() // x在边界上|| y < 0 // y在边界上|| y >= grid[0].size() // y在边界上|| grid[x][y] == 0) { // x,y位置是水域result++;}}}}}cout << result << endl;}