Tôi thấy các cách tiếp cận giải pháp có thể sau đây:
- Lý thuyết nặng nề. Tôi biết có một số tài liệu về Cuộc sống trên một hình xuyến, nhưng tôi chưa đọc nhiều về nó.
- Brute force về phía trước: cho mỗi bảng có thể, kiểm tra điểm số của nó. Về cơ bản, đây là những gì phương pháp của Matthew và Keith làm, mặc dù Keith giảm số lượng bảng để kiểm tra theo hệ số 4.
- Tối ưu hóa: đại diện chính tắc. Nếu chúng ta có thể kiểm tra xem một bảng có đại diện chính tắc nhanh hơn nhiều so với đánh giá điểm của nó hay không, chúng ta sẽ tăng tốc độ của một hệ số khoảng 8N ^ 2. (Cũng có cách tiếp cận một phần với các lớp tương đương nhỏ hơn).
- Tối ưu hóa: DP. Lưu trữ điểm số cho mỗi bảng, vì vậy thay vì đi qua chúng cho đến khi chúng hội tụ hoặc phân kỳ chúng ta chỉ cần đi bộ cho đến khi chúng ta tìm thấy một bảng mà chúng ta đã thấy trước đó. Về nguyên tắc, điều này sẽ giúp tăng tốc theo hệ số điểm trung bình / độ dài chu kỳ (có thể từ 20 trở lên), nhưng trong thực tế, chúng ta có khả năng bị tráo đổi mạnh. Ví dụ: với N = 6, chúng tôi cần dung lượng cho 2 ^ 36 điểm, với một byte cho mỗi điểm là 16 GB và chúng tôi cần truy cập ngẫu nhiên để chúng tôi không thể mong đợi địa phương bộ đệm tốt.
- Kết hợp cả hai. Với N = 6, biểu diễn chính tắc đầy đủ sẽ cho phép chúng tôi giảm bộ đệm DP xuống còn khoảng 60 điểm. Đây là một cách tiếp cận đầy hứa hẹn.
- Lực lượng vũ phu ngược. Điều này có vẻ kỳ lạ lúc đầu, nhưng nếu chúng ta giả sử rằng chúng ta có thể dễ dàng tìm thấy sự sống và chúng ta có thể dễ dàng đảo ngược
Next(board)
chức năng, chúng ta thấy rằng nó có một số lợi ích, mặc dù không nhiều như tôi mong đợi ban đầu.
- Chúng tôi không bận tâm với bảng phân kỳ ở tất cả. Không tiết kiệm nhiều, vì chúng khá hiếm.
- Chúng ta không cần lưu trữ điểm số cho tất cả các bảng, do đó sẽ có ít áp lực bộ nhớ hơn so với phương pháp DP chuyển tiếp.
- Làm việc ngược lại thực sự khá dễ dàng bằng cách thay đổi một kỹ thuật mà tôi đã thấy trong văn học trong bối cảnh liệt kê các cuộc sống vẫn còn. Nó hoạt động bằng cách coi mỗi cột là một chữ cái trong bảng chữ cái và sau đó quan sát rằng một chuỗi gồm ba chữ cái xác định chữ cái ở giữa trong thế hệ tiếp theo. Song song với việc liệt kê các cuộc sống vẫn còn rất gần mà tôi đã tái cấu trúc chúng lại với nhau thành một phương pháp hơi khó xử,
Prev2
.
- Có vẻ như chúng ta chỉ có thể chuẩn hóa các cuộc sống tĩnh lặng và có được thứ gì đó như tăng tốc 8N ^ 2 với chi phí rất thấp. Tuy nhiên, theo kinh nghiệm, chúng tôi vẫn nhận được một sự giảm mạnh về số lượng bảng được xem xét nếu chúng tôi hợp quy hóa ở mỗi bước.
- Một tỷ lệ cao đáng ngạc nhiên của các bảng có điểm 2 hoặc 3, do đó vẫn có áp lực bộ nhớ. Tôi thấy cần phải chuẩn hóa khi đang bay hơn là xây dựng thế hệ trước và sau đó chuẩn hóa. Có thể rất thú vị để giảm mức sử dụng bộ nhớ bằng cách thực hiện tìm kiếm theo chiều sâu thay vì tìm kiếm theo chiều rộng, nhưng làm như vậy mà không làm tràn ngăn xếp và không thực hiện các phép tính dự phòng đòi hỏi cách tiếp cận đồng quy / tiếp tục để liệt kê các bảng trước đó.
Tôi không nghĩ rằng tối ưu hóa vi mô sẽ cho phép tôi bắt kịp mã của Keith, nhưng vì lợi ích tôi sẽ đăng những gì tôi có. Điều này giải quyết N = 5 trong khoảng một phút trên máy 2GHz sử dụng Mono 2.4 hoặc .Net (không có PLINQ) và trong khoảng 20 giây sử dụng PLINQ; N = 6 chạy trong nhiều giờ.
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));
}
}
}