101. 孤岛的总面积
题目描述
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被陆地单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
现在你需要计算所有孤岛的总面积,岛屿面积的计算方式为组成岛屿的陆地的总数。
输入描述
第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0。
输出描述
输出一个整数,表示所有孤岛的总面积,如果不存在孤岛,则输出 0。
输入示例
4 5 1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1输出示例
1
cpp
// DFS
#include <iostream>
#include <vector>
using namespace std;
// 定义四个搜索方向:右、下、左、上
int dir[4][2]={0,1, 1,0, 0,-1, -1,0};
// DFS函数:从边界出发,将所有与边界相连的 '1' 染色(标记为已访问)
void dfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
// 剪枝:如果已经访问过,或者当前格子是水域(0),直接返回
if(visited[x][y] || graph[x][y]==0) return;
// 标记当前陆地被访问
visited[x][y]=true;
// 向四个方向递归探索
for(int i=0; i<4; i++){
int curx = x + dir[i][0];
int cruy = y + dir[i][1];
// 边界合法性检查
if(curx<0 || curx>=graph.size() || cruy<0 || cruy>=graph[0].size()) continue;
// 递归进入下一层(注意:无需在此处判断是否为1或是否visited,因为递归函数入口会判断)
dfs(graph, visited, curx, cruy);
}
}
int main(){
int n, m;
cin >> n >> m; // n为行数,m为列数
vector<vector<int>> graph(n, vector<int>(m));
vector<vector<bool>> visited(n, vector<bool>(m, false));
// 读取矩阵
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
cin >> graph[i][j];
}
}
// 【核心逻辑1:从边界开始入侵】
// 遍历左右两列边界
for(int i=0; i<n; i++){
if(graph[i][0]==1) dfs(graph, visited, i, 0);
if(graph[i][m-1]==1) dfs(graph, visited, i, m-1);
}
// 遍历上下两行边界
for(int j=0; j<m; j++){
if(graph[0][j]==1) dfs(graph, visited, 0, j);
if(graph[n-1][j]==1) dfs(graph, visited, n-1, j);
}
// 【核心逻辑2:统计剩余的封闭岛屿】
int res=0;
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
// 如果是陆地(1),且没有被边界污染(未访问),说明它是一个封闭岛屿
if(!visited[i][j] && graph[i][j]==1){
res++;
}
}
}
cout << res << endl;
return 0;
}
// BFS
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 定义四个搜索方向:右、下、左、上
int dir[4][2]={0,1, 1,0, 0,-1, -1,0};
// BFS函数:利用队列,将所有与边界相连的 '1' 染色(标记为已访问)
void bfs(vector<vector<int>>&graph, vector<vector<bool>>&visited, int x, int y){
queue<pair<int,int>> que;
que.push({x,y});
visited[x][y]=true; // 入队时即刻标记,防止同一节点被重复推入队列
while(!que.empty()){
pair<int,int> cur = que.front();
que.pop();
// 探索当前节点四个方向
for(int i=0; i<4; i++){
int curx = cur.first + dir[i][0];
int cruy = cur.second + dir[i][1];
// 边界合法性检查
if(curx<0 || curx>=graph.size() || cruy<0 || cruy>=graph[0].size()) continue;
// 如果是未访问过的陆地,加入队列并标记
if(!visited[curx][cruy] && graph[curx][cruy]==1){
que.push({curx, cruy});
visited[curx][cruy]=true;
}
}
}
}
int main(){
int n, m;
cin >> n >> m; // n为行数,m为列数
vector<vector<int>> graph(n, vector<int>(m));
vector<vector<bool>> visited(n, vector<bool>(m, false));
// 读取矩阵
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
cin >> graph[i][j];
}
}
// 【核心逻辑1:从边界开始入侵】
// 遍历左右两列边界
for(int i=0; i<n; i++){
if(graph[i][0]==1) bfs(graph, visited, i, 0);
if(graph[i][m-1]==1) bfs(graph, visited, i, m-1);
}
// 遍历上下两行边界
for(int j=0; j<m; j++){
if(graph[0][j]==1) bfs(graph, visited, 0, j);
if(graph[n-1][j]==1) bfs(graph, visited, n-1, j);
}
// 【核心逻辑2:统计剩余的封闭岛屿】
int res=0;
for(int i=0; i<n; i++){
for(int j=0; j<m; j++){
// 如果是陆地(1),且没有被边界污染(未访问),说明它是一个封闭岛屿
if(!visited[i][j] && graph[i][j]==1){
res++;
}
}
}
cout << res << endl;
return 0;
}
总结
1. 核心思想
本题不是直接找孤岛,而是反向思考:
先从地图四条边界上的陆地出发,把所有和边界连通的陆地都标记掉。
最后剩下没有被访问过的陆地,就是孤岛陆地。
2. DFS 和 BFS 的区别
- DFS:递归向四周搜索,代码更简洁。
- BFS:用队列一层一层扩展,不容易出现递归爆栈问题。
两种方法本质一样,都是为了标记边界连通块。
3. 复杂度分析
- 时间复杂度:
O(n * m) - 空间复杂度:
O(n * m)
102. 沉没孤岛
题目描述
给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿指的是由水平或垂直方向上相邻的陆地单元格组成的区域,且完全被水域单元格包围。孤岛是那些位于矩阵内部、所有单元格都不接触边缘的岛屿。
现在你需要将所有孤岛"沉没",即将孤岛中的所有陆地单元格(1)转变为水域单元格(0)。
输入描述
第一行包含两个整数 N, M,表示矩阵的行数和列数。
之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述
输出将孤岛"沉没"之后的岛屿矩阵。 注意:每个元素后面都有一个空格
输入示例
4 5 1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1输出示例
1 1 0 0 0 1 1 0 0 0 0 0 0 0 0 0 0 0 1 1
cpp
// DFS
#include <iostream>
#include <vector>
using namespace std;
// 四个方向:右、下、左、上
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
// DFS:把和边界连通的陆地标记为 2
void dfs(vector<vector<int>>& graph, int x, int y) {
// 遇到海洋或已经标记过的陆地,直接返回
if (graph[x][y] == 0 || graph[x][y] == 2) return;
// 将当前位置标记为 2
graph[x][y] = 2;
// 向四个方向搜索
for (int i = 0; i < 4; i++) {
int curx = x + dir[i][0];
int cruy = y + dir[i][1];
// 越界则跳过
if (curx < 0 || curx >= graph.size() ||
cruy < 0 || cruy >= graph[0].size()) {
continue;
}
dfs(graph, curx, cruy);
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m));
// 输入地图
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> graph[i][j];
}
}
// 从左右边界的陆地开始搜索
for (int i = 0; i < n; i++) {
if (graph[i][0] == 1) dfs(graph, i, 0);
if (graph[i][m - 1] == 1) dfs(graph, i, m - 1);
}
// 从上下边界的陆地开始搜索
for (int j = 0; j < m; j++) {
if (graph[0][j] == 1) dfs(graph, 0, j);
if (graph[n - 1][j] == 1) dfs(graph, n - 1, j);
}
// 输出结果:
// 标记为 2 的位置说明和边界连通,输出 1
// 其他位置输出 0
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (graph[i][j] == 2) cout << 1 << " ";
else cout << 0 << " ";
}
cout << endl;
}
return 0;
}
// BFS
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 四个方向:右、下、左、上
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
// BFS:把和边界连通的陆地标记为 2
void bfs(vector<vector<int>>& graph, int x, int y) {
queue<pair<int, int>> que;
// 起点入队,并标记
que.push({x, y});
graph[x][y] = 2;
while (!que.empty()) {
pair<int, int> cur = que.front();
que.pop();
// 向四个方向扩展
for (int i = 0; i < 4; i++) {
int curx = cur.first + dir[i][0];
int cruy = cur.second + dir[i][1];
// 越界则跳过
if (curx < 0 || curx >= graph.size() ||
cruy < 0 || cruy >= graph[0].size()) {
continue;
}
// 如果是未标记的陆地,加入队列
if (graph[curx][cruy] == 1) {
que.push({curx, cruy});
graph[curx][cruy] = 2;
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m));
// 输入地图
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> graph[i][j];
}
}
// 从左右边界的陆地开始搜索
for (int i = 0; i < n; i++) {
if (graph[i][0] == 1) bfs(graph, i, 0);
if (graph[i][m - 1] == 1) bfs(graph, i, m - 1);
}
// 从上下边界的陆地开始搜索
for (int j = 0; j < m; j++) {
if (graph[0][j] == 1) bfs(graph, 0, j);
if (graph[n - 1][j] == 1) bfs(graph, n - 1, j);
}
// 输出结果:
// 只有和边界连通的陆地输出 1
// 其他位置输出 0
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (graph[i][j] == 2) cout << 1 << " ";
else cout << 0 << " ";
}
cout << endl;
}
return 0;
}
总结
1. 核心思路
采用反向思维:
- 从地图四条边界出发
- 遇到陆地(1)就进行 DFS / BFS
- 将所有能连通到的陆地标记为
2 - 最后遍历输出:
2 → 1(保留)- 其他 → 0
本质是:
先标记"安全区域",再统一处理结果
2. DFS 与 BFS
两种方法本质完全一致,区别仅在实现方式:
- DFS:递归实现,代码简洁
- BFS:队列实现,更稳定,不依赖递归深度
在数据规模较大时,BFS 更安全
3. 和常见"孤岛问题"的区别
- 孤岛问题:统计不与边界连通的陆地数量
- 本题:直接保留与边界连通的陆地
实现上的区别:
- 孤岛问题:通常使用
visited数组 - 本题:直接修改原数组(1 → 2)
4. 复杂度分析
- 时间复杂度:O(n * m)
- 空间复杂度:
- DFS:递归栈空间
- BFS:队列空间
103. 高山流水
题目描述
现有一个 N × M 的矩阵,每个单元格包含一个数值,这个数值代表该位置的相对高度。矩阵的左边界和上边界被认为是第一组边界,而矩阵的右边界和下边界被视为第二组边界。
矩阵模拟了一个地形,当雨水落在上面时,水会根据地形的倾斜向低处流动,但只能从较高或等高的地点流向较低或等高并且相邻(上下左右方向)的地点。我们的目标是确定那些单元格,从这些单元格出发的水可以达到第一组边界和第二组边界。
输入描述
第一行包含两个整数 N 和 M,分别表示矩阵的行数和列数。
后续 N 行,每行包含 M 个整数,表示矩阵中的每个单元格的高度。
输出描述
输出共有多行,每行输出两个整数,用一个空格隔开,表示可达第一组边界和第二组边界的单元格的坐标,输出顺序任意。
输入示例
5 5 1 3 1 2 4 1 2 1 3 2 2 4 7 2 1 4 5 6 1 1 1 4 1 2 1输出示例
0 4 1 3 2 2 3 0 3 1 3 2 4 0 4 1
cpp
// DFS
#include <iostream>
#include <vector>
using namespace std;
// 四个方向:右、下、左、上
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
// 从边界开始反向搜索
// border 用来记录当前点是否可以到达某一边界
void dfs(vector<vector<int>>& graph, vector<vector<bool>>& border, int x, int y) {
// 如果已经访问过,直接返回
if (border[x][y]) return;
// 标记当前位置可以到达该边界
border[x][y] = true;
// 向四个方向搜索
for (int i = 0; i < 4; i++) {
int curx = x + dir[i][0];
int cruy = y + dir[i][1];
// 越界则跳过
if (curx < 0 || curx >= graph.size() ||
cruy < 0 || cruy >= graph[0].size()) {
continue;
}
// 反向搜索:
// 只有相邻位置高度 >= 当前高度时,才可以继续走
if (graph[x][y] <= graph[curx][cruy]) {
dfs(graph, border, curx, cruy);
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m));
// leftBorder:能流到左边界或上边界的点
vector<vector<bool>> leftBorder(n, vector<bool>(m, false));
// rightBorder:能流到右边界或下边界的点
vector<vector<bool>> rightBorder(n, vector<bool>(m, false));
// 输入高度矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> graph[i][j];
}
}
// 从左右边界开始搜索
for (int i = 0; i < n; i++) {
dfs(graph, leftBorder, i, 0); // 左边界
dfs(graph, rightBorder, i, m - 1); // 右边界
}
// 从上下边界开始搜索
for (int j = 0; j < m; j++) {
dfs(graph, leftBorder, 0, j); // 上边界
dfs(graph, rightBorder, n - 1, j); // 下边界
}
// 同时能到达两类边界的点,就是答案
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (leftBorder[i][j] && rightBorder[i][j]) {
cout << i << " " << j << endl;
}
}
return 0;
}
// BFS
#include <iostream>
#include <vector>
#include <queue>
using namespace std;
// 四个方向:右、下、左、上
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
// 从边界开始反向搜索
// border 用来记录当前点是否可以到达某一边界
void bfs(vector<vector<int>>& graph, vector<vector<bool>>& border, int x, int y) {
queue<pair<int, int>> que;
// 起点入队,并标记
que.push({x, y});
border[x][y] = true;
while (!que.empty()) {
pair<int, int> cur = que.front();
que.pop();
// 向四个方向扩展
for (int i = 0; i < 4; i++) {
int curx = cur.first + dir[i][0];
int cruy = cur.second + dir[i][1];
// 越界则跳过
if (curx < 0 || curx >= graph.size() ||
cruy < 0 || cruy >= graph[0].size()) {
continue;
}
// 反向搜索:
// 只有相邻位置高度 >= 当前高度,才能继续扩展
if (graph[cur.first][cur.second] <= graph[curx][cruy] &&
!border[curx][cruy]) {
que.push({curx, cruy});
border[curx][cruy] = true;
}
}
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m));
// leftBorder:能流到左边界或上边界的点
vector<vector<bool>> leftBorder(n, vector<bool>(m, false));
// rightBorder:能流到右边界或下边界的点
vector<vector<bool>> rightBorder(n, vector<bool>(m, false));
// 输入高度矩阵
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> graph[i][j];
}
}
// 从左右边界开始搜索
for (int i = 0; i < n; i++) {
bfs(graph, leftBorder, i, 0); // 左边界
bfs(graph, rightBorder, i, m - 1); // 右边界
}
// 从上下边界开始搜索
for (int j = 0; j < m; j++) {
bfs(graph, leftBorder, 0, j); // 上边界
bfs(graph, rightBorder, n - 1, j); // 下边界
}
// 同时能到达两类边界的点,就是答案
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (leftBorder[i][j] && rightBorder[i][j]) {
cout << i << " " << j << endl;
}
}
}
return 0;
}
总结
1. 核心思路
这题的关键是 反向搜索。
正常想法是从每个点出发,看它能不能流到两类边界,但这样会重复搜索很多次。
更好的做法是:
- 从左边界、上边界出发,找出所有能到达第一类边界的点
- 从右边界、下边界出发,找出所有能到达第二类边界的点
- 最后取两个结果的交集
也就是:
leftBorder[i][j] && rightBorder[i][j]
满足这个条件的点,就是答案。
2. 为什么要反向搜索
水流的方向是从高处流向低处。
如果从边界反向搜索,那么只能从低处往高处走,所以判断条件是:
graph[当前点] <= graph[下一个点]
这样找到的点,表示它们的水可以顺着高度下降流回边界。
3. DFS 与 BFS
两种方法思路一样:
- DFS:递归搜索,写法简洁
- BFS:队列搜索,更稳定
本题本质上都是从边界进行图的遍历。
4. 复杂度分析
- 时间复杂度:
O(n * m) - 空间复杂度:
O(n * m)
104. 建造最大岛屿
题目描述
给定一个由 1(陆地)和 0(水)组成的矩阵,你最多可以将矩阵中的一格水变为一块陆地,在执行了此操作之后,矩阵中最大的岛屿面积是多少。
岛屿面积的计算方式为组成岛屿的陆地的总数。岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。你可以假设矩阵外均被水包围。
输入描述
第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。
输出描述
输出一个整数,表示最大的岛屿面积。
输入示例
4 5 1 1 0 0 0 1 1 0 0 0 0 0 1 0 0 0 0 0 1 1输出示例
6
cpp
#include <iostream>
#include <vector>
#include <unordered_map>
#include <unordered_set>
using namespace std;
int cnt; // 记录当前岛屿的面积
// 四个方向:右、下、左、上
int dir[4][2] = {0, 1, 1, 0, 0, -1, -1, 0};
// DFS:给同一个岛屿的所有陆地打上相同编号 mark
void dfs(vector<vector<int>>& graph,
vector<vector<bool>>& visited,
int x,
int y,
int mark) {
// 如果已经访问过,或者当前位置是海洋,直接返回
if (visited[x][y] || graph[x][y] == 0) return;
// 标记当前位置已经访问
visited[x][y] = true;
// 将当前陆地改成岛屿编号
graph[x][y] = mark;
// 当前岛屿面积加一
cnt++;
// 向四个方向搜索
for (int i = 0; i < 4; i++) {
int curx = x + dir[i][0];
int cruy = y + dir[i][1];
// 越界则跳过
if (curx < 0 || curx >= graph.size() ||
cruy < 0 || cruy >= graph[0].size()) {
continue;
}
dfs(graph, visited, curx, cruy, mark);
}
}
int main() {
int n, m;
cin >> n >> m;
vector<vector<int>> graph(n, vector<int>(m));
vector<vector<bool>> visited(n, vector<bool>(m, false));
// key:岛屿编号
// value:该岛屿的面积
unordered_map<int, int> ump;
// 输入地图
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cin >> graph[i][j];
}
}
int mark = 2; // 岛屿编号从 2 开始,避免和 0、1 冲突
bool isAll = true; // 判断是否全是陆地
// 第一次遍历:给每个岛屿编号,并记录面积
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
// 只要存在海洋,就不是全陆地
if (graph[i][j] == 0) isAll = false;
// 遇到未访问过的陆地,就搜索整座岛
if (!visited[i][j] && graph[i][j] == 1) {
cnt = 0;
dfs(graph, visited, i, j, mark);
// 记录当前编号对应的岛屿面积
ump[mark++] = cnt;
}
}
}
// 如果全是陆地,最大面积就是整个矩阵大小
if (isAll) {
cout << n * m << endl;
return 0;
}
int res = 0;
// 用来去重,防止同一个岛屿被重复累加
unordered_set<int> ust;
// 第二次遍历:尝试把每一个 0 变成 1
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (graph[i][j] == 0) {
int sum = 1; // 当前 0 变成 1,所以初始面积为 1
ust.clear();
// 查看这个 0 周围四个方向连接了哪些岛屿
for (int k = 0; k < 4; k++) {
int curx = i + dir[k][0];
int cruy = j + dir[k][1];
// 越界则跳过
if (curx < 0 || curx >= n ||
cruy < 0 || cruy >= m) {
continue;
}
// 如果该岛屿没有被统计过,并且编号存在于 map 中
if (ust.find(graph[curx][cruy]) == ust.end() &&
ump.find(graph[curx][cruy]) != ump.end()) {
ust.insert(graph[curx][cruy]);
sum += ump[graph[curx][cruy]];
}
}
// 更新最大岛屿面积
res = max(res, sum);
}
}
}
cout << res << endl;
return 0;
}
总结
1. 核心思路
这题的关键是:先给每个岛屿编号,再枚举每个海洋格子。
第一次遍历矩阵时,遇到陆地就用 DFS 搜索整座岛屿,把同一座岛都标记成同一个 mark,并用 unordered_map 记录该岛屿的面积。
第二次遍历矩阵时,尝试把每个 0 变成 1,然后统计它四周能连接到哪些岛屿,计算合并后的最大面积。
2. 为什么 mark 从 2 开始
原图中:
0 表示海洋
1 表示未编号的陆地
所以岛屿编号从 2 开始,可以避免和原来的 0、1 冲突。
3. 去重的作用
一个海洋格子四周可能有多个方向连接到同一座岛。
所以需要用 unordered_set 去重,防止同一个岛屿面积被重复累加。
4. 特殊情况
如果整个矩阵全是陆地,那么不需要修改任何位置,最大岛屿面积就是:
n * m
5. 复杂度分析
- 时间复杂度:
O(n * m) - 空间复杂度:
O(n * m)