Leetcode - 127. 单词接龙

解题思路参考代码中的注释:
class Solution {

    // 为方便后文描述,我们先自定义一个概念:如果两个字符串只有某一位上的字符不同,则称它们为相邻字符串
    //
    // 我们可以将字符串看作是一个点,如果两个字符串是相邻字符串,则相当于这两个点之间有一条连线
    // 那么题目相当于要我们找到从起点到终点的最短路径的长度
    //
    // 假设起点和终点分别是s和e,且总共有[s-1-2-3-e, s-2-4-e, s-5-6-e]这三条路径,那么可以用广度优先算法:
    //   1. 第一层是[e]
    //   2. 剩下的点中,与第一层中的点相邻的点就位于第二层,也就是[3, 4, 6]
    //   3. 剩下的点中,与第二层中的点相邻的点就位于第三层,也就是[2, 2, 5]
    //      - 注意,第三层出现了两个2,这是因为从2出发,有两条长度为3的不同的转换路径可以到达终点
    //      - 在127题中,只需要找到最短路径,此时是可以去重的
    //      - 但如果要找到所有最短路径(第126题),则不应该去重
    //      - 这里,我选择不去重,让这部分代码可以同时用于126和127题
    //   4. 以此类推,第四层是[1, s],发现起点在第四层,说明接龙的最短长度就是4
    // 注意,一般来说,层数越高,位于该层的点就越多,效率也就降低,因此,为了提高效率,我们使用双向广度优先搜索
    // 也就是说分别从起点/终点开始往后/前搜索,哪一层的点的个数少,就用这一层往下搜索
    //
    // 另一方面,如何找到与某个点相邻的点呢?
    //   1. 可以直接遍历wordList,逐个和目标字符串进行比较,看看它们是不是相邻字符串
    //   2. 可以枚举出目标字符串的所有相邻字符串,然后看这些相邻字符串是否在wordList中
    // 这里我们采用第二种方式,因为第一种方式在wordList长度很大时效率很低
    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        return doubleBfs(beginWord, endWord, wordList, new DoubleBfsResultProvider<String, Integer>() {
            @Override
            public Integer onFail(Layer<String> beginLayer, Layer<String> endLayer) {
                return 0;
            }

            @Override
            public Integer onShortestFound(Layer<String> beginLayer, Layer<String> endLayer, Set<String> intersectValues) {
                return beginLayer.layerNum() + endLayer.layerNum() - 1;
            }
        });
    }
    
    private static <R> R doubleBfs(String beginWord, String endWord, List<String> wordList, DoubleBfsResultProvider<String, R> provider) {
        Set<String> wordSet = new HashSet<>(wordList);

        // beginLayer/endLayer代表当前的从起点/终点开始搜索的层
        Layer<String> beginLayer = new Layer<>(beginWord);
        Layer<String> endLayer = new Layer<>(endWord);

        // 如果没有结束单词,则查找失败
        if (!wordSet.contains(endWord)) {
            return provider.onFail(beginLayer, endLayer);
        }

        // 题目中说开始单词可能不在wordList中,因此需要手动将开始单词加入wordSet
        wordSet.add(beginWord);

        // 不断对beginLayer/endLayer进行扩展,直到两者相交或者确定两者无法相交
        while (true) {

            // 如果beginLayer更小,则对beginLayer进行扩展,而endLayer保持不动;反之同理
            boolean beginToEnd = beginLayer.size() <= endLayer.size();
            Layer<String> growLayer = beginToEnd ? beginLayer : endLayer;
            Layer<String> fixLayer = beginToEnd ? endLayer : beginLayer;

            // 记录位于下一层中的单词
            Set<String> nextLayerWords = new HashSet<>();

            Map<String, List<String>> growMap = new HashMap<>();
            for (String word : growLayer.values()) {
                List<String> wordNeighbors = new ArrayList<>();
                growMap.put(word, wordNeighbors);

                // 枚举出该单词的所有相邻字符串
                for (int i = 0; i < word.length(); i++) {
                    char[] arr = word.toCharArray();
                    for (char c = 'a';  c <= 'z'; c++) {
                        arr[i] = c;
                        String neighbor = new String(arr);

                        // 如果枚举的字符串不在单词字典中,或者当前层已经遍历过该单词,则忽略
                        if (!wordSet.contains(neighbor) || growLayer.historyContains(neighbor)) {
                            continue;
                        }

                        // 否则枚举的字符串就是当前单词的邻居节点,同时也是位于下一层的单词
                        wordNeighbors.add(neighbor);
                        nextLayerWords.add(neighbor);
                    }
                }
            }

            // 如果下一层没有元素,则一定无法从起点到达终点
            if (nextLayerWords.isEmpty()) {
                return provider.onFail(beginLayer, endLayer);
            }

            // 否则,将当前层扩展到下一层
            growLayer.grow(growMap);

            // 判断两层是否相交,如果是,说明找到了最短路径
            Set<String> intersectValues = new HashSet<>(growLayer.values());
            intersectValues.retainAll(fixLayer.values());
            if (!intersectValues.isEmpty()) {
                return provider.onShortestFound(beginLayer, endLayer, intersectValues);
            }
        }
    }
}

/**
 * 用于维护位于某一层中的节点,T代表节点的数据类型
 * 本类除了记录位于当前层中的节点之外,还会记录这些节点具体是如何演化过来的
 *   - 比如,假设当前层只有一个节点A,而A有两个相邻节点B和C
 *   - 那么,经过一次广度优先搜索后,当前层应该存储B和C节点
 *   - 而在这个过程中,我们会记录下B的前驱结点是A,C的前驱结点是A
 */
class Layer<T> {

    /**
     * 自定义一个链表节点,用来维护节点存储的数据和节点的演化信息
     */
    private static class Node<T> {
        T val;
        Node<T> pre;
        Node(T val, Node<T> pre) { this.val = val; this.pre = pre; }
    }

    /**
     * 本层当前包含的节点及其演化的路径
     * 注意,当前层是可能有重复节点的,但这些节点的演化路径一定是不相同的
     * 比如,假设节**有"A->B->C"和"A->D->C"两条演化路径,那么本层会存储两个C节点
     */
    private LinkedList<Node<T>> paths;

    /**
     * 当前层包含的节点
     */
    private Set<T> currentValues;

    /**
     * 曾经被本层遍历过的节点
     */
    private Set<T> historyValues;

    /**
     * 本层当前的层数
     */
    private int layerNum;

    /**
     * 初始化,需要调用者提供位于第一层中的节点
     */
    public Layer(T... values) {
        paths = new LinkedList<>();
        currentValues = new HashSet<>();
        historyValues = new HashSet<>();
        for (T val : values) {
            paths.add(new Node<>(val, null));
            currentValues.add(val);
            historyValues.add(val);
        }
        layerNum = 1;
    }

    /**
     * 当前层扩展到下一层;growMap中记录了某个节点对应的所有邻居节点
     */
    public void grow(Map<T, List<T>> growMap) {
        currentValues.clear();
        layerNum++;

        int size = paths.size();
        while (size-- > 0) {
            Node<T> node = paths.pollFirst();
            for (T neighborVal : growMap.getOrDefault(node.val, Collections.emptyList())) {
                paths.add(new Node<>(neighborVal, node));
                currentValues.add(neighborVal);
                historyValues.add(neighborVal);
            }
        }
    }

    /**
     * 获取指定节点的演化路径
     */
    public List<List<T>> getPathsOf(T val) {
        List<List<T>> ret = new ArrayList<>();
        for (Node<T> node : this.paths) {
            if (node.val.equals(val)) {
                List<T> list = new ArrayList<>();
                while (node != null) {
                    list.add(node.val);
                    node = node.pre;
                }
                ret.add(list);
            }
        }
        return ret;
    }

    public Set<T> values() { return Collections.unmodifiableSet(currentValues); }
    public int size() { return currentValues.size(); }
    public int layerNum() { return layerNum; }
    public boolean historyContains(T val) { return historyValues.contains(val); }
}

/**
 * 双向广度优先搜索时,使用该类获取计算结果
 */
interface DoubleBfsResultProvider<T, R> {
    R onFail(Layer<T> beginLayer, Layer<T> endLayer);
    R onShortestFound(Layer<T> beginLayer, Layer<T> endLayer, Set<T> intersectValues);
}
全部评论

相关推荐

点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务