Sudoku's in Perl
In 2006 was ik op reis, en had wat tijd over
(reizen is soms voor een groot deel wachten, zoals op vliegvelden). In de
lunchbox van een eerdere vlucht zaten twee Sudoku's, die semi-Japanse
cijferpuzzels. Ik maak die wel eens 'met de hand', en dacht: hoe moeilijk zou
het zijn om zo'n puzzel op de computer op te lossen? Ik weet wat strategieën,
kan ik die niet in een programma omzetten? Zo gezegd, zo gedaan... Tenminste,
zo begonnen.
Voor je je afvraagt 'is dit nu leuk': uiteraard ga ik niet met zo'n
programma voor mijn plezier andere Sudoku's oplossen, dat doe ik nog steeds met
de hand. Maar, mijn uitdaging was om te kijken of ik inderdaad een strategie
wist te bedenken om elke Sudoku op te lossen. Dit is een goede stap in die
richting (alle normale Sudoku's kan het volgens mij aan). Opdracht geslaagd, en
programma in het archief; de andere Sudoku's doe ik wel met de hand...
Het idee
Nu is programmeren voor een groot deel nadenken. Wat is het probleem (da's
hier duidelijk), hoe kan je het oplossen (je strategie -> algoritme), en hoe
sla je de gegevens op? Het laatste is vaak een van de sleutels tot een goed
programma.
Wat is het kernprobleem met Sudoku's: je hebt een bord met vakjes. Vakjes
kunnen op verschillende manieren bij groepen ingedeeld worden (rijen, kolommen,
en zo), en kunnen dus in meerdere groepen voorkomen. In elke groep mag
vervolgens elk cijfer (of symbool) maar een keer voorkomen. Bij
standaard-Sudoku's zijn die groepen bijvoorbeeld de rijen, kolommen, en de 3x3
vlakjes zoals normaal in de puzzel aangegeven. Soms zijn er ook extra regels,
zoals de groepen gevormd door de diagonalen (de diagonaal-Sudoku), of worden er
in plaats van 3x3 vakjes willekeuriger vormen gebruikt (vorm-Sudoku). We hebben
dus:
- Een 'bord' (meestal 9x9), met vakjes (cellen)
- Symbolen (meestal de cijfers 1 t/m 9) die hierop ingevuld moeten
worden
- Regels die zeggen hoe met de cellen (vakjes) de groepen gevormd worden,
waarin elk symbool maar een keer mag voorkomen.
De dataopslag (data structures)
Hoe sla ik dit op? Het bord deel ik op in vakjes, die ik bij een 9x9 bord
nummer van 0 tot 80 (programmeurs doen het vanaf 0). Dit wordt in het programma
voorgesteld door een normaal array (genaamd @cells ). In ieder
vakje kunnen symbolen voorkomen, en als de puzzel nog niet klaar is, zijn er
nog meerdere mogelijkheden per vakje. Als ik ieder symbool nu eens een bit geef
(in Perl zijn integers normaal 32 bits, kunnen dus 32 verschillende symbolen in
worden bijgehouden). Ik gebruik bit 0..8 voor de waardes 1..9. Is een bit gezet
('1'), dan is dat symbool nog mogelijk, is het gereset ('0') dan kan dat
symbool daar niet meer.
Voor de regels gebruik ik lijsten (implementatie als array) waarin ik
bijhoud welke cellen in de groep van die regel zitten. Bijvoorbeeld de eerste
rij is dan (0,1,2,3,4,5,6,7,8), de eerste kolom (0,9,18,27,36,45,54,63,72). Al
deze regels verzamel ik in een array (genaamd @rules ), zodat ik
makkelijk door deze heen kan stappen.
De 9 rijen kan ik in Perl in een keer als 9 lijsten in array
@rules aanmaken, als volgt (ga hier maar eens goed voor
zitten, dit is Perl voor gevorderden ;-)
for my $j (0..8) { push @rules, [ map { $j*9 + $_ } (0..8) ]
};
Op soortgelijke manier kan ik ook de kolommen, en met wat meer gepruts de
3x3 gebieden, maken.
Daarnaast nog wat administratie, zoals een variabele $found die
we ophogen iedere keer dat we een symbool kennen; als deze op 81 staat (9x9
bekende symbolen) dan is de puzzel opgelost.
Strategie 1
Mijn eerste strategie is simpel (en effectief). In de opgave zijn
een aantal cellen gegeven. Deze waardes kunnen dus niet in de andere cellen van
iedere groep voorkomen (in iedere groep mag elk symbool maar een keer
voorkomen). Voor iedere groep waar deze cel in zit kan dus deze mogelijke
waarde uit de andere cellen weggestreept worden (zie voorbeeld: als de '9' in
cel 3 zit, ik tel vanaf 0, dan kan de 9 als mogelijkheid uit die kolom, die rij
en dat 3x3 vak weggestreept worden: op de rode lijnen kan geen 9 staan).
Met een beetje mazzel (bij eenvoudige Sudoku's) levert dit weer nieuwe
cellen op waar nog maar een mogelijkheid over is. Door dan weer dezelfde
strategie toe te passen is er nog meer weg te strepen. En zo door, tot er
mogelijk een oplossing overblijft. In pseudo-programmeertaal (in de Perl source
is dit de sub 'update_singles() '):
Zolang we nog/weer nieuwe cellen met een unieke waarde hebben
doen we:
Voor iedere cel met een unieke waarde doen we:
Voor iedere groep waar deze cel in zit doen we:
Wis de waarde uit de cellen in deze
groep,
behalve als het onze begincel met
deze waarde is
Dit blijkt heel aardig te werken. Een simpele (3-sterren Sudoku) bleek met
deze methode gelijk opgelost te worden.
En eenvoudig te programmeren, binnen twee uur typen/testen had ik dit
geprogrammeerd en werkend (maar, ik had dus wel al voor die twee uur al een
goed beeld van wat en hoe...).
Strategie 2
Helaas, voor moeilijker Sudoku's bleek dit niet afdoende te zijn. Wat weet
ik nog meer voor strategieën?
Een aanvullende strategie is: als er binnen een groep een cel is
met meerdere mogelijkheden, waarvan een mogelijkheid alleen maar in die cel van
de groep voorkomt, dan moet die mogelijkheid ook de enige voor die cel zijn.
Klinkt moeilijk, maar kijk het voorbeeld hiernaast: als er in een groep drie
vakjes zijn met de mogelijkheden 421, 21, 21 (even aannemende dat in de andere
vakjes van de groep ook geen 4 voorkomt), dan kan de 4 alleen nog maar in dat
421 vakje staan, en kan dat vakje dus geen 1 of 2 bevatten. Door dus de 4
alleen in deze cel te zetten (de 1 en de 2 te wissen) hebben we een nieuwe
unieke waarde, en kunnen we strategie 1 weer toe gaan passen. Op een 5-ster
Sudoku blijkt dit inderdaad het veld verder op te ruimen!
Weer het algoritme is pseudo-code (in het Perl programma is dit de sub
'check_uniques() '):
Zolang we nog nieuwe waarden vinden doen we:
Voor alle groepen doen we:
Voor alle cellen in die groep doen we:
Als een symbool maar in een cel van
de groep voorkomt,
kunnen we de andere waarden in deze
cel elimineren
Als er iets gewijzigd is draaien we strategie 1 nog een keer
Ook weer relatief simpel, voor een mens. De werkelijke implementatie voor
een computer is nog wel even denken, een simpel zinnetje als 'Als dit symbool
maar in een cel van de groep voorkomt' is nog lastiger dan je zou verwachten.
Wat ik doe is voor alle cellen in de groep, behalve de cel waarmee we bezig
zijn) de patronen met een bitwise 'or' -functie combineren. Op die manier
hebben we in een waarde alle symbolen die in die cellen voorkomen. Als we nu
alle bits omdraaien hebben we de symbolen die juist niet in die cellen
voorkomen. Door nu een bitwise 'and'-functie toe te passen met de cel die we
aan het bekijken zijn, houden we die symbolen over die niet in andere cellen
maar wel in deze cel voor komt. Met een beetje mazzel is dat er een, hebben we
pech dan is dat er geen; zijn het er meer dan een dan klopt er echt iets
niet...
Maart 2009: een nieuwe strategie (nummer 3) toegevoegd; zie verderop op deze pagina
Strategie 9999
Helaas, een 5-sterren Sudoku draait nog steeds niet... Dan weet ik nog maar
een strategie: brute kracht... vandaar nummer 9999: als deze het niet vind, dan
vind niets het (ultieme maar botte strategie). Hopelijk hebben de twee eerste
strategieën al heel wat weggestreept (en we kunnen ze later nog vaker
toepassen). Wat is de brute kracht methode? In principe heel simpel: voor
iedere cel waar nog meerdere waardes mogelijk zijn gaan we ieder van die
waardes gewoon proberen. Dus, weer een stukje pseudo-code (deze keer van de
functie 'just_try() '):
Zolang we nog nieuwe waarden vinden doen we:
Bewaar de huidige toestand van het bord;
Voor iedere cel met meerdere waardes proberen we een voor een de
mogelijke waardes
Voor die waarde zetten de alleen die waarde in de
cel,
en passen we onze strategieën 1..3 op de zo
gevormde toestand toe
- Hebben we nu een oplossing dan zijn we klaar
- Lukt het niet dan zetten we de bewaarde
bord-toestand terug,
en proberen de volgende waarde
Dit blijkt te werken!! Let op: we maken hier gebruik van 'recursie': deze
functie kan zichzelf aanroepen (strategie 9999 gebruikt ook strategie 9999).
Maar, omdat de cel waarmee we bezig waren op dat moment nog maar een waarde
bevat (de waarde die we aan het proberen zijn), zijn er dus altijd minder
mogelijkheden dan waarmee we begonnen, en zal de recursie altijd ophouden: of
we vinden een oplossing, of we hebben geen mogelijkheden meer. En gezien we met
deze methode automatisch alle mogelijkheden afgaan, moeten we bij een geldige
Sudoku dus altijd het antwoord vinden.
En jawel, ook de 5-sterren Sudoku is nu in 2 seconden opgelost!
Een kleine update op 15 december 2006: het bleek dat bij sommige Sudoku's de
strategie wat onhandig was geïmplementeerd; door de boven beschreven aanpak
roept de routine zichzelf aan (recursie, strategie 3 gebruikt zichzelf); wat
ook de bedoeling is, maar wat hier tot gevolg heeft dat er een zogeheten
'depth-first' zoektocht wordt gedaan. Dit geeft een (te) lange zoektijd. Daarom
heb ik de recursie-diepte in het begin beperkt...
Invoer en uitvoer
Hoe krijgen we een Sudoku naar binnen? En
het resultaat naar buiten? Een Sudoku is gelukkig niet zo veel typewerk,
uiteindelijk zijn het maar 9x9 vakjes. Ik heb gekozen om de input via een
bestand te doen, geeft ook de mogelijkheid eventuele extra regels toe te voegen
(zie volgende hoofdstuk). zie hier de input file van de boven besproken 3
sterren Sudoku:
# 3-star sudoku for testing
@sudoku = ( # 9 regels
"--791----", # met 9 tekens
"---56248-",
"-----8---",
"-------2-",
"-96---73-",
"-4-8---6-",
"8--49----",
"57---6--2",
"1-------6",
);
Afdrukken gaat in een soortgelijk formaat, zie hiernaast. Denk er aan; Perl
is standaard een script-taal uitgevoerd via de DOS-prompt ofwel CMD window; ik
heb er geen grafische user interface omheen gebreeën. Wat je hiernaast ziet is
een screen capture van de output: de oplossing van de hierboven genoemde
3-sterren Sudoku.
Extra beperkingen -> nieuwe regels
Het is ook mogelijk nieuwe regels toe te
voegen. Zo zijn er bijvoorbeeld Sudoku's met extra voorwaarden, bijvoorbeeld
dat in extra gebieden de symbolen ook maar een keer mogen voorkomen. Dit kan op
de diagonalen zijn, of in andere gebieden zoals de rode vakken in het figuur
hiernaast. Dit kan mijn programma ook aan, door extra regels in de inputfile op
te nemen (zie bijvoorbeeld de file ' sudoku_extra.txt ' in
onderstaande zip file).
Dit gebeurd door voor die gebieden de lijst met de nummers van de vakjes toe
te voegen aan de bestaande lijst. De syntax hiervoor is vrij simpel:
onderstaande regels komen uit sudoku_extra.txt :
# New constraints: 4 extra squares
push @rules, [ (10,11,12, 19,20,21, 28,29,30) ];
push @rules, [ (14,15,16, 23,24,25, 32,33,34) ];
push @rules, [ (46,47,48, 55,56,57, 64,65,66) ];
push @rules, [ (50,51,52, 59,60,61, 68,69,70) ];
Het programma sudoku.pl
Het programma (oorspronkelijke versie 1.4, voor een verbeterde versie zie
verderop) is te downloaden als deze sudoku.zip, met daarin het Perl
programma, en drie voorbeeld-Sudoku's (die hierboven genoemd zijn). Onbekend
met Perl? Lees dan eerst mijn Perl pagina's.
Om het programma te draaien moet de Perl geïnstalleerd hebben. Dan, vanaf
een command prompt (aannemende dat je de input in de file
je_sudoku_input.txt hebt staan):
perl sudoku.pl je_sudoku_input.txt
Na wat tussenresultaten wordt vervolgens de oplossing afgedrukt (of een
foutmelding als dit niet lukt; bijvoorbeeld door een type-fout in de input
file).
Het programma zal de meeste types 3x3 Sudoku's aankunnen. Op dit moment kan
het nog niet standaard andere vormen aan, zoals bijvoorbeeld dubbele Sudoku's
met gedeeltelijke overlap. De strategieën zijn er OK voor, maar op dit moment
is de 9x9 hier en daar nog hard er in geprogrammeerd (nummering van de vakjes
moet uitgebreid worden). Technisch zeer wel mogelijk, maar voor mij op dit
moment nog niet interessant.
Uitbreidingen
Grote Sudoku's (16 bij 16)
Soms zie je ook Sudoku's die groter zijn, met name 16x16 Sudoku's.
Ondersteuning hiervoor is vrij eenvoudig toe te voegen, de regels zijn namelijk
exact hetzelfde. In plaats van de grootte hard te coderen, is er nu een
variabele $nrs geïntroduceerd, die standaard op 9 staat maar met
de -l optie op 16 gezet kan worden. Ook de symbolen zijn in dat
geval veranderd: in plaats van 123456789 gebruik ik
0123456789ABCDEF . Aanroep:
perl sudoku.pl -l grote_sudoku_input.txt
Een nieuwe strategie (nummer 3)
In maart 2009 heb ik een nieuwe strategie toegevoegd, met een functie
genaamd check_pairs() . Bij het met de hand invullen van een Sudoku
(ja, dat doe ik nog steeds) merkte ik dat ik deze strategie nogal eens
gebruikte: Als je twee cijfers (symbolen) in nog maar twee vakjes van een regel
hebt staan, dan weet je zeker dat een van die cijfers in het ene, en de andere
in het andere hokje moet. Dus: er zijn naast deze twee geen andere cijfers in
deze hokjes meer mogelijk, wat er verder nog staat kan worden weggestreept.
Best nog even lastig programmeren: je moet voor elke regel checken welke
cijfers exact twee keer voorkomen, en dan voor ieder gevonden paar in die regel
testen of ze beiden in twee dezelfde hokjes staan. Was even puzzelen (maar,
daar gaat het dan ook om). Is uit te breiden naar andere versies (zelfde
verhaal geldt bijvoorbeeld voor 3 cijfers en drie vakjes); heb ik echter nog
niet gedaan.
En zowaar: de Sudoku's die ik tot nu alleen met brute force (strategie 9999)
kon oplossen, worden nu opeens op deze manier gevonden (en een stukje
sneller).
Verbetering in strategie 9999
Kleine verbetering: het programma checkt nu regelmatig of het bord nog
geldig is (dus nergens 0 mogelijkheden heeft) met functie
sanity_check() : is dit niet het geval dan hadden we verkeerd
gegokt, en kan die poging (die recursie) afgebroken worden. Dit voorkomt dat er
te lang wordt doorgerekend op iets waarvan we toch eigenlijk al kunnen weten
dat het tot niets gaat leiden.
Meerdere oplossingen
Ook het zoeken van meerdere oplossingen is nu mogelijk, met behulp van de
-a command line optie. Normaal kan dit niet voorkomen; mocht dit
toch gebeuren dan is de Sudoku fout (of heb je een type-fout gemaakt bij
invoeren). Bij een lastige 16x16 Sudoku (bedankt, Mies) bleek er inderdaad een
fout te zitten in de oorspronkelijke puzzel: dankzij de nieuwe strategie 3 vond
mijn programma twee geldige oplossingen (en dus klopte de Sudoku niet).
De verbeterde versie 1.6
Download hier Sudoku versie 1.6
programma en voorbeelden (maar, om Perl te leren misschien eerst de
simpeler, oude versie proberen?)
|