Day59_20250207_图论part4_110.字符串接龙|105.有向图的完全可达性|106.岛屿的周长
110.字符串接龙
题目
题目描述
字典 strList 中从字符串 beginStr 和 endStr 的转换序列是一个按下述规格形成的序列:
- 序列中第一个字符串是 beginStr。
- 序列中最后一个字符串是 endStr。
- 每次转换只能改变一个字符。
- 转换过程中的中间字符串必须是字典 strList 中的字符串。
给你两个字符串 beginStr 和 endStr 和一个字典 strList,找到从 beginStr 到 endStr 的最短转换序列中的字符串数目。如果不存在这样的转换序列,返回 0。
输入描述
第一行包含一个整数 N,表示字典 strList 中的字符串数量。 第二行包含两个字符串,用空格隔开,分别代表 beginStr 和 endStr。 后续 N 行,每行一个字符串,代表 strList 中的字符串。
输出描述
输出一个整数,代表从 beginStr 转换到 endStr 需要的最短转换序列中的字符串数量。如果不存在这样的转换序列,则输出 0。
输入示例
text
6
abc def
efc
dbc
ebc
dec
dfc
yhn
输出示例
4
提示信息
从 startStr 到 endStr,在 strList 中最短的路径为 abc -> dbc -> dec -> def,所以输出结果为 4
数据范围:
2 <= N <= 500
思路
-
思路
- 求最短路径的长度
- 思考
- 图中的线是如何连在一起的
- 如果只差1个字符,就是有连接。
- 起点和终点的最短路径长度
- 无向图求最短路,广搜最合适,只要搜到了终点,一定是最短的路径。(起点中心向四周扩散)
- 无向图,用标记位(节点是否走过),否则是死循环
- 使用set检查字符串是否出现在字符串集合里更快一些
- 图中的线是如何连在一起的
- 核心
- 每次只能改一个字母
- 改完后必须是wordList里的单词
- 求最短转换路径(BFS)
-
代码
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<>(); //将2个转换词组加进去 wordList.add(beginStr); wordList.add(endStr); //读取n个单词到wordList中 for (int i = 0; i < n; i++) { wordList.add(scanner.nextLine()); } //bfs int count = bfs(beginStr, endStr, wordList); System.out.println(count); } /** * 广度优先搜索-寻找最短路径 */ public static int bfs(String beginStr, String endStr, List<String> wordList) { int len = 1;//路径的层数,初始路径长度为1 //1.set:判断单词是否在wordList里[查询] Set<String> set = new HashSet<>(wordList); //2.防止重复访问的单词集合 Set<String> visited = new HashSet<>(); //3.存储bfs层次遍历的队列[beginstr] Queue<String> q = new LinkedList<>(); visited.add(beginStr); q.add(beginStr); q.add(null); //4.每次修改1个字母,看看是否变成set里的单词 //6.如果队列为null,还没找到endStr,返回0 //bfs层次遍历 //区分不同的层次,确保计算的是最短路径的步数。 //null是层次标记,来标记当前层结束,进入下一层 while (!q.isEmpty()) { String node = q.remove();//取出beginstr头部单词 //遇到null,这一层结束 if (node == null) { //if q不为空,还有下一层的单词需要处理,增加len(层数+1) if (!q.isEmpty()) { len++;//进入下一层 q.add(null);//在队列尾部加入null作为下一层的标记 } continue;//继续下一次循环 } //寻找邻接单词 //改变单词的一个字母,寻找邻接单词,用bfs找出beginstr到endStr的最短路径 //(1)遍历当前单词node的每个字符 char[] charArray = node.toCharArray(); //(2)寻找邻接单词 for (int i = 0; i < charArray.length; i++) { //记录旧值,用于回滚修改 char old = charArray[i]; //(3)改变node的每个字符,用a-z26个字母替换charArray[i],生成新单词newWord for (char j = 'a'; j <= 'z'; j++) { charArray[i] = j; String newWord = new String(charArray); //(4)检查newWord是否在wordList里 //如果newWord在set里,并且没被访问过 if (set.contains(newWord) && !visited.contains(newWord)) { q.add(newWord);//加入队列,等待下一轮bfs visited.add(newWord);//记录访问,防止重复 //找到结尾 //(5)如果newWord是endStr,返回len+1最短路径长度 if (newWord.equals(endStr)) return len + 1; } } //4.恢复原始单词 charArray[i] = old; } } return 0; } }
总结
- 这题比较抽象。
105.有向图的完全可达性
题目
【题目描述】
给定一个有向图,包含 N 个节点,节点编号分别为 1,2,...,N。现从 1 号节点开始,如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
【输入描述】
第一行包含两个正整数,表示节点数量 N 和边的数量 K。 后续 K 行,每行两个正整数 s 和 t,表示从 s 节点有一条边单向连接到 t 节点。
【输出描述】
如果可以从 1 号节点的边可以到达任何节点,则输出 1,否则输出 -1。
【输入示例】
text
4 4
1 2
2 1
1 3
2 4
【输出示例】
1
【提示信息】
![](https://i-blog.csdnimg.cn/img_convert/370f964351ba565565d760caf9ecfa4a.png)
从 1 号节点可以到达任意节点,输出 1。
数据范围:
- 1 <= N <= 100;
- 1 <= K <= 2000。
思路
-
思路
-
有向图搜索全路径,只能用深搜(dfs)和广搜(bfs)来搜
-
深搜三部曲
-
确认递归函数和参数
- key当前得到的可以
- visited 记录访问过的房间
-
确认终止条件
-
处理当前访问的节点还是处理下一个要访问的节点,决定终止条件怎么写
-
什么是处理?
- visited数组来记录访问过的节点,默认数组是false,把元素标记为true是处理本节点了。
- 如果遇到true,终止本层递归,如果是false,处理本层递归的节点。
-
2种情况
-
处理当前节点,有终止条件
// 写法一:处理当前访问的节点 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
- dfs只访问可以到达的节点(visited[i]==true) 说明1可以到达i+1号节点
- 终止条件:是否全为true。
- 只遍历可达节点
-
bfs
-
-
代码1 dfs
import java.util.*; public class Main{ //1.构建邻接表adjList(图的结构) public static List<List<Integer>> adjList=new ArrayList<>(); public static void main(String[] agrs){ //输入 Scanner sc=new Scanner(System.in); int vertices_num=sc.nextInt(); int line_num=sc.nextInt(); //装载到LinkedList for(int i=0;i<vertices_num;i++){ adjList.add(new LinkedList<>()); } //初始化 for(int i=0;i<line_num;i++){ int s=sc.nextInt(); int t=sc.nextInt(); adjList.get(s-1).add(t-1); } //visited数组来记录访问过的节点是否true和false //dfs boolean[] visited=new boolean[vertices_num]; dfs(visited,0); //打印结果 for(int i=0;i<vertices_num;i++){ if(!visited[i]){//没有访问过,输出false System.out.println(-1); return; } } //访问过,输出true System.out.println(1); } //2.使用dfs和bfs遍历从1节点出发能到达的所有节点 public static void dfs(boolean[] visited,int key){ //3.终止条件 if(visited[key]){ return; } visited[key]=true; List<Integer> nextKeys=adjList.get(key); for(int nextKey:nextKeys){ dfs(visited,nextKey); } } }
-
代码2
import java.util.*; public class Main{ //1.构建邻接表adjList(图的结构) public static List<List<Integer>> adjList=new ArrayList<>(); public static void main(String[] agrs){ //输入 Scanner sc=new Scanner(System.in); int vertices_num=sc.nextInt(); int line_num=sc.nextInt(); //装载到LinkedList for(int i=0;i<vertices_num;i++){ adjList.add(new LinkedList<>()); } //初始化 for(int i=0;i<line_num;i++){ int s=sc.nextInt(); int t=sc.nextInt(); adjList.get(s-1).add(t-1); } //构造邻接表 boolean[] visited=new boolean[vertices_num]; //dfs //dfs(visited,0); //bfs bfs(visited,0); //打印结果 for(int i=0;i<vertices_num;i++){ if(!visited[i]){ System.out.println(-1); return; } } System.out.println(1); } //2.使用dfs和bfs遍历从1节点出发能到达的所有节点 public static void dfs(boolean[] visited,int key){ //3.终止条件 if(visited[key]){ return; } visited[key]=true; List<Integer> nextKeys=adjList.get(key);//从1号节点(索引0)开始 for(int nextKey:nextKeys){ dfs(visited,nextKey); } } //3.bfs public static void bfs(boolean[] visited,int key){ Queue<Integer> queue=new LinkedList<Integer>(); queue.add(key);//从1号节点索引0开始 visited[key]=true;//标记起点已访问 while(!queue.isEmpty()){ int curKey=queue.poll();//取出当前节点 List<Integer> list=adjList.get(curKey);//获得邻接点 for(int nextKey:list){ //只访问未访问的节点 if(!visited[nextKey]){ queue.add(nextKey);//加入队列,等到下一轮遍历 visited[nextKey]=true;//标记已访问 } } } } }
总结
- 难
106.岛屿的周长
题目
题目描述
给定一个由 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
提示信息
![](https://i-blog.csdnimg.cn/img_convert/2c8c81f99ad6a1bfed30c7a4724e4a16.png)
岛屿的周长为 14。
数据范围:
1 <= M, N <= 50。
思路
-
思路
- 遇到1时,默认贡献4条边
- 如果1的上下左右有相邻的1,就减少2条边
- 最终求得岛屿的总周长
-
代码
import java.util.*; public class Main { public static void main(String[] args) { Scanner sc = new Scanner(System.in); int N = sc.nextInt(); int M = sc.nextInt(); int[][] grid = new int[N][M]; // 读取输入 for (int i = 0; i < N; i++) { for (int j = 0; j < M; j++) { grid[i][j] = sc.nextInt(); } } // 计算岛屿周长 System.out.println(islandPerimeter(grid)); } public static int islandPerimeter(int[][] grid) { int perimeter = 0; int rows = grid.length, cols = grid[0].length; for (int i = 0; i < rows; i++) { for (int j = 0; j < cols; j++) { if (grid[i][j] == 1) { perimeter += 4; // 每个陆地默认贡献4条边 // 上方有陆地 if (i > 0 && grid[i - 1][j] == 1) perimeter -= 2; // 左方有陆地 if (j > 0 && grid[i][j - 1] == 1) perimeter -= 2; } } } return perimeter; } }
总结
- 避免惯性思维影响自己做题。