Saya melihat kemungkinan pendekatan solusi berikut:
- Teori berat. Saya tahu ada beberapa literatur tentang Kehidupan pada torus, tetapi saya belum banyak membaca.
- Maju dengan kasar: untuk setiap papan yang memungkinkan, periksa nilainya. Ini pada dasarnya adalah apa yang dilakukan oleh pendekatan Matthew dan Keith, meskipun Keith mengurangi jumlah papan untuk diperiksa dengan faktor 4.
- Optimasi: representasi kanonik. Jika kita dapat memeriksa apakah papan dalam representasi kanonik jauh lebih cepat daripada yang diperlukan untuk mengevaluasi nilainya, kita mendapatkan percepatan faktor sekitar 8N ^ 2. (Ada juga pendekatan parsial dengan kelas ekivalensi yang lebih kecil).
- Optimasi: DP. Cache skor untuk setiap papan, sehingga alih-alih menuntun mereka sampai mereka menyatu atau menyimpang kita hanya berjalan sampai kita menemukan papan yang telah kita lihat sebelumnya. Pada prinsipnya ini akan memberi percepatan dengan faktor skor rata-rata / panjang siklus (mungkin 20 atau lebih), tetapi dalam praktiknya kita cenderung bertukar berat. Misalnya untuk N = 6 kita membutuhkan kapasitas untuk 2 ^ 36 skor, yang pada byte per skor adalah 16GB, dan kita membutuhkan akses acak sehingga kita tidak dapat mengharapkan lokasi cache yang baik.
- Kombinasikan keduanya. Untuk N = 6, representasi kanonik lengkap akan memungkinkan kita untuk mengurangi cache DP menjadi sekitar 60 mega-skor. Ini adalah pendekatan yang menjanjikan.
- Brute force mundur. Ini kelihatannya aneh pada awalnya, tetapi jika kita berasumsi bahwa kita dapat dengan mudah menemukan
Next(board)
benda mati dan bahwa kita dapat dengan mudah membalik fungsi, kita melihat bahwa ia memiliki beberapa manfaat, meskipun tidak sebanyak yang saya harapkan semula.
- Kami tidak peduli dengan papan yang menyimpang sama sekali. Tidak banyak menghemat, karena mereka cukup langka.
- Kami tidak perlu menyimpan skor untuk semua papan, jadi harus ada lebih sedikit tekanan memori daripada pendekatan DP maju.
- Bekerja mundur sebenarnya cukup mudah dengan memvariasikan teknik yang saya lihat dalam literatur dalam konteks menghitung masih hidup. Ini bekerja dengan memperlakukan setiap kolom sebagai huruf dalam alfabet dan kemudian mengamati bahwa urutan tiga huruf menentukan yang tengah pada generasi berikutnya. Paralel dengan menghitung umur masih sangat dekat sehingga saya telah refactored mereka bersama menjadi metode yang hanya sedikit canggung
Prev2
,.
- Tampaknya kita dapat mengkanonisasi kehidupan diam, dan mendapatkan kecepatan seperti 8N ^ 2 dengan biaya yang sangat kecil. Namun, secara empiris kami masih mendapatkan pengurangan besar dalam jumlah papan yang dipertimbangkan jika kami dapat membuat secara resmi pada setiap langkah.
- Proporsi papan yang sangat tinggi memiliki skor 2 atau 3, sehingga masih ada tekanan memori. Saya merasa perlu untuk dikanonikalisasi dengan cepat daripada membangun generasi sebelumnya dan kemudian dikanonikalisasi. Mungkin menarik untuk mengurangi penggunaan memori dengan melakukan pencarian depth-first daripada breadth-first, tetapi melakukan hal itu tanpa meluap tumpukan dan tanpa melakukan perhitungan berlebihan memerlukan pendekatan co-rutin / lanjutan untuk menghitung papan sebelumnya.
Saya tidak berpikir bahwa optimasi mikro akan membiarkan saya mengejar kode Keith, tetapi demi kepentingan saya akan memposting apa yang saya miliki. Ini memecahkan N = 5 dalam waktu sekitar satu menit pada mesin 2GHz menggunakan Mono 2.4 atau .Net (tanpa PLINQ) dan dalam sekitar 20 detik menggunakan PLINQ; N = 6 berjalan selama berjam-jam.
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));
}
}
}