代码随想录Day53:图论(岛屿的周长、字符串接龙、有向图的完全可达性)

一、实战

106岛屿的周长

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字符串接龙

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有向图的完全可达性

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);
        }
    }
}

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值