图论汇总1

news2025/1/27 22:42:45

1.图论理论基础

图的基本概念

二维坐标中,两点可以连成线,多个点连成的线就构成了图。

当然图也可以就一个节点,甚至没有节点(空图)

图的种类

整体上一般分为 有向图 和 无向图。

有向图是指 图中边是有方向的:

无向图是指 图中边没有方向:

加权有向图,就是图中边是有权值的,例如:

加权无向图也是同理。

无向图中有几条边连接该节点,该节点就有几度。

例如,该无向图中,节点4的度为5,节点6的度为3。

在有向图中,每个节点有出度和入度。

出度:从该节点出发的边的个数。

入度:指向该节点边的个数。

例如,该有向图中,节点3的入度为2,出度为1,节点1的入度为0,出度为2。

连通性

在图中表示节点的连通情况,我们称之为连通性。

连通图

在无向图中,任何两个节点都是可以到达的,我们称之为连通图 ,如图:

如果有节点不能到达其他节点,则为非连通图,如图:

节点1 不能到达节点4。

强连通图

在有向图中,任何两个节点是可以相互到达的,我们称之为 强连通图。

这里有录友可能想,这和无向图中的连通图有什么区别,不是一样的吗?

我们来看这个有向图:

这个图是强连通图吗?

初步一看,好像这节点都连着呢,但这不是强连通图,节点1 可以到节点5,但节点5 不能到 节点1 。

强连通图是在有向图中任何两个节点是可以相互到达

下面这个有向图才是强连通图:

连通分量

在无向图中的极大连通子图称之为该图的一个连通分量。

只看概念大家可能不理解,我来画个图:

该无向图中 节点1、节点2、节点5 构成的子图就是 该无向图中的一个连通分量,该子图所有节点都是相互可达到的。

同理,节点3、节点4、节点6 构成的子图 也是该无向图中的一个连通分量。

那么无向图中 节点3 、节点4 构成的子图 是该无向图的联通分量吗?

不是!

因为必须是极大联通子图才能是连通分量,所以 必须是节点3、节点4、节点6 构成的子图才是连通分量。

在图论中,连通分量是一个很重要的概念,例如岛屿问题(后面章节会有专门讲解)其实就是求连通分量。

强连通分量

在有向图中极大强连通子图称之为该图的强连通分量。

如图:

节点1、节点2、节点3、节点4、节点5 构成的子图是强连通分量,因为这是强连通图,也是极大图。

节点6、节点7、节点8 构成的子图 不是强连通分量,因为这不是强连通图,节点8 不能达到节点6。

节点1、节点2、节点5 构成的子图 也不是 强连通分量,因为这不是极大图。

图的构造

我们如何用代码来表示一个图呢?

一般使用邻接表、邻接矩阵 或者用类来表示。

主要是 朴素存储、邻接表和邻接矩阵。

邻接矩阵

邻接矩阵 使用 二维数组来表示图结构。 邻接矩阵是从节点的角度来表示图,有多少节点就申请多大的二维数组。

例如: grid[2][5] = 6,表示 节点 2 连接 节点5 为有向图,节点2 指向 节点5,边的权值为6。

如果想表示无向图,即:grid[2][5] = 6,grid[5][2] = 6,表示节点2 与 节点5 相互连通,权值为6。

如图:

在一个 n (节点数)为8 的图中,就需要申请 8 * 8 这么大的空间。

图中有一条双向边,即:grid[2][5] = 6,grid[5][2] = 6

这种表达方式(邻接矩阵) 在 边少,节点多的情况下,会导致申请过大的二维数组,造成空间浪费。

而且在寻找节点连接情况的时候,需要遍历整个矩阵,即 n * n 的时间复杂度,同样造成时间浪费。

邻接矩阵的优点:

  • 表达方式简单,易于理解
  • 检查任意两个顶点间是否存在边的操作非常快
  • 适合稠密图,在边数接近顶点数平方的图中,邻接矩阵是一种空间效率较高的表示方法。

缺点:

  • 遇到稀疏图,会导致申请过大的二维数组造成空间浪费 且遍历 边 的时候需要遍历整个n * n矩阵,造成时间浪费。

邻接表

邻接表 使用 数组 + 链表的方式来表示。 邻接表是从边的数量来表示图,有多少边 才会申请对应大小的链表。

邻接表的构造如图:

这里表达的图是:

  • 节点1 指向 节点3 和 节点5
  • 节点2 指向 节点4、节点3、节点5
  • 节点3 指向 节点4
  • 节点4指向节点1

有多少边 邻接表才会申请多少个对应的链表节点。

从图中可以直观看出 使用 数组 + 链表 来表达 边的连接情况 。

邻接表的优点:

  • 对于稀疏图的存储,只需要存储边,空间利用率高
  • 遍历节点连接情况相对容易

缺点:

  • 检查任意两个节点间是否存在边,效率相对低,需要 O(V)时间,V表示某节点连接其他节点的数量。
  • 实现相对复杂,不易理解

图的遍历方式

图的遍历方式基本是两大类:

  • 深度优先搜索(dfs)
  • 广度优先搜索(bfs)

二叉树的递归遍历,是dfs 在二叉树上的遍历方式。

二叉树的层序遍历,是bfs 在二叉树上的遍历方式。

dfs 和 bfs 一种搜索算法,可以在不同的数据结构上进行搜索,在二叉树章节里是在二叉树这样的数据结构上搜索。

而在图论章节,则是在图(邻接表或邻接矩阵)上进行搜索。

2.深度优先搜索理论基础

dfs 与 bfs 区别

深度优先搜索(dfs)和广度优先搜索(bfs)区别

  • dfs是可一个方向去搜,不到黄河不回头,直到遇到绝境了,搜不下去了,再换方向(换方向的过程就涉及到了回溯)。
  • bfs是先把本节点所连接的所有节点遍历一遍,走到下一个节点的时候,再把连接节点的所有节点遍历一遍,搜索方向更像是广度,四面八方的搜索过程。

dfs 搜索过程

图一是一个无向图,我们要搜索从节点1到节点6的所有路径。

图一

那么dfs搜索的第一条路径是这样的: (假设第一次延默认方向,就找到了节点6),图二

图二

此时我们找到了节点6,(遇到黄河了,是不是应该回头了),那么应该再去搜索其他方向了。 如图三:

图三

路径2撤销了,改变了方向,走路径3(红色线), 接着也找到终点6。 那么撤销路径2,改为路径3,在dfs中其实就是回溯的过程。

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图四中,路径4撤销(回溯的过程),改为路径5。

图四

又找到了一条从节点1到节点6的路径,又到黄河了,此时再回头,下图图五,路径6撤销(回溯的过程),改为路径7,路径8 和 路径7,路径9, 结果发现死路一条,都走到了自己走过的节点。

图五

那么节点2所连接路径和节点3所链接的路径 都走过了,撤销路径只能向上回退,去选择撤销当初节点4的选择,也就是撤销路径5,改为路径10 。 如图图六:

图六

上图演示中,其实我并没有把 所有的 从节点1 到节点6的dfs(深度优先搜索)的过程都画出来,那样太冗余了,但 已经把dfs 关键的地方都涉及到了,关键就两点:

  • 搜索方向,是认准一个方向搜,直到碰壁之后再换方向
  • 换方向是撤销原路径,改为节点链接的下一个路径,回溯的过程。

深搜三部曲

在 二叉树递归讲解中,给出了递归三部曲。

回溯算法讲解中,给出了 回溯三部曲。

其实深搜也是一样的,深搜三部曲如下:

  1. 确认递归函数,参数
  2. 确认终止条件
  3. 处理目前搜索节点出发的路径

3.所有可达路径

卡码网题目链接(ACM模式)(opens new window)

【题目描述】

给定一个有 n 个节点的有向无环图,节点编号从 1 到 n。请编写一个函数,找出并返回所有从节点 1 到节点 n 的路径。每条路径应以节点编号的列表形式表示。

【输入描述】

第一行包含两个整数 N,M,表示图中拥有 N 个节点,M 条边

后续 M 行,每行包含两个整数 s 和 t,表示图中的 s 节点与 t 节点中有一条路径

【输出描述】

输出所有的可达路径,路径中所有节点的后面跟一个空格,每条路径独占一行,存在多条路径,路径输出的顺序可任意。

如果不存在任何一条路径,则输出 -1。

注意输出的序列中,最后一个节点后面没有空格! 例如正确的答案是 1 3 5,而不是 1 3 5, 5后面没有空格!

【输入示例】

5 5
1 3
3 5
1 2
2 4
4 5

【输出示例】

1 3 5
1 2 4 5  

提示信息

用例解释:

有五个节点,其中的从 1 到达 5 的路径有两个,分别是 1 -> 3 -> 5 和 1 -> 2 -> 4 -> 5。

因为拥有多条路径,所以输出结果为:

1 3 5
1 2 4 5

1 2 4 5
1 3 5

都算正确。

数据范围:

  • 图中不存在自环
  • 图中不存在平行边
  • 1 <= N <= 100
  • 1 <= M <= 500

采用深度优先搜索(DFS)算法来遍历图,找出从节点 1 到节点 n 的所有路径。具体步骤如下:

  1. 读取图的节点数和边数,并构建图的邻接矩阵。
  2. 从节点 1 开始进行深度优先搜索,在搜索过程中记录当前路径。
  3. 当到达目标节点 n 时,将当前路径添加到结果列表中。
  4. 最后输出所有找到的路径,如果没有找到任何路径,则输出 -1。

在无向图的邻接矩阵表示里,通常会创建一个 n x n 的二维数组 graph,这里的 n 是图中节点的数量。数组的行和列分别对应图中的节点,graph[i][j] 表示节点 i 到节点 j 是否存在边。当 graph[i][j] = 1 时,意味着节点 i 和节点 j 之间存在一条直接相连的边;若 graph[i][j] = 0,则表示这两个节点之间没有直接相连的边。

对于无向图而言,由于边是无方向的,即如果节点 i 到节点 j 有边,那么节点 j 到节点 i 也必然有边,所以邻接矩阵是对称的,也就是 graph[i][j] 始终等于 graph[j][i]

public class All_Reachable_Paths {
        static List<List<Integer>> result = new ArrayList<>();//维列表,用于存储所有从节点 1 到节点 n 的路径。每个路径是一个整数列表,而 result 列表包含所有这样的路径列表。
        static List<Integer> path = new ArrayList<>();//维列表,用于在深度优先搜索(DFS)过程中存储当前正在探索的路径。
        public static void dfs(int[][] graph, int x, int n) {//递归方法,用于执行深度优先搜索。int[][] graph 参数代表图的邻接矩阵,其中graph[i][j] = 1表示节点i和节点j之间有一条边。int x 是当前节点。int n 是目标节点。
            if (x == n) {//当当前节点 x 等于目标节点 n 时,说明找到了一条从节点 1 到节点 n 的路径。将当前路径 path 的副本添加到结果列表 result 中,然后返回。
                result.add(new ArrayList<>(path));
                return;
            }//对于图中的每个节点i(从1到n),检查是否存在从当前节点x到节点i的边(graph[x][i] == 1)。
            for (int i = 1; i <= n; i++) {//如果存在边,则将节点i添加到当前路径path中,并递归调用dfs方法,以节点i作为新的当前节点。
                if (graph[x][i] == 1) {
                    path.add(i);
                    dfs(graph, i, n);
                    path.remove(path.size() - 1);//在递归调用返回后,从当前路径path中移除最后一个节点,为了回溯到上一个状态,以便尝试其他可能的路径。
                }
            }
        }
        public static void main(String[] args) {
            Scanner scanner = new Scanner(System.in);//使用Scanner类从标准输入读取数据。
            int n = scanner.nextInt();//首先读取两个整数n和m,分别代表图中的节点数和边数。
            int m = scanner.nextInt();
            int[][] graph = new int[n + 1][n + 1];//创建一个n+1乘以n+1的二维数组graph,初始化为0,代表无向图的邻接矩阵。
            for (int i = 0; i < m; i++) {//循环m次,每次读取两个整数s和t,代表一条边的起点和终点,并将graph[s][t]和graph[t][s]设置为1,表示无向图的边。
                int s = scanner.nextInt();
                int t = scanner.nextInt();
                graph[s][t] = 1;
            }
            path.add(1);//将节点1添加到路径path中,因为搜索从节点1开始。
            dfs(graph, 1, n);//从节点1开始,调用dfs方法寻找所有到节点n的路径。
            if (result.isEmpty()) System.out.println(-1);//如果result为空,表示没有找到任何路径,输出-1。否则,遍历result中的每个路径,并打印出来。对于每个路径,除了最后一个节点外,其他节点后面都跟一个空格。
            for (List<Integer> pa : result) {
                for (int i = 0; i < pa.size() - 1; i++) {
                    System.out.print(pa.get(i) + " ");
                }
                System.out.println(pa.get(pa.size() - 1));
            }
        }
}


 假设输入如下:

4 4
1 2
2 3
3 4
1 3

这表示图中有 4 个节点(n = 4),4 条边,边分别为 (1, 2)(2, 3)(3, 4) 和 (1, 3)

1. 主方法 main 中的初始化操作

创建一个 5 x 5 的二维数组 graph 作为邻接矩阵,初始值全为 0。

2. 构建邻接矩阵

  • 第一次循环:s = 1t = 2,则 graph[1][2] = 1graph[2][1] = 1
  • 第二次循环:s = 2t = 3,则 graph[2][3] = 1graph[3][2] = 1
  • 第三次循环:s = 3t = 4,则 graph[3][4] = 1graph[4][3] = 1
  • 第四次循环:s = 1t = 3,则 graph[1][3] = 1graph[3][1] = 1

此时邻接矩阵 graph 如下:

  0  1  2  3  4
 0 [0, 0, 0, 0, 0]
 1 [0, 0, 1, 1, 0]
 2 [0, 1, 0, 1, 0]
 3 [0, 1, 1, 0, 1]
 4 [0, 0, 0, 1, 0]

3. 初始化路径并开始深度优先搜索

将节点 1 添加到路径 path 中,此时 path = [1],然后调用 dfs 方法从节点 1 开始搜索到节点 4 的所有路径。

4. 深度优先搜索 dfs 过程

第一次调用 dfs(graph, 1, 4)
  • 当前节点 x = 1,目标节点 n = 4x != n,进入 for 循环。
  • 当 i = 2 时,graph[1][2] == 1,将节点 2 添加到 path 中,path = [1, 2],递归调用 dfs(graph, 2, 4)
第二次调用 dfs(graph, 2, 4)
  • 当前节点 x = 2,目标节点 n = 4x != n,进入 for 循环。
  • 当 i = 3 时,graph[2][3] == 1,将节点 3 添加到 path 中,path = [1, 2, 3],递归调用 dfs(graph, 3, 4)
第三次调用 dfs(graph, 3, 4)
  • 当前节点 x = 3,目标节点 n = 4x != n,进入 for 循环。
  • 当 i = 4 时,graph[3][4] == 1,将节点 4 添加到 path 中,path = [1, 2, 3, 4]
  • 此时 x == n,将 path 的副本 [1, 2, 3, 4] 添加到 result 中,result = [[1, 2, 3, 4]],然后返回。
  • 返回到 dfs(graph, 3, 4) 后,执行 path.remove(path.size() - 1)path = [1, 2, 3]
回到第二次调用 dfs(graph, 2, 4)
  • 继续 for 循环,没有其他满足 graph[2][i] == 1 的节点,执行 path.remove(path.size() - 1)path = [1, 2]
  • 回到第一次调用 dfs(graph, 1, 4)
第一次调用 dfs(graph, 1, 4) 继续
  • 当 i = 3 时,graph[1][3] == 1,将节点 3 添加到 path 中,path = [1, 3],递归调用 dfs(graph, 3, 4)
第四次调用 dfs(graph, 3, 4)
  • 当前节点 x = 3,目标节点 n = 4x != n,进入 for 循环。
  • 当 i = 4 时,graph[3][4] == 1,将节点 4 添加到 path 中,path = [1, 3, 4]
  • 此时 x == n,将 path 的副本 [1, 3, 4] 添加到 result 中,result = [[1, 2, 3, 4], [1, 3, 4]],然后返回。
  • 返回到 dfs(graph, 3, 4) 后,执行 path.remove(path.size() - 1)path = [1, 3]
回到第一次调用 dfs(graph, 1, 4)
  • 继续 for 循环,没有其他满足 graph[1][i] == 1 的节点,执行 path.remove(path.size() - 1)path = [1]

5. 输出结果

由于 result 不为空,会依次输出 result 中的每个路径:

1 2 3 4
1 3 4

通过以上步骤,我们可以清晰地看到代码是如何利用深度优先搜索算法找出从节点 1 到节点 4 的所有路径的。

4.广度优先搜索理论基础

在深度优先搜索的讲解中,有深度优先搜索和广度优先搜索的区别。

广搜(bfs)是一圈一圈的搜索过程,和深搜(dfs)是一条路跑到黑然后再回溯。

广搜的使用场景

广搜的搜索方式就适合于解决两个点之间的最短路径问题。

因为广搜是从起点出发,以起始点为中心一圈一圈进行搜索,一旦遇到终点,记录之前走过的节点就是一条最短路。

当然,也有一些问题是广搜 和 深搜都可以解决的,例如岛屿问题,这类问题的特征就是不涉及具体的遍历方式,只要能把相邻且相同属性的节点标记上就行

广搜的过程

BFS是一圈一圈的搜索过程,但具体是怎么一圈一圈来搜呢。

我们用一个方格地图,假如每次搜索的方向为 上下左右(不包含斜上方),那么给出一个start起始位置,那么BFS就是从四个方向走出第一步。

图一

如果加上一个end终止位置,那么使用BFS的搜索过程如图所示:

图二

我们从图中可以看出,从start起点开始,是一圈一圈,向外搜索,方格编号1为第一步遍历的节点,方格编号2为第二步遍历的节点,第四步的时候我们找到终止点end。

正是因为BFS一圈一圈的遍历方式,所以一旦遇到终止点,那么一定是一条最短路径。

而且地图还可以有障碍,如图所示:

图三

在第五步,第六步 我只把关键的节点染色了,其他方向周边没有去染色,大家只要关注关键地方染色的逻辑就可以。

从图中可以看出,如果添加了障碍,我们是第六步才能走到end终点。

只要BFS只要搜到终点一定是一条最短路径。

5.岛屿数量

卡码网题目链接(ACM模式)(opens new window)

题目描述:

给定一个由 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

输出示例:

3

提示信息

根据测试案例中所展示,岛屿数量共有 3 个,所以输出 3。

数据范围:

  • 1 <= N, M <= 50

深搜(dfs)

public class Number_of_Islands_Depth_First_Search {
    public static int[][] dir ={
  
  {0,1},{1,0},{-1,0},{0,-1}};//二维数组,存储了深度优先搜索中可以探索的四个方向:右({0, 1})、下({1, 0})、左({-1, 0})、上({0, -1})。在 DFS 过程中,通过这个数组可以方便地获取当前单元格的相邻单元格坐标。
    public static void dfs(boolean[][] visited,int x,int y ,int [][]grid) {//递归方法,用于执行深度优先搜索。boolean[][] visited 参数是一个与grid同样大小的二维数组,用来标记某个单元格是否已经被访问过。int x 和 int y 分别是当前单元格的行和列索引。int[][] grid 是输入的二维数组,表示地图,其中1表示陆地,0表示水域。
        for (int i = 0; i < 4; i++) {//对于当前单元格的每一个可能的相邻单元格(右、下、左、上),遍历四个方向,计算当前单元格在每个方向上的相邻单元格坐标 nextX 和 nextY。
            int nextX=x+dir[i][0];
            int nextY=y+dir[i][1];
            if(nextY<0||nextX<0||nextX>= grid.length||nextY>=grid[0].length)//检查相邻单元格的坐标是否越界,如果越界则跳过该方向。
                continue;
            if(!visited[nextX][nextY]&&grid[nextX][nextY]==1)//若相邻单元格未被访问且为陆地(值为 1),则将其标记为已访问,并递归调用 dfs 方法继续探索该单元格的相邻单元格。
            {
                visited[nextX][nextY]=true;
                dfs(visited,nextX,nextY,grid);
            }
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。
        int m= sc.nextInt();//首先读取两个整数m和n,分别代表地图的行数和列数。
        int n = sc.nextInt();
        int[][] grid = new int[m][n];//创建一个大小为 m x n 的二维数组 grid,并循环读取 m x n 个整数填充该数组,这些整数表示地图中每个单元格的状态(0 或 1)。
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j]=sc.nextInt();
            }
        }
        boolean[][]visited =new boolean[m][n];//创建一个大小为m x n的布尔二维数组visited,初始化为false,表示所有单元格都未被访问。
        int ans = 0;//初始化一个整数ans用于存储岛屿的数量。
        for (int i = 0; i < m; i++) {//遍历grid中的每个单元格,对于每个值为1且未被访问的单元格,执行以下操作:将ans加1,表示找到一个新岛屿。将该单元格标记为已访问。调用dfs方法从该单元格开始搜索整个岛屿,将与该单元格相连的所有陆地单元格都标记为已访问。
            for (int j = 0; j < n; j++) {
                if(!visited[i][j]&&grid[i][j]==1)
                {
                    ans++;
                    visited[i][j]=true;
                    dfs(visited,i,j,grid);
                }
            }
        }
        System.out.println(ans);
    }
}

假设输入如下:

3 3
1 1 0
1 1 0
0 0 1

这表示地图是一个 3 行 3 列的二维网格,具体的地图内容为:

1 1 0
1 1 0
0 0 1

详细执行步骤

1. 输入读取和初始化

根据输入,grid 数组被初始化为:

[
  [1, 1, 0],
  [1, 1, 0],
  [0, 0, 1]
]

同时,创建一个 3 x 3 的布尔型二维数组 visited,初始值全为 false,表示所有单元格都未被访问:

[
  [false, false, false],
  [false, false, false],
  [false, false, false]
]

并将岛屿数量 ans 初始化为 0。

2. 遍历地图并统计岛屿数量
第一次发现陆地

当 i = 0j = 0 时,!visited[0][0] 为 true 且 grid[0][0] = 1,满足条件:

  • ans 加 1,此时 ans = 1
  • visited[0][0] 标记为 true
  • 调用 dfs(visited, 0, 0, grid) 开始深度优先搜索。

在 dfs 方法中:

  • 对于方向 i = 0(右),nextX = 0 + 0 = 0nextY = 0 + 1 = 1nextX 和 nextY 未越界,且 !visited[0][1] 为 true 且 grid[0][1] = 1,则 visited[0][1] 标记为 true,递归调用 dfs(visited, 0, 1, grid)
    • 在 dfs(visited, 0, 1, grid) 中,继续探索其相邻单元格,最终会将第一行的两个 1 以及第二行对应的两个 1 都标记为已访问。
  • 其他方向可能会遇到越界或者不是陆地的情况,会跳过相应的探索。
第二次发现陆地

当 i = 2j = 2 时,!visited[2][2] 为 true 且 grid[2][2] = 1,满足条件:

  • ans 加 1,此时 ans = 2
  • visited[2][2] 标记为 true
  • 调用 dfs(visited, 2, 2, grid) 开始深度优先搜索。由于这个 1 没有相邻的其他 1,所以在 dfs 方法中探索相邻单元格时,不会再递归调用 dfs 方法。
3. 输出结果

最终输出 ans 的值为 2,表示地图中有 2 个岛屿。

广搜(bfs)

public class Number_of_Islands_Breadth_First_Search {
  static class pair {//pair 是一个静态内部类,用于存储二维网格中单元格的坐标。first 表示行坐标,second 表示列坐标。
        int first;
        int second;
        pair(int first, int second) {//构造函数 pair(int first, int second) 用于初始化坐标对。
            this.first = first;
            this.second = second;
        }
    }
    public static int[][] dir = {
  
  {0, 1}, {1, 0}, {0, -1}, {-1, 0}};//二维数组,存储了广度优先搜索中可以探索的四个方向:右({0, 1})、下({1, 0})、左({0, -1})、上({-1, 0})。在 BFS 过程中,通过这个数组可以方便地获取当前单元格的相邻单元格坐标。
    public static void bfs(int[][] grid, boolean[][] visited, int x, int y) {//方法,用于执行广度优先搜索。int[][] grid 是输入的二维数组,表示地图,其中1表示陆地,0表示水域。boolean[][] visited 参数是一个与grid同样大小的二维数组,用来标记某个单元格是否已经被访问过。int x 和 int y 分别是当前单元格的行和列索引。
        Queue<pair> queue = new LinkedList<pair>();//创建一个队列queue,用于存储待访问的坐标对。
        queue.add(new pair(x, y));//将起始坐标 (x, y) 封装成 pair 对象添加到队列中,并将该单元格标记为已访问。
        visited[x][y] = true;
        while (!queue.isEmpty()) {//while 循环,只要队列不为空:从队列中取出队首元素,获取其行坐标 curX 和列坐标 curY。
            int curX = queue.peek().first;
            int curY = queue.poll().second;
            for (int i = 0; i < 4; i++) {//遍历四个方向,计算当前单元格在每个方向上的相邻单元格坐标 nextX 和 nextY。
                int nextX = curX + dir[i][0];
                int nextY = curY + dir[i][1];
                if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) {//检查相邻单元格的坐标是否越界,如果越界则跳过该方向。
                    continue;
                }
                if (!visited[nextX][nextY] && grid[nextX][nextY] == 1) {//若相邻单元格未被访问且为陆地(值为 1),则将其封装成 pair 对象添加到队列中,并将该单元格标记为已访问。
                    queue.add(new pair(nextX, nextY));
                    visited[nextX][nextY] = true;
                }
            }
        }
    }

    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);//创建一个 Scanner 对象,用于从标准输入(通常是键盘)读取数据。
        int m = sc.nextInt();//依次读取两个整数,分别赋值给 m 和 n,其中 m 表示地图的行数,n 表示地图的列数。
        int n = sc.nextInt();
        int[][] grid = new int[m][n];//创建一个大小为 m x n 的二维整数数组 grid,用于存储地图数据,其中 1 表示陆地,0 表示水域。
        boolean[][] visited = new boolean[m][n];//创建一个大小为m x n的布尔二维数组visited,初始化为false,表示所有单元格都未被访问。
        int ans = 0;//初始化一个整数变量 ans,用于记录地图中岛屿的数量,初始值为 0。
        for (int i = 0; i < m; i++) {//循环读取m x n个整数填充grid。
            for (int j = 0; j < n; j++) {
                grid[i][j] = sc.nextInt();
            }
        }
        for (int i = 0; i < m; i++) {//遍历grid中的每个单元格,对于每个值为1且未被访问的单元格,执行以下操作:将ans加1,表示找到一个新岛屿。调用 bfs 方法从当前单元格 (i, j) 开始进行广度优先搜索,将与该单元格相连的所有陆地单元格标记为已访问,这样可以确保一个岛屿的所有陆地单元格只会被统计一次。
            for (int j = 0; j < n; j++) {
                if (!visited[i][j] && grid[i][j] == 1) {
                    ans++;
                    bfs(grid, visited, i, j);
                }
            }
        }
        System.out.println(ans);
    }
}

假设输入的二维数组grid为:

1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1

执行过程如下:

  1. 初始化visited数组,所有值为falseans初始化为0。

  2. 遍历grid,从左上角的(0,0)开始,发现grid[0][0]为1且未被访问:

    • ans加1,变为1。

    • 调用bfs方法,将(0,0)加入队列,标记为已访问。

    • 队列不为空,开始循环:

      • 取出队首元素(0,0),遍历其四个方向:

        • (0,1)为1且未访问,加入队列,标记为已访问。

        • (1,0)为1且未访问,加入队列,标记为已访问。

        • 其他方向越界或为0,跳过。

      • 取出队首元素(0,1),遍历其四个方向:

        • (0,2)为0,跳过。

        • (1,1)为1且未访问,加入队列,标记为已访问。

        • 其他方向越界或已访问,跳过。

      • 取出队首元素(1,0),遍历其四个方向:

        • (1,1)已访问,跳过。

        • (2,0)为0,跳过。

        • 其他方向越界或已访问,跳过。

      • 取出队首元素(1,1),遍历其四个方向:

        • (1,2)为0,跳过。

        • (2,1)为0,跳过。

        • 其他方向越界或已访问,跳过。

      • 队列为空,BFS结束。

  3. 继续遍历grid,发现grid[2][2]为1且未被访问:

    • ans加1,变为2。

    • 调用bfs方法,将(2,2)加入队列,标记为已访问。

    • 队列不为空,开始循环:

      • 取出队首元素(2,2),遍历其四个方向:

        • (2,3)为0,跳过。

        • (3,2)为0,跳过。

        • 其他方向越界或已访问,跳过。

      • 队列为空,BFS结束。

  4. 继续遍历grid,发现grid[3][3]为1且未被访问:

    • ans加1,变为3。

    • 调用bfs方法,将(3,3)加入队列,标记为已访问。

    • 队列不为空,开始循环:

      • 取出队首元素(3,3),遍历其四个方向:

        • (3,4)为1且未访问,加入队列,标记为已访问。

        • (4,3)越界,跳过。

        • 其他方向越界或已访问,跳过。

      • 取出队首元素(3,4),遍历其四个方向:

        • (3,5)越界,跳过。

        • (4,4)越界,跳过。

        • 其他方向越界或已访问,跳过。

      • 队列为空,BFS结束。

  5. 继续遍历grid,未发现新的未访问且值为1的单元格,循环结束。

  6. 输出ans,结果为3,表示该地图中有三个岛屿。

6. 岛屿的最大面积

卡码网题目链接(ACM模式)(opens new window)

题目描述

给定一个由 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

输出示例

4

提示信息

样例输入中,岛屿的最大面积为 4。

数据范围:

  • 1 <= M, N <= 50。
public class Max_Area_Island {
    static final int[][] dir={
  
  {0,1},{1,0},{0,-1},{-1,0}};//静态常量二维数组,存储了深度优先搜索中可以探索的四个方向,分别是右({0, 1})、下({1, 0})、左({0, -1})、上({-1, 0})。在 DFS 过程中,通过这个数组可以方便地获取当前单元格的相邻单元格坐标。
    static int result=0;//用于存储最大岛屿的面积,初始值为 0。
    static int count=0;//用于在 DFS 过程中临时记录当前正在探索的岛屿的面积,初始值为 0。
    public static void main(String[] args){
        Scanner scanner = new Scanner(System.in);//使用 Scanner 类从标准输入读取地图的行数 n 和列数 m。
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        int[][] map = new int[n][m];//创建一个大小为 n x m 的二维整数数组 map,并通过两层嵌套的 for 循环读取 n x m 个整数填充该数组,这些整数表示地图中每个单元格的状态(0 或 1)。
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                map[i][j]=scanner.nextInt();
            }
        }
        boolean[][] visited = new boolean[n][m];//创建一个大小为 n x m 的二维布尔数组 visited,初始化为 false,用于标记地图中每个单元格是否已经被访问过。
        for (int i = 0; i < n; i++) {//使用两层嵌套的 for 循环遍历地图的每个单元格。
            for (int j = 0; j < m; j++) {
                if(!visited[i][j]&&map[i][j]==1){//对于每个未被访问过且值为 1 的单元格,将 count 重置为 0,表示开始计算一个新岛屿的面积。
                    count=0;
                    dfs(map,visited,i,j);//调用 dfs 方法从该单元格开始进行深度优先搜索,计算该岛屿的面积。
                    result= Math.max(count, result);//搜索结束后,使用 Math.max(count, result) 更新 result 的值,确保 result 始终存储最大岛屿的面积。
                }
            }
        }
        System.out.println(result);
    }

   public static void dfs(int[][] map,boolean[][] visited,int x,int y){//map:这是一个二维整数数组,代表整个地图。其中 1 表示陆地,0 表示水域。visited:一个与 map 大小相同的二维布尔数组,用于标记每个单元格是否已经被访问过。初始时,所有元素都为 false,表示所有单元格都未被访问。x 和 y:表示当前正在访问的单元格的行索引和列索引。
        count++;//每当进入 dfs 方法时,说明当前单元格 (x, y) 是陆地,因此将 count 加 1。count 是一个全局变量,用于记录当前正在探索的岛屿的面积。
        visited[x][y]=true;//将当前单元格标记为已访问,避免后续再次访问该单元格,防止陷入无限循环。
        for (int i = 0; i < 4; i++) {//通过循环遍历这四个方向,计算当前单元格 (x, y) 在每个方向上的相邻单元格的坐标 (nextX, nextY)。
            int nextX=x+dir[i][0];
            int nextY=y+dir[i][1];
            if(nextX<0||nextY<0//检查相邻单元格的坐标是否越界(小于 0),如果越界则跳过。
                    ||nextX>=map.length||nextY>=map[0].length//检查相邻单元格的坐标是否超出地图范围,如果超出则跳过。
                    ||visited[nextX][nextY]||map[nextX][nextY]==0)continue;//检查相邻单元格是否已经被访问过,如果已经访问过则跳过。检查相邻单元格是否为水域(值为 0),如果是水域则跳过。
            dfs(map,visited,nextX,nextY);//如果相邻单元格满足继续访问的条件,则递归调用 dfs 方法,以该相邻单元格为新的起始点继续进行深度优先搜索。
        }
    }
}

假设输入的地图为:

1 1 0 0 0
1 1 0 0 0
0 0 1 0 0
0 0 0 1 1

执行过程如下:

  1. 初始化

    • map:输入的地图。

    • visited:标记数组,初始值为false

    • result:最大岛屿面积,初始值为0。

    • count:当前岛屿面积,初始值为0。

  2. 遍历地图

    • 遍历到(0,0),发现map[0][0]为1且未访问:

      • count重置为0。

      • 调用dfs(map, visited, 0, 0)

        • count加1,标记(0,0)为已访问。

        • 遍历四个方向,发现(0,1)(1,0)为1且未访问,递归调用dfs

          • (0,1)count加1,标记为已访问,继续遍历四个方向,发现(1,1)为1且未访问,递归调用dfs

          • (1,1)count加1,标记为已访问,继续遍历四个方向,发现无符合条件的相邻单元格。

          • (1,0)count加1,标记为已访问,继续遍历四个方向,发现无符合条件的相邻单元格。

        • 最终,count为4,表示当前岛屿面积为4。

      • 更新resultMath.max(result, count),即result=4

    • 继续遍历,发现(2,2)为1且未访问:

      • count重置为0。

      • 调用dfs(map, visited, 2, 2)

        • count加1,标记(2,2)为已访问。

        • 遍历四个方向,发现无符合条件的相邻单元格。

      • 更新resultMath.max(result, count),即result=4

    • 继续遍历,发现(3,3)为1且未访问:

      • count重置为0。

      • 调用dfs(map, visited, 3, 3)

        • count加1,标记(3,3)为已访问。

        • 遍历四个方向,发现(3,4)为1且未访问,递归调用dfs

        • (3,4)count加1,标记为已访问。

      • 更新resultMath.max(result, count),即result=4

  3. 输出结果

    • 最终result为4,表示最大岛屿面积为4。

7.孤岛的总面积

卡码网:101. 孤岛的总面积(opens new window)

题目描述

给定一个由 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

提示信息:

在矩阵中心部分的岛屿,因为没有任何一个单元格接触到矩阵边缘,所以该岛屿属于孤岛,总面积为 1。

数据范围:

1 <= M, N <= 50。

public class Total_Area_of_Isolated_Islands {
    private static int count = 0;//用于存储所有孤岛的总面积,初始值为 0。
    private static final int[][] dir = {
  
  {0, 1}, {1, 0}, {-1, 0}, {0, -1}};//静态常量二维数组,定义了四个方向的移动,分别对应右({0, 1})、下({1, 0})、上({-1, 0})、左({0, -1})。在 BFS 过程中,通过这个数组可以方便地获取当前单元格的相邻单元格坐标。
    private static void bfs(int[][] grid, int x, int y) {//私有静态方法,用于执行广度优先搜索。int[][] grid 是输入的二维数组,表示地图,其中1表示陆地,0表示水域。int x 和 int y 是当前单元格的坐标。
        Queue<int[]> que = new LinkedList<>();//创建一个队列 que,将起始坐标 (x, y) 加入队列,并将该单元格标记为已访问(将 grid[x][y] 设置为 0)。同时,count 加 1,表示当前岛屿面积增加。
        que.add(new int[]{x, y});
        grid[x][y] = 0;
        count++;
        while (!que.isEmpty()) {//进入 while 循环,只要队列不为空,就从队列中取出一个坐标 (curx, cury)。
            int[] cur = que.poll();
            int curx = cur[0];
            int cury = cur[1];
            for (int i = 0; i < 4; i++) {//遍历四个方向,计算相邻单元格的坐标 (nextx, nexty)。
                int nextx = curx + dir[i][0];
                int nexty = cury + dir[i][1];
                if (nextx < 0 || nextx >= grid.length || nexty < 0 || nexty >= grid[0].length) continue;//检查相邻单元格的坐标是否越界,如果越界则跳过。
                if (grid[nextx][nexty] == 1) {
                    que.add(new int[]{nextx, nexty});//如果相邻单元格是陆地(值为 1),将其加入队列,count 加 1,并将该单元格标记为已访问(将 grid[nextx][nexty] 设置为 0)。
                    count++;
                    grid[nextx][nexty] = 0;
                }
            }
        }
    }

    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);
        int n = scanner.nextInt();//使用Scanner类从标准输入读取数据。首先读取两个整数n和m,分别代表地图的行数和列数。创建一个大小为n x m的二维数组grid,用于存储地图数据。循环读取n x m个整数填充grid。
        int m = scanner.nextInt();
        int[][] grid = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                grid[i][j] = scanner.nextInt();
            }//接下来,程序检查地图的边界上的陆地单元格,并对其执行BFS,以确保边界上的陆地不被重复计算。重置count为0,然后遍历整个地图,对所有未访问的陆地单元格执行BFS,计算每个孤岛的面积。最后,输出所有孤岛的总面积。
        }//主要目的是将与地图边界相连的陆地全部标记为已访问,这样在后续计算孤立岛屿面积时,就不会把这些与边界相连的陆地包含进去。
        for (int i = 0; i < n; i++) {//首先,通过外层的 for 循环遍历地图的左右边界(即每一行的第一列和最后一列)。如果发现边界上的单元格为陆地(值为 1),则调用 bfs 方法从该单元格开始进行广度优先搜索。在 bfs 过程中,会将与该边界陆地相连的所有陆地单元格标记为已访问(将其值置为 0),同时会增加 count 的值。
            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++) {//接着,通过内层的 for 循环遍历地图的上下边界(即第一行和最后一行的每一列)。同样,如果发现边界上的单元格为陆地,则调用 bfs 方法进行处理。
            if (grid[0][j] == 1) bfs(grid, 0, j);
            if (grid[n - 1][j] == 1) bfs(grid, n - 1, j);
        }
        count = 0;//由于之前调用 bfs 方法处理边界陆地时,count 已经记录了这些陆地的面积,而我们只关心孤立岛屿的面积,所以需要将 count 重置为 0,以便重新开始计算孤立岛屿的总面积。
        for (int i = 0; i < n; i++) {//使用嵌套的 for 循环遍历整个地图的每一个单元格。
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 1) bfs(grid, i, j);//如果发现某个单元格为陆地(值为 1),说明这是一个未被访问过的孤立岛屿的一部分,调用 bfs 方法从该单元格开始进行广度优先搜索。在 bfs 过程中,会将与该陆地单元格相连的所有陆地单元格标记为已访问(将其值置为 0),同时会不断增加 count 的值,从而计算出该孤立岛屿的面积。随着遍历的进行,会依次计算出所有孤立岛屿的面积并累加到 count 中。
            }
        }

        System.out.println(count);
    }
}

假设输入的地图为:

0 1 1 0 0
0 1 0 0 0
0 0 0 1 1
1 0 0 1 0

执行过程如下:

  1. 输入地图

    0 1 1 0 0
    0 1 0 0 0
    0 0 0 1 1
    1 0 0 1 0
  2. 处理边界陆地

    • 遍历边界,发现(0,1)(0,2)(3,0)(3,3)是边界上的陆地,调用bfs方法:

      • (0,1)(0,2)相连,形成一个与边界相连的陆地区域,面积为2。

      • (3,0)单独一个陆地,面积为1。

      • (3,3)单独一个陆地,面积为1。

    • 处理后地图变为:

      0 0 0 0 0
      0 0 0 0 0
      0 0 0 1 1
      0 0 0 0 0
  3. 计算孤立岛屿面积

    • 重置count为0。

    • 遍历地图,发现(2,3)(2,4)是孤立岛屿的一部分,调用bfs方法:

      • (2,3)(2,4)相连,形成一个孤立岛屿,面积为2。

    • 最终count为2。

  4. 输出结果

    • 孤立岛屿的总面积为2。

8.沉没孤岛

卡码网题目链接(ACM模式)(opens new window)

题目描述:

给定一个由 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

提示信息:

将孤岛沉没:

数据范围:

1 <= M, N <= 50

public class Sunken_Island {
    static int[][] dir = { {-1, 0}, {0, -1}, {1, 0}, {0, 1} };//静态二维数组,定义了四个方向的移动,分别对应上({-1, 0})、左({0, -1})、下({1, 0})、右({0, 1})。在深度优先搜索(DFS)过程中,通过这个数组可以方便地获取当前单元格的相邻单元格坐标。
    public static void dfs(int[][] grid, int x, int y) {//一个递归方法,用于深度优先搜索。int[][] grid 是输入的二维数组,表示地图。int x 和 int y 是当前单元格的坐标。
        grid[x][y] = 2;//先将当前土地格标记为2,表示已访问。
        for (int[] d : dir) {//遍历四个方向,计算当前单元格在每个方向上的相邻单元格坐标 (nextX, nextY)。
            int nextX = x + d[0];
            int nextY = y + d[1];
            if (nextX < 0 || nextX >= grid.length || nextY < 0 || nextY >= grid[0].length) continue;//检查相邻单元格的坐标是否越界,如果越界则跳过该方向。
            if (grid[nextX][nextY] == 0 || grid[nextX][nextY] == 2) continue;//检查相邻单元格是否为水域(值为 0)或已访问的土地(值为 2),如果是则跳过该方向。
            dfs(grid, nextX, nextY);//如果相邻单元格是未访问的陆地(值为 1),则递归调用 dfs 方法继续搜索。
        }
    }
    public static void main(String[] args) {
        Scanner scanner = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取两个整数n和m,分别代表地图的行数和列数。创建一个大小为n x m的二维数组grid,用于存储地图数据。循环读取n x m个整数填充grid。
        int n = scanner.nextInt();
        int m = scanner.nextInt();
        int[][] grid = new int[n][m];
        for (int i = 0; i < n; i++) {
            for (int j = 0; j < m; j++) {
                grid[i][j] = scanner.nextInt();
            }
        }
        for (int i = 0; i < n; i++) {//遍历地图的左右边界(即每一行的第一列和最后一列)。如果边界上的单元格为陆地(值为 1),则调用 dfs 方法从该单元格开始进行深度优先搜索。在 dfs 过程中,会将与该边界陆地相连的所有陆地单元格标记为 2,表示这些陆地与边界相连。
            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++) {//第二个 for 循环遍历地图的上下边界(即第一行和最后一行的每一列)。同样,如果边界上的单元格为陆地,则调用 dfs 方法进行处理。
            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 循环遍历整个地图的每一个单元格。
            for (int j = 0; j < m; j++) {
                if (grid[i][j] == 1) grid[i][j] = 0;//如果单元格的值为 1,说明它是不与边界相连的陆地(孤岛),将其值置为 0,表示将其 “沉没” 为水域。
                if (grid[i][j] == 2) grid[i][j] = 1;//如果单元格的值为 2,说明它是之前标记的与边界相连的陆地,将其值恢复为 1。
            }
        }
        for (int i = 0; i < n; i++) {//使用嵌套的 for 循环遍历处理后的地图,将每个单元格的值输出,同一行的单元格用空格分隔,不同行换行输出,从而展示出处理后的地图布局。
            for (int j = 0; j < m; j++) {
                System.out.print(grid[i][j] + " ");
            }
            System.out.println();
        }
        scanner.close();//关闭 Scanner 对象,释放相关资源,避免资源泄漏。
    }
}

假设输入的地图为:

1 1 0 0 0
1 1 0 0 0
0 0 1 1 0
0 0 1 1 0

执行过程如下:

  1. 输入地图

    1 1 0 0 0
    1 1 0 0 0
    0 0 1 1 0
    0 0 1 1 0
  2. 处理边界陆地

    • 遍历边界,发现(0,0)(0,1)是边界上的陆地,调用dfs方法:

      • (0,0)(0,1)相连,形成一个与边界相连的陆地区域,将其标记为2。

    • 处理后地图变为:

      2 2 0 0 0
      2 2 0 0 0
      0 0 1 1 0
      0 0 1 1 0
  3. 处理内部孤岛

    • 遍历地图,发现(2,2)(2,3)是未标记的陆地(值为1),将其标记为0(表示“沉没”)。

    • 处理后地图变为:

      2 2 0 0 0
      2 2 0 0 0
      0 0 0 0 0
      0 0 0 0 0
  4. 恢复边界标记

    • 将值为2的单元格恢复为1:

      1 1 0 0 0
      1 1 0 0 0
      0 0 0 0 0
      0 0 0 0 0
  5. 输出结果

    • 最终地图为:

      1 1 0 0 0
      1 1 0 0 0
      0 0 0 0 0
      0 0 0 0 0

9.水流问题

卡码网题目链接(ACM模式)(opens new window)

题目描述:

现有一个 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

提示信息:

图中的蓝色方块上的雨水既能流向第一组边界,也能流向第二组边界。所以最终答案为所有蓝色方块的坐标。

数据范围:

1 <= M, N <= 50

public class Water_Flow_Problem {
    public static void dfs(int[][] heights, int x, int y, boolean[][] visited, int preH) {//递归方法,用于执行深度优先搜索。int[][] heights 是输入的二维数组,表示地形的高度图。int x 和 int y 是当前单元格的坐标。boolean[][] visited 是一个布尔数组,用来标记某个单元格是否已经被访问过。int preH 是前一个单元格的高度,用于判断当前单元格是否为低洼地带,即水流可以流向的地方。
        if (x < 0 || x >= heights.length || y < 0 || y >= heights[0].length || visited[x][y]) return;//检查当前坐标是否越界或已被访问,如果是,则返回。
        if (heights[x][y] < preH) return;//检查当前单元格的高度是否小于前一个单元格的高度(heights[x][y] < preH),如果是,说明水流不能从当前单元格流向该方向,直接返回。
        visited[x][y] = true;//如果上述条件都不满足,将当前单元格标记为已访问
        dfs(heights, x + 1, y, visited, heights[x][y]);//递归地对当前单元格的四个相邻单元格(下、上、右、左)进行深度优先搜索,同时将当前单元格的高度作为下一次递归的 preH。
        dfs(heights, x - 1, y, visited, heights[x][y]);
        dfs(heights, x, y + 1, visited, heights[x][y]);
        dfs(heights, x, y - 1, visited, heights[x][y]);
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取两个整数m和n,分别代表地图的行数和列数。创建一个大小为m x n的二维数组heights,用于存储地图的高度数据。循环读取m x n个整数填充heights。创建两个布尔数组pacific和atlantic,分别用来标记单元格是否能够被太平洋和大西洋流入,初始值都为 false。
        int m = sc.nextInt();
        int n = sc.nextInt();
        int[][] heights = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                heights[i][j] = sc.nextInt();
            }//对地图的所有边界单元格执行DFS,从太平洋(左边界和上边界)开始,将能够流入的单元格标记为true在pacific数组中;从大西洋(右边界和下边界)开始,将能够流入的单元格标记为true在atlantic数组中。
        }
        boolean[][] pacific = new boolean[m][n];
        boolean[][] atlantic = new boolean[m][n];
        for (int i = 0; i < m; i++) {//对于左边界(列索引为 0)的每个单元格 (i, 0),调用 dfs 方法进行深度优先搜索。传入的 preH 为 Integer.MIN_VALUE,这确保边界单元格一定能被访问,因为任何单元格的高度都大于或等于 Integer.MIN_VALUE。在搜索过程中,如果某个单元格的水流可以从太平洋边界流到,就将 pacific 数组中对应位置标记为 true。
            dfs(heights, i, 0, pacific, Integer.MIN_VALUE);
            dfs(heights, i, n - 1, atlantic, Integer.MIN_VALUE);//对于上边界(行索引为 0)的每个单元格 (0, j),同样调用 dfs 方法进行搜索,并标记相应单元格。
        }
        for (int j = 0; j < n; j++) {//对于右边界(列索引为 n - 1)的每个单元格 (i, n - 1),调用 dfs 方法进行深度优先搜索,若某个单元格的水流可以从大西洋边界流到,就将 atlantic 数组中对应位置标记为 true。
            dfs(heights, 0, j, pacific, Integer.MIN_VALUE);
            dfs(heights, m - 1, j, atlantic, Integer.MIN_VALUE);//对于下边界(行索引为 m - 1)的每个单元格 (m - 1, j),进行同样的搜索和标记操作。
        }
        List<List<Integer>> res = new ArrayList<>();//创建一个列表res,用于存储同时能够被太平洋和大西洋流入的单元格的坐标。
        for (int i = 0; i < m; i++) {//使用两层嵌套的 for 循环遍历整个 heights 数组,检查 pacific 和 atlantic 数组中对应位置的元素。
            for (int j = 0; j < n; j++) {
                if (pacific[i][j] && atlantic[i][j]) {//如果两个数组中该位置都为 true,说明该单元格的水流既可以到达太平洋,也可以到达大西洋,将该单元格的坐标 (i, j) 封装成 List<Integer> 并添加到 res 列表中。
                    res.add(Arrays.asList(i, j));
                }
            }
        }//遍历整个地图,如果一个单元格在pacific和atlantic数组中都被标记为true,则将其坐标添加到res列表中。最后,遍历res列表,打印出所有同时能够被太平洋和大西洋流入的单元格的坐标。
        for (List<Integer> list : res) {//这是一个增强 for 循环,用于遍历列表 res 中的每个元素。res 是一个 List<List<Integer>> 类型的列表,其中每个内部列表 list 都存储着一个单元格的坐标(行索引和列索引)。
            for (int k = 0; k < list.size(); k++) {//遍历内部列表 list 中的每个元素。list 中通常包含两个元素,分别是单元格的行索引和列索引。
                if (k == 0) {
                    System.out.print(list.get(k) + " ");//当 k 等于 0 时,意味着当前正在处理内部列表 list 的第一个元素(即单元格的行索引)。此时,将该元素的值输出,并在后面添加一个空格,以符合输出格式要求。
                } else {
                    System.out.print(list.get(k));//当 k 不等于 0 时,意味着当前正在处理内部列表 list 的第二个元素(即单元格的列索引)。此时,直接将该元素的值输出,不添加额外的空格。
                }
            }
            System.out.println();//在外层循环的每次迭代结束后,使用 System.out.println() 输出一个换行符,这样每个单元格的坐标会单独占一行输出,使输出结果更加清晰易读。
        }
    }
}

“水流问题” 里,太平洋和大西洋并非现实地理意义上的海洋,而是一种概念化的设定,用于表示地图的特定边界区域,以此模拟水流的流向。具体定义如下:

太平洋

在该问题的设定里,太平洋代表地图的左边界和上边界。可以想象,水从这些边界开始,能顺着地势(高度不降低)向地图内部流动。代码里通过从左边界(列索引为 0)和上边界(行索引为 0)的各个单元格出发进行深度优先搜索(DFS),来标记那些水流可以从太平洋边界流到的内部单元格。

例如,对于一个 m x n 的地图:

  • 左边界的单元格坐标为 (i, 0),其中 i 的取值范围是从 0 到 m - 1
  • 上边界的单元格坐标为 (0, j),其中 j 的取值范围是从 0 到 n - 1

大西洋

大西洋代表地图的右边界和下边界。同样地,水从这些边界开始,能顺着地势向地图内部流动。代码里从右边界(列索引为 n - 1)和下边界(行索引为 m - 1)的各个单元格出发进行深度优先搜索,标记那些水流可以从大西洋边界流到的内部单元格。

具体来说:

  • 右边界的单元格坐标为 (i, n - 1),其中 i 的取值范围是从 0 到 m - 1
  • 下边界的单元格坐标为 (m - 1, j),其中 j 的取值范围是从 0 到 n - 1

假设输入的高度图为:

1 2 2 3 5
3 2 3 4 4
2 4 5 3 1
6 7 1 4 5
5 1 1 2 4

执行过程如下:

  1. 输入高度图

    1 2 2 3 5
    3 2 3 4 4
    2 4 5 3 1
    6 7 1 4 5
    5 1 1 2 4
  2. 从太平洋边界开始DFS

    • 太平洋边界包括左边界(列索引为0)和上边界(行索引为0)。

    • 对左边界和上边界的每个单元格调用dfs方法,将能够流入太平洋的单元格标记为truepacific数组中。

    • 例如,从(0,0)开始,水流可以流向(0,1)(1,0)等单元格,这些单元格在pacific数组中标记为true

  3. 从大西洋边界开始DFS

    • 大西洋边界包括右边界(列索引为n-1)和下边界(行索引为m-1)。

    • 对右边界和下边界的每个单元格调用dfs方法,将能够流入大西洋的单元格标记为trueatlantic数组中。

    • 例如,从(4,4)开始,水流可以流向(3,4)(4,3)等单元格,这些单元格在atlantic数组中标记为true

  4. 查找同时流入两个大洋的单元格

    • 遍历整个高度图,检查pacificatlantic数组中对应位置的元素。

    • 如果两个数组中某位置都为true,说明该单元格的水流既可以到达太平洋,也可以到达大西洋,将其坐标添加到结果列表中。

    • 在这个示例中,满足条件的单元格包括(0,4)(1,3)(2,2)(3,1)(4,0)

  5. 输出结果

    • 输出满足条件的单元格坐标:

      0 4
      1 3
      2 2
      3 1
      4 0

10.建造最大岛屿

卡码网题目链接(ACM模式)(opens new window)

题目描述:

给定一个由 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

提示信息

对于上面的案例,有两个位置可将 0 变成 1,使得岛屿的面积最大,即 6。

数据范围:

1 <= M, N <= 50。

public class Maximize_the_Area_of_an_Island {
    static int count;//用于在深度优先搜索(DFS)过程中计数当前岛屿的大小。
    static int mark;//用于标记新发现的岛屿,初始值为 2,后续每发现一个新岛屿,mark 的值就会加 1,这样可以在哈希表中区分不同的岛屿。
    static int[][] dirs = {
  
  {0, 1}, {0, -1}, {1, 0}, {-1, 0}};//定义了四个方向的移动,分别对应右({0, 1})、左({0, -1})、下({1, 0})、上({-1, 0}),用于在 DFS 过程中遍历相邻单元格。
    public static void dfs(int[][] grid, int x, int y, boolean[][] visited) {//递归方法,用于执行深度优先搜索,计算单个岛屿的面积。int[][] grid 是输入的二维数组,表示地图。int x 和 int y 是当前单元格的坐标。boolean[][] visited 是一个布尔数组,用来标记某个单元格是否已经被访问过。
        if (x < 0 || x >= grid.length || y < 0 || y >= grid[0].length) return;//检查当前坐标是否越界或该单元格是否已经被访问过,或者该单元格是否为水域(值为 0),如果是,则直接返回。
        if (visited[x][y] || grid[x][y] == 0) return;
        visited[x][y] = true;//如果当前单元格是陆地(grid[x][y] == 1),则将其标记为已访问,并增加岛屿计数。
        count++;
        grid[x][y] = mark;//将当前单元格的值更新为 mark,表示该单元格属于当前标记的岛屿。
        dfs(grid, x, y + 1, visited);//递归地对当前单元格的四个相邻单元格进行深度优先搜索。
        dfs(grid, x, y - 1, visited);
        dfs(grid, x + 1, y, visited);
        dfs(grid, x - 1, y, visited);
    }
    public static void main (String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取两个整数m和n,分别代表地图的行数和列数。创建一个大小为m x n的二维数组grid,用于存储地图数据。循环读取m x n个整数填充grid。初始化mark为2,用于标记不同的岛屿。visited数组用于记录每个单元格是否已经被访问过。
        int m = sc.nextInt();
        int n = sc.nextInt();
        int[][] grid = new int[m][n];
        for (int i = 0; i < m; i++) {
            for (int j = 0; j < n; j++) {
                grid[i][j] = sc.nextInt();
            }
        }
        mark = 2;
        boolean[][] visited = new boolean[m][n];
        HashMap<Integer, Integer> getSize = new HashMap<>();//创建一个HashMap<Integer, Integer> getSize用于存储每个岛屿的标记和对应的面积,键为岛屿标记,值为岛屿面积。
        HashSet<Integer> set = new HashSet<>();//创建一个HashSet<Integer> set来存储已经考虑过的岛屿标记,避免重复计算。
        boolean isAllIsland = true;//初始化为 true,用于判断整个地图是否全是陆地。
        for (int i = 0; i < m; i++) {//遍历地图计算每个岛屿的面积
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) isAllIsland = false;//如果遇到值为 0 的单元格(水域),则将 isAllIsland 置为 false。
                if (grid[i][j] == 1) {//当遇到值为 1 的单元格(陆地)时:将 count 重置为 0,用于统计当前岛屿的面积。
                    count = 0;
                    dfs(grid, i, j, visited);//调用 dfs 方法从该单元格开始进行深度优先搜索,计算当前岛屿的面积,搜索过程中会将该岛屿的所有单元格标记为已访问,并将其标记为当前的 mark 值。
                    getSize.put(mark, count);//将当前岛屿的面积 count 存入 getSize 中,键为当前的 mark 值。
                    mark++;//mark 加 1,为下一个新岛屿的标记做准备。
                }
            }
        }
        int result = 0;//初始化result为0,用于存储最大可能的岛屿面积。如果整个网格都是陆地(即isAllIsland为true),则最大岛屿面积就是整个网格的面积。
        if (isAllIsland) result =  m * n;
        for (int i = 0; i < m; i++) {//遍历整个地图,对每个水单元格,检查将其变为陆地后,周围岛屿的总面积,并更新result。最后,输出计算出的最大岛屿面积。
            for (int j = 0; j < n; j++) {
                if (grid[i][j] == 0) {
                    set.clear();//set 是一个 HashSet<Integer>,用于存储已经考虑过的岛屿标记,避免重复计算。在处理每个新的水域单元格之前,需要清空 set,确保它只记录当前水域单元格周围岛屿的标记。
                    int curSize = 1;//将 curSize 初始化为 1,表示将当前水域单元格变为陆地后,该单元格本身算一个面积单位。
                    for (int[] dir : dirs) {//检查当前水域单元格的四个相邻单元格
                        int curRow = i + dir[0];//算当前水域单元格 (i, j) 在四个方向上相邻单元格的坐标 (curRow, curCol)。
                        int curCol = j + dir[1];
                        if (curRow < 0 || curRow >= m || curCol < 0 || curCol >= n) continue;//检查相邻单元格的坐标是否越界,如果越界(即 curRow < 0 或 curRow >= m 或 curCol < 0 或 curCol >= n),则跳过该相邻单元格,继续检查下一个方向。
                        int curMark = grid[curRow][curCol];//获取相邻单元格所属岛屿的标记。在之前的 DFS 过程中,每个岛屿的单元格都被标记为同一个唯一的 mark 值。
                        if (set.contains(curMark) || !getSize.containsKey(curMark)) continue;//如果 set 中已经包含了该标记,说明这个岛屿已经在本次计算中被考虑过,避免重复计算,跳过该岛屿。如果 getSize 中不包含该标记,说明这个单元格可能是水域或者是之前未处理的特殊情况,也跳过该单元格。
                        set.add(curMark);//如果该岛屿标记未被考虑过,将其添加到 set 中,表示该岛屿已经被纳入本次计算。
                        curSize += getSize.get(curMark);//从 getSize 中获取该岛屿的面积,并累加到 curSize 中,更新当前将该水域单元格变为陆地后相连岛屿的总面积。
                    }
                    result = Math.max(result, curSize);//使用 Math.max 函数比较当前计算得到的 curSize 和之前记录的最大岛屿面积 result,将较大的值更新为新的 result。
                }
            }
        }
        System.out.println(result);//当遍历完所有的水域单元格后,result 中存储的就是将一个水域单元格变为陆地后能够得到的最大岛屿面积,将其输出。
    }
}

假设输入的地图为:

1 1 0 0 0
0 1 1 0 0
0 0 0 1 1
0 0 0 1 0
1. 输入地图并初始化

输入地图:

1 1 0 0 0
0 1 1 0 0
0 0 0 1 1
0 0 0 1 0

初始化mark为2,visited数组用于标记访问状态。

2. 计算每个岛屿的面积
  • 遍历地图:从左到右、从上到下遍历每个单元格。

  • 遇到陆地单元格(值为1)

    • 重置count为0。

    • 调用dfs方法,从该单元格开始深度优先搜索,计算当前岛屿的面积,并将岛屿的所有单元格标记为当前的mark值。

    • 将当前岛屿的面积存入getSize哈希表中,键为mark值。

  • 示例中的岛屿

    • 第一个岛屿(左上角)面积为3,标记为2。

    • 第二个岛屿(右下角)面积为3,标记为3。

3. 检查水域单元格并计算最大可能面积
  • 遍历地图:再次遍历地图,这次关注值为0的单元格(水域)。

  • 对于每个水域单元格

    • 清空set,用于存储周围岛屿的标记。

    • 初始化curSize为1,表示将当前水域单元格变为陆地后的初始面积。

    • 检查四个相邻单元格

      • 如果相邻单元格是陆地(值不为0),且其标记未在set中出现过,则将其面积累加到curSize中,并将该标记加入set

  • 示例中的计算

    • 假设将(1,2)(值为0)变为陆地:

      • 其左边是岛屿2(面积3),右边是岛屿3(面积3)。

      • curSize = 1(自身)+ 3(岛屿2)+ 3(岛屿3)= 7

    • 遍历所有水域单元格后,更新result为最大值。

4. 特殊情况处理
  • 如果整个地图都是陆地(isAllIslandtrue),则最大面积为整个地图的面积。

5. 输出结果

最终输出result,即通过将一个水域单元格变为陆地后能够形成的最大岛屿面积。

示例输出

对于输入地图:

1 1 0 0 0
0 1 1 0 0
0 0 0 1 1
0 0 0 1 0

最大可能的岛屿面积为7

11.字符串接龙

卡码网题目链接(ACM模式)(opens new window)

题目描述

字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列:

  1. 序列中第一个字符串是 beginStr。

  2. 序列中最后一个字符串是 endStr。

  3. 每次转换只能改变一个字符。

  4. 转换过程中的中间字符串必须是字典 strList 中的字符串。

给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。

输入描述

第一行包含一个整数 N,表示字典 strList 中的字符串数量。 第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。 后续 N 行,每行一个字符串,代表 strList 中的字符串。

输出描述

输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。

输入示例

6
abc def
efc
dbc
ebc
dec
dfc
yhn

输出示例

4

提示信息

从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4

数据范围:

2 <= N <= 500

这个单词转换问题的广度优先搜索(BFS)算法实现中,在 visitMap 里记录新单词的步数时要将当前步数加 1,这与 BFS 算法的特性以及问题的本质相关,下面为你详细解释:

1. BFS 算法特性

广度优先搜索是一种按层遍历的算法,它从起始节点开始,逐层地向外扩展搜索。每一层的节点距离起始节点的步数是相同的,并且下一层节点距离起始节点的步数比当前层节点多 1。

在这个单词转换问题里,队列 queue 存储着待处理的单词,每次从队列中取出一个单词 curWord 进行处理,就相当于在当前层。然后通过改变 curWord 中的一个字母生成新单词 newWord,这些新单词属于下一层。因为 BFS 是逐层扩展的,所以新单词距离起始单词的步数自然要比当前单词多 1。

2. 问题本质

问题要求计算从起始单词 beginWord 转换到目标单词 endWord 的最短转换序列长度,每次转换只能改变一个字母。这意味着每进行一次有效的转换,转换的步数就会增加 1。

当从当前单词 curWord 生成新单词 newWord 时,相当于进行了一次有效的转换。所以,新单词 newWord 距离起始单词的步数应该是当前单词 curWord 距离起始单词的步数加上 1。

假设起始单词是 "hit",目标单词是 "cog",单词列表包含 "hot""dot""dog""lot""log""cog"

  • 起始时,beginWord = "hit"visitMap.put("hit", 1),表示 "hit" 距离自身的步数为 1。
  • 从 "hit" 可以生成 "hot",此时 "hot" 距离 "hit" 的步数为 "hit" 的步数(1)加 1,即 2,所以 visitMap.put("hot", 2)
  • 从 "hot" 可以生成 "dot" 和 "lot",它们距离 "hit" 的步数为 "hot" 的步数(2)加 1,即 3,所以 visitMap.put("dot", 3) 和 visitMap.put("lot", 3)

以此类推,通过不断地生成新单词并记录步数,最终可以找到从起始单词到目标单词的最短转换序列长度。

这个单词转换问题的情境里,把起始单词 "hit" 距离自身的步数设为 1,主要是为了符合问题中对于转换序列长度的定义,方便后续统一计算和处理,下面详细解释原因:

1. 转换序列长度的定义

题目要求计算从起始单词 beginWord 转换到目标单词 endWord 的最短转换序列长度。转换序列长度指的是从起始单词开始,经过一系列每次改变一个字母的转换步骤,最终到达目标单词所经过的单词数量(包含起始单词和目标单词)。

当从起始单词开始时,起始单词本身就算作转换序列中的第一个单词,所以它距离自身的步数定义为 1。如果将起始单词距离自身的步数定义为 0,那么最终得到的转换序列长度就会比实际的转换序列少一个单词,不符合题目的要求。

2. 方便后续计算

在广度优先搜索(BFS)算法的实现中,每一次从当前单词生成新单词时,新单词距离起始单词的步数是基于当前单词的步数加 1 得到的。如果起始单词的步数为 1,那么后续生成的新单词的步数计算就会很直观和统一。

例如,从起始单词 "hit" 开始,生成了新单词 "hot",因为 "hot" 是通过 "hit" 改变一个字母得到的,所以 "hot" 距离 "hit" 的步数就是 "hit" 的步数(1)加 1,即 2。这样的计算方式使得整个转换过程中步数的计算逻辑清晰,易于理解和实现。

public class String_Consecutive {
    public static int ladderLength(String beginWord, String endWord, List<String> wordList) {//公共静态方法,用于计算从beginWord到endWord的最短转换序列长度。String beginWord 和 String endWord 分别是起始单词和目标单词。List<String> wordList 是包含所有有效单词的列表。
        HashSet<String> set = new HashSet<>(wordList);//将wordList转换为HashSet,以便快速检查一个单词是否存在于列表中。
        Queue<String> queue = new LinkedList<>();//创建一个Queue<String> queue用于存储待处理的单词,以及一个HashMap<String, Integer> visitMap用于存储每个单词到起始单词的距离(即转换步数)。
        HashMap<String, Integer> visitMap = new HashMap<>();
        queue.offer(beginWord);//初始时,将beginWord加入队列,并在visitMap中记录其步数为1。
        visitMap.put(beginWord, 1);
        while (!queue.isEmpty()) {//在循环中,只要队列不为空,就不断取出队列头部的单词(curWord),并获取其对应的步数(path)。该单词到起始单词的步数。
            String curWord = queue.poll();
            int path = visitMap.get(curWord);
            for (int i = 0; i < curWord.length(); i++) {//遍历当前单词的每个字母位置。
                char[] ch = curWord.toCharArray();//将当前单词转换为字符数组,方便修改字母。
                for (char k = 'a'; k <= 'z'; k++) {//对于curWord中的每个字母位置,生成所有可能的单词(通过替换当前字母为a到z),并检查每个新生成的单词:如果新单词等于endWord,则返回当前步数加1,因为这表示找到了最短路径。如果新单词存在于HashSet中且未被访问过,则将其加入队列,并在visitMap中记录其步数。
                    ch[i] = k;//替换当前字母位置的字母。
                    String newWord = new String(ch);//将字符数组转换为新的单词。
                    if (newWord.equals(endWord)) return path + 1;//如果新单词等于目标单词 endWord,说明找到了最短路径,返回当前步数加 1。
                    if (set.contains(newWord) && !visitMap.containsKey(newWord)) {//如果新单词存在于 set 中且未被访问过(即 visitMap 中不包含该单词),则将其加入队列,并在 visitMap 中记录其步数为当前步数加 1。
                        visitMap.put(newWord, path + 1);
                        queue.offer(newWord);
                    }
                }
            }
        }
        return 0;//如果队列遍历完后仍未找到目标单词,返回 0,表示无法从起始单词转换到目标单词。
    }
    public static void main (String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取一个整数N,表示wordList中的单词数量。读取两个单词beginWord和endWord。循环读取N个单词填充wordList。调用ladderLength方法计算最短转换序列长度,并输出结果。
        int N = sc.nextInt();
        sc.nextLine();//消耗掉 nextInt() 之后的换行符。因为 nextInt() 方法只读取整数,不会消耗换行符,而后续使用 nextLine() 读取字符串时,如果不消耗这个换行符,会导致 nextLine() 读取到空字符串。
        String[] strs = sc.nextLine().split(" ");//sc.nextLine():读取一行输入,这一行包含起始单词和目标单词,它们之间用空格分隔。split(" "):将读取的字符串按空格分割成字符串数组 strs,strs[0] 就是起始单词 beginWord,strs[1] 就是目标单词 endWord。
        List<String> wordList = new ArrayList<>();
        for (int i = 0; i < N; i++) {
            wordList.add(sc.nextLine());//sc.nextLine():在每次循环中,读取一行输入,这一行是一个单词,然后将其添加到 wordList 列表中。
        }
        int result = ladderLength(strs[0], strs[1], wordList);//调用 ladderLength 方法,传入起始单词 strs[0]、目标单词 strs[1] 和单词列表 wordList,计算从起始单词到目标单词的最短转换序列长度,并将结果存储在变量 result 中。
        System.out.println(result);
    }
}

假设输入的单词列表和起始、目标单词如下:

N = 6
beginWord = "hit"
endWord = "cog"
wordList = ["hot", "dot", "dog", "lot", "log", "cog"]
1. 输入数据

输入数据:

6
hit cog
hot
dot
dog
lot
log
cog
2. 初始化
  • wordList转换为HashSet,以便快速检查单词是否存在。

  • 初始化队列queue,将beginWord加入队列。

  • 初始化visitMap,记录beginWord的步数为1。

3. 广度优先搜索(BFS)
  • 队列初始状态queue = ["hit"]visitMap = {"hit": 1}

  • 循环处理队列

    1. 取出队列头部单词curWord = "hit"path = 1

      • 遍历"hit"的每个字母位置:

        • 第1个字母(h):

          • 替换为az,生成新单词:

            • "a" + "it" = "ait"(不在wordList中,跳过)。

            • "b" + "it" = "bit"(不在wordList中,跳过)。

            • ...

            • "h" + "it" = "hit"(是当前单词,跳过)。

            • ...

            • "o" + "it" = "oit"(不在wordList中,跳过)。

        • 第2个字母(i):

          • 替换为az,生成新单词:

            • "h" + "a" + "t" = "hat"(不在wordList中,跳过)。

            • "h" + "o" + "t" = "hot"(在wordList中且未访问过,加入队列,记录步数为2)。

            • ...

        • 第3个字母(t):

          • 替换为az,生成新单词:

            • "hi" + "a" = "hia"(不在wordList中,跳过)。

            • ...

            • "hi" + "g" = "hig"(不在wordList中,跳过)。

      • 队列状态queue = ["hot"]visitMap = {"hit": 1, "hot": 2}

    2. 取出队列头部单词curWord = "hot"path = 2

      • 遍历"hot"的每个字母位置:

        • 第1个字母(h):

          • 替换为az,生成新单词:

            • "a" + "ot" = "aot"(不在wordList中,跳过)。

            • ...

            • "d" + "ot" = "dot"(在wordList中且未访问过,加入队列,记录步数为3)。

            • ...

        • 第2个字母(o):

          • 替换为az,生成新单词:

            • "h" + "a" + "t" = "hat"(不在wordList中,跳过)。

            • ...

            • "h" + "l" + "t" = "hlt"(不在wordList中,跳过)。

        • 第3个字母(t):

          • 替换为az,生成新单词:

            • "ho" + "a" = "hoa"(不在wordList中,跳过)。

            • ...

            • "ho" + "g" = "hog"(不在wordList中,跳过)。

      • 队列状态queue = ["dot", "lot"]visitMap = {"hit": 1, "hot": 2, "dot": 3, "lot": 3}

    3. 取出队列头部单词curWord = "dot"path = 3

      • 遍历"dot"的每个字母位置:

        • 第1个字母(d):

          • 替换为az,生成新单词:

            • "a" + "ot" = "aot"(不在wordList中,跳过)。

            • ...

            • "c" + "ot" = "cot"(不在wordList中,跳过)。

        • 第2个字母(o):

          • 替换为az,生成新单词:

            • "d" + "a" + "t" = "dat"(不在wordList中,跳过)。

            • ...

            • "d" + "o" + "g" = "dog"(在wordList中且未访问过,加入队列,记录步数为4)。

        • 第3个字母(t):

          • 替换为az,生成新单词:

            • "do" + "a" = "doa"(不在wordList中,跳过)。

            • ...

            • "do" + "g" = "dog"(已访问过,跳过)。

      • 队列状态queue = ["lot", "dog"]visitMap = {"hit": 1, "hot": 2, "dot": 3, "lot": 3, "dog": 4}

    4. 取出队列头部单词curWord = "lot"path = 3

      • 遍历"lot"的每个字母位置:

        • 第1个字母(l):

          • 替换为az,生成新单词:

            • "a" + "ot" = "aot"(不在wordList中,跳过)。

            • ...

            • "c" + "ot" = "cot"(不在wordList中,跳过)。

        • 第2个字母(o):

          • 替换为az,生成新单词:

            • "l" + "a" + "t" = "lat"(不在wordList中,跳过)。

            • ...

            • "l" + "o" + "g" = "log"(在wordList中且未访问过,加入队列,记录步数为4)。

        • 第3个字母(t):

          • 替换为az,生成新单词:

            • "lo" + "a" = "loa"(不在wordList中,跳过)。

            • ...

            • "lo" + "g" = "log"(已访问过,跳过)。

      • 队列状态queue = ["dog", "log"]visitMap = {"hit": 1, "hot": 2, "dot": 3, "lot": 3, "dog": 4, "log": 4}

    5. 取出队列头部单词curWord = "dog"path = 4

      • 遍历"dog"的每个字母位置:

        • 第1个字母(d):

          • 替换为az,生成新单词:

            • "a" + "og" = "aog"(不在wordList中,跳过)。

            • ...

            • "c" + "og" = "cog"(在wordList中且未访问过,且等于endWord,返回path + 1 = 5)。

      • 返回结果5

4. 输出结果

最短转换序列长度为5

12.有向图的完全可达性

卡码网题目链接(ACM模式)(opens new window)

【题目描述】

给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。

【输入描述】

第一行包含两个正整数,表示节点数量 N 和边的数量 K。 后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。

【输出描述】

如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。

【输入示例】

4 4
1 2
2 1
1 3
2 4

【输出示例】

1

【提示信息】

从 1 号节点可以到达任意节点,输出 1。

数据范围:

  • 1 <= N <= 100;
  • 1 <= K <= 2000。
public class Complete_Reachability_in_Directed_Graphs {
    public static List<List<Integer>> adjList = new ArrayList<>();//adjList 是一个存储有向图邻接表的列表。外层列表的每个元素对应图中的一个顶点,内层列表存储从该顶点出发可以直接到达的所有顶点。例如,adjList.get(key) 返回一个列表,其中包含所有从顶点 key 出发可以直接到达的顶点。
    public static void dfs(boolean[] visited, int key) {//visited:一个布尔数组,用于标记每个顶点是否被访问过。key:当前正在访问的顶点。
        if (visited[key]) {//检查顶点 key 是否已经被访问过,如果是,则直接返回。
            return;
        }
        visited[key] = true;//将顶点 key 标记为已访问(visited[key] = true)。
        List<Integer> nextKeys = adjList.get(key);//获取顶点 key 的所有邻接顶点(adjList.get(key))。
        for (int nextKey : nextKeys) {//遍历这些邻接顶点,对每个未访问的邻接顶点递归调用 dfs 方法。
            dfs(visited, nextKey);
        }
    }
    public static void bfs(boolean[] visited, int key) {//visited:一个布尔数组,用于标记每个顶点是否被访问过。key:当前正在访问的顶点。
        Queue<Integer> queue = new LinkedList<Integer>();//创建一个队列 queue,并将起始顶点 key 加入队列。
        queue.add(key);//将起始顶点 key 标记为已访问(visited[key] = true)。
        visited[key] = true;
        while (!queue.isEmpty()) {//当队列不为空时,从队列中取出一个顶点,获取顶点 curKey 的所有邻接顶点(adjList.get(curKey))。遍历这些邻接顶点,对每个未访问的邻接顶点,将其加入队列并标记为已访问。
            int curKey = queue.poll();
            List<Integer> list = adjList.get(curKey);
            for (int nextKey : list) {
                if (!visited[nextKey]) {
                    queue.add(nextKey);
                    visited[nextKey] = true;
                }
            }
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取两个整数vertices_num和line_num,分别代表图中顶点的数量和边的数量。初始化邻接表adjList,为每个顶点创建一个空列表。
        int vertices_num = sc.nextInt();
        int line_num = sc.nextInt();
        for (int i = 0; i < vertices_num; i++) {
            adjList.add(new LinkedList<>());
        }
        for (int i = 0; i < line_num; i++) {//循环读取 line_num 条边的信息,每条边由起点 s 和终点 t 表示。注意,代码中顶点编号从 0 开始,而输入可能从 1 开始,所以使用 s - 1 和 t - 1 将顶点编号转换为从 0 开始。将每条边的终点添加到起点的邻接列表中。
            int s = sc.nextInt();
            int t = sc.nextInt();
            adjList.get(s - 1).add(t - 1);
        }
        boolean[] visited = new boolean[vertices_num];//创建一个布尔数组 visited,用于跟踪每个顶点的访问状态。从顶点 0 开始执行 DFS 遍历。
        dfs(visited, 0);
        for (int i = 0; i < vertices_num; i++) {//遍历 visited 数组,如果存在未访问的顶点,则输出 -1 并结束程序,表示不是所有顶点都可达。如果所有顶点都被访问过,则输出 1,表示从起始顶点 0 到其他所有顶点都是完全可达的。
            if (!visited[i]) {
                System.out.println(-1);
                return;
            }
        }
        System.out.println(1);
    }
}

假设输入的图如下:

顶点数:4
边数:4
边信息:
1 2
1 3
2 4
3 4

对应的有向图如下:

1 -> 2 -> 4
 \    /
  \-> 3
1. 输入数据

输入数据:

4 4
1 2
1 3
2 4
3 4
2. 初始化邻接表

邻接表adjList初始化为:

adjList[0] = [1, 2] // 顶点0(输入中的1)可以到达顶点1(输入中的2)和顶点2(输入中的3)
adjList[1] = [3]    // 顶点1(输入中的2)可以到达顶点3(输入中的4)
adjList[2] = [3]    // 顶点2(输入中的3)可以到达顶点3(输入中的4)
adjList[3] = []     // 顶点3(输入中的4)没有出边
3. 执行DFS

从顶点0开始执行DFS:

  • 访问顶点0

    • 标记visited[0] = true

    • 邻接顶点为1和2,递归访问顶点1和2。

  • 访问顶点1

    • 标记visited[1] = true

    • 邻接顶点为3,递归访问顶点3。

  • 访问顶点2

    • 标记visited[2] = true

    • 邻接顶点为3,递归访问顶点3。

  • 访问顶点3

    • 标记visited[3] = true

    • 顶点3没有邻接顶点,递归返回。

4. 检查访问状态

遍历visited数组:

visited = [true, true, true, true]

所有顶点都被访问过,因此输出1

13.岛屿的周长

卡码网题目链接(ACM模式)(opens new window)

题目描述

给定一个由 1(陆地)和 0(水)组成的矩阵,岛屿是被水包围,并且通过水平方向或垂直方向上相邻的陆地连接而成的。

你可以假设矩阵外均被水包围。在矩阵中恰好拥有一个岛屿,假设组成岛屿的陆地边长都为 1,请计算岛屿的周长。岛屿内部没有水域。

输入描述

第一行包含两个整数 N, M,表示矩阵的行数和列数。之后 N 行,每行包含 M 个数字,数字为 1 或者 0,表示岛屿的单元格。

输出描述

输出一个整数,表示岛屿的周长。

输入示例

5 5
0 0 0 0 0
0 1 0 1 0
0 1 1 1 0
0 1 1 1 0
0 0 0 0 0

输出示例

14

提示信息

岛屿的周长为 14。

数据范围:

1 <= M, N <= 50。

public class Island_Perimeter {
    static int[][] dirs = {
  
  {1, 0}, {-1, 0}, {0, 1}, {0, -1}};//二维数组,定义了四个方向的移动,分别对应下({1, 0})、上({-1, 0})、右({0, 1})、左({0, -1})。在后续计算陆地单元格周长时,通过这个数组可以方便地获取当前单元格的四个相邻单元格的坐标。
    static int count;//静态变量,用于临时存储单个陆地单元格的周长。
    public static void helper(int[][] grid, int x, int y) {//辅助方法,用于计算单个岛屿单元格的周长。int[][] grid 是输入的二维数组,表示地图。int x 和 int y 是当前岛屿单元格的坐标。
        for (int[] dir : dirs) {//使用 for 循环遍历 dirs 数组,计算当前陆地单元格在四个方向上相邻单元格的坐标 (nx, ny)。
            int nx = x + dir[0];
            int ny = y + dir[1];
            if (nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length//如果相邻单元格的坐标越界(即 nx < 0 或 nx >= grid.length 或 ny < 0 或 ny >= grid[0].length),说明该方向上是地图边界,当前陆地单元格在这个方向上有一条边贡献给周长,count 加 1。
                    || grid[nx][ny] == 0) {//如果相邻单元格的值为 0,表示该方向上是水域,当前陆地单元格在这个方向上有一条边贡献给周长,count 加 1。
                count++;
            }
        }
    }
    public static void main(String[] args) {
        Scanner sc = new Scanner(System.in);//使用Scanner类从标准输入读取数据。首先读取两个整数M和N,分别代表地图的行数和列数。创建一个大小为M x N的二维数组grid,用于存储地图数据。循环读取M x N个整数填充grid。
        int M = sc.nextInt();
        int N = sc.nextInt();
        int[][] grid = new int[M][N];
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                grid[i][j] = sc.nextInt();
            }
        }
        int result = 0;//初始化result为0,用于存储所有岛屿的周长总和。遍历整个地图,对于每个值为1的单元格(即岛屿的一部分),重置count为0,然后调用helper方法计算该单元格的周长,并将其累加到result中。
        for (int i = 0; i < M; i++) {
            for (int j = 0; j < N; j++) {
                if (grid[i][j] == 1) {
                    count = 0;
                    helper(grid, i, j);
                    result += count;
                }
            }
        }
        System.out.println(result);//最后,将计算得到的所有岛屿的总周长 result 输出到标准输出。
    }
}

假设输入的地图如下:

1 0 0 0
1 1 0 0
0 1 0 0
0 0 1 1
1. 输入数据

输入数据:

4 4
1 0 0 0
1 1 0 0
0 1 0 0
0 0 1 1
2. 初始化
  • 创建一个二维数组grid,并填充输入数据。

3. 计算每个陆地单元格的周长
  • 遍历地图:从左到右、从上到下遍历每个单元格。

  • 遇到陆地单元格(值为1)

    • 调用helper方法,计算当前陆地单元格的周长。

    • 将计算结果累加到result中。

4. helper方法的执行
  • 对于每个陆地单元格,检查其四个方向的相邻单元格:

    • 如果相邻单元格越界或为水域(值为0),则当前方向上有一条边贡献给周长,count加1。

  • 示例中的计算

    • 单元格(0,0)

      • 下:(1,0)是陆地,不贡献周长。

      • 上:越界,贡献1。

      • 右:(0,1)是水域,贡献1。

      • 左:越界,贡献1。

      • 总周长count = 3

    • 单元格(1,0)

      • 下:(2,0)是水域,贡献1。

      • 上:(0,0)是陆地,不贡献周长。

      • 右:(1,1)是陆地,不贡献周长。

      • 左:越界,贡献1。

      • 总周长count = 2

    • 单元格(1,1)

      • 下:(2,1)是陆地,不贡献周长。

      • 上:(0,1)是水域,贡献1。

      • 右:(1,2)是水域,贡献1。

      • 左:(1,0)是陆地,不贡献周长。

      • 总周长count = 2

    • 单元格(2,1)

      • 下:(3,1)是水域,贡献1。

      • 上:(1,1)是陆地,不贡献周长。

      • 右:(2,2)是水域,贡献1。

      • 左:越界,贡献1。

      • 总周长count = 3

    • 单元格(3,2)

      • 下:越界,贡献1。

      • 上:(2,2)是水域,贡献1。

      • 右:(3,3)是陆地,不贡献周长。

      • 左:(3,1)是水域,贡献1。

      • 总周长count = 3

    • 单元格(3,3)

      • 下:越界,贡献1。

      • 上:(2,3)越界,贡献1。

      • 右:越界,贡献1。

      • 左:(3,2)是陆地,不贡献周长。

      • 总周长count = 3

5. 累加所有单元格的周长
  • 总周长3 + 2 + 2 + 3 + 3 + 3 = 16

6. 输出结果

最终输出的岛屿周长为16

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2283333.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

小利特惠源码/生活缴费/电话费/油卡燃气/等充值业务类源码附带承兑系统

全新首发小利特惠/生活缴费/电话费/油卡燃气/等充值业务类源码附带U商承兑系统 安装教程如下 图片:

ESMC-600M蛋白质语言模型本地部署攻略

前言 之前介绍了ESMC-6B模型的网络接口调用方法&#xff0c;但申请token比较慢&#xff0c;有网友问能不能出一个本地部署ESMC小模型的攻略&#xff0c;遂有本文。 其实本地部署并不复杂&#xff0c;官方github上面也比较清楚了。 操作过程 环境配置&#xff1a;CUDA 12.1、…

Java 实现Excel转HTML、或HTML转Excel

Excel是一种电子表格格式&#xff0c;广泛用于数据处理和分析&#xff0c;而HTM则是一种用于创建网页的标记语言。虽然两者在用途上存在差异&#xff0c;但有时我们需要将数据从一种格式转换为另一种格式&#xff0c;以便更好地利用和展示数据。本文将介绍如何通过 Java 实现 E…

Ubuntu20.04 运行 PL-VIO

文章目录 运行后不知为何没有线特征 运行后不知为何没有线特征

centos操作系统上以service形式运行blackbox_exporter监控网页端口

文章目录 前言一、blackbox_exporter是什么二、使用步骤1.获取二进制文件2.准备部署脚本3.执行命令&#xff0c;进行部署4.prometheus中增加需要监控页面的job信息 三、查看部署结果四、配置到grafana中总结 前言 记录一下centos操作系统上以简单的service形式运行blackbox_ex…

Linux内核编程(二十一)USB驱动开发-键盘驱动

一、驱动类型 USB 驱动开发主要分为两种&#xff1a;主机侧的驱动程序和设备侧的驱动程序。一般我们编写的都是主机侧的USB驱动程序。 主机侧驱动程序用于控制插入到主机中的 USB 设备&#xff0c;而设备侧驱动程序则负责控制 USB 设备如何与主机通信。由于设备侧驱动程序通常与…

RV1126画面质量四:GOP改善画质

一&#xff0e; 什么是 GOP GOP 实际上就是两个 I 帧的间隔&#xff0c;比方说分辨率是 1920 * 1080 50 帧&#xff0c;假设 GOP 为 5&#xff0c;那就是大概 2s 插入一个 I 帧。我们再 回顾下&#xff0c;H264/H265 的帧结构。H264/H265 分别分为三种帧类型&#xff1a;I 帧、…

【2025年数学建模美赛F题】(顶刊论文绘图)模型代码+论文

全球网络犯罪与网络安全政策的多维度分析及效能评估 摘要1 Introduction1.1 Problem Background1.2Restatement of the Problem1.3 Literature Review1.4 Our Work 2 Assumptions and Justifications数据完整性与可靠性假设&#xff1a;法律政策独立性假设&#xff1a;人口统计…

Vivado生成X1或X4位宽mcs文件并固化到flash

1.生成mcs文件 01.在vivado里的菜单栏选择"tools"工具栏 02.在"tools"里选择"生成内存配置文件" 03.配置参数 按照FPGA板上的flash型号进行选型&#xff0c;相关配置步骤可参考下图。 注意&#xff1a;Flash数据传输位宽如果需要选择X4位宽&am…

idea plugin插件开发——入门级教程(IntelliJ IDEA Plugin)

手打不易&#xff0c;如果转摘&#xff0c;请注明出处&#xff01; 注明原文&#xff1a;idea plugin插件开发——入门级教程&#xff08;IntelliJ IDEA Plugin&#xff09;-CSDN博客 目录 前言 官方 官方文档 代码示例 开发前必读 Intellij、Gradle、JDK 版本关系 plu…

Linux的常用指令的用法

目录 Linux下基本指令 whoami ls指令&#xff1a; 文件&#xff1a; touch clear pwd cd mkdir rmdir指令 && rm 指令 man指令 cp mv cat more less head tail 管道和重定向 1. 重定向&#xff08;Redirection&#xff09; 2. 管道&#xff08;Pipes&a…

docker 简要笔记

文章目录 一、前提内容1、docker 环境准备2、docker-compose 环境准备3、流程说明 二、打包 docker 镜像1、基础镜像2、国内镜像源3、基础的dockerfile4、打包镜像 四、构建运行1、docker 部分2、docker-compose 部分2.1、构建docker-compose.yml2.1.1、同目录构建2.1.2、利用镜…

Windows 与 Linux 文件权限的对比与转换

Windows和Linux在文件权限管理方面存在显著差异。了解它们的对比和转换方法对于跨平台操作和管理文件非常重要。以下是详细的对比和转换方法&#xff1a; 一、Windows 文件权限 1. 权限类型 Windows使用基于用户和组的权限模型&#xff0c;常见的权限类型包括&#xff1a; 读…

FireFox | Google Chrome | Microsoft Edge 禁用更新 final版

之前的方式要么失效&#xff0c;要么对设备有要求&#xff0c;这次梳理一下对设备、环境几乎没有要求的通用方式&#xff0c;universal & final 版。 1.Firefox 方式 FireFox火狐浏览器企业策略禁止更新_火狐浏览器禁止更新-CSDN博客 这应该是目前最好用的方式。火狐也…

华硕笔记本装win10哪个版本好用分析_华硕笔记本装win10专业版图文教程

华硕笔记本装win10哪个版本好用&#xff1f;华硕笔记本还是建议安装win10专业版。Win分为多个版本&#xff0c;其中家庭版&#xff08;Home&#xff09;和专业版&#xff08;Pro&#xff09;是用户选择最多的两个版本。win10专业版在功能以及安全性方面有着明显的优势&#xff…

Android多语言开发自动化生成工具

在做 Android 开发的过程中&#xff0c;经常会遇到多语言开发的场景&#xff0c;尤其在车载项目中&#xff0c;多语言开发更为常见。对应多语言开发&#xff0c;通常都是在中文版本的基础上开发其他国家语言&#xff0c;这里我们会拿到中-外语言对照表&#xff0c;这里的工作难…

Java数据结构 (链表反转(LinkedList----Leetcode206))

1. 链表的当前结构 每个方框代表一个节点&#xff0c;每个节点包含两个部分&#xff1a; 左侧的数字&#xff1a;节点存储的值&#xff0c;例如 45、34 等。右侧的地址&#xff08;如 0x90&#xff09;&#xff1a;表示该节点 next 指针指向的下一个节点的内存地址。 例子中&a…

LabVIEW 太阳能光伏发电系统智能监控

本文介绍了基于 LabVIEW 的太阳能光伏发电监控系统的设计与实现&#xff0c;着重探讨了其硬件配置、软件架构以及系统的实现方法。该系统能够有效提高太阳能光伏发电的监控效率和精确性&#xff0c;实现了远程监控和数据管理的智能化。 ​ 项目背景 在当前能源紧张与环境污染…

记录让cursor帮我给ruoyi-vue后台管理项目整合mybatis-plus

自己整合过程中会出现 work.web.exception.GlobalExceptionHandler :100 | 请求地址/admin/device/install/detail/1,发生未知异常. org.apache.ibatis.binding.BindingException: Invalid bound statement (not found): com.fire.mapper.DeviceInstallMapper.selectById at o…

Prometheus+grafana实践:Doris数据库的监控

文章来源&#xff1a;乐维社区 Doris数据库背景 Doris&#xff08;Apache Doris&#xff09;是一个现代化的MPP&#xff08;Massive Parallel Processing&#xff0c;大规模并行处理&#xff09;数据库&#xff0c;主要用于在线分析处理&#xff08;OLAP&#xff09;场景。 D…