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>());
            }
        }
    }
    
  • 相关阅读:
    Python for Infomatics 第14章 数据库和SQL应用四(译)
    展望2017
    bing的简单英文字典工具
    自我安慰
    Python for Infomatics 第14章 数据库和SQL应用三(译)
    Python for Infomatics 第14章 数据库和SQL应用二(译)
    Python for Infomatics 第14章 数据库和SQL应用一(译)
    希望父亲早日恢复
    Python for Infomatics 第13章 网页服务四(译)
    Python for Infomatics 第13章 网页服务三(译)
  • 原文地址:https://www.cnblogs.com/chenrj97/p/14412771.html
Copyright © 2011-2022 走看看