代码随想录算法训练Day59|LeetCode127-单词接龙、LeetCode841-钥匙和房间、LeetCode463-岛屿的周长

单词接龙

题目描述

力扣127-单词接龙

字典 wordList 中从单词 beginWord 和 endWord 的 转换序列 是一个按下述规格形成的序列:

  • 序列中第一个单词是 beginWord 。
  • 序列中最后一个单词是 endWord 。
  • 每次转换只能改变一个字母。
  • 转换过程中的中间单词必须是字典 wordList 中的单词。
  • 给你两个单词 beginWord 和 endWord 和一个字典 wordList ,找到从 beginWord 到 endWord 的 最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 0。

示例 1:

  • 输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
  • 输出:5
  • 解释:一个最短转换序列是 “hit” -> “hot” -> “dot” -> “dog” -> “cog”, 返回它的长度 5。

示例 2:

  • 输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”]
  • 输出:0
  • 解释:endWord “cog” 不在字典中,所以无法进行转换。

解题思路

以示例1为例,从这个图中可以看出 hit 到 cog的路线,不止一条,有三条,一条是最短的长度为5,两条长度为6。

img

本题只需要求出最短路径的长度就可以了,不用找出路径。

所以这道题要解决两个问题:

  • 图中的线是如何连在一起的
  • 起点和终点的最短路径长度

首先题目中并没有给出点与点之间的连线,而是要我们自己去连,条件是字符只能差一个,所以判断点与点之间的关系,要自己判断是不是差一个字符,如果差一个字符,那就是有链接。

然后就是求起点和终点的最短路径长度,这里无向图求最短路,广搜最为合适,广搜只要搜到了终点,那么一定是最短的路径。因为广搜就是以起点中心向四周扩散的搜索。

本题如果用深搜,会比较麻烦,要在到达终点的不同路径中选则一条最短路。 而广搜只要达到终点,一定是最短路。

另外需要有一个注意点:

  • 本题是一个无向图,需要用标记位,标记着节点是否走过,否则就会死循环
  • 本题给出集合是数组型的,可以转成set结构,查找更快一些

java代码如下:(详细注释)

广搜 标记位

class Solution {
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {

        HashSet<String> wordSet = new HashSet<>(wordList);// 将 List 转换成 HashSet,提高查询速度
        // 如果 endWord 没有在 wordSet 中出现,直接返回 0
        if (!wordSet.contains(endWord))
            return 0;
        // 记录单词是否访问过
        Map<String, Integer> visitMap = new HashMap<>(); // <word, 查询到这个 word 路径长度>
        // 初始化队列
        Queue<String> queue = new LinkedList<>();
        queue.add(beginWord);
        // 初始化 visitMap
        visitMap.put(beginWord, 1);

        while (!queue.isEmpty()) {
            String word = queue.poll();
            int path = visitMap.get(word); // 这个 word 的路径长度
            char[] wordChars = word.toCharArray();
            for (int i = 0; i < wordChars.length; i++) {
                char originalChar = wordChars[i];
                for (char c = 'a'; c <= 'z'; c++) {
                    if (c == originalChar)
                        continue;
                    wordChars[i] = c;
                    String newWord = new String(wordChars);// 用一个新单词替换word,因为每次置换一个字母
                    if (newWord.equals(endWord))
                        return path + 1; // 找到了 endWord,返回 path+1
                    // wordSet 中出现了 newWord,并且 newWord 没有被访问过
                    if (wordSet.contains(newWord) && !visitMap.containsKey(newWord)) {
                        // 添加访问信息
                        visitMap.put(newWord, path + 1);
                        queue.add(newWord);
                    }
                }
                wordChars[i] = originalChar; // 恢复原单词
            }
        }
        return 0;
    }
}

参考解题

public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    HashSet<String> wordSet = new HashSet<>(wordList); //转换为hashset 加快速度
    if (wordSet.size() == 0 || !wordSet.contains(endWord)) {  //特殊情况判断
        return 0;
    }
    Queue<String> queue = new LinkedList<>(); //bfs 队列
    queue.offer(beginWord);
    Map<String, Integer> map = new HashMap<>(); //记录单词对应路径长度
    map.put(beginWord, 1);

    while (!queue.isEmpty()) {
        String word = queue.poll(); //取出队头单词
        int path  = map.get(word); //获取到该单词的路径长度
        for (int i = 0; i < word.length(); i++) { //遍历单词的每个字符
            char[] chars = word.toCharArray(); //将单词转换为char array,方便替换
            for (char k = 'a'; k <= 'z'; k++) { //从'a' 到 'z' 遍历替换
                chars[i] = k; //替换第i个字符
                String newWord = String.valueOf(chars); //得到新的字符串
                if (newWord.equals(endWord)) {  //如果新的字符串值与endWord一致,返回当前长度+1
                    return path + 1;
                }
                if (wordSet.contains(newWord) && !map.containsKey(newWord)) { //如果新单词在set中,但是没有访问过
                    map.put(newWord, path + 1); //记录单词对应的路径长度
                    queue.offer(newWord);//加入队尾
                }
            }
        }
    }
    return 0; //未找到
}

841.钥匙和房间

题目描述

力扣841-钥匙和房间

有 N 个房间,开始时你位于 0 号房间。每个房间有不同的号码:0,1,2,…,N-1,并且房间里可能有一些钥匙能使你进入下一个房间。

在形式上,对于每个房间 i 都有一个钥匙列表 rooms[i],每个钥匙 rooms[i] [j] 由 [0,1,…,N-1] 中的一个整数表示,其中 N = rooms.length。 钥匙 rooms[i] [j] = v 可以打开编号为 v 的房间。

最初,除 0 号房间外的其余所有房间都被锁住。

你可以自由地在房间之间来回走动。

如果能进入每个房间返回 true,否则返回 false。

示例 1:

  • 输入: [[1],[2],[3],[]]
  • 输出: true
  • 解释: 我们从 0 号房间开始,拿到钥匙 1。 之后我们去 1 号房间,拿到钥匙 2。 然后我们去 2 号房间,拿到钥匙 3。 最后我们去了 3 号房间。 由于我们能够进入每个房间,我们返回 true。

示例 2:

  • 输入:[[1,3],[3,0,1],[2],[0]]
  • 输出:false
  • 解释:我们不能进入 2 号房间。

解题思路

本题其实给我们是一个有向图, 意识到这是有向图很重要!

图中给我的两个示例: [[1],[2],[3],[]] [[1,3],[3,0,1],[2],[0]],画成对应的图如下:

img

我们可以看出图1的所有节点都是链接的,而图二中,节点2 是孤立的。

这就很容易让我们想起岛屿问题,只要发现独立的岛,就是不能进入所有房间。

此时也容易想到用并查集的方式去解决。

但本题是有向图,在有向图中,即使所有节点都是链接的,但依然不可能从0出发遍历所有边。 给大家举一个例子:

图3:[[5], [], [1, 3], [5]] ,如图:

img

在图3中,大家可以发现,节点0只能到节点5,然后就哪也去不了了。

所以本题是一个有向图搜索全路径的问题。 只能用深搜(DFS)或者广搜(BFS)来搜。

关于DFS的理论,如果大家有困惑,可以先看我这篇题解: DFS理论基础

以下dfs分析 大家一定要仔细看,本题有两种dfs的解法,很多题解没有讲清楚。 看完之后 相信你对dfs会有更深的理解。

深搜三部曲:

1.确认递归函数,参数

需要传入二维数组rooms来遍历地图,需要知道当前我们拿到的key,以至于去下一个房间。

同时还需要一个数组,用来记录我们都走过了哪些房间,这样好知道最后有没有把所有房间都遍历的,可以定义一个一维数组。

所以 递归函数参数如下:

// key 当前得到的可以 
// visited 记录访问过的房间 
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {

2.确认终止条件

遍历的时候,什么时候终止呢?

这里有一个很重要的逻辑,就是在递归中,我们是处理当前访问的节点,还是处理下一个要访问的节点

这决定 终止条件怎么写。

首先明确,本题中什么叫做处理,就是 visited数组来记录访问过的节点,该节点默认 数组里元素都是false,把元素标记为true就是处理 本节点了。

如果我们是处理当前访问的节点,当前访问的节点如果是 true ,说明是访问过的节点,那就终止本层递归,如果不是true,我们就把它赋值为true,因为这是我们处理本层递归的节点。

代码就是这样:

// 写法一:处理当前访问的节点
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
    if (visited[key]) { // 本层递归是true,说明访问过,立刻返回
        return;
    }
    visited[key] = true; // 给当前遍历的节点赋值true 
    List<Integer> keys = rooms.get(key);
    for (int nextKey : keys) {
        // 深度优先搜索遍历
        dfs(rooms, nextKey, visited);
    }
}

如果我们是处理下一层访问的节点,而不是当前层。那么就要在 深搜三部曲中第三步:处理目前搜索节点出发的路径的时候对 节点进行处理。

这样的话,就不需要终止条件,而是在 搜索下一个节点的时候,直接判断 下一个节点是否是我们要搜的节点。

代码就是这样的:

// 写法二:处理下一个要访问的节点
void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
    // 这里没有终止条件,而是在处理下一层节点的时候来判断
    List<Integer> keys = rooms.get(key);
    for (int nextKey : keys) { 
        if (!visited[nextKey]) { // 处理下一层节点,判断是否要进行递归
            visited[nextKey] = true;
            dfs(rooms, nextKey, visited);
        }       
    }
}

可以看出,如果看待 我们要访问的节点,直接决定了两种不一样的写法,很多录友对这一块很模糊,可能做过这道题,但没有思考到这个维度上。

3.处理目前搜索节点出发的路径

其实在上面,深搜三部曲 第二部,就已经讲了,因为终止条件的两种写法, 直接决定了两种不一样的递归写法。

这里还有细节:

看上面两个版本的写法中, 好像没有发现回溯的逻辑。

我们都知道,有递归就有回溯,回溯就在递归函数的下面, 那么之前我们做的dfs题目,都需要回溯操作,例如:797.所有可能的路径为什么本题就没有回溯呢?

代码中可以看到dfs函数下面并没有回溯的操作。

此时就要在思考本题的要求了,本题是需要判断 0节点是否能到所有节点,那么我们就没有必要回溯去撤销操作了,只要遍历过的节点一律都标记上。

那什么时候需要回溯操作呢?

当我们需要搜索一条可行路径的时候,就需要回溯操作了,因为没有回溯,就没法“调头”, 如果不理解的话,去看我写的 797.所有可能的路径 的题解。

以上分析完毕,DFS整体实现java代码如下:

// 写法一:处理当前访问的节点
class Solution {
    private void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
        if (visited[key]) {
            return;
        }
        visited[key] = true;
        List<Integer> keys = rooms.get(key);
        for (int nextKey : keys) {
            // 深度优先搜索遍历
            dfs(rooms, nextKey, visited);
        }
    }

    public boolean canVisitAllRooms(List<List<Integer>> rooms) {
        boolean[] visited = new boolean[rooms.size()];
        dfs(rooms, 0, visited);
        // 检查是否都访问到了
        for (boolean visit : visited) {
            if (!visit)
                return false;
        }
        return true;
    }
}

//写法二:处理下一个要访问的节点
class Solution {
    private void dfs(List<List<Integer>> rooms, int key, boolean[] visited) {
        List<Integer> keys = rooms.get(key);
        for (int nextKey : keys) {
            if (!visited[nextKey]) {
                visited[nextKey] = true;
                dfs(rooms, nextKey, visited);
            }
        }
    }

    public boolean canVisitAllRooms(List<List<Integer>> rooms) {
        boolean[] visited = new boolean[rooms.size()];
        visited[0] = true; // 0 节点是出发节点,一定被访问过
        dfs(rooms, 0, visited);
        // 检查是否都访问到了
        for (boolean visit : visited) {
            if (!visit)
                return false;
        }
        return true;
    }
}

岛屿的周长

力扣463-岛屿的周长

给定一个 row x col 的二维网格地图 grid ,其中:grid[i] [j] = 1 表示陆地, grid[i] [j] = 0 表示水域。

网格中的格子 水平和垂直 方向相连(对角线方向不相连)。整个网格被水完全包围,但其中恰好有一个岛屿(或者说,一个或多个表示陆地的格子相连组成的岛屿)。

岛屿中没有“湖”(“湖” 指水域在岛屿内部且不和岛屿周围的水相连)。格子是边长为 1 的正方形。网格为长方形,且宽度和高度均不超过 100 。计算这个岛屿的周长。

img

  • 输入:grid = [[0,1,0,0],[1,1,1,0],[0,1,0,0],[1,1,0,0]]
  • 输出:16
  • 解释:它的周长是上面图片中的 16 个黄色的边

示例 2:

  • 输入:grid = [[1]]
  • 输出:4

示例 3:

  • 输入:grid = [[1,0]]
  • 输出:4

解法一:

遍历每一个空格,遇到岛屿,计算其上下左右的情况,遇到水域或者出界的情况,就可以计算边了。

如图:

img

java代码如下:(详细注释)

class Solution {
    int[][] direction = {{0, 1}, {1, 0}, {-1, 0}, {0, -1}};
    
    public int islandPerimeter(int[][] grid) {
        int result = 0;
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j] == 1) {
                    for (int k = 0; k < 4; k++) {       // 上下左右四个方向
                        int x = i + direction[k][0];
                        int y = j + direction[k][1];    // 计算周边坐标x,y
                        if (x < 0                       // i在边界上
                                || x >= grid.length     // i在边界上
                                || y < 0                // j在边界上
                                || y >= grid[0].length  // j在边界上
                                || grid[x][y] == 0) {   // x,y位置是水域
                            result++;
                        }
                    }
                }
            }
        }
        return result;
    }
}

解法二:

计算出总的岛屿数量,因为有一对相邻两个陆地,边的总数就减2,那么在计算出相邻岛屿的数量就可以了。

result = 岛屿数量 * 4 - cover * 2;

如图:

img

java代码如下:(详细注释)

class Solution {
    public int islandPerimeter(int[][] grid) {
        int sum = 0; // 陆地数量
        int cover = 0; // 相邻数量
        for (int i = 0; i < grid.length; i++) {
            for (int j = 0; j < grid[0].length; j++) {
                if (grid[i][j] == 1) {
                    sum++;
                    // 统计上边相邻陆地
                    if (i - 1 >= 0 && grid[i - 1][j] == 1)
                        cover++;
                    // 统计左边相邻陆地
                    if (j - 1 >= 0 && grid[i][j - 1] == 1)
                        cover++;
                    // 为什么没统计下边和右边? 因为避免重复计算
                }
            }
        }
        return sum * 4 - cover * 2;
    }
}

ps:部分图片和代码来自代码随想录Leetcode官网

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值