Leetcode 126 单词接龙Ⅱ 跑某个测试用例时 超出时间限制

来源:6-5 BFS和图的最短路径 Perfect Squares

慕村510262

2022-08-02

图片描述
花了好几个小时来做这题,我尝试使用了三种方法来答这题,跑这测试用例时都超时了:
① 单向 BFS
② BFS(计算最短转换序列中的单词数目)+ DFS(回溯构建这样长度的所有路径)
③ 双向 BFS
最后没点子了,就拷贝了老师代码仓库中的代码试一下,也是倒在这个测试用例
这测试用例给我搞糊涂了,不知有啥解法可以通过这个测试用例

附上我第三种解法的代码:

class Solution {
public:
    vector<vector<string>> findLadders(string beginWord, string endWord, vector<string>& wordList) {
        unordered_set<string> dict(wordList.cbegin(), wordList.cend());
        if (dict.find(endWord) == dict.cend()) return {};

        unordered_multimap<string, vector<string>> head, tail, *phead, *ptail;
        head.insert({ beginWord, initializer_list<string>{ beginWord } });
        tail.insert({ endWord, initializer_list<string>{ endWord } });
        dict.erase(beginWord);
        dict.erase(endWord);

        vector<vector<string>> ans;
        while (!head.empty() && !tail.empty()) {
            if (head.size() < tail.size()) {
                phead = &head;
                ptail = &tail;
            } else {
                phead = &tail;
                ptail = &head;
            }

            unordered_set<string> del_word_set;
            unordered_multimap<string, vector<string>> temp;
            for (auto &pair: *phead) {
                string word = pair.first;
                for (int i = 0; i < word.size(); i++) {
                    char c = word[i];
                    for (int j = 'a'; j <= 'z'; j++) {
                        word[i] = j;

                        if (ptail->find(word) != ptail->end()) {
                            auto &path1 = pair.second;
                            
                            auto range = ptail->equal_range(word);
                            for (auto iter = range.first; iter != range.second; ++iter) {
                                auto &path2 = iter->second;
                                if (phead == &head) {
                                    ans.push_back(path1);
                                    ans.back().insert(ans.back().end(), path2.crbegin(), path2.crend());
                                } else {
                                    ans.push_back(path2);
                                    ans.back().insert(ans.back().end(), path1.crbegin(), path1.crend());
                                }
                            }
                        } else {
                            if (dict.count(word) == 1) {
                                del_word_set.insert(word);
                                vector<string> new_path = pair.second;
                                new_path.push_back(word);
                                temp.insert({ word, new_path });
                            }
                        }
                    }
                    word[i] = c;
                }
            }

            if (!ans.empty()) {
                break;
            }

            for (auto &del_word: del_word_set) {
                dict.erase(del_word);
            }
            phead->swap(temp);
        }
        return ans;
    }
};
写回答

1回答

liuyubobobo

2022-08-10

我测试了一遍,现在直接使用 bfs + 正向回溯 确实是超时的。力扣添加了测试用例。不过这也 make sense,毕竟是一个 hard 的问题,如果用最直接的思路就搞定了就太简单了:)


==========


要想“优化”,关键肯定不是 bfs。因为这个问题最多 500 个节点,边最多 500 * 500 也就是 25w 这个量级。bfs 是 O(V+E) 的,处理 25w 这个量级的数据肯定不会超时。所以把 bfs 改成双向 bfs 意义不大。(双向 bfs 减小一半的搜索量)。


这个问题超时的关键一定在回溯所有解的地方。


==========


在描述我的解决办法之前,我想先强调一下,这个问题其实出的并不好。因为一张图上不同最短路径的数量,可以是指数级的。


比如:

   2 
 /   \  
1     4
 \   /
   3

这张图中,从 1 到 4 的最短路径的条数有 2 条;


   2      5
 /   \  /   \
1     4       7
 \   /  \   /
   3      6

这张图中,从 1 到 7 的最短路径的条数有 4 条;


   2      5       8
 /   \  /   \   /   \
1     4       7      10
 \   /  \   /   \   /
   3      6       9

这张图中,从 1 到 10 的最短路径的条数有 8 条;


可以看到,我按照这个规律,每在途中添加 3 个节点,从 1 到最后一个节点的最短路径的条数就翻一倍,是指数级上升的。也正是因为这个原因,大多数问题只会问最短路径的长度,而不会问具体的最短路径有哪些。因为理论上,枚举一张图的所有最短路径是指数级的。


而这个问题就再让我们做这个枚举,所以他的理论复杂度是指数级的。我只能说它确保了测试用例中不存在这个指数级结果的可能。因此,是测试数据相关的(而非问题相关的),这是我说这个问题不好的原因。


==========


最后,如何不超时?非常简单。在回溯解的时候,从结尾出发,反向走到开头。因为从开头出发,可能会走到很多走不到结尾的“路”。但是从结尾出发,保证了走的每一条路,都是能从开头走过来的,节省了很多多余的搜索。


具体表现在代码上(我已经对 github 上的代码做了更新):https://github.com/liuyubobobo/Play-Leetcode/blob/master/0001-0500/0126-Word-Ladder-II/cpp-0126/main.cpp

48 行,初始路径的起点是 wordList[end]

49 行回溯函数的调用,初始 cur 传入的是 end;end 是 begin。也就是在找从 end 到 begin 的路径。(反向的)

88 行找到一条可以“走”的边条件是:dis[i] == dis[cur] - 1(而非 +1)。因为是反向的。

83 行找到一个路径以后,需要对这个路径 reverse,存到最后要返回的解中。


继续加油!:)


0
0

玩转算法面试-- Leetcode真题分门别类讲解

课程配套大量BAT面试真题,高频算法题解析,强化训练

7410 学习 · 1150 问题

查看课程