Najdłuższa, nie powtarzająca się sekwencja Game-of-Life


16

Biorąc pod uwagę dodatnią liczbę całkowitą N, określ wzór początkowy na siatce N x N, która daje najdłuższą niepowtarzalną sekwencję zgodnie z Regułami Gry Życia, a kończy się stałym wzorem (cykl długości 1), rozgrywanym na torusie.

Celem nie jest najkrótszy program, ale najszybszy.

Ponieważ świat jest skończony, ostatecznie skończysz w pętli, powtarzając w ten sposób już odwiedzony stan. Jeśli ta pętla ma okres 1, wówczas wzorzec początkowy jest prawidłowym kandydatem.

Dane wyjściowe: wzorzec początkowy i całkowita liczba unikalnych stanów w sekwencji (w tym wzorzec początkowy).

Teraz torus 1x1 jest wyjątkowy, ponieważ komórkę można uznać za sąsiadującą z nią lub nie, ale w praktyce nie ma problemu, jedna żywa komórka w obu przypadkach po prostu umrze (z powodu przeludnienia lub samotności). Zatem dane wejściowe 1 dają sekwencję o długości 2, przy czym sekwencja jest jedną komórką żyjącą, a następnie na zawsze martwą.

Motywacja tego pytania polega na tym, że jest to odpowiednik funkcji zajętego bobra, ale zdecydowanie mniej skomplikowane, ponieważ mamy ograniczoną pamięć. Będzie to również niezła sekwencja do włączenia w OEIS.

Dla N = 3 długość sekwencji wynosi 3, dowolny wzór po lewej stronie osiąga całkowicie czarny kwadrat 3 x 3, a następnie umiera. (Wszystkie wzory, które są częścią 1 cyklu usunięte).

Wykres stanów


1
Ach, w porządku. Najlepszy kod to ten, który potrafi obliczyć długość sekwencji dla największego N w ciągu, powiedzmy, 2 godzin. Oczywista złożoność to 2 ^ (N ^ 2), więc jeśli da się to poprawić, byłoby miło.
Per Alexandersson,

1
Przy nietrywialnych rozmiarach każdy wzór jest częścią klasy izomorficznej 8N ^ 2 wzorów, więc jeśli istnieje szybki sposób kanonizacji, daje to umiarkowane wzmocnienie.
Peter Taylor

1
Czy ta sekwencja została dodana do OEIS?
mbomb007,

1
Nie, że jestem tego świadomy, chętnie go tam zobaczę.
Per Alexandersson,

1
Przekazałem tę sekwencję (2, 2, 3, 10, 52, 91) do OEIS jako A294241 .
Peter Kagey,

Odpowiedzi:


13

C ++ do 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                                                         

Ta technika koncentruje się wokół szybkiej funkcji następnego stanu. Każda tablica jest reprezentowana jako maska ​​bitowa N ^ 2 bitów, a do obliczenia następnego stanu dla wszystkich komórek jednocześnie wykorzystywane są sztuczki typu „bit-twiddly”. nextkompiluje do około 200 100 instrukcji montażu dla N <= 8. Następnie wykonujemy standardowe try-all-stany-aż-one się powtarzają i wybieramy najdłuższą.

Trwa kilka sekund do 5x5, kilka godzin dla 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);
    }
  }
}

1
Możesz uzyskać umiarkowaną poprawę, nextlicząc, a nie sortując. #define H(x,y){x^=y;y&=(x^y);}a potem coś w styluH(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

Naprawdę fajne rozwiązanie!
Per Alexandersson

@PeterTaylor: dzięki, zaimplementowałem liczenie (inny schemat), zapisałem mnóstwo instrukcji.
Keith Randall

9

Widzę następujące możliwe rozwiązania:

  • Ciężka teoria. Wiem, że jest trochę literatury na temat życia na torusie, ale nie przeczytałem zbyt wiele.
  • Brute force forward: dla każdej możliwej planszy sprawdź jej wynik. Tak właśnie robią podejścia Matthew i Keitha, chociaż Keith zmniejsza liczbę plansz do sprawdzenia czterokrotnie.
    • Optymalizacja: reprezentacja kanoniczna. Jeśli możemy sprawdzić, czy tablica jest reprezentacją kanoniczną znacznie szybciej niż potrzeba do oceny jej wyniku, otrzymamy przyspieszenie o współczynnik około 8N ^ 2. (Istnieją również podejścia częściowe z mniejszymi klasami równoważności).
    • Optymalizacja: DP. Przechowuj w pamięci podręcznej wynik dla każdej planszy, aby zamiast przechodzić przez nią, aż się zbiegną lub rozejdą, po prostu idź, aż znajdziemy tablicę, którą widzieliśmy wcześniej. Zasadniczo dałoby to przyspieszenie o współczynnik średniej długości wyniku / cyklu (może 20 lub więcej), ale w praktyce prawdopodobnie będziemy mocno zamieniać. Np. Dla N = 6 potrzebowalibyśmy pojemności na 2 ^ 36 wyników, co przy bajcie na wynik to 16 GB, i potrzebujemy dostępu losowego, więc nie możemy oczekiwać dobrej lokalizacji pamięci podręcznej.
    • Połącz dwa. Dla N = 6 pełna reprezentacja kanoniczna pozwoliłaby nam zredukować pamięć podręczną DP do około 60 megapunktów. To obiecujące podejście.
  • Brutalna siła do tyłu. Z początku wydaje się to dziwne, ale jeśli założymy, że możemy łatwo znaleźć martwe natury i że możemy łatwo odwrócić Next(board)funkcję, widzimy, że ma ona pewne zalety, chociaż nie tak wiele, jak początkowo miałem nadzieję.
    • W ogóle nie zawracamy sobie głowy rozbieżnymi deskami. Niewiele oszczędności, ponieważ są dość rzadkie.
    • Nie musimy przechowywać wyników dla wszystkich płyt, więc presja pamięci powinna być mniejsza niż podejście DP do przodu.
    • Praca wstecz jest właściwie dość łatwa, zmieniając technikę, którą widziałem w literaturze w kontekście wyliczania martwych natur. Działa, traktując każdą kolumnę jako literę w alfabecie, a następnie obserwując, że ciąg trzech liter określa środkową w następnym pokoleniu. Równolegle z wyliczanie martwe natury jest tak blisko, że już refactored je razem w metodzie tylko nieco niewygodne, Prev2.
    • Mogłoby się wydawać, że możemy po prostu kanonizować martwe natury i uzyskać coś w rodzaju przyspieszenia 8N ^ 2 za niewielką opłatą. Jednak empirycznie nadal uzyskujemy znaczną redukcję liczby rozpatrywanych plansz, jeśli kanonizujemy na każdym etapie.
    • Zaskakująco wysoki odsetek płyt ma wynik 2 lub 3, więc nadal występuje presja pamięci. Uznałem za konieczne kanonizowanie w locie zamiast budowania poprzedniej generacji, a następnie kanonizowanie. Interesujące może być zmniejszenie zużycia pamięci przez wyszukiwanie w pierwszej kolejności, a nie w pierwszej kolejności, ale robienie tego bez przepełnienia stosu i bez przeprowadzania zbędnych obliczeń wymaga podejścia rutynowego / kontynuacji do wyliczania poprzednich płyt.

Nie sądzę, że mikrooptymalizacja pozwoli mi dogonić kod Keitha, ale ze względu na zainteresowanie opublikuję to, co mam. To rozwiązuje N = 5 w ciągu około minuty na maszynie 2 GHz przy użyciu Mono 2.4 lub .Net (bez PLINQ) i po około 20 sekundach przy użyciu PLINQ; N = 6 uruchomień przez wiele godzin.

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));
        }
    }
}

Pracuję również nad inną wersją, aby cofać się od ustalonych punktów. Wyliczyłem już punkty stałe do N = 8 (dla N = 8 jest ich 84396613 przed kanonizacją). Mam prosty poprzedni działający, ale jest zbyt wolny. Częścią problemu są tylko rozmiary rzeczy, dla N = 6 pusta tablica ma 574384901 poprzedników (przed kanonizacją).
Keith Randall

1
3 dni i 11 godzin, aby potwierdzić, że 91 to odpowiedź na 6x6.
Peter Taylor

1

Czynnik

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 ;

Niektóre statystyki czasowe:

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 } }
}

A testowanie 5 rozbiło REPL. Hmph. Najbardziej nieefektywną częścią programu jest prawdopodobnie funkcja sekwencji gry. Może później będę mógł to poprawić.


Chłodny! Myślę, że twój wynik jest o 1 za duży, dla wzorców 3x3 najdłuższa niepowtarzalna sekwencja ma długość 3, a nie 4 ...
Per Alexandersson
Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.