zoukankan      html  css  js  c++  java
  • Hard | LeetCode 127. 单词接龙 | 图 + 广度优先遍历

    127. 单词接龙

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

    • 序列中第一个单词是 beginWord
    • 序列中最后一个单词是 endWord
    • 每次转换只能改变一个字母。
    • 转换过程中的中间单词必须是字典 wordList 中的单词。

    给你两个单词 beginWordendWord 和一个字典 wordList ,找到从 beginWordendWord最短转换序列 中的 单词数目 。如果不存在这样的转换序列,返回 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 <= beginWord.length <= 10
    • endWord.length == beginWord.length
    • 1 <= wordList.length <= 5000
    • wordList[i].length == beginWord.length
    • beginWordendWordwordList[i] 由小写英文字母组成
    • beginWord != endWord
    • wordList 中的所有字符串 互不相同

    解题思路

    这道题实际上是一个图模型。把每个单词看做图的一个点, 如果两个单词只差一个字符, 那么说明这两个单词是连通的, 那么在这两个单词之间添加一条边即可。题目就转换成了从图的A点到B的最短路径, 使用图的广度优先遍历算法即可解决问题。

    方法一:领接表建图

    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
    
        // 所有单词具有相同的长度
        int L = beginWord.length();
    
        // 采用领接表结构存储图
        Map<String, List<String>> allComboDict = new HashMap<>();
    
        wordList.forEach(
                word -> {
                    for (int i = 0; i < L; i++) {
                        // key 是每个单词, 也是图中的每一个节点
                        // value 是与当前单词能够发生一次转换的所有单词列表, 也是图中与之相邻的所有节点
                        String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);
                        List<String> transformations = allComboDict.getOrDefault(newWord, new ArrayList<>());
                        transformations.add(word);
                        allComboDict.put(newWord, transformations);
                    }
                });
    
        // 广度优先遍历队列
        Queue<Pair<String, Integer>> Q = new LinkedList<>();
        Q.add(new Pair(beginWord, 1));
    
        // 访问数组
        Map<String, Boolean> visited = new HashMap<>();
        visited.put(beginWord, true);
    
        while (!Q.isEmpty()) {
            // 队列元素出队
            Pair<String, Integer> node = Q.remove();
            String word = node.getKey();
            int level = node.getValue();
            for (int i = 0; i < L; i++) {
    
                // Intermediate words for current word
                String newWord = word.substring(0, i) + '*' + word.substring(i + 1, L);
    
                // 遍历所有相邻节点
                for (String adjacentWord : allComboDict.getOrDefault(newWord, new ArrayList<>())) {
                    // 找到目标节点
                    if (adjacentWord.equals(endWord)) {
                        return level + 1;
                    }
                    //  访问并入队
                    if (!visited.containsKey(adjacentWord)) {
                        visited.put(adjacentWord, true);
                        Q.add(new Pair(adjacentWord, level + 1));
                    }
                }
            }
        }
    
        return 0;
    }
    

    方法二

    首先为了方便表示,我们先给每一个单词标号,即给每个单词分配一个 id。创建一个由单词 word 到 id 对应的映射 wordId,并将 beginWord 与 wordList 中所有的单词都加入这个映射中。之后我们检查 endWord 是否在该映射内,若不存在,则输入无解。我们可以使用哈希表实现上面的映射关系。

    然后我们需要建图,依据朴素的思路,我们可以枚举每一对单词的组合,判断它们是否恰好相差一个字符,以判断这两个单词对应的节点是否能够相连。但是这样效率太低,我们可以优化建图。

    具体地,我们可以创建虚拟节点。对于单词 hit,我们创建三个虚拟节点 *it、h*t、hi*,并让 hit 向这三个虚拟节点分别连一条边即可。如果一个单词能够转化为 hit,那么该单词必然会连接到这三个虚拟节点之一。对于每一个单词,我们枚举它连接到的虚拟节点,把该单词对应的 id 与这些虚拟节点对应的 id 相连即可。

    最后我们将起点加入队列开始广度优先搜索,当搜索到终点时,我们就找到了最短路径的长度。注意因为添加了虚拟节点,所以我们得到的距离为实际最短路径长度的两倍。同时我们并未计算起点对答案的贡献,所以我们应当返回距离的一半再加一的结果。

    class Solution {
        // 用来将每个单词映射为一个数字ID
        Map<String, Integer> wordId = new HashMap<String, Integer>();
        // 图模型, 用邻接表来保存
        List<List<Integer>> edge = new ArrayList<List<Integer>>();
        int nodeNum = 0;
    
        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            // 依据单词列表构造图
            for (String word : wordList) {
                addEdge(word);
            }
            addEdge(beginWord);
            // 对于不包含endWord的单词列表价进行特判
            if (!wordId.containsKey(endWord)) {
                return 0;
            }
            // 
            int[] dis = new int[nodeNum];
            Arrays.fill(dis, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord), endId = wordId.get(endWord);
            dis[beginId] = 0;
    
            Queue<Integer> que = new LinkedList<Integer>();
            que.offer(beginId);
            while (!que.isEmpty()) {
                int x = que.poll();
                if (x == endId) {
                    return dis[endId] / 2 + 1;
                }
                for (int it : edge.get(x)) {
                    if (dis[it] == Integer.MAX_VALUE) {
                        dis[it] = dis[x] + 1;
                        que.offer(it);
                    }
                }
            }
            return 0;
        }
    
        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }
    
        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }
    

    方法三:双向广度优先遍历

    根据给定字典构造的图可能会很大,而广度优先搜索的搜索空间大小依赖于每层节点的分支数量。假如每个节点的分支数量相同,搜索空间会随着层数的增长指数级的增加。考虑一个简单的二叉树,每一层都是满二叉树的扩展,节点的数量会以 22 为底数呈指数增长。

    如果使用两个同时进行的广搜可以有效地减少搜索空间。一边从 beginWord 开始,另一边从 endWord 开始。我们每次从两边各扩展一层节点,当发现某一时刻两边都访问过同一顶点时就停止搜索。这就是双向广度优先搜索,它可以可观地减少搜索空间大小,从而提高代码运行效率。

    class Solution {
        Map<String, Integer> wordId = new HashMap<String, Integer>();
        List<List<Integer>> edge = new ArrayList<List<Integer>>();
        int nodeNum = 0;
    
        public int ladderLength(String beginWord, String endWord, List<String> wordList) {
            for (String word : wordList) {
                addEdge(word);
            }
            addEdge(beginWord);
            if (!wordId.containsKey(endWord)) {
                return 0;
            }
    
            int[] disBegin = new int[nodeNum];
            Arrays.fill(disBegin, Integer.MAX_VALUE);
            int beginId = wordId.get(beginWord);
            disBegin[beginId] = 0;
            Queue<Integer> queBegin = new LinkedList<Integer>();
            queBegin.offer(beginId);
            
            int[] disEnd = new int[nodeNum];
            Arrays.fill(disEnd, Integer.MAX_VALUE);
            int endId = wordId.get(endWord);
            disEnd[endId] = 0;
            Queue<Integer> queEnd = new LinkedList<Integer>();
            queEnd.offer(endId);
    
            while (!queBegin.isEmpty() && !queEnd.isEmpty()) {
                int queBeginSize = queBegin.size();
                for (int i = 0; i < queBeginSize; ++i) {
                    int nodeBegin = queBegin.poll();
                    if (disEnd[nodeBegin] != Integer.MAX_VALUE) {
                        return (disBegin[nodeBegin] + disEnd[nodeBegin]) / 2 + 1;
                    }
                    for (int it : edge.get(nodeBegin)) {
                        if (disBegin[it] == Integer.MAX_VALUE) {
                            disBegin[it] = disBegin[nodeBegin] + 1;
                            queBegin.offer(it);
                        }
                    }
                }
    
                int queEndSize = queEnd.size();
                for (int i = 0; i < queEndSize; ++i) {
                    int nodeEnd = queEnd.poll();
                    if (disBegin[nodeEnd] != Integer.MAX_VALUE) {
                        return (disBegin[nodeEnd] + disEnd[nodeEnd]) / 2 + 1;
                    }
                    for (int it : edge.get(nodeEnd)) {
                        if (disEnd[it] == Integer.MAX_VALUE) {
                            disEnd[it] = disEnd[nodeEnd] + 1;
                            queEnd.offer(it);
                        }
                    }
                }
            }
            return 0;
        }
    
        public void addEdge(String word) {
            addWord(word);
            int id1 = wordId.get(word);
            char[] array = word.toCharArray();
            int length = array.length;
            for (int i = 0; i < length; ++i) {
                char tmp = array[i];
                array[i] = '*';
                String newWord = new String(array);
                addWord(newWord);
                int id2 = wordId.get(newWord);
                edge.get(id1).add(id2);
                edge.get(id2).add(id1);
                array[i] = tmp;
            }
        }
    
        public void addWord(String word) {
            if (!wordId.containsKey(word)) {
                wordId.put(word, nodeNum++);
                edge.add(new ArrayList<Integer>());
            }
        }
    }
    
  • 相关阅读:
    这4点微不足道的改变,正在带我起飞
    “挑三拣四”地学一学Java I/O
    做到这3点,你也能成为一个高情商的人
    深入浅出一下Java的HashMap
    前端学习 -- Css -- 有序列表和无序列表
    Python 文件I/O
    python 模块
    python 函数
    Python 日期和时间
    python 字典
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14412771.html
Copyright © 2011-2022 走看看