This page briefly introduces two bidirectional search algorithms: "bidirectional simultaneous search" and "Meet in the middle".
Bidirectional simultaneous search
Definition
The basic idea of bidirectional simultaneous search is to start simultaneously from the initial and terminal states in the state graph and perform BFS or DFS.
If the two ends of the search meet, a feasible solution can be considered to have been found.
Process
Steps of bidirectional BFS:
Add the starting node and target node to queue q
Mark the starting node with 1
Mark the target node with 2
while (queue q is not empty)
{
Expand s new nodes from q.front()
If a newly expanded node has already been marked by another number
Then the two ends of the search have collided
Then end the loop
If the s new nodes were expanded from the starting node
Then mark these s nodes with 1 and enqueue them into q
If the s new nodes were expanded from the target node
Then mark these s nodes with 2 and enqueue them into q
}
Examples
Example: the Eight Puzzle
On a board, eight pieces are placed, each marked with one of the numbers from to . One empty space remains on the board, represented by . A piece next to the empty space can be moved into it. The problem is: given an initial layout (initial state) and a target layout (for simplicity, let the target state be ), find a minimum-step sequence of moves that transforms the initial layout into the target layout.
Solution idea
It is easy to think of brute-force BFS. Brute-force BFS does not time out on this problem either. However, this is used as an example of bidirectional simultaneous search. We can use two BFSs: one searches forward from the initial state, and the other searches backward from the terminal state. Alternating between the two BFSs greatly reduces the size of the search trees. When one BFS finds a state already found by the other BFS, the answer is obtained.
Reference code
#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
This section does not introduce binary search (another translation of binary search is "half-interval search").
Introduction
Meet in the middle has no official translation. Common translations include "half-interval search", "bidirectional search", and "meeting in the middle".
It is suitable when the input data is small, but not so small that brute-force search can be used directly.
Process
The main idea of the Meet in the middle algorithm is to divide the entire search process into two halves, search them separately, and finally merge the results of the two halves.
Properties
The complexity of brute-force search is often exponential. After switching to Meet in the middle, the exponent of the complexity can be halved, reducing it from to .
Examples
Example: "USACO09NOV" Lights
There are lights, each connected to several lights, and each light has a switch. If the switch on a light is pressed, the switch states of that light and all lights connected to it change. Initially all lights are off. You need to turn on all the lights and find the minimum number of switch presses.
.
Solution idea
If brute-force DFS is used to search the states of turning lights on and off, the time complexity is , which clearly times out. However, with Meet in the middle, the time complexity can be optimized to . Meet in the middle first finds half of the states: namely, the states reachable using only switches numbered through ; it then finds the states reachable using only the other half of the switches. If the lights turned on by the first and second halves are complementary, merging the two halves gives a way to turn on all the lights. In the implementation, store the states of the first half and the minimum number of presses needed to reach each state in a map. When searching the second half, each time a solution is found, merge it with the complementary solution from the first half to update the answer.
Reference code
#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; // preprocessing
for (int i = 1; i <= m; ++i) { // process the input edges
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) { // search the first half
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) { // search the second half
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;
}