Urutan Game-of-Life terpanjang yang tidak berulang

16

Diberikan bilangan bulat positif N, tentukan pola awal pada kisi-kisi N x N yang menghasilkan urutan non-berulang terpanjang di bawah Game of Life-rules, dan berakhir dengan pola tetap (siklus panjang 1), dimainkan pada torus.

Tujuannya bukan program terpendek, tetapi tercepat.

Karena dunia terbatas, Anda pada akhirnya akan berakhir dalam satu lingkaran, sehingga mengulangi keadaan yang sudah dikunjungi. Jika loop ini memiliki periode 1, maka pola awal adalah kandidat yang valid.

Output: Pola awal dan jumlah total status unik dalam urutan (termasuk pola awal).

Sekarang, 1x1-torus adalah spesial, karena sebuah sel dapat dianggap bertetangga dengan dirinya sendiri atau tidak, tetapi dalam praktiknya, tidak ada masalah, sel hidup tunggal akan mati (karena kepadatan atau kesepian). Jadi, input 1 menghasilkan urutan panjang 2, urutannya adalah satu sel yang hidup, lalu mati selamanya.

Motivasi untuk pertanyaan ini adalah yang merupakan analog dari fungsi berang-berang yang sibuk, tetapi jelas kurang kompleks, karena kita memiliki ingatan. Ini juga akan menjadi urutan yang bagus untuk disertakan pada OEIS.

Untuk N = 3, panjang urutannya adalah 3, pola apa pun di sisi kiri mencapai kotak hitam 3x3, dan kemudian mati. (Semua pola yang merupakan bagian dari 1 siklus dihapus).

Grafik negara

Per Alexandersson
sumber
1
Ah, baiklah. Kode terbaik adalah kode yang berhasil menghitung panjang urutan untuk N terbesar dalam waktu, katakanlah 2 jam. Kompleksitas yang jelas adalah 2 ^ (N ^ 2), jadi jika memungkinkan untuk meningkatkan ini, ini akan menyenangkan.
Per Alexandersson
1
Pada ukuran non-sepele, masing-masing pola adalah bagian dari kelas isomorfisme pola 8N ^ 2, jadi jika ada cara cepat mengkanonisasi maka itu memberikan dorongan moderat.
Peter Taylor
1
Apakah urutan ini telah ditambahkan ke OEIS?
mbomb007
1
Bukannya aku sadar, akan senang melihatnya di sana.
Per Alexandersson
1
Saya telah mengirimkan urutan ini (2, 2, 3, 10, 52, 91) ke OEIS sebagai A294241 .
Peter Kagey

Jawaban:

13

C ++ hingga N = 6

3x3 answer 3: 111 000 000                                                                                        
4x4 answer 10: 1110 0010 1100 0000                                                                               
5x5 answer 52: 11010 10000 11011 10100 00000                                                                     
6x6 answer 91: 100011 010100 110011 110100 101000 100000                                                         

Teknik ini berpusat di sekitar fungsi keadaan cepat berikutnya. Setiap papan direpresentasikan sebagai bitmask N ^ 2 bit, dan trik bit-twiddly digunakan untuk menghitung status berikutnya untuk semua sel sekaligus. nextmengkompilasi hingga sekitar 200 100 instruksi perakitan untuk N <= 8. Kemudian kita lakukan standar coba-semua-keadaan-sampai-mereka-ulangi dan pilih yang terpanjang.

Memakan waktu beberapa detik hingga 5x5, beberapa jam untuk 6x6.

#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <algorithm>
using namespace std;

#define N 6

typedef uint64_t board;

// board is N bits by N bits, with indexes like this (N=4):                                                        
//  0  1  2  3                                                                                                     
//  4  5  6  7                                                                                                     
//  8  9 10 11                                                                                                     
// 12 13 14 15                                                                                                     

#if N==3
#define LEFT_COL (1 + (1<<3) + (1<<6))
#define RIGHT_COL ((1<<2) + (1<<5) + (1<<8))
#define ALL 0x1ffULL
#elif N==4
#define LEFT_COL 0x1111ULL
#define RIGHT_COL 0x8888ULL
#define ALL 0xffffULL
#elif N==5
#define LEFT_COL (1ULL + (1<<5) + (1<<10) + (1<<15) + (1<<20))
#define RIGHT_COL ((1ULL<<4) + (1<<9) + (1<<14) + (1<<19) + (1<<24))
#define ALL 0x1ffffffULL
#elif N==6
#define LEFT_COL (1 + (1<<6) + (1<<12) + (1<<18) + (1<<24) + (1ULL<<30))
#define RIGHT_COL ((1<<5) + (1<<11) + (1<<17) + (1<<23) + (1<<29) + (1ULL<<35))
#define ALL 0xfffffffffULL
#else
#error "bad N"
#endif

static inline board north(board b) {
  return (b >> N) + (b << N*N-N);
}
static inline board south(board b) {
  return (b << N) + (b >> N*N-N);
}
static inline board west(board b) {
  return ((b & ~LEFT_COL) >> 1) + ((b & LEFT_COL) << N-1);
}
static inline board east(board b) {
  return ((b & ~RIGHT_COL) << 1) + ((b & RIGHT_COL) >> N-1);
}

board next(board b) {
  board n1 = north(b);
  board n2 = south(b);
  board n3 = west(b);
  board n4 = east(b);
  board n5 = north(n3);
  board n6 = north(n4);
  board n7 = south(n3);
  board n8 = south(n4);

  // add all the bits bitparallel-y to a 2-bit accumulator with overflow
  board a0 = 0;
  board a1 = 0;
  board overflow = 0;
#define ADD(x) overflow |= a0 & a1 & x; a1 ^= a0 & x; a0 ^= x;

  a0 = n1; // no carry yet
  a1 ^= a0 & n2; a0 ^= n2; // no overflow yet
  a1 ^= a0 & n3; a0 ^= n3; // no overflow yet
  ADD(n4);
  ADD(n5);
  ADD(n6);
  ADD(n7);
  ADD(n8);
  return (a1 & (b | a0)) & ~overflow & ALL;
}
void print(board b) {
  for (int i = 0; i < N; i++) {
    for (int j = 0; j < N; j++) {
      printf("%d", (int)(b >> i*N+j & 1));
    }
    printf(" ");
  }
  if (b >> N*N) printf("*");
  printf("\n");
}

int main(int argc, char *argv[]) {
  // Somewhere in the starting pattern there are a 1 and 0 together.  Using translational                          
  // and rotational symmetry, we can put these in the first two bits.  So we need only consider                    
  // 1 mod 4 boards.                                                                                               

  board steps[10000];
  int maxsteps = -1;
  for (board b = 1; b < 1ULL << N*N; b += 4) {
    int nsteps = 0;
    board x = b;
    while (true) {
      int repeat = steps + nsteps - find(steps, steps + nsteps, x);
      if (repeat > 0) {
        if (repeat == 1 && nsteps > maxsteps) {
          printf("%d: ", nsteps);
          print(b);
          maxsteps = nsteps;
        }
        break;
      }
      steps[nsteps++] = x;
      x = next(x);
    }
  }
}
Keith Randall
sumber
1
Anda mungkin mendapatkan peningkatan moderat nextdengan menghitung daripada menyortir. #define H(x,y){x^=y;y&=(x^y);}dan kemudian sesuatu sepertiH(n1,n2);H(n3,n4);H(n5,n6);H(n7,n8);H(n1,n3);H(n5,n7);H(n2,n4);H(n6,n8);H(n1,n5);H(n3,n7);H(n2,n6);H(n2,n3);H(n2,n5); return n2 & (b | n1) & ~(n3|n4|n5|n6|n7|n8) & ALL;
Peter Taylor
Solusi yang sangat keren!
Per Alexandersson
@PeterTaylor: terima kasih, saya menerapkan (skema berbeda untuk) menghitung, menyimpan banyak instruksi.
Keith Randall
9

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));
        }
    }
}
Peter Taylor
sumber
Saya juga sedang mengerjakan versi lain untuk berjalan mundur dari titik-titik tertentu. Saya sudah menghitung poin tetap hingga N = 8 (untuk N = 8 ada 84396613 di antaranya sebelum kanonikisasi). Saya punya pekerjaan prev sederhana, tapi terlalu lambat. Sebagian dari masalahnya hanyalah ukuran benda, untuk N = 6 papan kosong memiliki 574384901 pendahulu (sebelum kanonikisasi).
Keith Randall
1
3 hari dan 11 jam untuk mengonfirmasi bahwa 91 adalah jawaban untuk 6x6.
Peter Taylor
1

Faktor

USING: arrays grouping kernel locals math math.functions math.parser math.order math.ranges math.vectors sequences sequences.extras ;
IN: longest-gof-pattern

:: neighbors ( x y game -- neighbors )
game length :> len 
x y game -rot 2array {
    { -1 -1 }
    { -1 0 }
    { -1 1 }
    { 0 -1 }
    { 0 1 }
    { 1 -1 }
    { 1 0 }
    { 1 1 }
} [
    v+ [
        dup 0 <
        [ dup abs len mod - abs len mod ] [ abs len mod ]
        if
    ] map
] with map [ swap [ first2 ] dip nth nth ] with map ;

: next ( game -- next )
dup [
    [
        neighbors sum
        [ [ 1 = ] [ 2 3 between? ] bi* and ]
        [ [ 0 = ] [ 3 = ] bi* and ] 2bi or 1 0 ?
    ] curry curry map-index
] curry map-index ;

: suffixes ( seq -- suffixes )
{ }
[ [ [ suffix ] curry map ] [ 1array 1array ] bi append ]
reduce ;

! find largest repeating pattern
: LRP ( seq -- pattern )
dup length iota
[ 1 + [ reverse ] dip group [ reverse ] map reverse ] with
map dup [ dup last [ = ] curry map ] map
[ suffixes [ t [ and ] reduce ] map [ ] count ] map
dup supremum [ = ] curry find drop swap nth last ;

: game-sequence ( game -- seq )
1array [
    dup [
        dup length 2 >
        [ 2 tail-slice* [ first ] [ last ] bi = not ]
        [ drop t ] if
    ] [ LRP length 1 > not ] bi and
] [ dup last next suffix ] while ;

: pad-to-with ( str len padstr -- rstr )
[ swap dup length swapd - ] dip [ ] curry replicate ""
[ append ] reduce prepend ;

:: all-NxN-games ( n -- games )
2 n sq ^ iota [
    >bin n sq "0" pad-to-with n group
    [ [ 48 = 0 1 ? ] { } map-as ] map
] map ;

: longest-gof-pattern ( n -- game )
all-NxN-games [ game-sequence ] map [ length ] supremum-by but-last ;

Beberapa statistik waktu:

IN: longest-gof-pattern [ 3 longest-gof-pattern ] time dup length . . 
Running time: 0.08850873500000001 seconds

3
{
   { { 1 1 1 } { 0 0 0 } { 0 0 0 } }
   { { 1 1 1 } { 1 1 1 } { 1 1 1 } }
   { { 0 0 0 } { 0 0 0 } { 0 0 0 } }
}

IN: longest-gof-pattern [ 4 longest-gof-pattern ] time dup length . . 
Running time: 49.667698828 seconds

10
{
  { { 0 1 1 0 } { 0 1 0 0 } { 0 1 0 0 } { 1 1 0 1 } }
  { { 0 1 1 0 } { 0 1 0 0 } { 0 1 0 0 } { 0 0 0 1 } }
  { { 0 1 1 0 } { 0 1 0 0 } { 0 0 1 0 } { 1 1 0 0 } }
  { { 0 1 1 0 } { 0 1 0 0 } { 0 0 1 0 } { 0 0 0 1 } }
  { { 0 1 1 0 } { 0 1 0 0 } { 0 0 1 0 } { 1 1 0 1 } }
  { { 0 1 1 0 } { 0 1 0 0 } { 0 0 1 1 } { 0 0 0 1 } }
  { { 0 1 0 1 } { 0 1 0 1 } { 0 0 1 1 } { 1 1 0 1 } }
  { { 1 1 0 1 } { 1 1 0 1 } { 0 0 0 0 } { 1 1 0 0 } }
  { { 1 1 0 1 } { 1 1 0 1 } { 0 0 1 1 } { 1 1 1 1 } }
  { { 0 0 0 0 } { 0 0 0 0 } { 0 0 0 0 } { 0 0 0 0 } }
}

Dan pengujian 5 crash REPL. Hmph. Bagian yang paling tidak efisien dari program ini mungkin adalah urutan fungsi permainan. Saya mungkin bisa membuatnya lebih baik nanti.

Matthew Rolph
sumber
Keren! Saya pikir output Anda 1 terlalu besar, untuk pola 3x3, urutan non-berulang terpanjang memiliki panjang 3, bukan 4 ...
Per Alexandersson