Bidirectional Search

This page briefly introduces two bidirectional search algorithms: "bidirectional simultaneous search" and "Meet in the middle".

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 3×33\times 3 board, eight pieces are placed, each marked with one of the numbers from 11 to 88. One empty space remains on the board, represented by 00. 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 123804765123804765), 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 O(ab)O(a^b) to O(ab/2)O(a^{b/2}).

Examples

Example: "USACO09NOV" Lights

There are nn 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.

1≤n≤351\le n\le 35.

Solution idea

If brute-force DFS is used to search the states of turning lights on and off, the time complexity is O(2n)O(2^{n}), which clearly times out. However, with Meet in the middle, the time complexity can be optimized to O(n2n/2)O(n2^{n/2}). Meet in the middle first finds half of the states: namely, the states reachable using only switches numbered 11 through mid\mathrm{mid}; 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;
}

This content comes from OI Wiki: Bidirectional Search. See the original for authors and revision history. AIOJ has adapted the formatting, in-site links, and resource paths.

Content is shared under CC BY-SA 4.0 and SATA license; additional terms are governed by the original notice. Thanks to the OI Wiki authors and contributors; per the SATA terms, please visit the OI Wiki repository and star it to show your support.