单向BFS只从起点一端开始搜索,双向BFS则是从起点和终点两边扩展节点,当节点发生重合时即找到最优解。
假设起点到终点深度为d,每个节点平均有n个分支,那么单向BFS需要扩展的节点个数为。而从起点终点同时扩展,则只需。
实现方法为:维护两个队列,分别保存从起点和终点扩展到的下一层,这样保证了两个队列中的节点处于相同的深度(即:距离起点或者终点深度相同)。则当拓展到时一定发生重合,得到最优解。
LeetcodeWord Ladder 代码如下:
class Solution {
public:
int BFS(bool dir, queue<string>& qu, unordered_set<string>& wordList){
queue<string> next;
while(!qu.empty()){
string cur = qu.front();
qu.pop();
int step = dis[dir][cur];
++step;
for(int i = 0; i < len; ++i){
char c = cur[i];
for(char j = 'a'; j <= 'z'; ++j){
if(j == c)
continue;
cur[i] = j;
if(wordList.find(cur) != wordList.end() && dis[dir][cur] == INT_MAX){
if(dis[!dir][cur] != INT_MAX)
return dis[!dir][cur] + step + 1;
next.push(cur);
dis[dir][cur] = step;
}
}
cur[i] = c;
}
}
qu = next;
return 0;
}
int ladderLength(string beginWord, string endWord, unordered_set<string>& wordList) {
len = beginWord.size();
queue<string> qu1, qu2;
for(unordered_set<string>::iterator it = wordList.begin(); it != wordList.end(); ++it){
dis[0].insert(make_pair(*it, INT_MAX));
dis[1].insert(make_pair(*it, INT_MAX));
}
dis[0][beginWord] = 0, dis[1][endWord] = 0;
qu1.push(beginWord), qu2.push(endWord);
bool dir = false;
while(!qu1.empty() && !qu2.empty()){
int t = BFS(dir, qu1, wordList);
if(t)
return t;
t = BFS(!dir, qu2, wordList);
if(t)
return t;
}
return 0;
}
private:
unordered_map<string, int> dis[2];
int len;
};
与单向bfs运行时间1200ms相比,以上朴素的双向BFS时间为212ms,差别还是很明显的。
当然,针对扩展方式还可以进行优化——交替逐层扩展。由于两端扩展节点个数可能差别很大,所以每次选择节点个数少的队列进行扩展。return的条件仍为有节点重叠。
正确性证明如下(证明不会因为提前break而错过最优解):
假设当前得到最优解为X,不失一般性,假设X现在是从起点搜索到的,则X已经被终点搜索过了,所以才终止搜索。使用反证法证明,假设存在最优解Y。
1. 若X和Y在同一层,则当且仅当从终点搜索时Y位于X前面层,Y才为最优解。设从起点搜索Y的前一个节点为z,由于终点方向从Y所在层,扩展到X所在层,这个过程是连续,所以在这个过程中会首先扩展到z,则z被当做最优解,程序return。如下图所示:
2. Y位于X的后面,令X和Y相隔i层,则Y为最优解的充分条件为:从终点方向,Y与X的层数之差大于i,记为j。令起点方向上,z是y的前j个节点。那么从终点方向搜索时,z应该不晚于x被搜到。因此也不会错过最优解。如下图所示:
所以使用交替逐层能保证算法正确性。代码如下:
class Solution {
public:
int ladderLength(string beginWord, string endWord, unordered_set<string>& wordList) {
queue<string> qu1, qu2;
qu1.push(beginWord);
qu2.push(endWord);
int len = beginWord.size();
unordered_map<string, int> dis[2];
for(unordered_set<string>::iterator it = wordList.begin(); it != wordList.end(); ++it){
dis[0].insert(make_pair(*it, INT_MAX));
dis[1].insert(make_pair(*it, INT_MAX));
}
dis[0][beginWord] = 0, dis[1][endWord] = 0;
bool dir = false;
while(!qu1.empty()){
queue<string> layer;
while(!qu1.empty()){
string cur = qu1.front();
qu1.pop();
int step = dis[dir][cur];
++step;
for(int i = 0; i < len; ++i){
char c = cur[i];
for(char j = 'a'; j <= 'z'; ++j){
if(j == c)
continue;
cur[i] = j;
if(wordList.find(cur) != wordList.end() && dis[dir][cur] == INT_MAX){
if(dis[!dir][cur] != INT_MAX)
return step + dis[!dir][cur] + 1;
layer.push(cur);
dis[dir][cur] = step;
}
}
cur[i] = c;
}
}
if(layer.size() > qu2.size()){
qu1 = qu2;
qu2 = layer;
dir = !dir;
}
else
qu1 = layer;
}
return 0;
}
};
时间比较如下:单向BFS1200ms,朴素双向BFS212ms,交替逐层优化为64ms。