双向广度优先搜索(Bi-Directional Breadth First Search)
双向广度优先搜索是对广度优先搜索的优化,但是有一个使用条件:搜索路径可逆。
搜索原理
双向广搜是同时从开始状态和目标状态展开搜索的,这样就会产生两棵搜索状态树。我们想象一下,让起始于开始状态的树从上往下生长,再让起始于目标状态的树从下往上生长,同时在它们的生长空间中遍布着一个一个的状态结点,等待着这两棵树延伸去触及。
由于任一个状态都是唯一存在的,当两棵搜索树都触及到了某个状态时,这两棵树就出现了交叉,搜索即告结束。
让两棵树从发生交叉的状态结点各自原路返回构建路径,然后算法把两条路径拼接起来,即为结果路径。
可用条件
对于拼图游戏来说,已经知道了开始状态(某个乱序的状态)和目标状态(图片复原时的状态),而这两个状态其实是可以互换的,完全可以从目标复原状态开始搜索,反向推进,直到找出拼图开始时的乱序状态。所以,我们的拼图游戏是路径可逆的,适合双向广搜。
单线程下的双向广搜
要实现双向广搜,并不需要真的用两条线程分别从开始状态和目标状态对向展开搜索,在单线程下也完全可以实现,实现的关键是于让两个开放队列交替出列元素。
在每一次循环中,比较两个开放队列的长度,每一次都选择最短的队列进行搜索,优先让较小的树生长出子结点。这样做能够使两个开放队列维持大致相同的长度,同步增长,达到均衡两棵搜索树的效果。
- (NSMutableArray *)search {
if (!self.startStatus || !self.targetStatus || !self.equalComparator) {
return nil;
}
NSMutableArray *path = [NSMutableArray array];
// 关闭堆,存放已搜索过的状态
NSMutableDictionary *positiveClose = [NSMutableDictionary dictionary];
NSMutableDictionary *negativeClose = [NSMutableDictionary dictionary];
// 开放队列,存放由已搜索过的状态所扩展出来的未搜索状态
NSMutableArray *positiveOpen = [NSMutableArray array];
NSMutableArray *negativeOpen = [NSMutableArray array];
[positiveOpen addObject:self.startStatus];
[negativeOpen addObject:self.targetStatus];
while (positiveOpen.count > 0 || negativeOpen.count > 0) {
// 较短的那个扩展队列
NSMutableArray *open;
// 短队列对应的关闭堆
NSMutableDictionary *close;
// 另一个关闭堆
NSMutableDictionary *otherClose;
// 找出短队列
if (positiveOpen.count && (positiveOpen.count < negativeOpen.count)) {
open = positiveOpen;
close = positiveClose;
otherClose = negativeClose;
}
else {
open = negativeOpen;
close = negativeClose;
otherClose = positiveClose;
}
// 出列
id status = [open firstObject];
[open removeObjectAtIndex:0];
// 排除已经搜索过的状态
NSString *statusIdentifier = [status statusIdentifier];
if (close[statusIdentifier]) {
continue;
}
close[statusIdentifier] = status;
// 如果本状态同时存在于另一个已检查堆,则说明正反两棵搜索树出现交叉,搜索结束
if (otherClose[statusIdentifier]) {
NSMutableArray *positivePath = [self constructPathWithStatus:positiveClose[statusIdentifier] isLast:YES];
NSMutableArray *negativePath = [self constructPathWithStatus:negativeClose[statusIdentifier] isLast:NO];
// 拼接正反两条路径
[positivePath addObjectsFromArray:negativePath];
path = positivePath;
break;
}
// 否则,扩展出子状态
[open addObjectsFromArray:[status childStatus]];
}
NSLog(@"总搜索数量: %@", @(positiveClose.count + negativeClose.count - 1));
return path;
}