A few days ago the YouTube channel Alpha Phoenix uploaded a video about running Conway’s Game of Life backwards. It’s a great video, but the solution Alpha Phoenix used was based on using a constraint solver and it inspired me to try implementing my own solver for reverse Game of Life to see how a backtracking search would perform in comparison. So I wrote a backtracking search solution in Julia. It runs pretty quickly on small inputs but gets exponentially slower for larger inputs.

Here is an example of one of my test cases:


The largest test I did so far was a rasterization of the text REVERSE. My implementation only managed to run it 3 steps backwards and got stuck on the 4th step:


The problem with my solutions is that they grow in size, each predecessor state requires searching over more pixels which slows down the search a lot. For the REVERSE example the 3rd step took 15 seconds but the 4th step did not complete: I stopped it after more than 4 hours.

Update: I rewrote and optimized my search function and finally the 4th iteration of the REVERSE example finished without a solution after 15 hours. If my code is correct the 3rd iteration shown above is a so-called Garden of Eden, that is, a game state which has no predecessors.

Julia Code

Anyway here is my Julia code if you want to try it for yourself:

b_north = 1<<0 + 1<<1 + 1<<2
b_south = 1<<6 + 1<<7 + 1<<8
b_ew = 1<<3 + 1<<4 + 1<<5  # line east-west
b_west = 1<<0 + 1<<3 + 1<<6
b_east = 1<<2 + 1<<5 + 1<<8
b_ns = 1<<1 + 1<<4 + 1<<7 # line north-south

neighbors(G, r, c) = G[r-1, c] + G[r-1, c-1] + G[r-1, c+1] + G[r+1, c] + G[r+1, c-1] + G[r+1, c+1] + G[r, c-1] + G[r, c+1]

function sim(G)
    H, W = size(G)
    F = fill(0, H, W)
    for r in 2:(H-1)
        for c in 2:(W-1)
            n = neighbors(G, r, c)
            F[r,c] = n == 3 || G[r,c] + n == 3
        end
    end
    return F
end

function disp(A::Matrix{Int}; border=true)
    H,W = size(A)
    if border
        println("."^W)
    end
    for r in 2:(H-1)
        if border
            print(".")
        end
        for c in 2:(W-1)
            print(" #"[A[r, c] + 1])
        end
        if border
            println(".")
        else
            println()
        end
    end
    if border
        println("."^W)
    end
end

function disp(A::Int)
    println("."^5)
    for r in 0:2
        print(".")
        for c in 0:2
            print(" #"[(A >> (r*3 + c))&1 + 1])
        end
        println(".")
    end
    println("."^5)
end

function load_input(filename::String, pad=0)
    f = open(filename, "r")
    lines = readlines(f)
    H = length(lines) + 2 + 2*pad
    W = max(map(x->length(x), lines)...) + 2 + 2*pad

    G = fill(0, H, W)
    for r in 1:length(lines)
        for c in 1:length(lines[r])
            if lines[r][c] == '#'
                G[r+1+pad,c+1+pad] = 1
            end
        end
    end
    return G
end

function find_ancestors()
    global live, dead
    G = fill(0, 5, 5)
    live = Set{Int}()
    dead = Set{Int}()
    for i in 0:(1<<9-1)
        for r in 0:2
            for c in 0:2
                G[r+2, c+2] = (i >> (r*3 + c)) & 1
            end
        end
        F = sim(G)
        if F[3,3] == 1
            push!(live, i)
        else
            push!(dead, i)
        end
        # disp(G)
    end
end

function search(P)
    H, W = size(P)
    L = []
    for c in 2:(W-1)
        for r in 2:(H-1)
            push!(L, (r,c))
        end
    end
    S = fill(1, length(L))
    Q = fill(0, length(L))
    i = 1
    while i <= length(L)
        r,c = L[i]
        mask = value = 0
        if r > 2
            value = Q[i-1] >> 3
            mask = b_north | b_ew
        end
        if c > 2
            value |= (Q[i-H+2] >> 1) & (b_west | b_ns)
            mask |= b_west | b_ns
        end
        value &= mask
        while S[i] <= length(P[r,c])
            p = P[r,c][S[i]]
            if (p & mask) == value
                Q[i] = p
                break
            end
            S[i] += 1
        end
        if S[i] <= length(P[r,c])
            i += 1
        else
            S[i] = 1
            i -= 1
            if i == 0
                return nothing
            end
            S[i] += 1
        end
    end
    F = fill(0, H, W)
    for i in 1:length(L)
        F[L[i]...] = (Q[i] >> 4) & 1
    end
    return F
end

function bitmask(u, v)
    c = 0
    for x in -1:1
        for y in -1:1
            if -1 <= y+u  <= 1 && -1 <= x+v <= 1
                c += 1 << ((y+u+1)*3 + x+v+1)
            end
        end
    end
    return c
end

function cull(G::Matrix{Int}, P, H, W)
    mask = fill(0, 3, 3)
    for x in -1:1
        for y in -1:1
            mask[y+2, x+2] = bitmask(y, x)
        end
    end
    culled = 0
    for r in 2:(H-1)
        for c in 2:(W-1)
            if G[r,c] == 1
                continue
            end
            diff = Set{Int}()
            for p in P[r,c]
                for (u,v) in [(1,0), (-1,0), (0,1), (0,-1)]
                    if 2 <= r+u <= H-1 && 2 <= c+v <= W-1
                        if G[r+u, c+v] == 1 && count_ones(mask[u+2, v+2] & p) > 4
                            push!(diff, p)
                        end
                    end
                end
            end
            culled += length(diff)
            setdiff!(P[r,c], diff)
        end
    end
end

function reverse(filename::String)
    find_ancestors()

    G = load_input(filename)
    H,W = size(G)
    possible = fill(Set{Int}(), H, W)
    global live, dead
    for r in 2:(H-1)
        for c in 2:(W-1)
            if G[r,c] == 1
                possible[r,c] = copy(live)
            else
                possible[r,c] = copy(dead)
            end
        end
    end

    for r in 2:(H-1)
        diff = Set{Int}()
        for p in possible[r,2]
            if (p & b_west) != 0 || count_ones(p & b_ns) == 3
                push!(diff, p)
            end
        end
        setdiff!(possible[r,2], diff)
        diff = Set{Int}()
        for p in possible[r,W-1]
            if (p & b_east) != 0 || count_ones(p & b_ns) == 3
                push!(diff, p)
            end
        end
        setdiff!(possible[r,W-1], diff)
    end
    for c in 2:(W-1)
        diff = Set{Int}()
        for p in possible[2,c]
            if (p & b_north) != 0 || count_ones(p & b_ew) == 3
                push!(diff, p)
            end
        end
        setdiff!(possible[2,c], diff)
        diff = Set{Int}()
        for p in possible[H-1,c]
            if (p & b_south) != 0 || count_ones(p & b_ew) == 3
                push!(diff, p)
            end
        end
        setdiff!(possible[H-1,c], diff)
    end

    cull(G, possible, H, W)

    P = possible
    possible = fill([], size(P))
    for r in 1:H
        for c in 1:W
            possible[r,c] = sort(collect(P[r,c]), by=count_ones)
        end
    end

    F = search(possible)
    if F isa Nothing
        println("No solution found!")
    else
        disp(F)
    end
end

filename = "big_reverse.txt"
@time reverse(filename)

This script expects a file named big_reverse.txt in the working directory containing a text representation of a Game of Life board where # denotes a living cell (all other non-newline characters are treated as dead). For example:

    ..................................................
    .#####  ###### #    # ###### #####   ####  ######.
    .#    # #      #    # #      #    # #      #     .
    .#    # #####  #    # #####  #    #  ####  ##### .
    .#####  #      #    # #      #####       # #     .
    .#   #  #       #  #  #      #   #  #    # #     .
    .#    # ######   ##   ###### #    #  ####  ######.
    ..................................................

C Code

Here is my C implementation:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <time.h>
#include <stdbool.h>
#include <stdint.h>

#define b_north ((1<<0) + (1<<1) + (1<<2))
#define b_south ((1<<6) + (1<<7) + (1<<8))
#define b_ew    ((1<<3) + (1<<4) + (1<<5))
#define b_west  ((1<<0) + (1<<3) + (1<<6))
#define b_east  ((1<<2) + (1<<5) + (1<<8))
#define b_ns    ((1<<1) + (1<<4) + (1<<7))
#define b_n (b_north | b_ew)
#define b_s (b_south | b_ew)
#define b_e (b_east | b_ns)
#define b_w (b_west | b_ns)

typedef struct Line Line;
struct Line {
  char* text;
  Line* next;
};

typedef struct {
  int n;
  uint64_t data[];
} BitSet;

typedef struct {
  int n;
  unsigned* data;
} Array;

static int count_ones(uint64_t x)
{
  int s = 0;
  while (x != 0) {
    s += x&1;
    x >>= 1;
  }
  return s;
}

static BitSet* bs(int n)
{
  int k = (n + 63) / 64;
  BitSet* bs = malloc(sizeof(BitSet) + 8 * k);
  bs->n = k;
  memset(bs->data, 0, 8 * k);
  return bs;
}

static int bs_size(BitSet* bs)
{
  int s = 0;
  for (int i = 0; i < bs->n; ++i) {
    s += count_ones(bs->data[i]);
  }
  return s;
}

static void set_bit(BitSet* bs, int b)
{
  bs->data[b>>6] |= (1ULL<<(b & 0x3F));
}

static void clear_bit(BitSet* bs, int b)
{
  bs->data[b>>6] &= ~(1ULL<<(b & 0x3F));
}

static int get_bit(BitSet* bs, int b)
{
  return (bs->data[b>>6] >> (b & 0x3F)) & 1;
}

void disp(unsigned* board, int H, int W)
{
  for (int r = 0; r < H; ++r) {
    for (int c = 0; c < W; ++c) {
      printf("%c", board[r * W + c] ? '#' : ' ');
    }
    printf("\n");
  }
}

int neighbors[][2] = {
  {-1, -1}, {-1, 0}, {-1, 1},
  {0, -1 },          {0, 1 },
  {1, -1 }, {1, 0 }, {1, 1 },
};
int nsew[][2] = {
                  {-2, 0},
                  {-1, 0},
 {0, -2}, {0, -1},         {0, 1}, {0, 2},
                  {1, 0 },
                  {2, 0 },
};

int count(unsigned* B, int H, int W, int r, int c)
{
  int n = 0;
  for (int i = 0; i < 8; ++i) {
    n += B[(r + neighbors[i][0])*W + c + neighbors[i][1]];
  }
  return n;
}

void sim(unsigned* start, unsigned* next, int H, int W)
{
  for (int r = 1; r < H-1; ++r) {
    for (int c = 1; c < W-1; ++c) {
      // count neighbors
      int n = count(start, H, W, r, c);
      if (start[r*W + c]) {
        // 2-3 neighbors stay alive
        next[r * W + c] = (n == 2 || n == 3) ? 1 : 0;
      } else {
        // 3 neighbors get born
        next[r * W + c] = n == 3 ? 1 : 0;
      }
    }
  }
}

static void enum_states(Array* live, Array* dead)
{
  int scratch[5*5];
  int out[5*5];
  memset(scratch, 0, sizeof(int) * 5 * 5);
  for (int p = 0; p < (1<<9); ++p) {
    for (int r = 0; r < 3; ++r) {
      for (int c = 0; c < 3; ++c) {
        scratch[(r+1)*5 + c + 1] = (p >> (r*3 + c)) & 1;
      }
    }
    sim(scratch, out, 5, 5);
    if (out[2*5 + 2]) {
      live->data[live->n++] = p;
    } else {
      dead->data[dead->n++] = p;
    }
  }
}

static void search(Array* A, int H, int W)
{
  int n = 0;
  int L[H*W][2];
  for (int c = 1; c < W-1; ++c) {
    for (int r = 1; r < H-1; ++r) {
      L[n][0] = r;
      L[n++][1] = c;
    }
  }
  int Q[n];
  int S[n];
  S[0] = 0;
  int i = 0;
  while (true) {
    int r = L[i][0];
    int c = L[i][1];
    int mask = 0;
    int value = 0;
    if (r > 1) {
      value = Q[i-1] >> 3;
      mask = b_north | b_ew;
    }
    if (c > 1) {
      value |= (Q[i-H+2] >> 1) & (b_west | b_ns);
      mask |= b_west | b_ns;
    }
    value &= mask;
    Array aa = A[r*W + c];
    while (S[i] < aa.n) {
      int p = aa.data[S[i]];
      if ((p & mask) == value) {
        Q[i] = p;
        break;
      }
      S[i] += 1;
    }
    if (S[i] < aa.n) {
      i += 1;
      if (i == n) {
        break;
      }
      S[i] = 0;
    } else {
      S[i] = 0;
      i -= 1;
      if (i < 0) {
        fprintf(stderr, "Input is a Garden of Eden.\n");
        return;
      }
      S[i] += 1;
    }
  }
  int board[H*W];
  memset(board, 0, sizeof(int)*H*W);
  for (i = 0; i < n; ++i) {
    int r = L[i][0];
    int c = L[i][1];
    board[r*W + c] = (Q[i] >> 4) & 1;
  }
  disp(board, H, W);
}

static void cull(Array* A, int H, int W)
{
  BitSet* d1 = bs(H*W);
  BitSet* d2 = bs(H*W);
  Array q1 = {
    .n = 0,
    .data = malloc(sizeof(int) * H*W * 2),
  };
  Array q2 = {
    .n = 0,
    .data = malloc(sizeof(int) * H*W * 2),
  };
  for (int r = 1; r < H-1; ++r) {
    for (int c = 1; c < W-1; ++c) {
      q1.data[q1.n++] = r;
      q1.data[q1.n++] = c;
    }
  }
  int pp[][5] = {
    { -1,  0, 0, 3, b_north | b_ew },
    { -2,  0, 0, 6, b_north },
    {  1,  0, 3, 0, b_south | b_ew },
    {  2,  0, 6, 0, b_south },
    {  0, -1, 0, 1, b_west | b_ns },
    {  0, -2, 0, 2, b_west },
    {  0,  1, 1, 0, b_east | b_ns },
    {  0,  2, 2, 0, b_east },
    { -1, -1, 0, 4, (1<<0) | (1<<1) | (1<<3) | (1<<4) },
    {  1,  1, 4, 0, (1<<4) | (1<<5) | (1<<7) | (1<<8) },
    { -1,  1, 0, 2, (1<<1) | (1<<2) | (1<<4) | (1<<5) },
    {  1, -1, 2, 0, (1<<3) | (1<<4) | (1<<6) | (1<<7) },
  };
  int N = sizeof(pp) / sizeof(pp[0]);
  int culled = 0;
  bool change = true;
  while (change) {
    change = false;
    for (int k = 0; k < q1.n; k += 2) {
      int r = q1.data[k];
      int c = q1.data[k + 1];
      Array* A0 = A + (r*W + c);
      for (int i = 0; i < A0->n; ++i) {
        int p = A0->data[i];
        int match = 0;
        for (int w = 0; w < N; ++w) {
          int r1 = r + pp[w][0];
          int c1 = c + pp[w][1];
          if (r1 >= 0 && r1 < H && c1 >= 0 && c1 < W) {
            Array A1 = A[r1*W + c1];
            int u = pp[w][2];
            int v = pp[w][3];
            int b = pp[w][4];
            for (int j = 0; j < A1.n; ++j) {
              if ((p & b) == ((u ? (A1.data[j] << u) : (A1.data[j] >> v)) & b)) {
                match |= 1<<w;
                break;
              }
            }
          } else {
            match |= 1<<w;
          }
        }
        if (match != (1<<N)-1) {
          A0->data[i] = A0->data[A0->n-1];
          A0->n -= 1;
          culled += 1;
          change = true;
          for (int w = 0; w < N; ++w) {
            int r1 = r + pp[w][0];
            int c1 = c + pp[w][1];
            if (r1 >= 0 && r1 < H && c1 >= 0 && c1 < W && !get_bit(d2, r1*W + c1)) {
              set_bit(d2, r1*W + c1);
              q2.data[q2.n++] = r1;
              q2.data[q2.n++] = c1;
            }
          }
          break;
        }
      }
    }
    BitSet* d = d1;
    d1 = d2;
    d2 = d;
    memset(d2->data, 0, 8*d1->n);
    Array q = q1;
    q1 = q2;
    q2 = q;
    q2.n = 0;
  }
  fprintf(stderr, "culled: %d\n", culled);
  free(d1);
  free(d2);
  free(q1.data);
  free(q2.data);
}

static int cmp_live(const void* pa, const void* pb)
{
  int a = count_ones(*(unsigned*) pa);
  int b = count_ones(*(unsigned*) pb);
  if ((a&(1<<4)) && !(b&(1<<4))) {
    return -1;
  }
  if (!(a&(1<<4)) && (b&(1<<4))) {
    return 1;
  }
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

static int cmp_dead(const void* pa, const void* pb)
{
  int a = count_ones(*(unsigned*) pa);
  int b = count_ones(*(unsigned*) pb);
  if ((a&(1<<4)) && !(b&(1<<4))) {
    return 1;
  }
  if (!(a&(1<<4)) && (b&(1<<4))) {
    return -1;
  }
  if (a < b) return -1;
  if (a > b) return 1;
  return 0;
}

static void reverse(unsigned* ref, int H, int W)
{
  Array live = {
    .n = 0,
    .data = alloca(sizeof(unsigned)<<9),
  };
  Array dead = {
    .n = 0,
    .data = alloca(sizeof(unsigned)<<9),
  };
  enum_states(&live, &dead);
  qsort(live.data, live.n, sizeof(int), cmp_live);
  qsort(dead.data, dead.n, sizeof(int), cmp_dead);
  Array A[H*W];
  for (int i = 0; i < H*W; ++i) {
    Array* set;
    if (ref[i]) {
      set = &live;
    } else {
      set = &dead;
    }
    A[i].n = set->n;
    A[i].data = malloc(sizeof(unsigned) * set->n);
    memcpy(A[i].data, set->data, sizeof(unsigned) * set->n);
  }
  for (int r = 0; r < H; ++r) {
    Array* pA = A + (r*W);
    int i = 0;
    while (i < pA->n) {
      if (pA->data[i] & b_w) {
        pA->n -= 1;
        pA->data[i] = pA->data[pA->n];
      } else {
        i += 1;
      }
    }
    pA = A + (r*W + W-1);
    i = 0;
    while (i < pA->n) {
      if (pA->data[i] & b_e) {
        pA->n -= 1;
        pA->data[i] = pA->data[pA->n];
      } else {
        i += 1;
      }
    }
  }
  for (int c = 0; c < W; ++c) {
    Array* pA = A + c;
    int i = 0;
    while (i < pA->n) {
      if (pA->data[i] & b_n) {
        pA->n -= 1;
        pA->data[i] = pA->data[pA->n];
      } else {
        i += 1;
      }
    }
    pA = A + ((H-1)*W + c);
    i = 0;
    while (i < pA->n) {
      if (pA->data[i] & b_s) {
        pA->n -= 1;
        pA->data[i] = pA->data[pA->n];
      } else {
        i += 1;
      }
    }
  }

  cull(A, H, W);

#if 1
  for (int r = 0; r < H; ++r) {
    for (int c = 0; c < W; ++c) {
      char buf[30];
      snprintf(buf, sizeof(buf), "%d", A[r*W+c].n);
      fprintf(stderr, "%3s ", buf);
    }
    fprintf(stderr, "\n");
  }
#endif

  search(A, H, W);

  for (int i = 0; i < H*W; ++i) {
    free(A[i].data);
  }
}

int main(int argc, char** argv)
{
  int H = 0;
  int W = 0;
  char temp[1<<10];
  size_t bufsize = sizeof(temp);
  Line* head = NULL;
  Line* tail = NULL;
  while (1) {
    char* buf = NULL;
    size_t t;
    ssize_t r = getline(&buf, &t, stdin);
    if (r < 0) {
      free(buf);
      break;
    }
    Line* line = malloc(sizeof(Line));
    *line = (Line) {
      .text = buf,
      .next = NULL,
    };
    if (!head) {
      head = tail = line;
    } else {
      tail->next = line;
      tail = line;
    }
    H += 1;
  }

  tail = head;
  while (tail) {
    char* p = tail->text;
    tail = tail->next;
    int linew = 0;
    while (*p) {
      if (*p != '\n' && *p != '\r') {
        linew += 1;
      }
      p++;
    }
    if (linew > W) W = linew;
  }

  H += 2;
  W += 2;
  size_t boardsz = H * W * sizeof(unsigned);
  unsigned* start = calloc(H * W, sizeof(unsigned));
  tail = head;
  int r = 0;
  int pop = 0;
  while (tail) {
    char* p = tail->text;
    tail = tail->next;
    int c = 0;
    while (*p) {
      if (*p == '#' || *p == 'O') {
        start[(r + 1)*W + c + 1] = 1;
        pop += 1;
        c += 1;
      } else if (*p != '\n' && *p != '\r') {
        c += 1;
      }
      p++;
    }
    r += 1;
  }

  if (argc > 1 && !strcmp(argv[1], "sim")) {
    struct timespec ts = {
      .tv_sec = 0,
      .tv_nsec = 300000000,
    };
    unsigned* next = malloc(boardsz);
    memcpy(next, start, boardsz);
    int step = 0;
    printf("\033[2J"); // Clear screen.
    while (1) {
      printf("\033[Hstep %d\n", step++);
      disp(start, H, W);
      fflush(stdout);
      nanosleep(&ts, NULL);

      sim(start, next, H, W);
      unsigned* t = start;
      start = next;
      next = t;
    }
  } else {
    fprintf(stderr, "Population: %d\n", pop);
    reverse(start, H, W);
  }

  free(start);
  while (head) {
    tail = head;
    tail = head->next;
    free(head->text);
    free(head);
    head = tail;
  }
  return 0;
}