解题记录 LeetCode 字符串建图问题

链接:
https://leetcode-cn.com/problems/word-ladder/
https://leetcode-cn.com/problems/word-ladder-ii/

题意

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

要求找到一个最短的转换序列, 很显然, 这是一个找最短路的问题, 也就是说, 我们需要将单词转化为一个图, 然后找出最短的一条路 (起点和终点都已给出)

如何建图?

建图规则: 根据题意, 两个单词之间有路当且仅当两个单词之间仅差一个单词不同, 那么两个单词之间则有路, 那么循环改变一个单词每一个位置的字母, 再检查改变后的单词是否存在于哈希表, 若存在则说明两个单词之间有路

建图以及解决算法: 遍历过的单词在后续中不应再次被遍历, 所以每一个单词被访问的时间应该是第一次访问到的时间, 并且题目求最短路径, 那么可以用 bfs 来解决这个问题, 对于访问过的点用一个哈希表来记录, 访问时间同样也用一个哈希表来记录, 当 bfs 到目标点时, 返回对应的时间即可

代码:

public class Solution {

    public int ladderLength(String beginWord, String endWord, List<String> wordList) {
        // 判断扩展出的单词是否在 wordList 里
        Set<String> set1 = new HashSet<>(wordList);
        if (!set1.contains(endWord)) {
            return 0;
        }
        set1.remove(beginWord);

        // bfs 建图
        // 记录扩展出的单词的访问时间
        Map<String, Integer> time = new HashMap<>();
        time.put(beginWord, 0);
        int step = 1;
        int len = beginWord.length();
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currWord = queue.poll();
                char[] now = currWord.toCharArray();
                // 改变单词每一位
                for (int j = 0; j < len; j++) {
                    char origin = now[j];
                    for (char i = 'a'; i <= 'z'; i++) {
                        now[j] = i;
                        String nextWord = String.valueOf(now);
                        if (!set1.contains(nextWord)) {
                            continue;
                        }
                        // 避免重复访问
                        set1.remove(nextWord);
                        // 加入队列
                        queue.offer(nextWord);
                        // 记录 nextWord 的 step
                        time.put(nextWord, step);
                        if (nextWord.equals(endWord)) {
                            return time.get(endWord) + 1;
                        }
                    }
                    now[j] = origin;
                }
            }
            step++;
        }

        return 0;
    }
}

由于每一个单词都需要遍历每一个位置的字母并循环24次来判断是否在哈希表中有对应单词对应, 这样似乎有点浪费时间, 如何优化呢?

若两个单词之间有路, 说明两个单词之间只有一个字母不同, 比如单词 yes, 那么连向它的路可能为 *es, y*s, ye*, 那么让两个单词通过我们构建的 *es 来进行连接, 就不用对每个位置循环24次了

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 s : wordList) {
            addEdge(s);
        }
        addEdge(beginWord);
        if(wordId.get(endWord) == null) {
            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;
        Deque<Integer> que = new ArrayDeque<>();
        que.offer(beginId);
        // bfs 找目标点
        while (!que.isEmpty()) {
            int x = que.poll();
            if (x == endId) {
                // 由于有了带 * 的单词, 所以需 / 2 再 + 1
                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 id = wordId.get(word);
        char[] array = word.toCharArray();
        for(int i = 0; i < array.length; i++) {
            char tmp = array[i];
            array[i] = '*';
            addWord(new String(array));
            edge.get(id).add(wordId.get(new String(array)));
            edge.get(wordId.get(new String(array))).add(id);
            array[i] = tmp;
        }
    }
    // 对每个单词编号
    public void addWord(String word) {
        if(wordId.get(word) == null) {
            wordId.put(word, nodeNum++);
            edge.add(new ArrayList<Integer>());
        }
    }
}

时间从 50ms 提升到了 30ms

在这里建完图之后, 再用 dijkstra 跑也是可以的, 时间还会得到进一步的优化

升级版题意

题意和原来唯一不同之处要求返回所有的最短的转换序列

输入:beginWord = “hit”, endWord = “cog”, wordList = [“hot”,“dot”,“dog”,“lot”,“log”,“cog”]
输出:[[“hit”,“hot”,“dot”,“dog”,“cog”],[“hit”,“hot”,“lot”,“log”,“cog”]]
解释:存在 2 种最短的转换序列:
“hit” -> “hot” -> “dot” -> “dog” -> “cog”
“hit” -> “hot” -> “lot” -> “log” -> “cog”

解决思路: 由于要求输出所有的最短序列, 那么第一时间就应该想到 dfs, 利用 dfs 的回溯可以解决如何存储序列的问题, 因此首先仍然是 bfs 建图, 再利用 dfs 将答案保存, 最后返回, 在实现时仍然有许多注意的地方

这里贴两份代码, 一份是从 endWord dfs 到 beginWord 一份是从 beginWord 到 endWord

endWord dfs 到 beginWord:

public class Solution {
    public List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        List<List<String>> ans = new ArrayList<>();
        // 判断扩展出的单词是否在 wordList 里
        Set<String> dict = new HashSet<>(wordList);
        if (!dict.contains(endWord)) {
            return ans;
        }
        dict.remove(beginWord);

        // bfs 建图
        // 记录时间
        Map<String, Integer> time = new HashMap<>();
        time.put(beginWord, 0);
        // 记录了单词是从哪些单词扩展而来
        Map<String, List<String>> from = new HashMap<>();
        int step = 1;
        boolean found = false;
        int wordLen = beginWord.length();
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currWord = queue.poll();
                char[] charArray = currWord.toCharArray();
                // 将每一位替换成 26 个小写英文字母
                for (int j = 0; j < wordLen; j++) {
                    char origin = charArray[j];
                    for (char c = 'a'; c <= 'z'; c++) {
                        charArray[j] = c;
                        String nextWord = String.valueOf(charArray);
                        if (time.containsKey(nextWord) && step == time.get(nextWord)) {
                            from.get(nextWord).add(currWord);
                        }
                        if (!dict.contains(nextWord)) {
                            continue;
                        }
                        // dict 用于防止将放入过队列的元素再次放进队列而造成资源浪费
                        dict.remove(nextWord);
                        // 进入队列
                        queue.offer(nextWord);

                        // 记录 nextWord 从 currWord 而来
                        from.putIfAbsent(nextWord, new ArrayList<>());
                        from.get(nextWord).add(currWord);
                        // 记录 nextWord 的 step
                        time.put(nextWord, step);
                        if (nextWord.equals(endWord)) {
                            found = true;
                        }
                    }
                    charArray[j] = origin;
                }
            }
            step++;
            if (found) {
                break;
            }
        }

        // dfs 找到所有解,从 endWord 到 beginWord ,每次操作 path 的头部
        if (found) {
            Deque<String> path = new ArrayDeque<>();
            path.add(endWord);
            dfs(from, path, beginWord, endWord, ans);
        }
        return ans;
    }

    public void dfs(Map<String, List<String>> from, Deque<String> path, String beginWord, String cur, List<List<String>> ans) {
        if (cur.equals(beginWord)) {
            ans.add(new ArrayList<>(path));
            return;
        }
        for (String tmp : from.get(cur)) {
            path.addFirst(tmp);
            dfs(from, path, beginWord, tmp, ans);
            path.removeFirst();
        }
    }
}

beginWord 到 endWord:

public class Solution {
    public  List<List<String>> findLadders(String beginWord, String endWord, List<String> wordList) {
        List<List<String>> res = new ArrayList<>();
        // 判断扩展出的单词是否在 wordList 里
        Set<String> dict = new HashSet<>(wordList);
        if (!dict.contains(endWord)) {
            return res;
        }

        dict.remove(beginWord);

        // bfs 建图
        // 记录时间
        Map<String, Integer> time = new HashMap<>();
        time.put(beginWord, 0);
        // 记录了单词扩展到哪些单词
        Map<String, List<String>> to = new HashMap<>();
        int step = 1;
        boolean found = false;
        int wordLen = beginWord.length();
        Queue<String> queue = new LinkedList<>();
        queue.offer(beginWord);
        while (!queue.isEmpty()) {
            int size = queue.size();
            for (int i = 0; i < size; i++) {
                String currWord = queue.poll();
                to.putIfAbsent(currWord, new ArrayList<>());
                char[] charArray = currWord.toCharArray();
                // 将每一位替换成 26 个小写英文字母
                for (int j = 0; j < wordLen; j++) {
                    char origin = charArray[j];
                    for (char c = 'a'; c <= 'z'; c++) {
                        charArray[j] = c;
                        String nextWord = String.valueOf(charArray);
                        if (time.containsKey(nextWord) && step == time.get(nextWord)) {
                            to.get(currWord).add(nextWord);
                        }
                        if (!dict.contains(nextWord)) {
                            continue;
                        }
                        // dict 用于防止将放入过队列的元素再次放进队列而造成资源浪费
                        dict.remove(nextWord);
                        // 这一层扩展出的单词进入队列
                        queue.offer(nextWord);
                        // 记录 currWord 扩展到 nextWord

                        to.get(currWord).add(nextWord);
                        // 记录 nextWord 的 time
                        time.put(nextWord, step);
                        if (nextWord.equals(endWord)) {
                            found = true;
                        }
                    }
                    charArray[j] = origin;
                }
            }
            step++;
            if (found) {
                break;
            }
        }

        // dfs 找到所有解,从 beginWord 到 endWord,每次操作 path 的头部
        if (found) {
            Deque<String> path = new ArrayDeque<>();
            path.add(beginWord);
            dfs(to, path, beginWord, endWord, res);
        }
        return res;
    }

    public void dfs(Map<String, List<String>> to, Deque<String> path, String cur, String endWord, List<List<String>> res) {
        if (cur.equals(endWord)) {
            res.add(new ArrayList<>(path));
            return;
        }
        if(to.get(cur) == null) return;
        for (String tmp : to.get(cur)) {
            path.addLast(tmp);
            dfs(to, path, tmp, endWord, res);
            path.removeLast();
        }
    }
}

你可能感兴趣的:(数据结构与图论,leetcode,图论,算法)