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. class Solution {
  4. public:
  5. vector<vector<string> > findLadders(const string& start,
  6. const string& end, const unordered_set<string> &dict) {
  7. queue<string> q;
  8. unordered_map<string, int> visited; // 判重
  9. unordered_map<string, vector<string> > father; // DAG
  10. auto state_is_valid = [&](const string& s) {
  11. return dict.find(s) != dict.end() || s == end;
  12. };
  13. auto state_is_target = [&](const string &s) {return s == end; };
  14. auto state_extend = [&](const string &s) {
  15. unordered_set<string> result;
  16. const int new_depth = visited[s] + 1;
  17. for (size_t i = 0; i < s.size(); ++i) {
  18. string new_state = s;
  19. for (char c = 'a'; c <= 'z'; c++) {
  20. // 防止同字母替换
  21. if (c == new_state[i]) continue;
  22. swap(c, new_state[i]);
  23. if (state_is_valid(new_state)) {
  24. auto visited_iter = visited.find(new_state);
  25. if (visited_iter != visited.end()) {
  26. const int depth = visited_iter->second;
  27. if (depth < new_depth) {
  28. // do nothing
  29. }
  30. else if (depth == new_depth) {
  31. result.insert(new_state);
  32. }
  33. else { // not possible
  34. throw std::logic_error("not possible to get here");
  35. }
  36. }
  37. else {
  38. result.insert(new_state);
  39. }
  40. }
  41. swap(c, new_state[i]); // 恢复该单词
  42. }
  43. }
  44. return result;
  45. };
  46. vector<vector<string>> result;
  47. q.push(start);
  48. visited[start] = 0;
  49. while (!q.empty()) {
  50. // 千万不能用 const auto&,pop() 会删除元素,
  51. // 引用就变成了悬空引用
  52. const auto state = q.front();
  53. q.pop();
  54. // 如果当前路径长度已经超过当前最短路径长度,
  55. // 可以中止对该路径的处理,因为我们要找的是最短路径
  56. if (!result.empty() && visited[state] + 1 > result[0].size()) break;
  57. if (state_is_target(state)) {
  58. vector<string> path;
  59. gen_path(father, start, state, path, result);
  60. continue;
  61. }
  62. // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
  63. // 那么目标节点就会在q中出现两次,输出路径就会翻倍
  64. // visited.insert(state);
  65. // 扩展节点
  66. const auto& new_states = state_extend(state);
  67. for (const auto& new_state : new_states) {
  68. if (visited.find(new_state) == visited.end()) {
  69. q.push(new_state);
  70. visited[new_state] = visited[state] + 1;
  71. }
  72. father[new_state].push_back(state);
  73. }
  74. }
  75. return result;
  76. }
  77. private:
  78. void gen_path(unordered_map<string, vector<string> > &father,
  79. const string &start, const string &state, vector<string> &path,
  80. vector<vector<string> > &result) {
  81. path.push_back(state);
  82. if (state == start) {
  83. if (!result.empty()) {
  84. if (path.size() < result[0].size()) {
  85. result.clear();
  86. }
  87. else if (path.size() == result[0].size()) {
  88. // do nothing
  89. }
  90. else { // not possible
  91. throw std::logic_error("not possible to get here ");
  92. }
  93. }
  94. result.push_back(path);
  95. reverse(result.back().begin(), result.back().end());
  96. }
  97. else {
  98. for (const auto& f : father[state]) {
  99. gen_path(father, start, f, path, result);
  100. }
  101. }
  102. path.pop_back();
  103. }
  104. };

双队列

  1. // Word Ladder II
  2. // 时间复杂度O(n),空间复杂度O(n)
  3. class Solution {
  4. public:
  5. vector<vector<string> > findLadders(const string& start,
  6. const string& end, const unordered_set<string> &dict) {
  7. // 当前层,下一层,用unordered_set是为了去重,例如两个父节点指向
  8. // 同一个子节点,如果用vector, 子节点就会在next里出现两次,其实此
  9. // 时 father 已经记录了两个父节点,next里重复出现两次是没必要的
  10. unordered_set<string> current, next;
  11. unordered_set<string> visited; // 判重
  12. unordered_map<string, vector<string> > father; // DAG
  13. int level = -1; // 层次
  14. auto state_is_valid = [&](const string& s) {
  15. return dict.find(s) != dict.end() || s == end;
  16. };
  17. auto state_is_target = [&](const string &s) {return s == end;};
  18. auto state_extend = [&](const string &s) {
  19. unordered_set<string> result;
  20. for (size_t i = 0; i < s.size(); ++i) {
  21. string new_word(s);
  22. for (char c = 'a'; c <= 'z'; c++) {
  23. // 防止同字母替换
  24. if (c == new_word[i]) continue;
  25. swap(c, new_word[i]);
  26. if (state_is_valid(new_word) &&
  27. visited.find(new_word) == visited.end()) {
  28. result.insert(new_word);
  29. }
  30. swap(c, new_word[i]); // 恢复该单词
  31. }
  32. }
  33. return result;
  34. };
  35. vector<vector<string> > result;
  36. current.insert(start);
  37. while (!current.empty()) {
  38. ++ level;
  39. // 如果当前路径长度已经超过当前最短路径长度,可以中止对该路径的
  40. // 处理,因为我们要找的是最短路径
  41. if (!result.empty() && level+1 > result[0].size()) break;
  42. // 1. 延迟加入visited, 这样才能允许两个父节点指向同一个子节点
  43. // 2. 一股脑current 全部加入visited, 是防止本层前一个节点扩展
  44. // 节点时,指向了本层后面尚未处理的节点,这条路径必然不是最短的
  45. for (const auto& state : current)
  46. visited.insert(state);
  47. for (const auto& state : current) {
  48. if (state_is_target(state)) {
  49. vector<string> path;
  50. gen_path(father, path, start, state, result);
  51. continue;
  52. }
  53. const auto new_states = state_extend(state);
  54. for (const auto& new_state : new_states) {
  55. next.insert(new_state);
  56. father[new_state].push_back(state);
  57. }
  58. }
  59. current.clear();
  60. swap(current, next);
  61. }
  62. return result;
  63. }
  64. private:
  65. void gen_path(unordered_map<string, vector<string> > &father,
  66. vector<string> &path, const string &start, const string &word,
  67. vector<vector<string> > &result) {
  68. path.push_back(word);
  69. if (word == start) {
  70. if (!result.empty()) {
  71. if (path.size() < result[0].size()) {
  72. result.clear();
  73. result.push_back(path);
  74. } else if(path.size() == result[0].size()) {
  75. result.push_back(path);
  76. } else {
  77. // not possible
  78. throw std::logic_error("not possible to get here");
  79. }
  80. } else {
  81. result.push_back(path);
  82. }
  83. reverse(result.back().begin(), result.back().end());
  84. } else {
  85. for (const auto& f : father[word]) {
  86. gen_path(father, path, start, f, result);
  87. }
  88. }
  89. path.pop_back();
  90. }
  91. };

图的广搜

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

  1. // Word Ladder II
  2. // 时间复杂度O(n),空间复杂度O(n)
  3. class Solution {
  4. public:
  5. vector<vector<string> > findLadders(const string& start,
  6. const string& end, const unordered_set<string> &dict) {
  7. queue<string> q;
  8. unordered_map<string, int> visited; // 判重
  9. unordered_map<string, vector<string> > father; // DAG
  10. // only used by state_extend()
  11. const unordered_map<string, unordered_set<string> >& g = build_graph(dict);
  12. auto state_is_valid = [&](const string& s) {
  13. return dict.find(s) != dict.end() || s == end;
  14. };
  15. auto state_is_target = [&](const string &s) {return s == end; };
  16. auto state_extend = [&](const string &s) {
  17. vector<string> result;
  18. const int new_depth = visited[s] + 1;
  19. auto iter = g.find(s);
  20. if (iter == g.end()) return result;
  21. const auto& list = iter->second;
  22. for (const auto& new_state : list) {
  23. if (state_is_valid(new_state)) {
  24. auto visited_iter = visited.find(new_state);
  25. if (visited_iter != visited.end()) {
  26. const int depth = visited_iter->second;
  27. if (depth < new_depth) {
  28. // do nothing
  29. }
  30. else if (depth == new_depth) {
  31. result.push_back(new_state);
  32. } else { // not possible
  33. throw std::logic_error("not possible to get here");
  34. }
  35. }
  36. else {
  37. result.push_back(new_state);
  38. }
  39. }
  40. }
  41. return result;
  42. };
  43. vector<vector<string>> result;
  44. q.push(start);
  45. visited[start] = 0;
  46. while (!q.empty()) {
  47. // 千万不能用 const auto&,pop() 会删除元素,
  48. // 引用就变成了悬空引用
  49. const auto state = q.front();
  50. q.pop();
  51. // 如果当前路径长度已经超过当前最短路径长度,
  52. // 可以中止对该路径的处理,因为我们要找的是最短路径
  53. if (!result.empty() && visited[state] + 1 > result[0].size()) break;
  54. if (state_is_target(state)) {
  55. vector<string> path;
  56. gen_path(father, start, state, path, result);
  57. continue;
  58. }
  59. // 必须挪到下面,比如同一层A和B两个节点均指向了目标节点,
  60. // 那么目标节点就会在q中出现两次,输出路径就会翻倍
  61. // visited.insert(state);
  62. // 扩展节点
  63. const auto& new_states = state_extend(state);
  64. for (const auto& new_state : new_states) {
  65. if (visited.find(new_state) == visited.end()) {
  66. q.push(new_state);
  67. visited[new_state] = visited[state] + 1;
  68. }
  69. father[new_state].push_back(state);
  70. }
  71. }
  72. return result;
  73. }
  74. private:
  75. void gen_path(unordered_map<string, vector<string> > &father,
  76. const string &start, const string &state, vector<string> &path,
  77. vector<vector<string> > &result) {
  78. path.push_back(state);
  79. if (state == start) {
  80. if (!result.empty()) {
  81. if (path.size() < result[0].size()) {
  82. result.clear();
  83. }
  84. else if (path.size() == result[0].size()) {
  85. // do nothing
  86. }
  87. else { // not possible
  88. throw std::logic_error("not possible to get here ");
  89. }
  90. }
  91. result.push_back(path);
  92. reverse(result.back().begin(), result.back().end());
  93. }
  94. else {
  95. for (const auto& f : father[state]) {
  96. gen_path(father, start, f, path, result);
  97. }
  98. }
  99. path.pop_back();
  100. }
  101. unordered_map<string, unordered_set<string> > build_graph(
  102. const unordered_set<string>& dict) {
  103. unordered_map<string, unordered_set<string> > adjacency_list;
  104. for (const auto& word : dict) {
  105. for (size_t i = 0; i < word.size(); ++i) {
  106. string new_word(word);
  107. for (char c = 'a'; c <= 'z'; c++) {
  108. // 防止同字母替换
  109. if (c == new_word[i]) continue;
  110. swap(c, new_word[i]);
  111. if ((dict.find(new_word) != dict.end())) {
  112. auto iter = adjacency_list.find(word);
  113. if (iter != adjacency_list.end()) {
  114. iter->second.insert(new_word);
  115. }
  116. else {
  117. adjacency_list.insert(pair<string,
  118. unordered_set<string >> (word, unordered_set<string>()));
  119. adjacency_list[word].insert(new_word);
  120. }
  121. }
  122. swap(c, new_word[i]); // 恢复该单词
  123. }
  124. }
  125. }
  126. return adjacency_list;
  127. }
  128. };

相关题目

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