|
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?)
|