Erstellen Sie einen Sudoku Solver in Java

Erstellen Sie einen Sudoku Solver in Java

1. Überblick

In diesem Artikel werden wir uns mit dem Sudoku-Puzzle und den Algorithmen befassen, mit denen es gelöst wird.

Als Nächstes implementieren wir Lösungen in Java. Die erste Lösung wird ein einfacher Brute-Force-Angriff sein. Die zweite Methode verwendet dieDancing Links-Technik.

Denken Sie daran, dass wir uns auf die Algorithmen und nicht auf das OOP-Design konzentrieren werden.

2. Das Sudoku-Puzzle

Einfach ausgedrückt ist Sudoku ein kombinatorisches Zahlenpuzzle mit einem 9 x 9-Zellenraster, das teilweise mit Zahlen von 1 bis 9 ausgefüllt ist. Das Ziel ist es, die verbleibenden, leeren Felder mit den restlichen Zahlen zu füllen, sodass jede Zeile und Spalte nur eine Zahl jeder Art enthält.

Darüber hinaus kann in jedem 3 x 3-Unterabschnitt des Rasters keine Nummer dupliziert werden. Der Schwierigkeitsgrad steigt natürlich mit der Anzahl der leeren Felder in jeder Tafel.

2.1. Test Board

Um unsere Lösung interessanter zu gestalten und den Algorithmus zu validieren, verwenden wir die“world’s hardest sudoku”-Karte:

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

2.2. Gelöstes Board

Und um die Lösung schnell zu verderben, liefert das richtig gelöste Rätsel das folgende Ergebnis:

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

3. Backtracking-Algorithmus

3.1. Einführung

Der Backtracking-Algorithmus versucht, das Rätsel zu lösen, indem jede Zelle auf eine gültige Lösung getestet wird.

Wenn keine Verletzung von Einschränkungen vorliegt, wechselt der Algorithmus zur nächsten Zelle, füllt alle möglichen Lösungen aus und wiederholt alle Überprüfungen.

Wenn ein Verstoß vorliegt, wird der Zellenwert erhöht. Sobald der Wert der Zelle 9 erreicht und immer noch eine Verletzung vorliegt, kehrt der Algorithmus zur vorherigen Zelle zurück und erhöht den Wert dieser Zelle.

Es versucht alle möglichen Lösungen.

3.2. Lösung

Definieren wir zunächst unser Board als zweidimensionales Array von Ganzzahlen. Wir werden 0 als leere Zelle verwenden.

int[][] board = {
  { 8, 0, 0, 0, 0, 0, 0, 0, 0 },
  { 0, 0, 3, 6, 0, 0, 0, 0, 0 },
  { 0, 7, 0, 0, 9, 0, 2, 0, 0 },
  { 0, 5, 0, 0, 0, 7, 0, 0, 0 },
  { 0, 0, 0, 0, 4, 5, 7, 0, 0 },
  { 0, 0, 0, 1, 0, 0, 0, 3, 0 },
  { 0, 0, 1, 0, 0, 0, 0, 6, 8 },
  { 0, 0, 8, 5, 0, 0, 0, 1, 0 },
  { 0, 9, 0, 0, 0, 0, 4, 0, 0 }
};

Erstellen wir einesolve()-Methode, dieboard als Eingabeparameter verwendet und durch Zeilen, Spalten und Werte iteriert, wobei jede Zelle auf eine gültige Lösung getestet wird:

private boolean solve(int[][] board) {
    for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
        for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
            if (board[row][column] == NO_VALUE) {
                for (int k = MIN_VALUE; k <= MAX_VALUE; k++) {
                    board[row][column] = k;
                    if (isValid(board, row, column) && solve(board)) {
                        return true;
                    }
                    board[row][column] = NO_VALUE;
                }
                return false;
            }
        }
    }
    return true;
}

Eine andere Methode, die wir brauchten, ist die MethodeisValid(), mit der Sudoku-Einschränkungen überprüft werden, d. H. Überprüfen, ob die Zeile, die Spalte und das 3 x 3-Raster gültig sind:

private boolean isValid(int[][] board, int row, int column) {
    return (rowConstraint(board, row)
      && columnConstraint(board, column)
      && subsectionConstraint(board, row, column));
}

Diese drei Prüfungen sind relativ ähnlich. Beginnen wir zunächst mit Zeilenprüfungen:

private boolean rowConstraint(int[][] board, int row) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
      .allMatch(column -> checkConstraint(board, row, constraint, column));
}

Als nächstes verwenden wir fast identischen Code, um die Spalte zu validieren:

private boolean columnConstraint(int[][] board, int column) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    return IntStream.range(BOARD_START_INDEX, BOARD_SIZE)
      .allMatch(row -> checkConstraint(board, row, constraint, column));
}

Außerdem müssen wir 3 x 3-Unterabschnitte validieren:

private boolean subsectionConstraint(int[][] board, int row, int column) {
    boolean[] constraint = new boolean[BOARD_SIZE];
    int subsectionRowStart = (row / SUBSECTION_SIZE) * SUBSECTION_SIZE;
    int subsectionRowEnd = subsectionRowStart + SUBSECTION_SIZE;

    int subsectionColumnStart = (column / SUBSECTION_SIZE) * SUBSECTION_SIZE;
    int subsectionColumnEnd = subsectionColumnStart + SUBSECTION_SIZE;

    for (int r = subsectionRowStart; r < subsectionRowEnd; r++) {
        for (int c = subsectionColumnStart; c < subsectionColumnEnd; c++) {
            if (!checkConstraint(board, r, constraint, c)) return false;
        }
    }
    return true;
}

Schließlich benötigen wir einecheckConstraint()-Methode:

boolean checkConstraint(
  int[][] board,
  int row,
  boolean[] constraint,
  int column) {
    if (board[row][column] != NO_VALUE) {
        if (!constraint[board[row][column] - 1]) {
            constraint[board[row][column] - 1] = true;
        } else {
            return false;
        }
    }
    return true;
}

Sobald alles erledigt ist, kann die MethodeisValid()einfachtrue zurückgeben.

Wir sind fast bereit, die Lösung jetzt zu testen. Der Algorithmus ist fertig. Es werden jedoch nurtrue oderfalse zurückgegeben.

Um die Karte visuell zu überprüfen, müssen wir nur das Ergebnis ausdrucken. Anscheinend ist dies nicht Teil des Algorithmus.

private void printBoard() {
    for (int row = BOARD_START_INDEX; row < BOARD_SIZE; row++) {
        for (int column = BOARD_START_INDEX; column < BOARD_SIZE; column++) {
            System.out.print(board[row][column] + " ");
        }
        System.out.println();
    }
}

Wir haben erfolgreich einen Backtracking-Algorithmus implementiert, der das Sudoku-Rätsel löst!

Offensichtlich gibt es Raum für Verbesserungen, da der Algorithmus jede mögliche Kombination immer wieder naiv überprüft (obwohl wir wissen, dass die jeweilige Lösung ungültig ist).

4.1. Genaue Abdeckung

Schauen wir uns eine andere Lösung an. Sudoku kann alsExact Cover-Problem beschrieben werden, das durch eine Inzidenzmatrix dargestellt werden kann, die die Beziehung zwischen zwei Objekten zeigt.

Wenn wir zum Beispiel Zahlen von 1 bis 7 und die Sammlung von MengenS = \{A, B, C, D, E, F} nehmen, wobei:

  • A = \ {1, 4, 7}

  • B = \ {1, 4}

  • C = \ {4, 5, 7}

  • D = \ {3, 5, 6}

  • E = \ {2, 3, 6, 7}

  • F = \ {2, 7}

Unser Ziel ist es, solche Teilmengen auszuwählen, dass jede Zahl nur einmal vorkommt und keine wiederholt wird, daher der Name.

Wir können das Problem mithilfe einer Matrix darstellen, in der Spalten Zahlen und Zeilen Mengen sind:

  | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
A | 1 | 0 | 0 | 1 | 0 | 0 | 1 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
C | 0 | 0 | 0 | 1 | 1 | 0 | 1 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
E | 0 | 1 | 1 | 0 | 0 | 1 | 1 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |

Untersammlung S * = \ {B, D, F} ist genaues Cover:

  | 1 | 2 | 3 | 4 | 5 | 6 | 7 |
B | 1 | 0 | 0 | 1 | 0 | 0 | 0 |
D | 0 | 0 | 1 | 0 | 1 | 1 | 0 |
F | 0 | 1 | 0 | 0 | 0 | 0 | 1 |

Jede Spalte hat in allen ausgewählten Zeilen genau eine 1.

4.2. Algorithmus X.

Der Algorithmus X ist ein“trial-and-error approach”, um alle Lösungen für das genaue Deckungsproblem zu finden, d.h. Beginnen Sie mit unserer BeispielsammlungS = \{A, B, C, D, E, F} und suchen Sie die UntersammlungS* = \{B, D, F}.

Algorithmus X funktioniert wie folgt:

  1. Wenn die MatrixA keine Spalten enthält, ist die aktuelle Teillösung eine gültige Lösung. erfolgreich beenden, andernfalls wählen Sie eine Spaltec (deterministisch)

  2. Wählen Sie eine Zeiler so, dassA ~r,c ~ = 1 (nicht deterministisch, d. H. Alle Möglichkeiten ausprobieren)

  3. Fügen Sie Zeiler in die Teillösung ein

  4. Für jede Spaltej, so dassA ~r,j ~ = 1, für jede Zeilei, so dassA ~i) s,j ~ = 1, Zeilei aus MatrixA und_ delete column _j aus MatrixA löschen

  5. Wiederholen Sie diesen Algorithmus rekursiv für die reduzierte MatrixA

Eine effiziente Implementierung des Algorithmus X ist der von Dr. vorgeschlagene AlgorithmusDancing Links(kurz DLX). Donald Knuth.

Die folgende Lösung wurde stark von der Java-Implementierung vonthisinspiriert.

4.3. Genaues Deckungsproblem

Zuerst müssen wir eine Matrix erstellen, die das Sudoku-Puzzle als genaues Deckungsproblem darstellt. Die Matrix hat 9 3 Zeilen, d. H. Eine Zeile für jede einzelne mögliche Position (9 Zeilen x 9 Spalten) jeder möglichen Zahl (9 Zahlen).

Die Spalten repräsentieren die Tafel (wieder 9 x 9) multipliziert mit der Anzahl der Einschränkungen.

Wir haben bereits drei Einschränkungen definiert:

  • Jede Reihe wird nur eine Nummer von jeder Art haben

  • Jede Spalte hat nur eine Nummer von jeder Art

  • Jeder Unterabschnitt hat nur eine Nummer jeder Art

Zusätzlich gibt es eine implizite vierte Einschränkung:

  • In einer Zelle kann sich nur eine Nummer befinden

Dies ergibt insgesamt vier Einschränkungen und daher 9 x 9 x 4 Spalten in der Exact Cover-Matrix:

private static int BOARD_SIZE = 9;
private static int SUBSECTION_SIZE = 3;
private static int NO_VALUE = 0;
private static int CONSTRAINTS = 4;
private static int MIN_VALUE = 1;
private static int MAX_VALUE = 9;
private static int COVER_START_INDEX = 1;
private int getIndex(int row, int column, int num) {
    return (row - 1) * BOARD_SIZE * BOARD_SIZE
      + (column - 1) * BOARD_SIZE + (num - 1);
}
private boolean[][] createExactCoverBoard() {
    boolean[][] coverBoard = new boolean
      [BOARD_SIZE * BOARD_SIZE * MAX_VALUE]
      [BOARD_SIZE * BOARD_SIZE * CONSTRAINTS];

    int hBase = 0;
    hBase = checkCellConstraint(coverBoard, hBase);
    hBase = checkRowConstraint(coverBoard, hBase);
    hBase = checkColumnConstraint(coverBoard, hBase);
    checkSubsectionConstraint(coverBoard, hBase);

    return coverBoard;
}

private int checkSubsectionConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row += SUBSECTION_SIZE) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column += SUBSECTION_SIZE) {
            for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
                for (int rowDelta = 0; rowDelta < SUBSECTION_SIZE; rowDelta++) {
                    for (int columnDelta = 0; columnDelta < SUBSECTION_SIZE; columnDelta++) {
                        int index = getIndex(row + rowDelta, column + columnDelta, n);
                        coverBoard[index][hBase] = true;
                    }
                }
            }
        }
    }
    return hBase;
}

private int checkColumnConstraint(boolean[][] coverBoard, int hBase) {
    for (int column = COVER_START_INDEX; column <= BOARD_SIZE; c++) {
        for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
            for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

private int checkRowConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; r++) {
        for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++, hBase++) {
            for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

private int checkCellConstraint(boolean[][] coverBoard, int hBase) {
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++, hBase++) {
            for (int n = COVER_START_INDEX; n <= BOARD_SIZE; n++) {
                int index = getIndex(row, column, n);
                coverBoard[index][hBase] = true;
            }
        }
    }
    return hBase;
}

Als nächstes müssen wir das neu erstellte Board mit unserem anfänglichen Puzzle-Layout aktualisieren:

private boolean[][] initializeExactCoverBoard(int[][] board) {
    boolean[][] coverBoard = createExactCoverBoard();
    for (int row = COVER_START_INDEX; row <= BOARD_SIZE; row++) {
        for (int column = COVER_START_INDEX; column <= BOARD_SIZE; column++) {
            int n = board[row - 1][column - 1];
            if (n != NO_VALUE) {
                for (int num = MIN_VALUE; num <= MAX_VALUE; num++) {
                    if (num != n) {
                        Arrays.fill(coverBoard[getIndex(row, column, num)], false);
                    }
                }
            }
        }
    }
    return coverBoard;
}

Wir sind bereit, jetzt mit der nächsten Stufe fortzufahren. Erstellen wir zwei Klassen, die unsere Zellen miteinander verbinden.

4.4. Tanzknoten

Der Dancing Links-Algorithmus basiert auf einer grundlegenden Beobachtung, die folgende Operation mit doppelt verknüpften Knotenlisten durchführt:

node.prev.next = node.next
node.next.prev = node.prev

Entfernt den Knoten, während:

node.prev = node
node.next = node

stellt den Knoten wieder her.

Jeder Knoten in DLX ist mit dem Knoten links, rechts, oben und unten verbunden.

Die KlasseDancingNodeverfügt über alle Operationen, die zum Hinzufügen oder Entfernen von Knoten erforderlich sind:

class DancingNode {
    DancingNode L, R, U, D;
    ColumnNode C;

    DancingNode hookDown(DancingNode node) {
        assert (this.C == node.C);
        node.D = this.D;
        node.D.U = node;
        node.U = this;
        this.D = node;
        return node;
    }

    DancingNode hookRight(DancingNode node) {
        node.R = this.R;
        node.R.L = node;
        node.L = this;
        this.R = node;
        return node;
    }

    void unlinkLR() {
        this.L.R = this.R;
        this.R.L = this.L;
    }

    void relinkLR() {
        this.L.R = this.R.L = this;
    }

    void unlinkUD() {
        this.U.D = this.D;
        this.D.U = this.U;
    }

    void relinkUD() {
        this.U.D = this.D.U = this;
    }

    DancingNode() {
        L = R = U = D = this;
    }

    DancingNode(ColumnNode c) {
        this();
        C = c;
    }
}

4.5. Spaltenknoten

Die Klasse vonColumnNodeverknüpft Spalten miteinander:

class ColumnNode extends DancingNode {
    int size;
    String name;

    ColumnNode(String n) {
        super();
        size = 0;
        name = n;
        C = this;
    }

    void cover() {
        unlinkLR();
        for (DancingNode i = this.D; i != this; i = i.D) {
            for (DancingNode j = i.R; j != i; j = j.R) {
                j.unlinkUD();
                j.C.size--;
            }
        }
    }

    void uncover() {
        for (DancingNode i = this.U; i != this; i = i.U) {
            for (DancingNode j = i.L; j != i; j = j.L) {
                j.C.size++;
                j.relinkUD();
            }
        }
        relinkLR();
    }
}

4.6. Löser

Als nächstes müssen wir ein Raster erstellen, das aus unseren ObjektenDancingNode undColumnNode besteht:

private ColumnNode makeDLXBoard(boolean[][] grid) {
    int COLS = grid[0].length;

    ColumnNode headerNode = new ColumnNode("header");
    List columnNodes = new ArrayList<>();

    for (int i = 0; i < COLS; i++) {
        ColumnNode n = new ColumnNode(Integer.toString(i));
        columnNodes.add(n);
        headerNode = (ColumnNode) headerNode.hookRight(n);
    }
    headerNode = headerNode.R.C;

    for (boolean[] aGrid : grid) {
        DancingNode prev = null;
        for (int j = 0; j < COLS; j++) {
            if (aGrid[j]) {
                ColumnNode col = columnNodes.get(j);
                DancingNode newNode = new DancingNode(col);
                if (prev == null) prev = newNode;
                col.U.hookDown(newNode);
                prev = prev.hookRight(newNode);
                col.size++;
            }
        }
    }

    headerNode.size = COLS;

    return headerNode;
}

Wir verwenden die heuristische Suche, um Spalten zu finden und eine Teilmenge der Matrix zurückzugeben:

private ColumnNode selectColumnNodeHeuristic() {
    int min = Integer.MAX_VALUE;
    ColumnNode ret = null;
    for (
      ColumnNode c = (ColumnNode) header.R;
      c != header;
      c = (ColumnNode) c.R) {
        if (c.size < min) {
            min = c.size;
            ret = c;
        }
    }
    return ret;
}

Schließlich können wir rekursiv nach der Antwort suchen:

private void search(int k) {
    if (header.R == header) {
        handleSolution(answer);
    } else {
        ColumnNode c = selectColumnNodeHeuristic();
        c.cover();

        for (DancingNode r = c.D; r != c; r = r.D) {
            answer.add(r);

            for (DancingNode j = r.R; j != r; j = j.R) {
                j.C.cover();
            }

            search(k + 1);

            r = answer.remove(answer.size() - 1);
            c = r.C;

            for (DancingNode j = r.L; j != r; j = j.L) {
                j.C.uncover();
            }
        }
        c.uncover();
    }
}

Wenn es keine Spalten mehr gibt, können wir das gelöste Sudoku-Board ausdrucken.

5. Benchmarks

Wir können diese beiden unterschiedlichen Algorithmen vergleichen, indem wir sie auf demselben Computer ausführen (auf diese Weise können wir Unterschiede bei den Komponenten, der Geschwindigkeit von CPU oder RAM usw. vermeiden). Die tatsächlichen Zeiten sind von Computer zu Computer unterschiedlich.

Wir sollten jedoch in der Lage sein, relative Ergebnisse zu sehen, und dies sagt uns, welcher Algorithmus schneller läuft.

Der Backtracking-Algorithmus benötigt ca. 250 ms, um die Karte zu lösen.

Wenn wir dies mit den Dancing Links vergleichen, die ungefähr 50 ms dauern, können wir einen klaren Gewinner erkennen. Dancing Links ist beim Lösen dieses Beispiels etwa fünfmal schneller.

6. Fazit

In diesem Tutorial haben wir zwei Lösungen für ein Sudoku-Puzzle mit Kern-Java besprochen. Der Backtracking-Algorithmus, ein Brute-Force-Algorithmus, kann das Standard-9 × 9-Rätsel problemlos lösen.

Der etwas kompliziertere Dancing Links-Algorithmus wurde ebenfalls besprochen. Beide lösen die schwierigsten Rätsel innerhalb von Sekunden.

Schließlich kann wie immer der während der Diskussion verwendete Codeover on GitHub gefunden werden.