Je vois les approches de solution possibles suivantes:
- Théorie lourde. Je sais qu'il existe de la littérature sur la vie sur un tore, mais je n'en ai pas beaucoup lu.
- Force brute en avant: pour chaque planche possible, vérifiez son score. C'est essentiellement ce que font les approches de Matthew et Keith, bien que Keith réduit le nombre de planches à vérifier par un facteur de 4.
- Optimisation: représentation canonique. Si nous pouvons vérifier si une planche est en représentation canonique beaucoup plus rapidement qu'il n'en faut pour évaluer son score, nous obtenons une accélération d'un facteur d'environ 8N ^ 2. (Il existe également des approches partielles avec des classes d'équivalence plus petites).
- Optimisation: DP. Mettez en cache le score de chaque tableau, de sorte qu'au lieu de les parcourir jusqu'à ce qu'ils convergent ou divergent, nous marchons jusqu'à ce que nous trouvions un tableau que nous avons vu auparavant. En principe, cela donnerait une accélération d'un facteur de la durée moyenne du score / cycle (peut-être 20 ou plus), mais dans la pratique, nous allons probablement échanger fortement. Par exemple, pour N = 6, nous aurions besoin d'une capacité de 2 ^ 36 scores, qui à un octet par score est de 16 Go, et nous avons besoin d'un accès aléatoire, donc nous ne pouvons pas nous attendre à une bonne localité de cache.
- Combinez les deux. Pour N = 6, la représentation canonique complète nous permettrait de réduire le cache DP à environ 60 méga-scores. Il s'agit d'une approche prometteuse.
- Force brute en arrière. Cela semble étrange au début, mais si nous supposons que nous pouvons facilement trouver des natures mortes et que nous pouvons facilement inverser la
Next(board)
fonction, nous constatons qu'elle présente certains avantages, bien que pas autant que je l'espérais à l'origine.
- Nous ne nous soucions pas du tout des planches divergentes. Pas beaucoup d'économies, car elles sont assez rares.
- Nous n'avons pas besoin de stocker les scores pour toutes les cartes, il devrait donc y avoir moins de pression sur la mémoire que l'approche DP vers l'avant.
- Le travail en arrière est en fait assez facile en faisant varier une technique que j'ai vue dans la littérature dans le contexte de l'énumération des natures mortes. Il fonctionne en traitant chaque colonne comme une lettre dans un alphabet, puis en observant qu'une séquence de trois lettres détermine celle du milieu dans la prochaine génération. Le parallèle avec l' énumération des natures mortes est si proche que je l' ai refactorisé - les ensemble dans une méthode peu maladroite,
Prev2
.
- Il peut sembler que nous pouvons simplement canoniser les natures mortes et obtenir quelque chose comme l'accélération 8N ^ 2 pour très peu de frais. Cependant, empiriquement, nous obtenons toujours une grande réduction du nombre de cartes prises en compte si nous canonisons à chaque étape.
- Une proportion étonnamment élevée de planches ont un score de 2 ou 3, il y a donc toujours une pression mémoire. J'ai trouvé nécessaire de canoniser à la volée plutôt que de construire la génération précédente, puis de canoniser. Il peut être intéressant de réduire l'utilisation de la mémoire en effectuant une recherche en profondeur d'abord plutôt qu'en largeur, mais le faire sans déborder la pile et sans effectuer de calculs redondants nécessite une approche de co-routine / continuation pour énumérer les cartes précédentes.
Je ne pense pas que la micro-optimisation me permettra de rattraper le code de Keith, mais pour l'intérêt, je posterai ce que j'ai. Cela résout N = 5 en environ une minute sur une machine à 2 GHz en utilisant Mono 2.4 ou .Net (sans PLINQ) et en environ 20 secondes en utilisant PLINQ; N = 6 s'exécute pendant plusieurs heures.
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));
}
}
}