Probieren geht über Studieren: Backtracking
Veröffentlicht: 13.06.2011 um 21:33 von nrg
Aktualisiert: 14.06.2011 um 13:38 von nrg (Sourcen als ZIP hinzugefügt)
Aktualisiert: 14.06.2011 um 13:38 von nrg (Sourcen als ZIP hinzugefügt)
Stichworte
backtracking
,
damenproblem
,
rekursion
,
sudoku
Nachdem Backtracking häufiger Thema hier im Forum ist, dachte ich mir, ich schreibe dazu mal einen kurzen Blog.
Backtracking geht nach dem Versuch-und-Irrtums-Prinzip vor, was nichts anderes bedeutet als "Probiere solange sämtliche Möglichkeiten aus, bis das Problem gelöst ist“. Anwendungsgebiete sind zum Beispiel das Damenproblem, Sudoku, Rucksackproblem oder Springerproblem.
Am besten lässt sich der Algorithmus rekursiv implementieren und folgt auch zumeist einem bestimmten Muster, das ich mal eben in Form von Java/Pseudocode veranschaulichen möchte:
In der privaten Methode
Um den Algorithmus auch praxisnah zu zeigen und zu analysieren habe ich mich für einen simplen SudokuSolver entschieden. Dieser akzeptiert nur die gängigen 3x3 Sudokus, kann aber auch ohne weiteres universeller geschrieben werden.
SudukoSolver.java
SudokuSolverTest.java
InvalidSudokuException.java
UnsolveableSudokuException.java
Ausgabe
Zur Analyse des Programmablaufs habe ich mal ein "kleines" GIF erstellt, das einen Mitschnitt vom Debugger in Eclipse zeigt:

Abschließend lässt sich noch sagen, dass diese Vorgehensweiße eher für Computer geeignet ist. Der Mensch selbst könnte beim Lösen von einem Sudoku nur bedingt so vorgehen, weil es einfach zu viele Kombinationen sind, die geprüft werden müssen. Eine logische Vorgehensweiße wäre an der Stelle deutlich schneller aber würde natürlich nie auf die Lösungszeit kommen, die ein Prozessor dafür benötigt
.
Aber auch ein Computer kann hiermit an seine Grenzen kommen, weil die Zeit, die benötigt wird um das Problem zu lösen, logischerweise exponentiell zu der "Größe" des Problems anwächst. Das sieht man sehr schnell, wenn man aus dem 8-Damen-Problem ein 100-Damen-Problem macht
.
Zu einem
.
Backtracking geht nach dem Versuch-und-Irrtums-Prinzip vor, was nichts anderes bedeutet als "Probiere solange sämtliche Möglichkeiten aus, bis das Problem gelöst ist“. Anwendungsgebiete sind zum Beispiel das Damenproblem, Sudoku, Rucksackproblem oder Springerproblem.
Am besten lässt sich der Algorithmus rekursiv implementieren und folgt auch zumeist einem bestimmten Muster, das ich mal eben in Form von Java/Pseudocode veranschaulichen möchte:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 public class XYProblem { gelöst = false; public löse() { löse(0); } private löse(aktuellePosition) { if (aktuellePosition > letztePosition) gelöst; while (Möglichkeit vorhanden) { setzen; löse(akuellePosition + 1); } if (ungelöst) zurücksetzen; } }
In der privaten Methode
löse wird nur "gesetzt", wenn mindestens eine Möglichkeit an dieser Stelle existiert. Für den Fall, dass es keine Möglichkeit gibt, erfolgt auch kein rekursiver Aufruf und die Methode endet. Dadurch bewegt man sich logischerweise eine Position (zur aufrufenden Methode) zurück und kann dort die nächste Möglichkeit ausprobieren. Dieses Spielchen wird solange wiederholt, bis das Problem gelöst ist oder die Methode ohne Lösung verlassen wird. Für letzteres gilt das Problem als unlösbar.Um den Algorithmus auch praxisnah zu zeigen und zu analysieren habe ich mich für einen simplen SudokuSolver entschieden. Dieser akzeptiert nur die gängigen 3x3 Sudokus, kann aber auch ohne weiteres universeller geschrieben werden.
SudukoSolver.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 import java.io.BufferedReader; import java.io.File; import java.io.FileReader; import java.io.IOException; import java.util.ArrayList; import java.util.List; /** * 3x3 sudokusolver * @author nrg */ public class SudokuSolver { private int[][] sudoku; private boolean solved; /** * Constructs a sudokusolver. Note that there is no intialization since yet. * You have to read the sudoku from file or use the set-method before * invoking the solve-method. */ public SudokuSolver() { this.solved = false; } /** * Constructs a sudokusolver * @param sudoku 2d array represent the soduko, 0 expected for empty fields */ public SudokuSolver(int[][] sudoku) { setSudoku(sudoku); } /** * Sets sudoku * @param sudoku 2d array represent the soduko, 0 expected for empty fields */ public void setSudoku(int[][] sudoku) { this.solved = false; this.sudoku = sudoku; } /** * Returns the current sate of the sudoku * @return 2d array represent the soduko */ public int[][] getSudoku() { return this.sudoku; } /** * Reads a suduko from a file, space as delimiter and point for empty fields are expected * * for example * * 2 . 7 . 8 6 . . . * . 5 . 7 1 . . 8 * . 9 8 . 4 . 1 7 6 * . . 4 . . 9 . . 7 * . 8 . 7 . 5 . 3 . * . . . 4 . . 9 2 5 * 8 . 3 . . . . 5 2 * 7 . . . 2 . 3 . . * . . . 6 5 . . . 1 * * represents a 3x3 sudoku * * @param file the filepath * @throws IOException */ public void read(String file) throws IOException { read(new File(file)); } /** * Reads a suduko from a file, space as delimiter and point for empty fields is expected * for example and further details see {@link SudokuSolver#read(String)} * @param file the fileobjekt * @throws IOException */ public void read(File file) throws IOException { BufferedReader bisr = new BufferedReader(new FileReader(file)); List<String> lines = new ArrayList<String>(); String line = null; while ((line = bisr.readLine()) != null) lines.add(line); solved = false; sudoku = new int[lines.size()][]; for (int i = 0; i < lines.size(); i++) sudoku[i] = parseLine(lines.get(i).split("\\s")); } /** * solve the current given sudoku * @return the solve-time in nanoseconds * @throws IllegalStateException if sudoku == null * @throws UnsolveableSudokuException if the sudoko can not be solved * @throws InvalidSudokuException if the sudoko is not 3x3 */ public long solve() { if (sudoku == null) throw new IllegalStateException("initialize sodoku first"); if (sudoku.length != 9) throw new InvalidSudokuException("number of rows must be 9"); for (int i = 0; i < sudoku.length; i++) { if (sudoku[i].length != 9) throw new InvalidSudokuException("number of columns in row " + (i+1) + " must be 9"); } long start = System.nanoTime(); if (!solved) solve(0, 0); if (!solved) throw new UnsolveableSudokuException("sudoku cannot be solved"); return System.nanoTime() - start; } /** * backtracking recursion * @param row current row * @param col current col */ private void solve(int row, int col) { if (solved = row >= sudoku.length) return; if (sudoku[row][col] == 0) { for (int n : getPossibilities(row, col)) { if (solved) break; sudoku[row][col] = n; solve(nextRow(row, col), nextCol(row, col)); } if (!solved) sudoku[row][col] = 0; } else { solve(nextRow(row, col), nextCol(row, col)); } } /** * returns the next row * @param row current row * @param col current col * @return next row */ private int nextRow(int row, int col) { return col + 1 < sudoku.length ? row : row + 1; } /** * returns the next col * @param row current row * @param col current col * @return next col */ private int nextCol(int row, int col) { return col + 1 < sudoku.length ? col + 1 : 0; } /** * returns all possibilities for the current field * @param row current row * @param col current col * @return all possibilities in a int array */ private int[] getPossibilities(int row, int col) { CoatSet cs = new CoatSet(10); for (int i = 0; i < sudoku.length; i++) { cs.coat(sudoku[row][i]); cs.coat(sudoku[i][col]); } int rowStart = row - row % 3, colStart = col - col % 3; for (int r = rowStart; r < rowStart + 3; r++) { for (int c = colStart; c < colStart + 3; c++) cs.coat(sudoku[r][c]); } return cs.getUncoated(); } /** * returns a String representing the sudoku * @return String representation of the sudoku */ @Override public String toString() { StringBuilder sb = new StringBuilder(); final String lineSep = System.getProperty("line.separator"); final char space = ' '; for (int[] a : sudoku) { for (int i = 0; i < a.length; i++) { if (i > 0) sb.append(space); sb.append(a[i]); } sb.append(lineSep); } return sb.toString(); } /** * private class to coat numbers * @author nrg */ private class CoatSet { private boolean[] set; private int uncoated; /** * Constructs a coatset of the given size * @param size size of the coatset */ private CoatSet(int size) { this.uncoated = size; this.set = new boolean[size]; for (int i = 0; i < set.length; i++) set[i] = true; } /** * coats a number * @param n the number * @throws ArrayIndexOutOfBoundsException if n < 0 or n >= size */ private void coat(int n) { if (set[n]) uncoated--; set[n] = false; } /** * returns all uncoated numbers * @return a int-array contains all uncoated numbers */ private int[] getUncoated() { int[] array = new int[uncoated]; for (int i = 1, j = 0; i < set.length; i++) if (set[i]) array[j++] = i; return array; } } /** * parses a string-array to a int-array * @param a the string array * @return the int array * @throws InvalidSudokuException if any field != point or field != [1-9] */ private static int[] parseLine(String[] a) { int[] array = new int[a.length]; for (int i = 0; i < a.length; i++) { if (!a[i].matches("[1-9]|\\.")) throw new InvalidSudokuException("at field " + a[i]); array[i] = a[i].equals(".") ? 0 : Integer.parseInt(a[i]); } return array; } }
SudokuSolverTest.java
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.io.IOException; public class SudokuSolverTest { public static void main(String[] args) { SudokuSolver ss = new SudokuSolver(new int[][] { {2, 0, 7, 0, 8, 6, 0, 0, 0}, {0, 0, 5, 0, 7, 1, 0, 0, 8}, {0, 9, 8, 0, 4, 0, 1, 7, 6}, {0, 0, 4, 0, 0, 9, 0, 0, 7}, {0, 8, 0, 7, 0, 5, 0, 3, 0}, {0, 0, 0, 4, 0, 0, 9, 2, 5}, {8, 0, 3, 0, 0, 0, 0, 5, 2}, {7, 0, 0, 0, 2, 0, 3, 0, 0}, {0, 0, 0, 6, 5, 0, 0, 0, 1}, }); System.out.println("solved in " + ss.solve()/1000000. + " milliseconds"); System.out.println(ss); } }
InvalidSudokuException.java
1 2 3 4 5 6 7 8 public class InvalidSudokuException extends RuntimeException { private static final long serialVersionUID = 1L; public InvalidSudokuException(String msg) { super(msg); } }
UnsolveableSudokuException.java
1 2 3 4 5 6 7 8 public class UnsolveableSudokuException extends RuntimeException { private static final long serialVersionUID = 1L; public UnsolveableSudokuException(String msg) { super(msg); } }
Ausgabe
Code:
solved in 2.372598 milliseconds 2 1 7 9 8 6 5 4 3 6 4 5 3 7 1 2 9 8 3 9 8 5 4 2 1 7 6 5 3 4 2 6 9 8 1 7 9 8 2 7 1 5 6 3 4 1 7 6 4 3 8 9 2 5 8 6 3 1 9 7 4 5 2 7 5 1 8 2 4 3 6 9 4 2 9 6 5 3 7 8 1

Abschließend lässt sich noch sagen, dass diese Vorgehensweiße eher für Computer geeignet ist. Der Mensch selbst könnte beim Lösen von einem Sudoku nur bedingt so vorgehen, weil es einfach zu viele Kombinationen sind, die geprüft werden müssen. Eine logische Vorgehensweiße wäre an der Stelle deutlich schneller aber würde natürlich nie auf die Lösungszeit kommen, die ein Prozessor dafür benötigt
.Aber auch ein Computer kann hiermit an seine Grenzen kommen, weil die Zeit, die benötigt wird um das Problem zu lösen, logischerweise exponentiell zu der "Größe" des Problems anwächst. Das sieht man sehr schnell, wenn man aus dem 8-Damen-Problem ein 100-Damen-Problem macht
.Zu einem
java.lang.StackOverflowError sollte es aber in keinem Fall kommen, weil Java einen Stack von über 7300 Methoden zulässt
.Kommentare 0







