Tìm kiếm hai chiều
本页面将简要介绍两种双向搜索算法:「双向同时搜索」和「Meet in the middle」.
双向同时搜索
定义
双向同时搜索的基本思路是从状态图上的起点和终点同时开始进行 广搜 或 深搜 .
如果发现搜索的两端相遇了,那么可以认为是获得了可行解.
过程
双向广搜的步骤:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 将开始结点和目标结点加入队列 q
标记开始结点为 1
标记目标结点为 2
while (队列 q 不为空)
{
从 q.front() 扩展出新的 s 个结点
如果 新扩展出的结点已经被其他数字标记过
那么 表示搜索的两端碰撞
那么 循环结束
如果 新的 s 个结点是从开始结点扩展来的
那么 将这个 s 个结点标记为 1 并且入队 q
如果 新的 s 个结点是从目标结点扩展来的
那么 将这个 s 个结点标记为 2 并且入队 q
}
例题
例题 八数码难题
在 \(3\times 3\) 的棋盘上,摆有八个棋子,每个棋子上标有 \(1\) 至 \(8\) 的某一数字.棋盘中留有一个空格,空格用 \(0\) 来表示.空格周围的棋子可以移到空格中.要求解的问题是:给出一种初始布局(初始状态)和目标布局(为了使题目简单,设目标状态为 \(123804765\) ),找到一种最少步骤的移动方法,实现从初始布局到目标布局的转变.
解题思路
很好想出暴力 bfs.本题使用暴力 bfs 也不会超时.但是这里把它作为双向同时搜索的例题.我们可以使用两个 bfs,一个从起点状态开始正着搜,一个从终点状态开始反着搜,交替使用两个 bfs,搜索树的大小会大大减小.当其中一个 bfs 搜出另一个 bfs 已经搜出的状态,即可得到答案.
参考代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82 #include <iostream>
#include <map>
#include <queue>
#include <string>
using namespace std ;
struct State {
int A [ 3 ][ 3 ];
State () = default ;
State ( string s ) {
for ( int i = 0 ; i < 3 ; i ++ ) {
for ( int j = 0 ; j < 3 ; j ++ ) {
A [ i ][ j ] = s [ i * 3 + j ] - '0' ;
}
}
}
friend bool operator < ( const State & a , const State & b ) {
for ( int i = 0 ; i < 3 ; i ++ ) {
for ( int j = 0 ; j < 3 ; j ++ ) {
if ( a . A [ i ][ j ] != b . A [ i ][ j ]) {
return a . A [ i ][ j ] < b . A [ i ][ j ];
}
}
}
return false ;
}
};
int dir [ 4 ][ 2 ] = {{ 1 , 0 }, { -1 , 0 }, { 0 , 1 }, { 0 , -1 }};
void bfs ( queue < State > & q , map < State , int > & m1 , map < State , int > & m2 ) {
auto u = q . front ();
q . pop ();
int xx , yy ;
for ( int i = 0 ; i < 3 ; i ++ ) {
for ( int j = 0 ; j < 3 ; j ++ ) {
if ( u . A [ i ][ j ] == 0 ) {
xx = i ;
yy = j ;
}
}
}
for ( int i = 0 ; i < 4 ; i ++ ) {
int tx = dir [ i ][ 0 ] + xx , ty = dir [ i ][ 1 ] + yy ;
if ( tx >= 0 && tx < 3 && ty >= 0 && ty < 3 ) {
auto v = u ;
swap ( v . A [ xx ][ yy ], v . A [ tx ][ ty ]);
if ( m2 . count ( v )) {
cout << m1 [ u ] + m2 [ v ] << endl ;
exit ( 0 );
}
if ( ! m1 . count ( v )) {
m1 [ v ] = m1 [ u ] + 1 ;
q . push ( v );
}
}
}
}
int main () {
string I , O ;
cin >> I ;
O = "123804765" ;
State in = I , ou = O ;
queue < State > q1 , q2 ;
map < State , int > mp1 , mp2 ;
q1 . push ( in );
mp1 [ in ] = 0 ;
q2 . push ( ou );
mp2 [ ou ] = 1 ;
if ( I == O ) {
cout << 0 ;
return 0 ;
}
while ( 1 ) {
bfs ( q1 , mp1 , mp2 );
bfs ( q2 , mp2 , mp1 );
}
return 0 ;
}
Meet in the middle
Warning
本节要介绍的不是 二分搜索 (二分搜索的另外一个译名为「折半搜索」).
引入
Meet in the middle 算法没有正式译名,常见的翻译为「折半搜索」、「双向搜索」或「中途相遇」.
它适用于输入数据较小,但还没小到能直接使用暴力搜索的情况.
过程
Meet in the middle 算法的主要思想是将整个搜索过程分成两半,分别搜索,最后将两半的结果合并.
性质
暴力搜索的复杂度往往是指数级的,而改用 meet in the middle 算法后复杂度的指数可以减半,即让复杂度从 \(O(a^b)\) 降到 \(O(a^{b/2})\) .
例题
例题 「USACO09NOV」灯 Lights
有 \(n\) 盏灯,每盏灯与若干盏灯相连,每盏灯上都有一个开关,如果按下一盏灯上的开关,这盏灯以及与之相连的所有灯的开关状态都会改变.一开始所有灯都是关着的,你需要将所有灯打开,求最小的按开关次数.
\(1\le n\le 35\) .
解题思路
如果这道题暴力 DFS 找开关灯的状态,时间复杂度就是 \(O(2^{n})\) , 显然超时.不过,如果我们用 meet in middle 的话,时间复杂度可以优化至 \(O(n2^{n/2})\) .meet in middle 就是让我们先找一半的状态,也就是找出只使用编号为 \(1\) 到 \(\mathrm{mid}\) 的开关能够到达的状态,再找出只使用另一半开关能到达的状态.如果前半段和后半段开启的灯互补,将这两段合并起来就得到了一种将所有灯打开的方案.具体实现时,可以把前半段的状态以及达到每种状态的最少按开关次数存储在 map 里面,搜索后半段时,每搜出一种方案,就把它与互补的第一段方案合并来更新答案.
参考代码
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55 #include <algorithm>
#include <iostream>
#include <map>
using namespace std ;
int n , m , ans = 0x7fffffff ;
map < long long , int > f ;
long long a [ 40 ];
int main () {
cin >> n >> m ;
a [ 0 ] = 1 ;
for ( int i = 1 ; i < n ; ++ i ) a [ i ] = a [ i - 1 ] * 2 ; // 进行预处理
for ( int i = 1 ; i <= m ; ++ i ) { // 对输入的边的情况进行处理
int u , v ;
cin >> u >> v ;
-- u ;
-- v ;
a [ u ] |= (( long long ) 1 << v );
a [ v ] |= (( long long ) 1 << u );
}
for ( int i = 0 ; i < ( 1 << ( n / 2 )); ++ i ) { // 对前一半进行搜索
long long t = 0 ;
int cnt = 0 ;
for ( int j = 0 ; j < n / 2 ; ++ j ) {
if (( i >> j ) & 1 ) {
t ^= a [ j ];
++ cnt ;
}
}
if ( ! f . count ( t ))
f [ t ] = cnt ;
else
f [ t ] = min ( f [ t ], cnt );
}
for ( int i = 0 ; i < ( 1 << ( n - n / 2 )); ++ i ) { // 对后一半进行搜索
long long t = 0 ;
int cnt = 0 ;
for ( int j = 0 ; j < ( n - n / 2 ); ++ j ) {
if (( i >> j ) & 1 ) {
t ^= a [ n / 2 + j ];
++ cnt ;
}
}
if ( f . count (((( long long ) 1 << n ) - 1 ) ^ t ))
ans = min ( ans , cnt + f [((( long long ) 1 << n ) - 1 ) ^ t ]);
}
cout << ans ;
return 0 ;
}
外部链接
Last updated on this page: , Update history
Found an error? Want to help improve? Edit this page on GitHub!
Contributors to this page:FFjet, ChungZH, frank-xjh, hsfzLZH1, Xarfa, AndrewWayne, hcx1204
All content on this page is provided under the terms of the CC BY-SA 4.0 and SATA license, additional terms may apply