Veo los siguientes posibles enfoques de solución:
- Teoría pesada Sé que hay literatura sobre la vida en un toro, pero no he leído mucho.
- Fuerza bruta hacia delante: para cada tablero posible, verifique su puntaje. Esto es básicamente lo que hacen los enfoques de Matthew y Keith, aunque Keith reduce el número de tableros para verificar en un factor de 4.
- Optimización: representación canónica. Si podemos verificar si un tablero está en representación canónica mucho más rápido de lo que se necesita para evaluar su puntaje, obtenemos una aceleración de un factor de aproximadamente 8N ^ 2. (También hay enfoques parciales con clases de equivalencia más pequeñas).
- Optimización: DP. Guarde en caché el puntaje de cada tabla, de modo que en lugar de guiarlos hasta que converjan o diverjan, simplemente caminamos hasta encontrar una tabla que hayamos visto antes. En principio, esto daría una aceleración por un factor del puntaje promedio / duración del ciclo (tal vez 20 o más), pero en la práctica es probable que estemos intercambiando mucho. Por ejemplo, para N = 6 necesitaríamos capacidad para 2 ^ 36 puntajes, que a un byte por puntaje es de 16 GB, y necesitamos acceso aleatorio, por lo que no podemos esperar una buena localidad de caché.
- Combina los dos. Para N = 6, la representación canónica completa nos permitiría reducir el caché DP a aproximadamente 60 megapuntos. Este es un enfoque prometedor.
- Fuerza bruta hacia atrás. Esto parece extraño al principio, pero si suponemos que podemos encontrar fácilmente bodegones y que podemos invertir fácilmente la
Next(board)
función, vemos que tiene algunos beneficios, aunque no tantos como esperaba originalmente.
- No nos molestamos con tablas divergentes en absoluto. No es un gran ahorro, porque son bastante raros.
- No necesitamos almacenar puntuaciones para todos los tableros, por lo que debería haber menos presión de memoria que el enfoque DP directo.
- Trabajar hacia atrás es bastante fácil variando una técnica que vi en la literatura en el contexto de enumerar bodegones. Funciona tratando cada columna como una letra en un alfabeto y luego observando que una secuencia de tres letras determina la del medio en la próxima generación. El paralelo con la enumeración de las naturalezas muertas es tan estrecha que los he refactorizado juntos en un método muy poco incómoda,
Prev2
.
- Puede parecer que podemos canonizar las naturalezas muertas y obtener algo como la aceleración 8N ^ 2 por un costo muy bajo. Sin embargo, empíricamente todavía obtenemos una gran reducción en el número de tableros considerados si canonicalizamos en cada paso.
- Una proporción sorprendentemente alta de tableros tiene una puntuación de 2 o 3, por lo que todavía hay presión de memoria. Me pareció necesario canonizar sobre la marcha en lugar de construir la generación anterior y luego canonizar. Puede ser interesante reducir el uso de memoria haciendo una búsqueda de profundidad primero en lugar de amplitud, pero hacerlo sin desbordar la pila y sin hacer cálculos redundantes requiere un enfoque de co-rutina / continuación para enumerar los tableros anteriores.
No creo que la microoptimización me permita ponerme al día con el código de Keith, pero por interés, publicaré lo que tengo. Esto resuelve N = 5 en aproximadamente un minuto en una máquina de 2 GHz usando Mono 2.4 o .Net (sin PLINQ) y en aproximadamente 20 segundos usando PLINQ; N = 6 carreras por muchas horas.
using System;
using System.Collections.Generic;
using System.Linq;
namespace Sandbox {
class Codegolf9393 {
internal static void Main() {
new Codegolf9393(4).Solve();
}
private readonly int _Size;
private readonly uint _AlphabetSize;
private readonly uint[] _Transitions;
private readonly uint[][] _PrevData1;
private readonly uint[][] _PrevData2;
private readonly uint[,,] _CanonicalData;
private Codegolf9393(int size) {
if (size > 8) throw new NotImplementedException("We need to fit the bits in a ulong");
_Size = size;
_AlphabetSize = 1u << _Size;
_Transitions = new uint[_AlphabetSize * _AlphabetSize * _AlphabetSize];
_PrevData1 = new uint[_AlphabetSize * _AlphabetSize][];
_PrevData2 = new uint[_AlphabetSize * _AlphabetSize * _AlphabetSize][];
_CanonicalData = new uint[_Size, 2, _AlphabetSize];
InitTransitions();
}
private void InitTransitions() {
HashSet<uint>[] tmpPrev1 = new HashSet<uint>[_AlphabetSize * _AlphabetSize];
HashSet<uint>[] tmpPrev2 = new HashSet<uint>[_AlphabetSize * _AlphabetSize * _AlphabetSize];
for (int i = 0; i < tmpPrev1.Length; i++) tmpPrev1[i] = new HashSet<uint>();
for (int i = 0; i < tmpPrev2.Length; i++) tmpPrev2[i] = new HashSet<uint>();
for (uint i = 0; i < _AlphabetSize; i++) {
for (uint j = 0; j < _AlphabetSize; j++) {
uint prefix = Pack(i, j);
for (uint k = 0; k < _AlphabetSize; k++) {
// Build table for forwards checking
uint jprime = 0;
for (int l = 0; l < _Size; l++) {
uint count = GetBit(i, l-1) + GetBit(i, l) + GetBit(i, l+1) + GetBit(j, l-1) + GetBit(j, l+1) + GetBit(k, l-1) + GetBit(k, l) + GetBit(k, l+1);
uint alive = GetBit(j, l);
jprime = SetBit(jprime, l, (count == 3 || (alive + count == 3)) ? 1u : 0u);
}
_Transitions[Pack(prefix, k)] = jprime;
// Build tables for backwards possibilities
tmpPrev1[Pack(jprime, j)].Add(k);
tmpPrev2[Pack(jprime, i, j)].Add(k);
}
}
}
for (int i = 0; i < tmpPrev1.Length; i++) _PrevData1[i] = tmpPrev1[i].ToArray();
for (int i = 0; i < tmpPrev2.Length; i++) _PrevData2[i] = tmpPrev2[i].ToArray();
for (uint col = 0; col < _AlphabetSize; col++) {
_CanonicalData[0, 0, col] = col;
_CanonicalData[0, 1, col] = VFlip(col);
for (int rot = 1; rot < _Size; rot++) {
_CanonicalData[rot, 0, col] = VRotate(_CanonicalData[rot - 1, 0, col]);
_CanonicalData[rot, 1, col] = VRotate(_CanonicalData[rot - 1, 1, col]);
}
}
}
private ICollection<ulong> Prev2(bool stillLife, ulong next, ulong prev, int idx, ICollection<ulong> accum) {
if (stillLife) next = prev;
if (idx == 0) {
for (uint a = 0; a < _AlphabetSize; a++) Prev2(stillLife, next, SetColumn(0, idx, a), idx + 1, accum);
}
else if (idx < _Size) {
uint i = GetColumn(prev, idx - 2), j = GetColumn(prev, idx - 1);
uint jprime = GetColumn(next, idx - 1);
uint[] succ = idx == 1 ? _PrevData1[Pack(jprime, j)] : _PrevData2[Pack(jprime, i, j)];
foreach (uint b in succ) Prev2(stillLife, next, SetColumn(prev, idx, b), idx + 1, accum);
}
else {
// Final checks: does the loop round work?
uint a0 = GetColumn(prev, 0), a1 = GetColumn(prev, 1);
uint am = GetColumn(prev, _Size - 2), an = GetColumn(prev, _Size - 1);
if (_Transitions[Pack(am, an, a0)] == GetColumn(next, _Size - 1) &&
_Transitions[Pack(an, a0, a1)] == GetColumn(next, 0)) {
accum.Add(Canonicalise(prev));
}
}
return accum;
}
internal void Solve() {
DateTime start = DateTime.UtcNow;
ICollection<ulong> gen = Prev2(true, 0, 0, 0, new HashSet<ulong>());
for (int depth = 1; gen.Count > 0; depth++) {
Console.WriteLine("Length {0}: {1}", depth, gen.Count);
ICollection<ulong> nextGen;
#if NET_40
nextGen = new HashSet<ulong>(gen.AsParallel().SelectMany(board => Prev2(false, board, 0, 0, new HashSet<ulong>())));
#else
nextGen = new HashSet<ulong>();
foreach (ulong board in gen) Prev2(false, board, 0, 0, nextGen);
#endif
// We don't want the still lifes to persist or we'll loop for ever
if (depth == 1) {
foreach (ulong stilllife in gen) nextGen.Remove(stilllife);
}
gen = nextGen;
}
Console.WriteLine("Time taken: {0}", DateTime.UtcNow - start);
}
private ulong Canonicalise(ulong board)
{
// Find the minimum board under rotation and reflection using something akin to radix sort.
Isomorphism canonical = new Isomorphism(0, 1, 0, 1);
for (int xoff = 0; xoff < _Size; xoff++) {
for (int yoff = 0; yoff < _Size; yoff++) {
for (int xdir = -1; xdir <= 1; xdir += 2) {
for (int ydir = 0; ydir <= 1; ydir++) {
Isomorphism candidate = new Isomorphism(xoff, xdir, yoff, ydir);
for (int col = 0; col < _Size; col++) {
uint a = canonical.Column(this, board, col);
uint b = candidate.Column(this, board, col);
if (b < a) canonical = candidate;
if (a != b) break;
}
}
}
}
}
ulong canonicalValue = 0;
for (int i = 0; i < _Size; i++) canonicalValue = SetColumn(canonicalValue, i, canonical.Column(this, board, i));
return canonicalValue;
}
struct Isomorphism {
int xoff, xdir, yoff, ydir;
internal Isomorphism(int xoff, int xdir, int yoff, int ydir) {
this.xoff = xoff;
this.xdir = xdir;
this.yoff = yoff;
this.ydir = ydir;
}
internal uint Column(Codegolf9393 _this, ulong board, int col) {
uint basic = _this.GetColumn(board, xoff + col * xdir);
return _this._CanonicalData[yoff, ydir, basic];
}
}
private uint VRotate(uint col) {
return ((col << 1) | (col >> (_Size - 1))) & (_AlphabetSize - 1);
}
private uint VFlip(uint col) {
uint replacement = 0;
for (int row = 0; row < _Size; row++)
replacement = SetBit(replacement, row, GetBit(col, _Size - row - 1));
return replacement;
}
private uint GetBit(uint n, int bit) {
bit %= _Size;
if (bit < 0) bit += _Size;
return (n >> bit) & 1;
}
private uint SetBit(uint n, int bit, uint value) {
bit %= _Size;
if (bit < 0) bit += _Size;
uint mask = 1u << bit;
return (n & ~mask) | (value == 0 ? 0 : mask);
}
private uint Pack(uint a, uint b) { return (a << _Size) | b; }
private uint Pack(uint a, uint b, uint c) {
return (((a << _Size) | b) << _Size) | c;
}
private uint GetColumn(ulong n, int col) {
col %= _Size;
if (col < 0) col += _Size;
return (_AlphabetSize - 1) & (uint)(n >> (col * _Size));
}
private ulong SetColumn(ulong n, int col, uint value) {
col %= _Size;
if (col < 0) col += _Size;
ulong mask = (_AlphabetSize - 1) << (col * _Size);
return (n & ~mask) | (((ulong)value) << (col * _Size));
}
}
}