Word Ladder II

描述

Given two words (start and end), and a dictionary, find all shortest transformation sequence(s) from start to end, such that:

  • Only one letter can be changed at a time
  • Each intermediate word must exist in the dictionary
    For example, Given:
  1. start = "hit"
  2. end = "cog"
  3. dict = ["hot","dot","dog","lot","log"]

Return

  1. [
  2. ["hit","hot","dot","dog","cog"],
  3. ["hit","hot","lot","log","cog"]
  4. ]

Note:

  • All words have the same length.
  • All words contain only lowercase alphabetic characters.

    分析

跟 Word Ladder比,这题是求路径本身,不是路径长度,也是BFS,略微麻烦点。

求一条路径和求所有路径有很大的不同,求一条路径,每个状态节点只需要记录一个前驱即可;求所有路径时,有的状态节点可能有多个父节点,即要记录多个前驱。

如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的处理,因为我们要找的是最短路径。

单队列

  1. // Word Ladder II
  2. // 时间复杂度O(n),空间复杂度O(n)
  3. public class Solution {
  4. public List<List<String>> findLadders(String beginWord, String endWord,
  5. Set<String> wordList) {
  6. Queue<String> q = new LinkedList<>();
  7. HashMap<String, Integer> visited = new HashMap<>(); // 判重
  8. HashMap<String, ArrayList<String>> father = new HashMap<>(); // DAG
  9. final Function<String, Boolean> stateIsValid = (String s) ->
  10. wordList.contains(s) || s.equals(endWord);
  11. final Function<String, Boolean> stateIsTarget = (String s) ->
  12. s.equals(endWord);
  13. final Function<String, HashSet<String> > stateExtend = (String s) -> {
  14. HashSet<String> result = new HashSet<>();
  15. char[] array = s.toCharArray();
  16. for (int i = 0; i < array.length; ++i) {
  17. final char old = array[i];
  18. for (char c = 'a'; c <= 'z'; c++) {
  19. // 防止同字母替换
  20. if (c == array[i]) continue;
  21. array[i] = c;
  22. String newState = new String(array);
  23. final int newDepth = visited.get(s) + 1;
  24. if (stateIsValid.apply(newState)) {
  25. if (visited.containsKey(newState)) {
  26. final int depth = visited.get(newState);
  27. if (depth < newDepth) {
  28. // do nothing
  29. } else if (depth == newDepth) {
  30. result.add(newState);
  31. } else {
  32. throw new IllegalStateException("not possible to get here");
  33. }
  34. } else {
  35. result.add(newState);
  36. }
  37. }
  38. array[i] = old; // 恢复该单词
  39. }
  40. }
  41. return result;
  42. };
  43. List<List<String>> result = new ArrayList<>();
  44. q.offer(beginWord);
  45. visited.put(beginWord, 0);
  46. while (!q.isEmpty()) {
  47. String state = q.poll();
  48. // 如果当前路径长度已经超过当前最短路径长度,
  49. // 可以中止对该路径的处理,因为我们要找的是最短路径
  50. if (!result.isEmpty() && (visited.get(state) + 1) > result.get(0).size()) break;
  51. if (stateIsTarget.apply(state)) {
  52. ArrayList<String> path = new ArrayList<>();
  53. genPath(father, beginWord, state, path, result);
  54. continue;
  55. }
  56. // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
  57. // 那么目标节点就会在q中出现两次,输出路径就会翻倍
  58. // visited.insert(state);
  59. // 扩展节点
  60. HashSet<String> newStates = stateExtend.apply(state);
  61. for (String newState : newStates) {
  62. if (!visited.containsKey(newState)) {
  63. q.offer(newState);
  64. visited.put(newState, visited.get(state)+1);
  65. }
  66. ArrayList<String> parents = father.getOrDefault(newState, new ArrayList<>());
  67. parents.add(state);
  68. father.put(newState, parents);
  69. }
  70. }
  71. return result;
  72. }
  73. private static void genPath(HashMap<String, ArrayList<String>> father,
  74. String start, String state, List<String> path,
  75. List<List<String>> result) {
  76. path.add(state);
  77. if (state.equals(start)) {
  78. if (!result.isEmpty()) {
  79. if (path.size() < result.get(0).size()) {
  80. result.clear();
  81. } else if (path.size() == result.get(0).size()) {
  82. // do nothing
  83. } else {
  84. throw new IllegalStateException("not possible to get here");
  85. }
  86. }
  87. ArrayList<String> tmp = new ArrayList<>(path);
  88. Collections.reverse(tmp);
  89. result.add(tmp);
  90. } else {
  91. for (String f : father.get(state)) {
  92. genPath(father, start, f, path, result);
  93. }
  94. }
  95. path.remove(path.size() - 1);
  96. }
  97. }

双队列

  1. // Word Ladder II
  2. // 时间复杂度O(n),空间复杂度O(n)
  3. public class Solution {
  4. public List<List<String>> findLadders(String beginWord, String endWord,
  5. Set<String> wordList) {
  6. // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向
  7. // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此
  8. // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的
  9. HashSet<String> current = new HashSet<>();
  10. HashSet<String> next = new HashSet<>();
  11. HashSet<String> visited = new HashSet<>(); // 判重
  12. HashMap<String, ArrayList<String>> father = new HashMap<>(); // DAG
  13. int level = -1; // 层次
  14. final Function<String, Boolean> stateIsValid = (String s) ->
  15. wordList.contains(s) || s.equals(endWord);
  16. final Function<String, Boolean> stateIsTarget = (String s) ->
  17. s.equals(endWord);
  18. final Function<String, HashSet<String> > stateExtend = (String s) -> {
  19. HashSet<String> result = new HashSet<>();
  20. char[] array = s.toCharArray();
  21. for (int i = 0; i < array.length; ++i) {
  22. final char old = array[i];
  23. for (char c = 'a'; c <= 'z'; c++) {
  24. // 防止同字母替换
  25. if (c == array[i]) continue;
  26. array[i] = c;
  27. String newState = new String(array);
  28. if (stateIsValid.apply(newState) &&
  29. !visited.contains(newState)) {
  30. result.add(newState);
  31. }
  32. array[i] = old; // 恢复该单词
  33. }
  34. }
  35. return result;
  36. };
  37. List<List<String>> result = new ArrayList<>();
  38. current.add(beginWord);
  39. while (!current.isEmpty()) {
  40. ++ level;
  41. // 如果当前路径长度已经超过当前最短路径长度,
  42. // 可以中止对该路径的处理,因为我们要找的是最短路径
  43. if (!result.isEmpty() && level + 1 > result.get(0).size()) break;
  44. // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
  45. // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
  46. // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
  47. for (String state : current)
  48. visited.add(state);
  49. for (String state : current) {
  50. if (stateIsTarget.apply(state)) {
  51. ArrayList<String> path = new ArrayList<>();
  52. genPath(father, beginWord, state, path, result);
  53. continue;
  54. }
  55. // 扩展节点
  56. HashSet<String> newStates = stateExtend.apply(state);
  57. for (String newState : newStates) {
  58. next.add(newState);
  59. ArrayList<String> parents = father.getOrDefault(newState, new ArrayList<>());
  60. parents.add(state);
  61. father.put(newState, parents);
  62. }
  63. }
  64. current.clear();
  65. // swap
  66. HashSet<String> tmp = current;
  67. current = next;
  68. next = tmp;
  69. }
  70. return result;
  71. }
  72. private static void genPath(HashMap<String, ArrayList<String>> father,
  73. String start, String state, List<String> path,
  74. List<List<String>> result) {
  75. path.add(state);
  76. if (state.equals(start)) {
  77. if (!result.isEmpty()) {
  78. if (path.size() < result.get(0).size()) {
  79. result.clear();
  80. } else if (path.size() == result.get(0).size()) {
  81. // do nothing
  82. } else {
  83. throw new IllegalStateException("not possible to get here");
  84. }
  85. }
  86. ArrayList<String> tmp = new ArrayList<>(path);
  87. Collections.reverse(tmp);
  88. result.add(tmp);
  89. } else {
  90. for (String f : father.get(state)) {
  91. genPath(father, start, f, path, result);
  92. }
  93. }
  94. path.remove(path.size() - 1);
  95. }
  96. }

图的广搜

前面的解法,在状态扩展的时候,每次都是从'a'到'z'全部枚举一遍,重复计算,比较浪费,其实当给定字典dict后,单词与单词之间的路径就固定下来了,本质上单词与单词之间构成了一个无向图。如果事先把这个图构建出来,那么状态扩展就会大大加快。

  1. import java.util.*;
  2. import java.util.function.Predicate;
  3. import java.util.function.Function;
  4. // Word Ladder II
  5. // 时间复杂度O(n),空间复杂度O(n)
  6. public class Solution {
  7. public List<List<String>> findLadders(String beginWord, String endWord,
  8. Set<String> wordList) {
  9. Queue<String> q = new LinkedList<>();
  10. HashMap<String, Integer> visited = new HashMap<>(); // 判重
  11. HashMap<String, ArrayList<String>> father = new HashMap<>(); // DAG
  12. // only used by stateExtend()
  13. final HashMap<String, HashSet<String>> g = buildGraph(wordList);
  14. final Function<String, Boolean> stateIsValid = (String s) ->
  15. wordList.contains(s) || s.equals(endWord);
  16. final Function<String, Boolean> stateIsTarget = (String s) ->
  17. s.equals(endWord);
  18. final Function<String, List<String> > stateExtend = (String s) -> {
  19. List<String> result = new ArrayList<>();
  20. final int newDepth = visited.get(s) + 1;
  21. HashSet<String> list = g.get(s);
  22. if (list == null) return result;
  23. for (String newState : list) {
  24. if (stateIsValid.apply(newState)) {
  25. if (visited.containsKey(newState)) {
  26. final int depth = visited.get(newState);
  27. if (depth < newDepth) {
  28. // do nothing
  29. } else if (depth == newDepth) {
  30. result.add(newState);
  31. } else {
  32. throw new IllegalStateException("not possible to get here");
  33. }
  34. } else {
  35. result.add(newState);
  36. }
  37. }
  38. }
  39. return result;
  40. };
  41. List<List<String>> result = new ArrayList<>();
  42. q.offer(beginWord);
  43. visited.put(beginWord, 0);
  44. while (!q.isEmpty()) {
  45. String state = q.poll();
  46. // 如果当前路径长度已经超过当前最短路径长度,
  47. // 可以中止对该路径的处理,因为我们要找的是最短路径
  48. if (!result.isEmpty() && (visited.get(state) + 1) > result.get(0).size()) break;
  49. if (stateIsTarget.apply(state)) {
  50. ArrayList<String> path = new ArrayList<>();
  51. genPath(father, beginWord, state, path, result);
  52. continue;
  53. }
  54. // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
  55. // 那么目标节点就会在q中出现两次,输出路径就会翻倍
  56. // visited.insert(state);
  57. // 扩展节点
  58. List<String> newStates = stateExtend.apply(state);
  59. for (String newState : newStates) {
  60. if (!visited.containsKey(newState)) {
  61. q.offer(newState);
  62. visited.put(newState, visited.get(state)+1);
  63. }
  64. ArrayList<String> parents = father.getOrDefault(newState, new ArrayList<>());
  65. parents.add(state);
  66. father.put(newState, parents);
  67. }
  68. }
  69. return result;
  70. }
  71. private static void genPath(HashMap<String, ArrayList<String>> father,
  72. String start, String state, List<String> path,
  73. List<List<String>> result) {
  74. path.add(state);
  75. if (state.equals(start)) {
  76. if (!result.isEmpty()) {
  77. if (path.size() < result.get(0).size()) {
  78. result.clear();
  79. } else if (path.size() == result.get(0).size()) {
  80. // do nothing
  81. } else {
  82. throw new IllegalStateException("not possible to get here");
  83. }
  84. }
  85. ArrayList<String> tmp = new ArrayList<>(path);
  86. Collections.reverse(tmp);
  87. result.add(tmp);
  88. } else {
  89. for (String f : father.get(state)) {
  90. genPath(father, start, f, path, result);
  91. }
  92. }
  93. path.remove(path.size() - 1);
  94. }
  95. private static HashMap<String, HashSet<String>> buildGraph(Set<String> dict) {
  96. HashMap<String, HashSet<String>> adjacency_list = new HashMap<>();
  97. for (String word: dict) {
  98. char[] array = word.toCharArray();
  99. for (int i = 0; i < array.length; ++i) {
  100. final char old = array[i];
  101. for (char c = 'a'; c <= 'z'; c++) {
  102. // 防止同字母替换
  103. if (c == array[i]) continue;
  104. array[i] = c;
  105. String newWord = new String(array);
  106. if (dict.contains(newWord)) {
  107. HashSet<String> list = adjacency_list.getOrDefault(
  108. word, new HashSet<>());
  109. list.add(newWord);
  110. adjacency_list.put(word, list);
  111. }
  112. array[i] = old; // 恢复该单词
  113. }
  114. }
  115. }
  116. return adjacency_list;
  117. }
  118. }

相关题目

原文: https://soulmachine.gitbooks.io/algorithm-essentials/content/java/bfs/word-ladder-ii.html