试题 F: 岛屿个数
时间限制: 2.0s 内存限制: 256.0MB 本题总分: 15 分
【问题描述】
小蓝得到了一副大小为 M × N 的格子地图, 可以将其视作一个只包含字符 ‘0’(代表海水) 和 ‘1’(代表陆地) 的二维数组, 地图之外可以视作全部是海水, 每个岛屿由在上/下/左/右四个方向上相邻的 ‘1’ 相连接而形成。
在岛屿 A 所占据的格子中, 如果可以从中选出 k 个不同的格子, 使得 他们的坐标能够组成一个这样的排列: (x0 ; y0 ); (x1 ; y1 ); : : : ; (xk−1 ; yk−1),其中 (x(i+1)%k ; y(i+1)%k) 是由 (xi ; yi ) 通过上/下/左/右移动一次得来的 (0 ≤ i ≤ k − 1), 此时这 k 个格子就构成了一个 “环”。如果另一个岛屿 B 所占据的格子全部位于 这个 “环” 内部, 此时我们将岛屿 B 视作是岛屿 A 的子岛屿。若 B 是 A 的子 岛屿, C 又是 B 的子岛屿,那 C 也是 A 的子岛屿。
请问这个地图上共有多少个岛屿?在进行统计时不需要统计子岛屿的数目。
【输入格式】
第一行一个整数 T,表示有 T 组测试数据。
接下来输入 T 组数据。对于每组数据, 第一行包含两个用空格分隔的整数 M 、N 表示地图大小; 接下来输入 M 行, 每行包含 N 个字符, 字符只可能是 ‘0’ 或 ‘1’。
【输出格式】
对于每组数据,输出一行,包含一个整数表示答案。
【样例输入】
2
5 5
01111
试题F: 岛屿个数 10
11001
10101
10001
11111
5 6
111111
100001
010101
100001
111111
【样例输出】
1
3
【样例说明】
对于第一组数据,包含两个岛屿,下面用不同的数字进行了区分:
01111
11001
10201
10001
11111
岛屿 2 在岛屿 1 的 “环” 内部,所以岛屿 2 是岛屿 1 的子岛屿,答案为 1。
对于第二组数据,包含三个岛屿,下面用不同的数字进行了区分:
111111
100001
020301
100001
111111
注意岛屿 3 并不是岛屿 1 或者岛屿 2 的子岛屿, 因为岛屿 1 和岛屿 2 中均没有 “环”。
【评测用例规模与约定】
对于 30% 的评测用例, 1 ≤ M; N ≤ 10。
对于 100% 的评测用例, 1 ≤ T ≤ 10 ,1 ≤ M; N ≤ 50。
我的答案:
这个问题可以通过深度优先搜索(DFS)来解决。我们需要遍历二维数组,每次遇到 '1',即陆地,就开始执行深度优先搜索,以此来识别整个岛屿。同时,我们需要有一个方法来判断一个岛屿是否是另一个岛屿的子岛屿。
1. 信息
- 输入:一个二维数组表示地图,其中 '0' 表示海水,'1' 表示陆地。
- 输出:非子岛屿的数量。
2. 分析
- 我们可以使用深度优先搜索来探索每个岛屿。
- 每找到一个新岛屿,我们就检查它是否是已知岛屿的子岛屿。
- 我们需要一种方法来标记已经访问过的陆地格子,以防重复计数。
- 判断子岛屿的逻辑比较复杂,需要特别注意。
3. 算法设计
- 遍历每个格子。
- 如果格子是陆地 ('1') 且未被访问过,则进行深度优先搜索。
- 在深度优先搜索中,标记所有属于同一岛屿的陆地。
- 对于每个新发现的岛屿,检查它是否是已知岛屿的子岛屿。
- 计算非子岛屿的数量。
4. 代码实现(C++)
#include <iostream>
#include <vector>
using namespace std;
void dfs(vector<vector<char>>& grid, int x, int y) {
int m = grid.size(), n = grid[0].size();
if (x < 0 || y < 0 || x >= m || y >= n || grid[x][y] == '0') return;
grid[x][y] = '0'; // 标记已访问
dfs(grid, x + 1, y); // 下
dfs(grid, x - 1, y); // 上
dfs(grid, x, y + 1); // 右
dfs(grid, x, y - 1); // 左
}
int numIslands(vector<vector<char>>& grid) {
int m = grid.size(), n = grid[0].size(), count = 0;
for (int i = 0; i < m; ++i) {
for (int j = 0; j < n; ++j) {
if (grid[i][j] == '1') {
dfs(grid, i, j);
++count;
// 这里需要添加检查子岛屿的代码
}
}
}
return count;
}
int main() {
// 读取输入数据并调用 numIslands 函数
int T;
cin >> T;
while (T--) {
int M, N;
cin >> M >> N;
vector<vector<char>> grid(M, vector<char>(N));
for (int i = 0; i < M; ++i) {
for (int j = 0; j < N; ++j) {
cin >> grid[i][j];
}
}
cout << numIslands(grid) << endl;
}
return 0;
}
部分通过:
5. 实现代码过程中可能遇到的问题
- 二维数组的边界条件处理:确保在 DFS 过程中不会超出数组边界。
- 重复计数问题:标记已经访问过的陆地,以避免重复计数。
- 判断子岛屿:这部分逻辑较复杂,需要仔细设计算法来识别子岛屿。
目前的实现中还没有加入识别子岛屿的逻辑,这部分需要额外的思考和实现。由于题目描述中对于子岛屿的定义比较复杂,它涉及到判断一个岛屿是否完全被另一个岛屿的 "环" 包
正确答案:
一、信息
题目的有用信息:
- 二维数组地图,'1' 代表陆地,'0' 代表海水。
- 地图外全部视为海水。
- 岛屿由相邻(上下左右)的陆地构成。
- 需要计算地图上非子岛屿的数量。
二、分析
每个信息的作用及思考过程:
-
二维数组地图:
- 作用:提供了用于搜索岛屿的基本结构。
- 思考:需要遍历数组来查找所有可能的岛屿起点。
-
岛屿的定义:
- 作用:确定如何将连续的 '1' 视为一个岛屿。
- 思考:使用广度优先搜索(BFS)来标识整个岛屿。
-
子岛屿的定义:
- 作用:指出需要排除的岛屿类型。
- 思考:通过检查岛屿是否与边界相连来判断是否为子岛屿。如果岛屿的水域能够与边界相连,则该岛屿不是子岛屿。
三、算法设计
-
初始化:
- 创建一个访问状态数组以避免重复搜索。
-
寻找岛屿:
- 遍历整个地图,使用 BFS 在找到 '1' 时标记整个岛屿。
-
判断子岛屿:
- 对于每个岛屿,使用 BFS 从其边界水域出发进行搜索。
- 如果水域能够到达边界,则该岛屿不是子岛屿。
-
计数:
- 统计非子岛屿的数量。
四、代码实现(用C++)
#include<bits/stdc++.h>
using namespace std;
const int N = 100;
int dx[ 8 ] = {0,0,1,-1,1,1,-1,-1};
int dy[ 8 ] = {1,-1,0,0,1,-1,1,-1};
int n, m;
string s[N];
vector< vector<bool> >vis;
bool inmap( int x, int y ){ // 判断点(x,y)是否在地图内
if( x < 1 || x > n ) return false;
if( y < 1 || y > m ) return false;
return true;
}
void bfs( int x, int y ){ // 寻找与(x,y)联通的陆地
queue< pair<int,int> >q; q.push(make_pair(x,y)); vis[x][y] = true;
while( !q.empty() ){
x = q.front().first; y = q.front().second; q.pop();
for( int i = 0 , tx , ty ; i < 4 ; i++ ){
tx = x+dx[i]; ty = y+dy[i];
if( !inmap(tx,ty) ) continue;
if( s[tx][ty] != '1' ) continue;
if( vis[tx][ty] ) continue;
vis[tx][ty] = true;
q.push( make_pair(tx,ty) );
}
}
}
bool check( int x, int y ){ // 判断(x,y)所在的联通块能否通过海水与边界相连
vector< vector<bool> > st(n+1,vector<bool>(m+1,0));
queue< pair<int,int> >q; q.push(make_pair(x,y)); st[x][y] = true;
while( !q.empty() ){
x = q.front().first; y = q.front().second; q.pop();
if( x == 1 || x == n || y == 1 || y == m ) return true; // 海水与边界相连
for( int i = 0 , tx , ty ; i < 8 ; i++ ){
tx = x+dx[i]; ty = y+dy[i];
if( !inmap(tx,ty) ) continue;
if( s[tx][ty] != '0' ) continue;
if( st[tx][ty] ) continue;
st[tx][ty] = true;
q.push( make_pair(tx,ty) );
}
}
return false;
}
void solve(){
cin >> n >> m;
for( int i = 1 ; i <= n ; i++ ){
cin >> s[ i ]; s[ i ] = "?" + s[ i ];
}
vis = vector< vector<bool> >(n+1,vector<bool>(m+1,0));
int ans = 0;
for( int i = 1 ; i <= n ; i++ )
for( int j = 1 ; j <= m ; j++ ) if( !vis[i][j] && s[i][j] == '1' ){
bfs( i , j );
if( check(i,j) ) ans++;
}
cout << ans << "\n";
}
int main(){
ios::sync_with_stdio(0); cin.tie(0); cout.tie(0);
int tt = 1; cin >> tt;
while( tt-- ) solve();
return 0;
}
五、实现代码过程中可能遇到的问题
- 边界处理:需要确保在搜索时不会超出地图的边界。
- 性能问题:由于需要对每个岛屿执行两次 BFS,对于较大的地图,性能可能是一个考虑因素。
- 复杂数据结构:使用队列和二维数组可能在理解和调试时造成困难。
- 多次遍历:由于需要遍历整个地图多次,可能会导致效率问题,特别是在地图较大时。
- 逻辑错误:在实现 BFS 和检查子岛屿的过程中,逻辑错误可能导致错误的计数或者漏掉一些情况。
六、我的答案错哪里了?
-
错误的边界处理:
- 我的
bfsWater
函数在判断是否触碰边界时做得不够。它应该检查从岛屿周围的水域出发是否能够触及地图边界,而不是从岛屿的陆地部分开始。我的初始代码没有考虑到从岛屿周围水域开始的情况,这可能导致错误地标记非子岛屿为子岛屿。
- 我的
-
不恰当的搜索启动点:
bfsWater
函数应该从岛屿的周围水域开始,而不是岛屿本身。这是判断岛屿是否为子岛屿的关键。如果从岛屿本身开始,那么搜索可能无法正确判断岛屿是否完全被水域包围。
-
对访问状态的处理:
- 在
bfsIsland
中将岛屿部分标记为 '0',而在bfsWater
中将访问过的水域部分标记为 '2'。这种状态标记可能会导致混淆,并可能影响bfsWater
函数的准确性。如果在搜索过程中不正确地处理这些标记,可能会漏掉一些路径或错误地计算岛屿数量。
- 在
-
理解题目要求:
- 题目要求特别考虑子岛屿的情况。我的初步解决方案没有完全理解并解决如何检测和排除子岛屿的问题。识别子岛屿需要仔细地分析岛屿与其周围水域的关系,以及这些水域与地图边界的关系。
综上所述,我之前的代码在理解题目需求、算法设计以及具体实现细节方面都存在不足。感谢您指出这些问题,我会继续学习和改进,以提供更准确的解决方案。
七、总结:
从这道题目和过程中出现的错误中,我们可以学到很多宝贵的教训和提升编程技能的机会。以下是一些关键的学习点和反思:
1. 问题理解的重要性:
- 深入理解题目:彻底理解题目的每个细节是解题的第一步。在这个问题中,理解子岛屿的定义和如何判定是关键。
- 明确要求:清晰地知道输入、输出和所需计算的内容可以有效指导算法设计。
2. 算法设计的关键性:
- 选择合适的算法:对于岛屿问题,广度优先搜索(BFS)是一个常用和有效的方法。理解不同算法的适用场景和优势至关重要。
- 细节处理:算法的正确实现依赖于对问题细节的准确处理,如边界检查和正确的搜索起点。
3. 代码实现的准确性:
- 清晰的状态标记:在进行搜索时,明确和一致的状态标记可以防止混淆和错误。
- 边界条件处理:正确处理边界条件是防止错误和意外行为的关键。
4. 调试和测试的重要性:
- 测试案例:使用不同的测试案例来检验代码的正确性。特别是边界情况和复杂案例,可以揭示隐藏的错误。
- 逐步调试:当发现问题时,逐步检查和调试代码可以帮助定位错误的来源。
5. 学习和改进的持续过程:
- 从错误中学习:每个错误都提供了学习和改进的机会。理解为什么会出错和如何避免类似错误是成长的关键。
- 知识的积累:对常见算法和数据结构的熟悉可以在解决新问题时提供更好的工具和思路。
6. 编码风格和习惯:
- 代码组织:良好的代码组织和清晰的命名约定可以使代码更易于理解和维护。
- 简洁性和效率:追求代码的简洁性和效率,但不应牺牲可读性和正确性。
通过这些学习点和反思,我们不仅可以提升解决特定问题的能力,还可以在编程思维、算法理解和代码实现等方面得到整体提升。