当前位置:   article > 正文

Leetcode-双向BFS-752. 打开转盘锁

Leetcode-双向BFS-752. 打开转盘锁

题目:752. 打开转盘锁

题解:

单向BFS:

  1. class Solution {
  2. public:
  3. int openLock(vector<string>& deadends, string target) {
  4. unordered_set<string> dead;
  5. for (auto& s:deadends) dead.insert(s);
  6. //unordered_set<string> dead(deadends.begin(), deadends.end());
  7. if(target == "0000") return 0;
  8. if(dead.count(target) || dead.count("0000")) {return -1;}
  9. int ops[2] = {-1,1};
  10. int res = 0;
  11. unordered_set<string> used;
  12. used.insert("0000");
  13. queue<string> q;
  14. q.emplace("0000");
  15. while(!q.empty())
  16. {
  17. int n = q.size();
  18. for (int i = 0; i < n; i++)
  19. {
  20. string tmp = q.front();
  21. q.pop();
  22. if(tmp == target){return res;}
  23. for(int pos = 0; pos < 4; pos++)
  24. {
  25. for(int step = 0; step < 2; step ++)
  26. {
  27. string next = tmp;
  28. // +10保证 0-1->9
  29. next[pos] = (next[pos]-'0'+ops[step]+10)%10+'0';
  30. //死亡组不加入队列
  31. if (dead.count(next) || used.count(next)) continue;
  32. q.emplace(next);
  33. used.insert(next);
  34. }
  35. }
  36. }
  37. //遍历完一层,操作数+1
  38. res++;
  39. }
  40. return -1;
  41. }
  42. };

双向BFS(cr:褴褛青衫)

为啥需要双向BFS?
众所周知朴素BFS搜索时,每一层的搜索节点数量会爆炸级增加。假设每一次搜索都有 nn 个新的状态,并假设从起点到目标路径长为 mm,那就要搜索: 个状态,状态数就是 n^{m+1} 数量级的!

以本题为例,每一次搜索最多可以有8个新的状态。这要是bfs搜索层数太深了的话,贫穷的复杂度小家哪里遭受得住这压迫啊!

所以「双向BFS」闪亮登场!

双向BFS是啥?
双向BFS就是同时从起点和终点两个方向开始搜索,一旦搜索到另一个方向已经搜索过的位置(或者说出现某个状态被两个方向均访问到了),就意味着找到了一条联通起点和终点的最短路径。

双向BFS好处具体是啥?
双向BFS同时从起点和终点两个方向进行搜索,呈「两面包夹芝士」向最短路中间的某一点汇集,在路径中点相遇。则双向BFS的状态数是 数量级的。
芜湖~就这样美滋滋的优化了复杂度!

双向BFS解题思路
双向BFS其实并不麻烦!

朴素BFS就是单向BFS,单向BFS需要一个队列来进行搜索,需要一个哈希表来解决重复搜索和记录搜索层数。

那双向BFS就需要「两个队列」来进行双向搜索!需要「两个哈希表」来分别解决各自方向的重复搜索和记录搜索层数(也是字符串转换次数)!

既然两个队列,那我每次该让哪个方向进行搜索啊?
为了尽量让两个方向均匀搜索,即尽量保持两个方向搜索前进进度差不多,所以每次进行BFS搜索时,选择容量较少的队列进行BFS搜索,扩充该队列。(容量较少的队列之所以容量少就是因为他曾经拿去bfs搜索的节点太废物了,另外一个方向的队列每次搜索可以带回来好几个新状态,而他这队列每次搜索一不小心就颗粒无收还推出去一个...所以我们要安慰下容量较少的队列让他多搜几次,免得这方向上止步不前)

当然,你选择两个方向一边搜索一次这样交替搜索也是可以的,只是会稍微慢一点点点点。

BFS搜索什么时候结束?
结束分为:「双方向成功汇集」和「双方向无法汇集」两种。

如果在某一个方向搜索过程中「搜索到另一个方向搜索过的节点」,说明找到了最短路径,搜索结束。
如果「某一个队列空了」,但搜索还没结束,那说明从该方向搜穿了,没路可走了,都搜不到另一个方向搜索过的节点,即两个方向不可能有所汇集,也就是说两个节点之间无最短路,此时搜索也宣告结束。无需把两个方向都搜到底。

image.png

「双向 BFS」的基本实现思路如下:

创建「两个队列」分别用于两个方向的搜索;
创建「两个哈希表」用于「解决相同节点重复搜索」和「记录转换次数」;
为了尽可能让两个搜索方向“平均”,每次从队列中取值进行扩展时,先判断哪个队列容量较少;
如果在搜索过程中「搜索到对方搜索过的节点」,说明找到了最短路径。
「双向 BFS」基本思路对应的伪代码大致如下:

  1. d1、d2 为两个方向的队列
  2. m1、m2 为两个方向的哈希表,记录每个节点距离起点的
  3. // 只有两个队列都不空,才有必要继续往下搜索
  4. // 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
  5. while(!d1.isEmpty() && !d2.isEmpty()) {
  6. if (d1.size() < d2.size()) {
  7. update(d1, m1, m2);
  8. } else {
  9. update(d2, m2, m1);
  10. }
  11. }
  12. // update 为从队列 d 中取出一个元素进行「一次完整扩展」的逻辑
  13. void update(Deque d, Map cur, Map other) {}

具体过程见代码和相关注释~

  1. /**
  2. * @dead: deadends字符串的哈希集合
  3. * @q1:从起点开始搜索的队列
  4. * @q2:从终点开始搜索的队列
  5. * @mp1:起点方向开始搜索的哈希表,key---搜索过的字符串, value---该字符串所处层数(即转换次数)
  6. * @mp2:终点方向开始搜索的哈希表,key/value同上
  7. **/
  8. class Solution {
  9. public:
  10. int openLock(vector<string>& deadends, string target) {
  11. unordered_set<string> dead(deadends.begin(), deadends.end());
  12. if (dead.count("0000")) {
  13. return -1;
  14. }
  15. if (target == "0000") {
  16. return 0;
  17. }
  18. queue<string> q1, q2;
  19. q1.emplace("0000");
  20. q2.emplace(target);
  21. unordered_map<string, int> mp1, mp2;
  22. mp1["0000"] = 0;
  23. mp2[target] = 0;
  24. int res = -1;
  25. //如果其中一个队列空了,搜索结束
  26. while (!q1.empty() && !q2.empty()) {
  27. //选择一个容量更少的队列进行BFS搜索
  28. if (q1.size() <= q2.size()) {
  29. res = bfs(q1, mp1, mp2, dead);
  30. } else {
  31. res = bfs(q2, mp2, mp1, dead);
  32. }
  33. if (res != -1) return res;
  34. }
  35. return -1;
  36. }
  37. //字符正向转函数
  38. char next_c(char c) {
  39. return c == '9' ? '0' : c + 1;
  40. }
  41. //字符反向转函数
  42. char prev_c(char c) {
  43. return c == '0' ? '9' : c - 1;
  44. }
  45. //q为本方向的搜索队列,mine为本方向的哈希表,other为另外一方向的哈希表
  46. int bfs(queue<string>& q, unordered_map<string, int>& mine, unordered_map<string, int>& other, unordered_set<string>& dead) {
  47. string cur = q.front();
  48. int step = mine[cur];
  49. q.pop();
  50. for (int i = 0; i < 4; ++i) {
  51. //j为-1时,字符反向转(即减1)
  52. //j为1时,字符正向转(即加1)
  53. for (int j = -1; j <= 1; j += 2) {
  54. //转换字符
  55. cur[i] = (j == -1) ? prev_c(cur[i]) : next_c(cur[i]);
  56. //如果字符转换后的字符串不是dead字符串,也没有搜索过,则对该字符串进行更新。
  57. if (!mine.count(cur) && !dead.count(cur)) {
  58. //如果该字符串在另一个方向已经找到过,说明两个方向在本字符串处汇集,找到了最短路
  59. //否则加入队列
  60. if (other.count(cur)) {
  61. return step + 1 + other[cur];
  62. } else {
  63. mine[cur] = step + 1;
  64. q.emplace(cur);
  65. }
  66. }
  67. //恢复前面字符的转换,保证本次bfs中下一次循环时原本字符串cur不变
  68. cur[i] = (j == -1) ? next_c(cur[i]) : prev_c(cur[i]);
  69. }
  70. }
  71. return -1;
  72. }
  73. };
  1. class Solution {
  2. public:
  3. string target;
  4. unordered_set<string> dead;
  5. int openLock(vector<string>& deadends, string tar)
  6. {
  7. target = tar;
  8. for (auto& s:deadends) dead.insert(s);
  9. //unordered_set<string> dead(deadends.begin(), deadends.end());
  10. if(target == "0000") return 0;
  11. if(dead.count(target) || dead.count("0000")) {return -1;}
  12. int res = 0;
  13. /*
  14. * used1 和 used2分别记录两个方向出现的单词是经过多少次转换而来
  15. */
  16. unordered_map<string,int> used1,used2;
  17. used1["0000"]=0; used2[target]=0;
  18. queue<string> q1,q2;
  19. q1.emplace("0000");
  20. q2.emplace(target);
  21. /*
  22. * 只有两个队列都不空,才有必要继续往下搜索
  23. * 如果其中一个队列空了,说明从某个方向搜到底都搜不到该方向的目标节点
  24. * e.g.
  25. * 例如,如果 d1 为空了,说明从 beginWord 搜索到底都搜索不到 endWord,反向搜索也没必要进行了
  26. */
  27. while(!q1.empty() && !q2.empty())
  28. {
  29. if(q1.size() < q2.size())
  30. {
  31. res = bfs(q1,used1,used2);
  32. }
  33. else
  34. {
  35. res = bfs(q2,used2,used1);
  36. }
  37. if(res != -1){return res;}
  38. }
  39. return -1;
  40. }
  41. int bfs(queue<string> &q, unordered_map<string,int> & mine, unordered_map<string,int> & other)
  42. {
  43. string tmp = q.front();
  44. q.pop();
  45. int nowstep = mine[tmp];
  46. for(int pos = 0; pos < 4; pos++)
  47. {
  48. for(int step = -1; step <= 1; step +=2)
  49. {
  50. string next = tmp;
  51. // +10保证 0-1->9
  52. next[pos] = (next[pos]-'0'+step+10)%10+'0';
  53. //死亡组不加入队列
  54. if (dead.count(next) || mine.count(next)) continue;
  55. else if (other.count(next)){ return other[next]+1+nowstep;}
  56. else
  57. {
  58. q.emplace(next);
  59. mine[next] = nowstep+1;
  60. }
  61. }
  62. }
  63. return -1;
  64. }
  65. };

启发式搜索

看这里吧,之后再填坑

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/Gausst松鼠会/article/detail/718655
推荐阅读
相关标签
  

闽ICP备14008679号