Sudoku Ivo Doko, Saša Buzov PMF Matematički odsjek, Sveučilište u Zagrebu ivo.doko@gmail.com, sasa.buzov@gmail.com Sažetak: U ovom članku opisujemo kako smo riješili problem generiranja novih sudoku slagalica s jedinstvenim rješenjem, te rješavanja istih. Ključne riječi: sudoku, rješavanje sudoku, generiranje sudoku, backtracking 1. Uvod Sudoku je vrsta logičke slagalice, čija povijest seže skroz do 19. stoljeća. Bilo je naravno više različitih varijanti, a moderna sudoku (kakva nam je danas poznata svima) se prvi put pojavila u američkom časopisu Dell magazine, pod nazivom Number place 1979. godine. Objavio ju je Howard Garns, umirovljeni arhitekt, inače zaljubljenik u enigmatiku. On je uveo ograničenje da se u svakom 3x3 bloku jedna znamenka smije pojaviti samo jednom. Popularizacija sudoku je uslijedila nakon objave u japanskom časopisu Monthly Nikolist, gdje je objavljena 1984.g. pod imenom Sūji wa dokushin ni kagiru ( 数字は独身に限る ), što u doslovnom prijevodu znači broj se smije pojaviti samo jednom. Uzimanjem samo prvih slogova, dobili smo današnji naziv slagalice sudoku. 2. Sudoku i varijacije Sudoku slagalica se sastoji od jednog kvadrata, podijeljenog na 81 manjih kvadrata (polja), koji su organizirani u 3x3 blokove (ukupno 9 takvih). Cilj igre je u svako polje upisati prirodni broj od 1 do 9 tako da se niti u jednom retku, stupcu ili 3x3 bloku ista znamenka ne pojavljuje dvaput. Početni problem je djelomično popunjena slagalica s ne više od 32 broja upisana u slagalicu. Sudoku ne mora imati jedinstveno rješenje (zamislimo da nam je početni problem potpuno prazna slagalica i vidimo da rješenja ima jako puno), ali mi želimo generirati baš one koje imaju jedinstveno rješenje. Postoji puno varijacija sudokua, npr. može biti 4x4 podjeljena na 2x2 blokove, 6x6 s 2x3 blokovima, pa do 16x16 ili više (u biti, nema ograničenja na veličinu). Poznate varijante su i Wordoku gdje umjesto brojeva upisujemo slova, zatim nonomino ili Jigsaw gdje slagalica nije podijeljena na YxY blokove već na blokove random oblika, te Hypersudoku koja je ista kao i obična sudoku, ali s dodatnim
ograničenjima, tj. određena su područja na kojima se moraju pojaviti sve znamenke točno jednom (kombinacija sudoku i jigsaw sudoku). Slika 1: Lijevo je primjer sudoku slagalice, a s desne strane rješenje iste. Slika 2: lijevo je primjer jigsaw sudoku slagalice, a s desne strane rješenje iste. 3. Implementacija problema Sad kad smo pojasnili što je sudoku, odakle je i od kad je, vrijeme je da pojasnimo što smo mi ovdje radili. Mi smo željeli napisati program koji će generirati
nove sudoku slagalice (pošto dnevne novine nisu zadovoljavale naše potrebe za novim slagalicama) uz mali dodatni uvjet, da sudoku ima jedinstveno rješenje. Radili smo u programskom jeziku C++ (pošto ga dobro poznajemo tako nam je bilo najlakše). Naravno, kako bi bilo moguće provjeriti egzistenciju i eventualnu jedinstvenost rješenja, morali smo implementirati i algoritam za rješavanje sudoku slagalice. 3.1 Prezentacija problema Prvo i osnovno, treba nam prezentacija problema. Logično je bilo prezentirati sudoku kao dvodimenzionalno polje, dimenzije 9x9, pa smo tako i napravili: cell table[9][9]; gdje je cell struktura koja predstavlja polje u tablici u koje upisujemo broj, a izgleda ovako: struct cell{ int value; bool possiblevalues[9]; int numberofpossiblevalues; U strukturi cell, value predstavlja vrijednost koja je upisana u polje (0 ako nema vrijednosti na tom polju), possiblevalues je niz u koji bilježimo moguće vrijednosti za to polje (ako je vrijednost i moguća onda je possiblevalues[i-1] == true), a u numberofpossiblevalues spremamo broj mogućih vrijednosti za to polje. Zatim smo uveli još čitav niz pomoćnih varijabli i funkcija, koje nećemo sve nabrojati i objasniti jer ih ima puno, a nije ih potrebno sve razraditi kako bi se shvatio naš program, tako da ćemo objasniti samo one koje su neophodne i to kad ih spomenemo. Sad, kad imamo sudoku, krećemo dalje. Kao što smo već spomenuli, da bi mogli provjeriti egzistenciju i jedinstvenost rješenja, moramo implementirati algoritam za rješavanje sudokua. 3.2 Rješavanje sudoku slagalice Jedan od najprimitivnijih mogućih načina za rješavanje sudoku slagalice bi svakako bio isprobavanje svih mogućih kombinacija, no to i nije najbolji izbor. Uzmimo da su na početku zadana 32 broja, ostaje nam 81-32=49 praznih mjesta, za svako prazno mjesto ima 9 mogućih znamenki, što daje ukupno 49! mogućih kombinacija, a to je jako velik proj pokušaja, čak i za današnja računala. Da ubrzamo algoritam moramo iskoristiti informacije koje imamo, kako bismo odmah u startu odbacili rješenja za koja znamo da nemaju smisla. Npr. ako u gornjem lijevom uglu sudoku slagalice imamo upisan broj 1, znamo da nema smisla pokušavati s brojem 1 u prvom retku sudokua. Isto tako vrijedi za prvi stupac te pripadajući blok 3x3 polja.
1 Slika 3: koristeći informaciju da je na mjestu (1,1) upisan broj 1, znatno smo smanjili broj mogućih kombinacija na čak 20 drugih mjesta. Kako smo mi zamislili rješavanje problema, najbolje ćemo prikazati pomoću pseudokoda: unsigned long solve(char *file, unsigned long depth){ do{ do{ do{ do{ -provjeri vodi li ovaj put do mogućeg rješenja, ako ne, vrati backtrack. -ako smo pronašli rješenje, vrati ga, povećaj za 1 ukupni broj rješenja te poništi sve promjene. -provjeri postoji li mjesto u tablici za koje imamo samo jednu moguću vrijednost i ako postoji popuni ga. while (singleschange) -provjeri vodi li ovaj put do mogućeg rješenja, ako ne, vrati backtrack. -ako smo pronašli rješenje, vrati ga, povećaj za 1 ukupni broj rješenja te poništi sve promjene. -provjeri postoji li redak u koji se neka vrijednost upisati na samo jedno mjesto i ako postoji upiši tu vrijednost. while (rowschange) može -provjeri vodi li ovaj put do mogućeg rješenja, ako ne, vrati backtrack. -ako smo pronašli rješenje, vrati ga, i povećaj za 1 ukupni broj rješenja, te poništi sve promjene. -provjeri postoji li stupac u koji se neka vrijednost može upisati na samo jedno mjesto i ako postoji upiši tu vrijednost.
while (columnschange) -provjeri vodi li ovaj put do mogućeg rješenja, ako ne, vrati backtrack. -ako smo pronašli rješenje, vrati ga, i povećaj za 1 ukupni broj rješenja, te poništi sve promjene. -provjeri postoji li 3x3 blok u koji se neka vrijednost može upisati na samo jedno mjesto i ako postoji upiši tu vrijednost. while (blockschange) bestposition = mjesto u tablici za koje postoji najmanje mogućih vrijednosti broj rješenja = 0; kreiraj listu listofvalues i u nju upiši sve moguće vrijednosti za poziciju bestposition. ako je zadano da pretražujemo nasumično, ispermutiraj listofvalues. while(listofvalues nije prazna){ int current = vrijednost s početka liste valuelist; makni tu vrijednost iz liste. na poziciju bestposition postavi vrijednost current. broj nađenih rješenja += solve(file, depth+1); izbriši vrijednost koju smo postavili na bestposition. poništi sve vrijednosti koje smo promijenili (tj izbriši sve brojeve koje smo upisivali, kako bi se backtracking mogao vratiti) vrati broj pronađenih rješenja; ; Gdje su redom: bool singleschange, rowschange, columnschange, blockschange; singleschange = true ako postoji mjesto u tablici koje ima samo jednu moguću vrijednost rowschange = true ako postoji redak u koji se neka vrijednost može upisati na samo jedno mjesto columnchange = true ako postoji stupac u koji se neka vrijednost može upisati na samo jedno mjesto
blockchange = true ako postoji 3x3 blok u koji se neka vrijednost može upisati na samo jedno mjesto Promjene koje smo spominjali spremamo u varijablu changes, koja je tipa log, a log je klasa koju smo definirali kao: class log{ deque< pair<int,int> > memory; public: void add(int a, int b){ memory.push_back(make_pair(a, b)); void undo(){ while(!memory.empty()){ table[memory.front().first][memory.front().second].value= 0; memory.pop_front(); Memory služi za zapisivanje svih promjena koje je trenutni poziv funkcije solve napravio (da te promjene možemo vratiti). add dodaje poziciju u memory, a undo vraća sve promjene zapisane u memory. To bi bio grubi opis našeg algoritma, jer u stvarnom programu funkcija solve ima dodatne mogućnosti, a to su da tražimo samo jedno rješenje ili sva rješenja, da pretražujemo rješenja deterministički ili slučajno, ili da samo provjeravamo je li rješenje jedinstveno, te naravno mogućnost da pokrećemo program samo za rješavanje slagalice, ali to su promjene u pseudokodu koje nam nisu zanimljive, zanimljiv nam je algoritam kojim tražimo rješenje/a. 3.3 Generiranje sudoku slagalice Tek sad, nakon podužeg uvoda, dolazimo do biti našeg programa, do dijela koji stvarno generira sudoku slagalicu. U našem programu, to radi funkcija: void generate(char *file); Opet nam je najjednostavnije objasniti rad algoritma, koristeći pseudokod. void generate(char *file){ -upiši kanonski korijen u slagalicu; (nama je to: 1 2 3 4 6 7 8 9
) - nađi jedno rješenje za gornju slagalicu (graf se prilikom traženja rješenja pretražuje na slučajan način, kako program ne bi uvijek generirao identičnu slagalicu) koristeći funkciju solve. npr.: 9 7 2 3 1 6 4 8 4 8 2 6 7 9 1 3 1 3 6 8 9 4 2 7 8 4 1 2 3 7 9 6 2 7 9 4 6 3 8 1 6 1 3 7 8 9 4 2 7 4 3 1 2 8 6 9 9 2 8 6 7 1 3 4 3 6 1 9 4 8 2 7 - nasumično ispermutiraj retke (unutar blok redaka) - nasumično ispermutiraj stupce (unutar blok stupaca) - nasumično ispermutiraj blok retke - nasumično ispermutiraj blok stupce - nasumično ispermutiraj vrijednosti u tablici - na slučajan način generiraj vektor order[81], koji nam govori kojim redoslijedom ćemo uklanjati vrijednosti iz tablice for(i=0; i<81; i++){ -obriši vrijednost s pozicije order[i]; -provjeri je li rješenje jedinstveno, ako nije, vrati vrijednost na poziciju order[i]; 4. Zaključak Pokretanje ovog programa bezbroj puta, dok smo radili na njemu, nam ipak nije moglo dati predodžbu kakav je naš algoritam, tj. je li brz ili spor. Znali smo samo da generira sudoku s jedinstvenim rješenjem, ali da bi znali je li brz, morali smo ga s nečim usporediti. Koristeći se funkcijama iz datoteke <cfile>, napravili smo jednostavnu štopericu, i izračunali da programu u prosjeku treba 0.046s da generira sudoku slagalicu. Zatim smo za probu zakomentirali dio koda koji je provjeravao postoje li vrijednosti koje se mogu upisati na samo jedno mjesto i postoje li mjesta za koje postoji samo jedna moguća vrijednost (za stupce, retke i blokove) te pokušali rješiti jednu sudoku slagalicu samo backtrackingom. Nakon pola sata backtracking algoritam je bio na 8. dubini i tko zna koliko još daleko od rješenja, pa smo
zaključili da ne trebamo niti pokušavati na taj način generirati sudoku. Mali test nam je bio dovoljan da budemo zadovoljni performansama našeg algoritma, jer možda nije najbolji i najbrži, ali je daleko bolji nego brute force pristup i sasvim pristojan za upotrebu jer generira sudoku mnogo brže nego što mi rješavamo (bez računala, naravno). Brzine smo testirali na osobnom računalu s intelovim q6600 procesorom radnog takta 3200mhz. Literatura 1. http://web. math. hr/nastava/ui/ 2. http://en.wikipedia.org/wiki/algorithmics_of_sudoku 3. http://en.wikipedia.org/wiki/sudoku