Ich sehe folgende Lösungsansätze:
- Schwere Theorie. Ich weiß, dass es Literatur über das Leben auf einem Torus gibt, aber ich habe nicht viel davon gelesen.
- Brute Force Forwards: Überprüfen Sie für jedes mögliche Board dessen Punktzahl. Dies ist im Grunde das, was Matthew und Keith tun, obwohl Keith die Anzahl der zu überprüfenden Bretter um den Faktor 4 reduziert.
- Optimierung: kanonische Darstellung. Wenn wir überprüfen können, ob eine Tafel in kanonischer Darstellung ist, erhalten wir eine Beschleunigung um den Faktor 8N ^ 2. (Es gibt auch Teilansätze mit kleineren Äquivalenzklassen).
- Optimierung: DP. Zwischenspeichern Sie die Punktzahl für jedes Brett, sodass Sie nicht durch das Brett laufen müssen, bis es konvergiert oder auseinander läuft, sondern bis Sie ein Brett finden, das Sie zuvor gesehen haben. Im Prinzip würde dies eine Beschleunigung um einen Faktor der durchschnittlichen Punktzahl / Zykluslänge (vielleicht 20 oder mehr) ergeben, aber in der Praxis werden wir wahrscheinlich stark tauschen. ZB für N = 6 benötigen wir Kapazität für 2 ^ 36 Scores, was bei einem Byte pro Score 16 GB entspricht, und wir benötigen Direktzugriff, sodass wir keine gute Cache-Lokalität erwarten können.
- Kombiniere die beiden. Für N = 6 würde die vollständige kanonische Darstellung es uns ermöglichen, den DP-Cache auf etwa 60 Mega-Scores zu reduzieren. Dies ist ein vielversprechender Ansatz.
- Brute Force rückwärts. Dies erscheint zunächst seltsam, aber wenn wir davon ausgehen, dass wir Stillleben leicht finden und die
Next(board)
Funktion leicht umkehren können, sehen wir, dass dies einige Vorteile hat, wenn auch nicht so viele, wie ich ursprünglich gehofft hatte.
- Wir kümmern uns überhaupt nicht um divergierende Boards. Keine große Ersparnis, weil sie ziemlich selten sind.
- Wir müssen nicht für alle Boards Scores speichern, daher sollte der Speicherdruck geringer sein als beim Forward-DP-Ansatz.
- Rückwärts zu arbeiten ist eigentlich recht einfach, wenn ich eine Technik, die ich in der Literatur gesehen habe, im Kontext der Aufzählung von Stillleben verändere. Es funktioniert, indem jede Spalte als Buchstabe in einem Alphabet behandelt wird und dann beobachtet wird, dass eine Folge von drei Buchstaben den mittleren Buchstaben in der nächsten Generation bestimmt. Die Parallele zur Aufzählung von Stillleben ist so eng, dass ich sie zu einer nur geringfügig umständlichen Methode zusammengefügt habe
Prev2
.
- Es könnte den Anschein haben, als könnten wir einfach die Stillleben kanonisieren und etwas wie die 8N ^ 2-Beschleunigung zu sehr geringen Kosten erhalten. Empirisch gesehen lässt sich die Anzahl der berücksichtigten Boards jedoch immer noch stark reduzieren, wenn wir bei jedem Schritt eine Kanonisierung vornehmen.
- Ein überraschend hoher Anteil der Boards hat eine Punktzahl von 2 oder 3, so dass immer noch Speicherdruck besteht. Ich fand es notwendig, spontan zu kanonisieren, anstatt die vorherige Generation aufzubauen und dann zu kanonisieren. Es könnte interessant sein, die Speichernutzung durch eine Tiefensuche anstelle einer Breitensuche zu reduzieren, aber ohne den Stapel zu überlaufen und ohne redundante Berechnungen zu machen, ist ein Co-Routine- / Fortsetzungsansatz für die Aufzählung der vorherigen Karten erforderlich.
Ich glaube nicht, dass ich durch die Mikrooptimierung mit dem Code von Keith Schritt halten kann, aber aus Gründen des Interesses werde ich posten, was ich habe. Dies löst N = 5 in ungefähr einer Minute auf einer 2-GHz-Maschine unter Verwendung von Mono 2.4 oder .Net (ohne PLINQ) und in ungefähr 20 Sekunden unter Verwendung von PLINQ; N = 6 läuft viele Stunden.
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));
}
}
}