This document is also available in English. Dieses Dokument ist auch auf Deutsch erhältlich.

Marko Mäkelä: ohjelmointi: Pentomino-palapelien ratkaiseminen

Pidän kombinatorisista ongelmista, mutta minulla on tapana ratkaista ne raa’alla voimalla. Selvitin 6×10-kokoisen Pentomino-palapelin kaikki ratkaisut parissa suoritinviikossa muutamalla 266 MHz:n Pentium II -työasemalla joskus vuoden 2000 aikoihin.

C-kielinen ratkaisuni käyttää syvyyshakua. Vuonna 2013 tekemäni yksinkertainen parannus karsii ne osaratkaisut, joissa laudalla on vapaita alueita, joiden koko ei ole viidellä jaollinen. Karsiminen onnistuu täyttämällä kukin vapaa alue ja laskemalla samalla niiden koko. Kokeilemassani 2,5 GHz:n kellotaajuudella toimivassa Intel Core i5-2520M -järjestelmässä optimoitu C-käännös käy läpi koko hakuavaruuden noin 4 minuutissa.

C++-kielinen ratkaisuni hyödyntää STL-kirjastoa. Kokeilemassani järjestelmässä optimoitu C++-käännös oli C-ohjelmaa hitaampi täyttörutiinin työjonon dynaamisen muistinhallinnan vuoksi. C-ohjelma käytti pinosta varattua taulukkoa, C++-ohjelma std::list-tietorakennetta.

Ajoajan ero poistui tammikuussa 2014, kun huomasin, että työjonon esittämiseen sopii luontevammin joukko. Alkuperäinen täyttöalgoritmin toteutus saattoi lisätä saman työalkion jonon perälle moneen kertaan. Joukko voidaan tässä tapauksessa esittää tehokkaasti bittikarttana, tarkemmin sanottuna yhdellä 64-bittisellä kokonaisluvulla siinä missä koko 60-alkioinen pelilautakin. C-kielisenkin ohjelman ajoaika lyheni vähän yli puoleen, kun keksin ennen seuraavan alkion hakemista poistaa bittikartan esittämästä työjonosta jo täytetyt alueet. Ajoaika lyheni lisää (kaikki 2 339 ratkaisua löytyvät 40 sekunnissa) poistettuani ensimmäisen palan symmetriset asennot. Peilikuvat poistettuani I-kirjaimen muotoisen palan 56 eri asennosta jäljelle jäi 14.

Joulukuussa 2016 muutin C++-toteutuksen käyttämään C++14-kielen constexpr-avainsanaa, niin että palojen bittikuviot (jopa 8 erilaista asentoa kullekin palalle) voidaan muuntaa käännösaikana. Kokeilin myös alustuttaa palojen kaikki 2 014 sijoittelua pelilaudalle. Se oli huono ajatus jo Kolmogorov-kompleksisuuden vuoksi (sijoittelut laskeva ohjelmakoodi on lyhempi kuin 2 014×64 bittiä). Lisäksi clang rajoittaa käännösaikaisen laskennan määrää.

Elokuussa 2019 korvasin ohjelmallisen rekursion silmukalla, jossa taulukko esittää hakupinoa. Kiinteästi varatulla pinolla se on hieman nopeampi kuin alkuperäinen ohjelmallinen rekursio (-DRECURSION). Dynaamisesti varattu pino (-DHEAP_STACK, vain C++-toteutus) on odotetusti hieman hitaampi kuin kiinteästi varattu. Huomio: ohjelmallinen rekursio (-DRECURSION) käy hakuavaruuden läpi eri järjestyksessä kuin hakupino.

Aiemmin kokeilin 12-tasoisen rekursiopinon laventamista käännösaikana mallineiden (template) avulla, mutta ajoaika piteni hieman.

clang 8.0.1 käänsi valitsimia -O3 -DNDEBUG -mtune=native -march=native käytettäessä C++-ohjelman 8% nopeammaksi kuin C-ohjelman. GCC 9.2.1 samoilla valitsimilla käänsi C-ohjelmasta suunnilleen 3% nopeamman kuin C++-ohjelmasta. Ohjelmat ovat keskenään suunnilleen yhtä nopeita, mutta clang-käännetty C-ohjelma on selkeästi muita hitaampi.

Ajoaikoja (Intel® Xeon® E5-2630 v4, 2,2 GHz)
ohjelma GCC 9.2.1 clang 8.0.1
C -DRECURSION 27,7 s31,2 s
C++ -DRECURSION 28,7 s27,6 s
C 26,9 s29,4 s
C++ 27,6 s27,1 s
C++ -DHEAP_STACK 27,7 s28,1 s

Kokeemme perusteella GCC tuottaa usein nopeampaa C-koodia kuin clang ja clang nopeampaa C++-koodia kuin GCC. Ero pieneni, kun poistin tehottoman viitteen const Board& 64-bittiseen arvoon.

Kolmiulotteisten palapelien ratkaiseminen

Vuoden 2014 alussa vaimoni löysi kirpputorilta 4×4×4-palapelin, jossa on yksi neljän tilavuusyksikön pala ja kaksitoista viiden tilavuusyksikön palaa eli yhteensä 4×4×4=4+12×5=64 tilavuusyksikköä. Toisin kuin Pentomino-palapelissä kyseessä eivät ole kaikki mahdolliset kyseisen tilavuiset palat. Ratkaisuja näyttäisi olevan 20 434. Pienin muutoksin sama ratkaisuperiaate on sovellettavissa:

  1. Esitetään palat yksinkertaisuuden vuoksi merkkijonoina.
  2. Muunnetaan palat pelilaudan esitysmuotoon (64-bittiseksi luvuksi).
  3. Muodostetaan jokaisesta palasta kiertämällä ja siirtämällä kaikki mahdolliset asennot pelilaudalla.
  4. Aloitetaan syvyyshaku 4 tilavuusyksikön palasta.
  5. Jos jokin vapaa tila ei ole jaollinen 5:llä, osaratkaisu on mahdoton, koska kaikki 5 tilavuusyksikön palat eivät voi sopia jäljellä olevaan tilaan.

Joulukuussa 2016 kirjoittaessani ratkaisimen hyödyntämään C++14-kielen ominaisuuksia huomasin tehneeni virheen symmetristen ratkaisujen poistamisessa. Kun palaa pyöräytetään kuinkin akselin ympäri, mahdollisia vaihtoehtoja on 4×4×4 eli 64. Palapelini pienelle palalle näistä 12 on erilaisia. Mutta monet näistä 12 asennosta ovat itse asiassa toistensa kanssa symmetrisiä, kun pala asetetaan kuution samaan nurkkaan ja pyöräytetään koko laatikkoa kaikilla 64 tavalla siirtämättä samalla palaa laatikon sisällä. Osoittautuu, että pieni pala voidaan asettaa laatikkoon vain 15 erilaisella tavalla eikä 261 tavalla, niin kuin virheellinen ratkaisuni teki.

GCC 9.2.1 tuottaa ratkaisimelleni selvästi nopeamman AMD64-koodin kuin clang 8.0.1. Kokeillessani elokuussa 2019 haku valmistui 14 063 sekunnissa (3 tunnissa, 54 minuutissa ja 23 sekunnissa) GCC:n kääntämällä ja 15 599 sekunnissa (4 tunnissa, 19 minuutissa ja 59 sekunnissa) clang:in kääntämällä ohjelmalla.

Maaliskuussa 2024 en osannut koota kuudesta suorakulmaisen särmiön muotoisesta 2×2×8-mittaisesta puutikusta muodostuvaa palapeliä, joten tein ohjelman ratkaisujen etsimiseksi. Yksi puutikku on kokonainen, ja muista tikuista puuttuu kuution muotoisia paloja 2×2×4-kokoiselta keskialueelta. Vaikka en toteuttanut minkäänlaista hakuavaruuden karsintaa, ratkaisut löytyvät noin 0,2 sekunnissa.

Lataukset

pentomino.c
C-kielinen Pentomino-ratkaisija
pentomino.C
C++-kielinen Pentomino-ratkaisija
pentomino.png
kuva 6×10-kokoisen Pentomino-pelin kaikista 2 339 ratkaisusta
pentomino.pl
pieni apuohjelma kuvan koostamiseksi
puzzle3d.C
C++-kielinen 3-ulotteisen 4×4×4-palapelin ratkaisija
puzzle3d.txt.gz
4×4×4-palapelin kaikki 20 434 ratkaisua
puzzle3dx.C
C++-kielinen ratkaisija ristille, jossa 6 tikun 2×2×4-kokoiset keskiosat limittyvät 4×4×4-kokoisessa tilassa
puzzle3dx.txt
puutikkupalapelin kaikki 2 ratkaisua