一、实战
106岛屿的周长
思路1:本题不需要使用BFS或者DFS,但是在过程中需要类似的思考。
遍历每一个空格,遇到岛屿则计算其上下左右的空格情况:
1.如果该陆地上下左右的空格是有水域,则说明是一条边
2.如果该陆地上下左右的空格出界了,则说明是一条边


思路2:计算出总的岛屿数量,总的变数为:岛屿数量 * 4。因为有一对相邻两个陆地,边的总数就要减2,如图红线部分,有两个陆地相邻,总边数就要减2。那么只需要在计算出相邻岛屿的数量就可以了,相邻岛屿数量为cover。结果 result = 岛屿数量 * 4 - cover * 2;
具体代码使用思路1版本
package org.example;//具体运行时去掉
import java.util.Scanner;
public class Main {
// 四个方向偏移量:下、上、右、左
static int[][] dirs = {{1, 0}, {-1, 0}, {0, 1}, {0, -1}};
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
// 读取矩阵的行数 M 和列数 N
int M = sc.nextInt();
int N = sc.nextInt();
// 创建 M×N 的网格
int[][] grid = new int[M][N];
// 输入每个格子的值:1 表示陆地,0 表示水
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
grid[i][j] = sc.nextInt();
}
}
int result = 0; // 记录岛屿的总周长
// 遍历整个网格
for (int i = 0; i < M; i++) {
for (int j = 0; j < N; j++) {
// 如果当前格子是陆地(1)
if (grid[i][j] == 1) {
// 检查四个方向的邻居
for (int k = 0; k < 4; k++) {
int nx = i + dirs[k][0]; // 邻居的行坐标
int ny = j + dirs[k][1]; // 邻居的列坐标
// 如果邻居越界 或 是水域(0),则这一边是岛屿的边界,周长+1
if (nx < 0 || nx >= grid.length || ny < 0 || ny >= grid[0].length || grid[nx][ny] == 0) {
result++;
}
}
}
}
}
// 输出岛屿的总周长
System.out.println(result);
}
}
110字符串接龙
输入示例
6
abc def
efc
dbc
ebc
dec
dfc
yhn
以示例1为例,从这个图中可以看出 abc 到 def的路线 不止一条,但最短的一条路径上是4个节点。本题只需要求出最短路径的长度就可以了,不用找出具体路径。
需要解决的两个问题:
1.图中的线是如何连在一起的。在搜索的过程中可以枚举,用26个字母替换当前字符串的每一个字符,在看替换后 是否在 strList里出现过,就可以判断 两个字符串 是否是链接的。
2.起点和终点的最短路径长度。首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个。所以判断点与点之间的关系,需要判断是不是差一个字符,如果差一个字符,那就是有链接。然后就是求起点和终点的最短路径长度,在无权图中,求最短路,用深搜或者广搜就行,没必要用最短路算法。在无权图中,用广搜求最短路最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。
注意点:
- 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环!
- 使用set来检查字符串是否出现在字符串集合里更快一些
import java.util.*;
public class Main {
public static void main(String[] args) {
Scanner scanner = new Scanner(System.in);
int n = scanner.nextInt(); // 额外单词数量
scanner.nextLine(); // 清除换行
String beginStr = scanner.next(); // 起始单词
String endStr = scanner.next(); // 目标单词
scanner.nextLine(); // 清除换行
List<String> wordList = new ArrayList<>();
wordList.add(beginStr);
wordList.add(endStr);
for (int i = 0; i < n; i++) {
wordList.add(scanner.nextLine()); // 添加其他单词
}
int steps = bfs(beginStr, endStr, wordList);
System.out.println(steps);
}
/**
* BFS 求从 beginStr 到 endStr 的最短变换步数
* 每次只能改变一个字母,新单词必须在 wordList 中
*/
public static int bfs(String beginStr, String endStr, List<String> wordList) {
int level = 1; // 当前处理的是第几层(beginStr 所在层为第1层)
Set<String> dict = new HashSet<>(wordList); // 合法单词字典
Set<String> visited = new HashSet<>(); // 防止重复访问
Queue<String> queue = new LinkedList<>(); // BFS 队列
visited.add(beginStr);
queue.add(beginStr);
queue.add(null); // 层分隔符
while (!queue.isEmpty()) {
String word = queue.remove();
// 遇到层分隔符:进入下一层
if (word == null) {
if (!queue.isEmpty()) {
level++; // 进入下一层
queue.add(null); // 添加新的分隔符
}
continue;
}
// 尝试修改 word 的每一位
char[] chars = word.toCharArray();
for (int i = 0; i < chars.length; i++) {
char original = chars[i]; // 保存原字符
// 尝试 a~z
for (char c = 'a'; c <= 'z'; c++) {
chars[i] = c;
String newWord = new String(chars);
// 如果是合法且未访问的单词
if (dict.contains(newWord) && !visited.contains(newWord)) {
visited.add(newWord);
queue.add(newWord);
// 找到目标单词
if (newWord.equals(endStr)) {
return level + 1;
}
}
}
chars[i] = original; // 恢复原字符
}
}
return 0; // 无法到达
}
}
105有向图的完全可达性
思路:一方面知道有向图是有方向的,即使所有节点都是链接的,但依然不一定从0出发遍历所有边。本题是一个有向图搜索全路径的问题。 只能用深搜(DFS)或者广搜(BFS)来搜。
深搜三部曲:
- 确认递归函数,参数:需要传入地图,需要知道当前我们拿到的key,以至于去下一个房间。同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。
- 确认终止条件:这里有一个很重要的逻辑,就是在递归中是处理当前访问的节点,还是处理下一个要访问的节点。首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。
// 写法一:处理当前访问的节点
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
if (visited[key]) {
return;
}
visited[key] = true;
list<int> keys = graph[key];
for (int key : keys) {
// 深度优先搜索遍历
dfs(graph, key, visited);
}
}
// 写法二:处理下一个要访问的节点
void dfs(const vector<list<int>>& graph, int key, vector<bool>& visited) {
list<int> keys = graph[key];
for (int key : keys) {
if (visited[key] == false) { // 确认下一个是没访问过的节点
visited[key] = true;
dfs(graph, key, visited);
}
}
}
- 处理目前搜索节点出发的路径。这里注意本题就没有回溯,本题是需要判断 1节点 是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。什么时候需要回溯操作呢?当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”
代码dfs+邻接表版本:注意在设计邻接表的时候由于节点从1开始,建议大小直接n+1,0不使用,在之后找位置处理会更加方便
package org.example;//具体运行时去掉
import java.util.*;
public class Main {
// 邻接表:让索引从 1 开始使用,所以大小为 n+1
public static List<List<Integer>> adjList = new ArrayList<>();
public static void main(String[] args) {
Scanner sc = new Scanner(System.in);
int vertices_num = sc.nextInt(); // 节点数(1 ~ n)
int line_num = sc.nextInt(); // 边数
// 初始化 adjList,大小为 vertices_num + 1(索引 0 不用,用 1~n)
adjList.add(new ArrayList<>()); // 占位:索引0,不用
for (int i = 1; i <= vertices_num; i++) {
adjList.add(new LinkedList<>()); // 索引1~n 对应节点1~n
}
// 构造有向图
for (int i = 0; i < line_num; i++) {
int s = sc.nextInt(); // 起点(1开始)
int t = sc.nextInt(); // 终点(1开始)
adjList.get(s).add(t); // 直接使用 s 和 t,无需减1
}
// visited 数组也从 1 开始使用,大小为 n+1
boolean[] visited = new boolean[vertices_num + 1];
// 从节点 1 开始遍历(对应题目中的第一个节点)
dfs(visited, 1);
// 检查节点 1 ~ n 是否都被访问到
for (int i = 1; i <= vertices_num; i++) {
if (!visited[i]) {
System.out.println(-1); // 有节点不可达
return;
}
}
System.out.println(1); // 所有节点都可达
}
// 从 key 出发,标记所有能访问到的节点
public static void dfs(boolean[] visited, int key) {
if (visited[key]) {
return;
}
visited[key] = true;
List<Integer> nextKeys = adjList.get(key); // 获取邻居
for (int nextKey : nextKeys) {// 对邻居继续进行遍历
dfs(visited, nextKey);
}
}
}