C99 - tablica 3x3 w 0,084s
Edycja: Zmodyfikowałem kod i przeprowadziłem głębszą analizę wyników.
Dalsze zmiany: Dodano przycinanie przez symetrie. To sprawia, że 4 konfiguracje algorytmów: z lub bez symetrii X z przycinaniem alfa-beta lub bez
Najdalsze edycje : Dodano zapamiętywanie za pomocą tabeli skrótów, w końcu osiągnięcie niemożliwego: rozwiązanie planszy 3x3!
Główne cechy:
- prosta implementacja minimax z przycinaniem alfa-beta
- bardzo mało zarządzania pamięcią (utrzymuje dll prawidłowych ruchów; aktualizacje O (1) na gałąź w wyszukiwaniu drzewa)
- drugi plik z przycinaniem przez symetrie. Nadal osiąga aktualizacje O (1) na gałąź (technicznie O (S), gdzie S jest liczbą symetrii. Jest to 7 dla kwadratowych desek i 3 dla desek nie kwadratowych)
- trzeci i czwarty plik dodają zapamiętywanie. Masz kontrolę nad rozmiarem tablicy mieszającej (
#define HASHTABLE_BITWIDTH
). Gdy ten rozmiar jest większy lub równy liczbie ścian, gwarantuje to brak kolizji i aktualizacji O (1). Mniejsze tablice skrótów będą miały więcej kolizji i będą nieco wolniejsze.
- Połącz z
-DDEBUG
wydrukami
Potencjalne ulepszenia:
naprawić mały wyciek pamięci naprawiony podczas pierwszej edycji
przycinanie alfa / beta dodano w 2. edycji
przycinamy symetrie dodane w 3. edycji (zauważ, że symetrie nie są obsługiwane przez zapamiętywanie, więc pozostaje to osobną optymalizacją).
zapamiętywanie dodane w 4. edycji
- obecnie memoizacja używa bitu wskaźnika dla każdej ściany. Tablica 3x4 ma 31 ścian, więc ta metoda nie poradzi sobie z tablicami 4x4 niezależnie od ograniczeń czasowych. poprawa polegałaby na emulacji liczb całkowitych X-bit, gdzie X jest co najmniej tak duża, jak liczba ścian.
Kod
Z powodu braku organizacji liczba plików wymknęła się spod kontroli. Cały kod został przeniesiony do tego repozytorium Github . W edycji notatki dodałem plik makefile i skrypt testowy.
Wyniki
Uwagi na temat złożoności
Podejścia brutalnych sił do kropek i pudełek bardzo szybko się komplikują .
Rozważ tablicę z R
rzędami i C
kolumnami. Istnieją R*C
kwadraty, R*(C+1)
ściany pionowe i C*(R+1)
ściany poziome. To w sumieW = 2*R*C + R + C
.
Ponieważ Lembik poprosił nas o rozwiązanie gry z minimaxem, musimy przejść do liści drzewa gry. Na razie zignorujmy przycinanie, ponieważ liczy się rząd wielkości.
Istnieją W
opcje pierwszego ruchu. Dla każdego z nich następny gracz może zagrać w dowolną z W-1
pozostałych ścian itp. To daje nam pole przeszukiwania SS = W * (W-1) * (W-2) * ... * 1
, lub SS = W!
. Czynniki są ogromne, ale to dopiero początek. SS
to liczba węzłów liści w przestrzeni wyszukiwania. Bardziej istotna dla naszej analizy jest całkowita liczba decyzji, które musiały zostać podjęte (tj. Liczba gałęzi B
drzewa). Pierwsza warstwa gałęzi ma W
opcje. Dla każdego z nich następny poziom ma W-1
itp.
B = W + W*(W-1) + W*(W-1)*(W-2) + ... + W!
B = SUM W!/(W-k)!
k=0..W-1
Spójrzmy na kilka małych rozmiarów stołu:
Board Size Walls Leaves (SS) Branches (B)
---------------------------------------------------
1x1 04 24 64
1x2 07 5040 13699
2x2 12 479001600 1302061344
2x3 17 355687428096000 966858672404689
Te liczby stają się śmieszne. Przynajmniej wyjaśniają, dlaczego kod brutalnej siły wydaje się zawieszać na zawsze na płycie 2x3. Przestrzeń wyszukiwania na tablicy 2x3 jest 742560 razy większa niż 2x2 . Jeśli wykonanie 2x2 zajmuje 20 sekund, zachowawcza ekstrapolacja przewiduje ponad 100 dni czasu wykonania dla 2x3. Oczywiście musimy przycinać.
Analiza przycinania
Zacząłem od dodania bardzo prostego przycinania przy użyciu algorytmu alfa-beta. Zasadniczo przestaje szukać, czy idealny przeciwnik nigdy nie dałby mu obecnych możliwości. „Hej, spójrz - dużo wygrywam, jeśli mój przeciwnik pozwala mi zdobyć każdy kwadrat!”, Pomyślał, że nie ma AI.
edytuj Dodałem również przycinanie oparte na symetrycznych tablicach. Nie używam metody zapamiętywania, na wszelki wypadek, gdy pewnego dnia dodam zapamiętywanie i chcę oddzielić tę analizę od siebie. Zamiast tego działa to tak: większość linii ma „parę symetryczną” gdzieś na siatce. Istnieje do 7 symetrii (pozioma, pionowa, obrót o 180 stopni, obrót o 90 stopni, obrót o 270 stopni, przekątna i druga przekątna). Wszystkie 7 dotyczą desek kwadratowych, ale ostatnie 4 nie dotyczą desek kwadratowych. Każda ściana ma wskaźnik do „pary” dla każdej z tych symetrii. Jeśli wchodząc w zakręt, plansza jest symetryczna poziomo, wówczas należy zagrać tylko jedną z każdej pary poziomej .
edytuj edytuj Zapamiętywanie! Każda ściana ma unikalny identyfikator, który wygodnie ustawiam jako bit wskaźnikowy; n-ta ściana ma identyfikator 1 << n
. Skrót tablicy jest więc tylko salą wszystkich granych ścian. Jest to aktualizowane w każdym oddziale w czasie O (1). Rozmiar tablicy hashtable jest ustawiony na #define
. Wszystkie testy zostały przeprowadzone z rozmiarem 2 ^ 12, ponieważ dlaczego nie? Gdy jest więcej ścian niż bitów indeksujących tablicę mieszającą (w tym przypadku 12 bitów), najmniej znaczące 12 jest maskowane i używane jako indeks. Kolizje są obsługiwane za pomocą połączonej listy w każdym indeksie tablicy mieszającej. Poniższa tabela to moja szybka i brudna analiza wpływu wielkości tabeli mieszającej na wydajność. Na komputerze z nieskończoną pamięcią RAM zawsze ustawialiśmy rozmiar stołu na liczbę ścian. Tablica 3x4 miałaby tablicę mieszającą o długości 2 ^ 31. Niestety nie mamy tego luksusu.
Ok, wracając do przycinania. Zatrzymując wyszukiwanie wysoko w drzewie, możemy zaoszczędzić dużo czasu, nie schodząc do liści. „Czynnik przycinania” to ułamek wszystkich możliwych gałęzi, które musieliśmy odwiedzić. Brute-force ma współczynnik przycinania równy 1. Im jest mniejszy, tym lepiej.