蓝桥杯十四届c/c++省赛:岛屿个数(Java版代码详解!)

在做蓝桥杯这道题时,看题解,发现和力扣上一道“岛屿数量”很像很像,于是乎,先去把力扣上这道题搞懂了,蓝桥杯这道题就相对来说简单了。其实核心是图论中的深度优先法(DFS)

做这道题之前呢,先分析一波力扣上的这道题,也是熟练DFS了。(借鉴自代码随想录深搜版

岛屿数量

给你一个由 '1'(陆地)和 '0'(水)组成的的二维网格,请你计算网格中岛屿的数量。

岛屿总是被水包围,并且每座岛屿只能由水平方向和/或竖直方向上相邻的陆地连接形成。

此外,你可以假设该网格的四条边均被水包围。

示例 1:

输入:grid = [
  ["1","1","1","1","0"],
  ["1","1","0","1","0"],
  ["1","1","0","0","0"],
  ["0","0","0","0","0"]
]
输出:1

示例 2:

输入:grid = [
  ["1","1","0","0","0"],
  ["1","1","0","0","0"],
  ["0","0","1","0","0"],
  ["0","0","0","1","1"]
]
输出:3

提示:

  • m == grid.length
  • n == grid[i].length
  • 1 <= m, n <= 300
  • grid[i][j] 的值为 '0' 或 '1'

这道题DFS和BFS(广度优先搜索)都能做,下面先给出DFS版的代码,再给出BFS版本的,其中DFS有两种方法:1. 利用visited数组标记法。2. 染色法(把走过的路染色成字符0,省去了多开辟一个数组的空间)

DFS法1(标记数组):

class Solution {
    boolean[][] visited;	// 标记是否走过,初始时,默认为false都没走过
    int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};
    public int numIslands(char[][] grid) {
        // 与广搜的思路一样,外面先来个遍历,里面深搜判断岛屿
        int count = 0;
    	visited = new boolean[grid.length][grid[0].length]; // 初始值默认都为false
    	for(int i = 0; i < grid.length; i++) {
    		for(int j = 0; j < grid[i].length; j++) {
    			if(!visited[i][j] && grid[i][j] == '1') {	// !visited[i][j]表示遇到的第一个没走过的坐标(i, j)之后
    				visited[i][j] = true;
                    dfs(grid, i, j);	// 把以(i, j)为起始点的岛屿全部遍历一遍,并把对应的visited标记为true
    				count++;
    			}
    		}
    	}
        return count;
    }
    private void dfs(char[][] grid, int x, int y) {
        // 2. 递归终止条件,似乎是不用写的
        // 3. 单纯递归逻辑
        for(int i = 0; i < dir.length; i++) {
            int nextX = x + dir[i][0];
            int nextY = y + dir[i][1];
            if(nextX < 0 || nextX == grid.length || nextY < 0 || nextY == grid[0].length) {
                continue;
            }
            if(!visited[nextX][nextY] && grid[nextX][nextY] =='1') {
                visited[nextX][nextY] = true;
                dfs(grid, nextX, nextY);
                // 不用回溯操作
            }
        }
    }
}

DFS法2(染色法):

class Solution {
    public int numIslands(char[][] grid) {
    int res = 0; //记录找到的岛屿数量
    for(int i = 0;i < grid.length;i++){
        for(int j = 0;j < grid[0].length;j++){
        	//找到“1”,res加一,同时淹没这个岛
            if(grid[i][j] == '1'){
                res++;
                dfs(grid,i,j);
            }
        }
    }
    return res;
    }
    //使用DFS“染色”岛屿
    public void dfs(char[][] grid, int i, int j){
        //搜索边界:索引越界或遍历到了"0"
        if(i < 0 || i >= grid.length || j < 0 || j >= grid[0].length || grid[i][j] == '0') return;
        //将这块土地标记为"0"
        grid[i][j] = '0';
        //根据"每座岛屿只能由水平方向或竖直方向上相邻的陆地连接形成",对上下左右的相邻顶点进行dfs
        dfs(grid,i - 1,j);
        dfs(grid,i + 1,j);
        dfs(grid,i,j + 1);
        dfs(grid,i,j - 1);
    }
}

当然,这道题BFS(广度优先搜索)也是可以做的,思路也是一样的,当遇到岛屿时,计数器count++,进入广度优先遍历,把这一坐岛其他部分标记上,下次外层遍历将不再会记数这座岛屿了。思路是借鉴代码随想录广搜版本了。

BFS法:

class Solution {
	// 1. BFS法
	boolean[][] visited;	// 标记是否可以走,即遇到地图上的字符1表示可走,赋值对应坐标为true,遇到字符0标记为不可走
    int[][] dir = {{0, 1}, {1, 0}, {0, -1}, {-1, 0}};	// 四个方向
    public int numIslands(char[][] grid) {
    	/* 思路:先正常两个for循环遍历整个地图,当遇到没有走过的路(visited==flase),并且grid==1时,进入广搜函数中,
         	把整个岛屿走一遍,一边走,一遍把岛屿标记为走过,这样,当BFS走完后,整个岛屿就都变成“走过的”了,然后在外面for循环遍历时,
        	就不再会走同一个岛屿了,这就是本题思路*/
    	int count = 0;
    	visited = new boolean[grid.length][grid[0].length]; // 初始值默认都为false
    	for(int i = 0; i < grid.length; i++) {
    		for(int j = 0; j < grid[i].length; j++) {
    			if(!visited[i][j] && grid[i][j] == '1') {	// !visited[i][j]表示遇到的第一个没走过的坐标(i, j)之后
    				bfs(grid, i, j);	// 把以(i, j)为起始点的岛屿全部遍历一遍,并把对应的visited标记为true
    				count++;
    			}
    		}
    	}
        return count;
    }
    // 遍历岛屿的关键是根据所给的起始坐标x和y去,广度优先搜索这片岛屿有多大
	private void bfs(char[][] grid, int x, int y) {
		Deque<int[]> queue = new ArrayDeque<>();
		queue.offer(new int[] {x,y}); // new int[] {x,y}这段的意思是,创建一个一维数组,里面元素是{x, y}
		visited[x][y] = true;	// 入队则标记
		// 下面开始广度优先搜索
		while(!queue.isEmpty()) {
			int[] cur = queue.poll();
			for(int i = 0; i < 4; i++) {	// 根据dir,顺时针探索(x, y)四周的坐标,是1并且没走过则标记且入队
				int nextX = cur[0] + dir[i][0];
				int nextY = cur[1] + dir[i][1];
				if(nextX < 0 || nextX == grid.length || nextY < 0 || nextY == grid[0].length) {// nextY等于行数时,即为越界,不是大于
					continue;// 数组越界的直接跳过
				}
				if(!visited[nextX][nextY] && grid[nextX][nextY] == '1') {
					queue.offer(new int[] {nextX, nextY});
					visited[nextX][nextY] = true;
				}
			}
		}
	}
}

好来,知道了岛屿怎么数之后,下面正式开始进入蓝桥杯的这道题:蓝桥杯2023年第十四届省赛真题-岛屿个数。

蓝桥杯-岛屿个数

题目描述

小蓝得到了一副大小为 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
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(染色法+合并)-Dotcpp编程社区,现在知道如何数岛屿了,那如何确定哪些是子岛屿呢?(即子岛屿不用数)借用这位大佬的思路就是想办法“合并”。

把子岛屿合并到父岛屿上,使得子岛屿与父岛屿变成一体,这样不就变成上面力扣这道“岛屿数量”问题了。

那如何合并呢?我的理解是,把示例1的地图,想象成一个平面的铁盒,0代表空地,1代表墙(紧密相连的1之间墙是连着的),我们从原本的铁盒范围扩大一圈(即变成7*7)在(0,0)这个点开始倒水,水能淹没的地方赋值为2,这之后,示例1会变成下面这样:

2 22222 2

2 21111 2

2 11001 2

2 10101 2

2 10001 2

2 11111 2

很显然,如果有环,墙里面的空地(0)是不会被水淹没的(不会被赋值为2),那么,这样一来,墙里面的0和1都是一体的(即都算作一座岛),那么为了好记数,我们把剩下的0,全部变成1,问题就变成力扣的“岛屿数量”了。可以说是相当滴巧妙了。

然后,还有一个问题,如何模拟水的淹没呢?其实就可以用上面介绍过的DFS染色法(但是我写的代码其实是DFS法1的变式),不同之处是需要走8个方向,用水的例子就很好理解了,水可以斜着走,4个方向明显不够,其实用题中所给的样例2去模拟一遍走4个方向和8个方向就知道区别了。总结:就是2次DFS(1次模拟倒水把0变2,1次数岛屿数量)。

代码

import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.InputStreamReader;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.util.Scanner;

public class Main {
	static Scanner in = new Scanner(new BufferedReader(new InputStreamReader(System.in)));
	static PrintWriter out = new PrintWriter(new BufferedWriter(new OutputStreamWriter(System.out)));
	static int[][] dir8 = {{0, 1}, {1, 1}, {1, 0}, {1, -1}, {0, -1}, {-1,-1}, {-1, 0}, {-1, 1}};	// 八连通!!千万别写错了!!
	public static void main(String[] args) {
		int T = in.nextInt();
		while(T-- > 0) {
			int m = in.nextInt();
			int n = in.nextInt();
			char[][] grid = new char[m + 2][n + 2];
			// 地图初始化
			for(int i = 0; i < m + 2; i++) {
				for(int j = 0; j < n + 2; j++) {
					grid[i][j] = '0';
				}
			}
			// 输入地图,只输入m*n的区域
			for(int i = 1; i <= m; i++) {
				String str = in.next();
				for(int j = 1; j <= n; j++) {
					grid[i][j] = str.charAt(j - 1);
				}
			}
			// 第一次DFS,从(0, 0)开始,把所有八连通的0变成2,为什么是八连通呢?模拟一遍样例1和样例2就知道了,只有八连通,剩下在岛屿内部的0才说明属于一个岛屿
			dfs1(grid, 0, 0);
			// 遍历,将岛屿内部的0,全都化为1
			for(int i = 1; i <= m; i++) {
				for(int j = 1; j <= n; j++) {
					if(grid[i][j] == '0') {
//						// 为了等等数岛屿的时候共用方法,设置了一个遇到走过的值就设置成flag,
//						// 把走过的0,设置为1,且退出递归的条件刚好是等于1时(即等于flag时退出)
//						dfs2(grid, i, j, '1');// 把走过的路设置成1
						
						// 本来是把dfs2抽象了一下,重复使用,但是发现好像没这个必要,直接遇到0全部赋值为1就行了,但是这个思想还是要养成的
						grid[i][j] = '1';
					}
				}
			}
			// 下面开始数岛屿
			int count = 0;
			for(int i = 1; i <= m; i++) {
				for(int j = 1; j <= n; j++) {
					if(grid[i][j] == '1') {
						count++;
						dfs2(grid, i, j, '0');// 把走过的路设置成0
					}
				}
			}
			out.println(count);
		}
		out.close();
	}	
	private static void dfs2(char[][] grid, int x, int y, char flag) {
		if(x < 0 || x == grid.length || y < 0 || y == grid[0].length || grid[x][y] == flag || grid[x][y] == '2') {
			return;
		}
		// DFS法2
		grid[x][y] = flag;	// 把走过的路设置成flag
		dfs2(grid, x + 0, y + 1, flag);
		dfs2(grid, x + 1, y + 0, flag);
		dfs2(grid, x + 0, y - 1, flag);
		dfs2(grid, x - 1, y + 0, flag);

	}
	private static void dfs1(char[][] grid, int x, int y) {
		// DFS法1的变式,本质是将0变2的那一步,用循环是为了简写代码
		for(int i = 0; i < 8; i++) {
			int nextX = x + dir8[i][0];
			int nextY = y + dir8[i][1];
			if(nextX < 0 || nextX == grid.length || nextY < 0 || nextY == grid[0].length || grid[nextX][nextY] == '1' || grid[nextX][nextY] == '2') { 
				continue;
			}
			// 走到这,说明是没有越界,且为0的海水
			grid[nextX][nextY] = '2';// 将连通的海水设为2
			// 递归:
			dfs1(grid, nextX, nextY);
		}
	}
}

  • 27
    点赞
  • 30
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值