Voorbeeld van het gebruik van multidimensionale array's: Sudoku oplosser.

© Harry Broeders.

Deze pagina is bestemd voor studenten van de THRijswijk groepen EPm en EPv die de module SOPX1 volgen.

Sudoku oplosser.

Op deze pagina wordt een programma beschreven waarmee de op dit moment populaire Sudoku puzzels kunnen worden opgelost. De Sudoku puzzel moet in een invoerbestand worden opgeslagen waarbij voor elk leeg hokje een nul moet worden ingevoerd.

De bovenstaande puzzel moet als volgt in het invoerbestand sudoku.txt worden opgeslagen:

1 0 0 0 0 0 0 0 6
0 0 6 0 2 0 7 0 0
7 8 9 4 5 0 1 0 3
0 0 0 8 0 7 0 0 4
0 0 0 0 3 0 0 0 0
0 9 0 0 0 4 2 0 1
3 1 2 9 7 0 0 4 0
0 4 0 0 1 2 0 7 8
9 0 8 0 0 0 0 0 0

Andere voorbeelden (afkomstig uit het boekje "Sudoku boek 3" ISBN: 90-5515-631-0) zijn sudoku098.txt, sudoku099.txt en sudoku100.txt.

Het programma vraagt om de naam van het invoerbestand en geeft dan de oplossing.

De sourcecode.

De sourcecode van het programma sudoku.c is als volgt:

#include <stdio.h>

int leesin(int s[][9]) {
    char naam[80];
    FILE* invoer;
    int r, k;
    printf("Geef de naam van het invoerbestand: ");
    if (scanf("%79s", naam)!=1) {
        printf("Fout bij inlezen bestandsnaam.\n");
        return 0;
    }
    invoer=fopen(naam, "r");
    if (invoer==0) {
        printf("De file %s kan  niet gevonden of niet gelezen worden.\n", naam);
        return 0;
    }
    for (r=0; r<9; r++) {
        for (k=0; k<9; k++) {
            if (fscanf(invoer, "%d", &s[r][k])!=1) {
                printf("Fout bij inlezen %s.\n"
                       "Dit bestand moet 81 getallen van een sudoku bevatten.\n"
                       "Lege plaatsen moeten met een 0 worden aangegeven.", naam);
                fclose(invoer);
                return 0;
            }
            if (s[r][k]<0 || s[r][k]>9) {
                printf("Fout bij inlezen %s.\n"
                       "Dit bestand moet 81 getallen tussen 0 en 10 van een sudoku bevatten.\n"
                       "Lege plaatsen moeten met een 0 worden aangegeven.", naam);
                fclose(invoer);
                return 0;
            }
        }
    }
    fclose(invoer);
    return 1;
}

void drukaf(int s[][9]) {
    int r, k;
    for (r=0; r<9; r++) {
        for (k=0; k<9; k++) {
            printf("%d ", s[r][k]);
        }
        printf("\n");
    }
}

int controleer(int s[][9]) {
    int r, k, b;
    // rij
    for (r=0; r<9; r++) {
        int afvinklijst[9]={0};
        for (k=0; k<9; k++) {
            if (s[r][k]!=0 && ++afvinklijst[s[r][k]-1]>1) {
                return 0;
            }
        }
    }
    // kolom
    for (k=0; k<9; k++) {
        int afvinklijst[9]={0};
        for (r=0; r<9; r++) {
            if (s[r][k]!=0 && ++afvinklijst[s[r][k]-1]>1) {
                return 0;
            }
        }
    }
    // blok
    for (b=0; b<9; b++) {
        int afvinklijst[9]={0};
        int rb=b/3 * 3;
        int kb=b%3 * 3;
        for (r=rb; r<rb+3; r++) {
            for (k=kb; k<kb+3; k++) {
                if (s[r][k]!=0 && ++afvinklijst[s[r][k]-1]>1) {
                    return 0;
                }
            }
        }
    }
    return 1;
}

int losop(int s[][9]) {
    int probeer(int [][9], int);
    int i;
    for (i=1; i<10; i++) {
        if (probeer(s, i)==1) {
            return 1;
        }
    }
    return 0;
}

int probeer(int s[][9], int p) {
    int r, k, i;
    for (r=0; r<9; r++) {
        for (k=0; k<9; k++) {
            if (s[r][k]==0) {
                s[r][k]=p;
                if (controleer(s)==0) {
                    s[r][k]=0;
                    return 0;
                }
                for (i=1; i<10; i++) {
                    if (probeer(s, i)==1) {
                        return 1;
                    }
                }
                s[r][k]=0;
                return 0;
            }
        }
    }
    return 1;
}

int main() {
    int sudoku[9][9];
    int leesin(int [][9]);
    void drukaf(int [][9]);
    int controleer(int [][9]);
    int losop(int [][9]);

    if (leesin(sudoku)) {
        if (controleer(sudoku)==0) {
            printf("Deze sudoku is niet geldig!\n");
        }
        if (losop(sudoku)==0) {
            printf("Deze sudoku kun je niet oplossen!\n");
        }
        drukaf(sudoku);
    }

    fflush(stdin);
    getchar();
    return 0;
}

Stap voor stap verklaring.

We kunnen het best beginnen met het verklaren van het hoofdprogramma.

De getallen uit de Sudoku puzzel worden opgeslagen in de 2 dimensionale array sudoku. Deze array bevat 9 x 9 = 81 int getallen. Een lege plaats wordt aangegeven met een 0. In main worden de prototypes van 4 functies gedefinieerd en worden deze functies aangeroepen. De functie leesin leest de als argument meegegeven 2 dimensionale array in vanuit een tekstfile. Als het inlezen gelukt is geeft deze functie de waarde 1 terug en als het inlezen niet gelukt is wordt de waarde 0 teruggegeven. De functie controleer kijkt of de als argument meegegeven 2 dimensionale array voldoet aan de regels van een Sudoku puzzel. Daarbij worden de niet ingevulde vakjes (met de waarde 0) buiten beschouwing gelaten. De functie losop probeert de als argument meegegeven sudoku puzzel op te lossen. Als dit gelukt is geeft deze functie de waarde 1 terug en als het oplossen niet gelukt is wordt de waarde 0 teruggegeven. Omdat al deze 4 functies een array als argument gebruiken werken ze allemaal met call by reference. In feite wordt alleen het beginadres van de array doorgegeven.

Bij het doorgeven van een meerdimensionale array aan een functie mag alleen het aantal elementen van de eerste dimensie worden weggelaten. Het aantal elementen van de overige dimensies is namelijk nodig om het adres van een bepaald element in de meerdimensionale array te kunnen berekenen. Dit is in de les verder uitgelegd. Is hier nog verdere uitleg nodig?

De functie leesin leest de sudoku uit het invoerbestand en vult de 2 dimensionale array.

Bij het inlezen van bestandsnaam wordt bufferoverrun voorkomen. Als het openen van het invoerbestand niet lukt dan wordt een foutmelding gegeven. Als het openen wel gelukt is wordt regel voor regel en dan kolom voor kolom ingelezen. Met behulp van fscanf wordt telkens een int getal ingelezen uit het invoerbestand en in het "hokje" s[r][k] geplaatst. Als bij het inlezen een fout optreedt dan wordt een foutmelding gegeven en het inlezen afgebroken. De foutmeldingen zijn hier opgegeven als 3 losse C-strings achter elkaar maar de compiler maakt hier 1 lange C-string van!

De functie drukaf heeft geen verder verklaring nodig.

De functie losop roept de functie probeer aan met als tweede parameter het getal wat in het eerste vrije hokje moet worden geprobeerd. De getallen 1 t/m 9 worden achtereenvolgens geprobeerd. Deze functie geeft 1 terug zodra de functie probeer aangeeft dat het invullen van de geprobeerde waarde gelukt is. Als alle getallen van 1 t/m 9 geprobeerd zijn er geen enkele geprobeerde waarde gelukt is dan geeft de functie losop 0 terug om aangeven dat geen oplossing gevonden is.

De functie probeer is de belangrijkste functie in het programma. Deze functie is recursief, dat wil zeggen dat deze functie zichzelf aanroept. De functie probeer zoekt eerst een leeg hokje waarvoor geldt: s[r][k]==0. In dit hokje wordt de waarde die geprobeerd moet worden ingevuld: s[r][k]=p. Met de functie controleer wordt nu bepaald of de sudoku nog geldig is. Als dit niet het geval is (controleer(s)==0) dan wordt het hokje weer leeggemaakt met: s[r][k]=0 en geeft de functie 0 terug om aan te geven dat de geprobeerde waarde niet juist is. Als de functie controleer aangeeft dat de sudoku nog geldig is mag nog niet  geconcludeerd worden dat het invullen van deze waarde gelukt is. Het invullen levert misschien niet meteen een ongeldige sudoku op maar kan er voor zorgen dat je bij het verder oplossen van de puzzel vastloopt. Je kunt ontdekken of de geprobeerde waarde echt de juiste waarde is door te proberen in het volgende lege vakje een geldige waarde te vinden. Dit doe je door de functie probeer aan te roepen!

    for (i=1; i<10; i++) {
        if (probeer(s, i)==1) {
            return 1;
        }
    }

In het volgende hokje worden dus achtereenvolgens de getallen 1 t/m 9 geprobeerd. Zodra 1 van deze getallen correct is (de returnwaarde van probeer is 1) weet je dat dit hokje ook correct is (en kun je 1 teruggeven). Als geen van de geprobeerde waarden voor het volgende hokje een oplossing oplevert dan was de geprobeerde waarde dus niet juist. Dan wordt het hokje weer leeggemaakt met: s[r][k]=0 en geeft de functie 0 terug om aan te geven dat de geprobeerde waarde niet juist is. Deze oplossingsmethode wordt backtracking genoemd.

Tot slot de functie controlleer. Deze functie bepaalt of de sudoku geldig is waarbij de lege hokjes buiten beschouwing worden gelaten.

Als eerste wordt per rij gekeken of de rij dubbele getallen bevat. Dit wordt gedaan met behulp van de array afvinklijst die bij het begin van het controlleren van de rij met 9 nullen wordt gevuld. Op de eerste plaats (met index 0) van de afvinklijst worden de 1-en geteld. Op de tweede plaats (met index 1) van de afvinklijst worden de 2-en geteld. Enz. Deze afvinklijst wordt als volgt gebruikt:

    if (s[r][k]!=0 && ++afvinklijst[s[r][k]-1]>1) {
        return 0;
    }

s[r][k]!=0 test of het vakje niet leeg is. Als het vakje leeg is dan is de voorwaarde van de if altijd false en wordt de rest van de voorwaarde niet uitgerekend. s[r][k] is de waarde die in het te controlleren hokje zit. s[r][k]-1 is de index in afvinklijst waar deze waarde bijgehouden (geteld) wordt. Het getal in de afvinklijst op positie s[r][k]-1 wordt eerst met 1 verhoogd (door er ++ voor te zetten) en daarna wordt gekeken of deze waarde in de afvanklijst >1 is. Als dit zo is dan heb je hetzelfde getal al eerder op deze rij gezien en is de sudoku niet geldig.

Als alle rijen geldig zijn wordt per kolom gekeken of de kolom dubbele getallen bevat. Dit gaat op dezelfde manier als bij het controlleren van de rijen.

Als alle kolommen geldig zijn wordt per blok gekeken of het blok dubbele getallen bevat. Dit gaat op dezelfde manier als bij het controlleren van de rijen. Het bepalen van de rij en kolomnummers van elk blok is iets ingewikkelder. Maak zelf eens een tabel waarin je de waarden van rb en kb berekent voor de verschillende waarden van b (0 t/m 8).

Als ook alle blokken geldig zijn dan is de sudoku geldig en wordt de waarde 1 teruggegeven.

Uitdagingen!

Kun je een programma schrijven dat de puzzel op een logische manier oplost? Dit programma kan dan ook gebruikt worden om een hint te geven. Zoiets als sudoku_stap_voor_stap.exe.

Kun je een programma schrijven dat een sudoku puzzel aanmaakt? Hoe kun je puzzles met verschillende moeilijkheidsgraden maken?