EINFÜHRUNG |
Denkspiele mit mathematischem Hintergrund sind sehr beliebt und weit verbreitet. Es geht in diesem Kapitel nicht darum, dir die Freude am eigenständigen Lösen solcher Rätsel "von Hand", also mit Papier und Schreibstift zu verderben. Vielmehr sollst du erleben, dass der Computer durch systematisches Lösen mit Backtracking die Lösung zwar auch finden kann, allerdings mit zwei wichtigen Unterschieden: Falls im Lösungsprozess nicht weitere Einschränkungen und Strategien eingebaut werden, kann zur Lösung trotz eines schnellen Computers enorm viel Zeit benötigt werden, im Gegenzug kann aber der Computer im Prinzip alle Lösungen finden, was von Hand meist sehr schwierig ist. Dadurch ist es möglich, mit dem Computer den Beweis zu liefern, dass eine bestimmte Lösung die einfachste (kürzeste) ist. |
SUDOKU |
Das Spiel hat sich seit 1980 boomartig verbreitet und ist heute sehr populär. Du findest Spielvorgaben in der Rätselecke von vielen Tages- und Wochenzeitungen. Es handelt sich um ein typisches Zahlenrätsel mit einfachen Spielregeln. In der Standardvariante müssen in einem 9x9 Gitter die Zahlen 1 bis 9 derart in die Felder gesetzt werden, dass in jeder Zeile und jeder Spalte alle Zahlen genau einmal vorkommen. Zudem wird das Gitter in 9 Untergitter mit 3x3 Feldern aufgeteilt, wobei auch in diesen die Zahlen genau einmal auftreten müssen. Zu Beginn des Spiels ist bereits eine bestimmte Anzahl der Zellen besetzt. Bei idealer Spielvorgabe sollte es genau eine Lösung geben. Je nach Spielvorgabe ist das Rätsel leichter oder schwieriger zu lösen. Der geübte Sudoku-Spieler geht mit gewissen allgemein bekannten oder auch persönlichen Lösungsstrategien vor. Beim Backtracking mit dem Computer werden systematisch die offenen Felder der Reihe nach mit Zahlen von 1 bis 9 so gefüllt, dass sich kein Widerspruch zu den Spielregeln ergibt. Ist dies nicht möglich, so wird der letzte Zug zurückgenommen.
startState = [ \ Wie gewohnt wirst du das Programm schrittweise entwickeln. Deine erste Aufgabe besteht darin, den Spielzustand im GameGrid darzustellen und dem Benutzer die Möglichkeit zu geben, mit einem Mausklicks die leeren Zellen zu belegen. Dies ist zwar für die automatische Lösungssuche überflüssig, erlaubt dir aber, interaktiv auf das Spiel einzuwirken, was besonders in der Testphase hilfreich ist. Zudem kannst du so das Rätsel am Bildschirm statt auf Papier lösen. from gamegrid import * def pressEvent(e): loc = toLocationInGrid(e.getX(), e.getY()) if loc in fixedLocations: setStatusText("Location fixed") return x = loc.x y = loc.y value = startState[y][x] value = (value + 1) % 10 startState[y][x] = value showState(startState) def showState(state): removeAllActors() for y in range(9): for x in range(9): loc = Location(x, y) value = state[y][x] if value != 0: if loc in fixedLocations: c = Color.black else: c = Color.red digit = TextActor(str(value), c, Color.white, Font("Arial", Font.BOLD, 20)) addActorNoRefresh(digit, loc) refresh() makeGameGrid(9, 9, 50, Color.red, False, mousePressed = pressEvent) show() setTitle("Sudoku") setBgColor(Color.white) getBg().setLineWidth(3) getBg().setPaintColor(Color.red) for x in range(4): getBg().drawLine(150 * x, 0, 150 * x, 450) for y in range(4): getBg().drawLine(0, 150 * y, 450, 150 * y) startState = [ [0, 6, 0, 7, 9, 8, 0, 1, 2], [7, 9, 4, 1, 0, 5, 0, 6, 8], [2, 0, 1, 4, 0, 0, 9, 5, 7], [0, 0, 0, 2, 1, 0, 5, 0, 0], [0, 5, 6, 3, 0, 0, 2, 4, 1], [0, 1, 2, 5, 4, 0, 7, 3, 9], [6, 3, 0, 8, 7, 4, 0, 0, 0], [1, 0, 5, 6, 0, 2, 8, 0, 0], [4, 2, 8, 9, 0, 1, 0, 7, 0]] fixedLocations = [] for x in range(9): for y in range(9): if startState[y][x] != 0: fixedLocations.append(Location(x, y)) showState(startState) Programmcode markieren
from gamegrid import * def pressEvent(e): loc = toLocationInGrid(e.getX(), e.getY()) if loc in fixedLocations: setStatusText("Location fixed") return xs = loc.x // 3 ys = loc.y // 3 x = loc.x % 3 y = loc.y % 3 value = startState[ys][xs][y][x] value = (value + 1) % 10 startState[ys][xs][y][x] = value showState(startState) if isValid(startState): setStatusText("State valid") else: setStatusText("Invalid state") def showState(state): removeAllActors() for ys in range(3): for xs in range(3): for y in range(3): for x in range(3): loc = Location(x + 3 * xs, y + 3 * ys) value = state[ys][xs][y][x] if value != 0: if loc in fixedLocations: c = Color.black else: c = Color.red digit = TextActor(str(value), c, Color.white, Font("Arial", Font.BOLD, 20)) addActorNoRefresh(digit, loc) refresh() def isValid(state): # Check lines for ys in range(3): for y in range(3): line = [] for xs in range(3): for x in range(3): value = state[ys][xs][y][x] if value > 0 and value in line: return False else: line.append(value) # Check rows for xs in range(3): for x in range(3): row = [] for ys in range(3): for y in range(3): value = state[ys][xs][y][x] if value > 0 and value in row: return False else: row.append(value) # Check subgrids for ys in range(3): for xs in range(3): subgrid = state[ys][xs] square = [] for y in range(3): for x in range(3): value = subgrid[y][x] if value > 0 and value in square: return False else: square.append(value) return True makeGameGrid(9, 9, 50, Color.red, False, mousePressed = pressEvent) show() setTitle("Sudoku") addStatusBar(30) visited = [] setBgColor(Color.white) getBg().setLineWidth(3) getBg().setPaintColor(Color.red) for x in range(4): getBg().drawLine(150 * x, 0, 150 * x, 450) for y in range(4): getBg().drawLine(0, 150 * y, 450, 150 * y) stateWiki = [[[[0, 3, 0], [0, 0, 0], [0, 0, 8]], [[0, 0, 0], [1, 9, 5], [0, 0, 0]], [[0, 0, 0], [0, 0, 0], [0, 6, 0]]], [[[8, 0, 0], [4, 0, 0], [0, 0, 0]], [[0, 6, 0], [8, 0, 0], [0, 2, 0]], [[0, 0, 0], [0, 0, 1], [0, 0, 0]]], [[[0, 6, 0], [0, 0, 0], [0, 0, 0]], [[0, 0, 0], [4, 1, 9], [0, 0, 0]], [[2, 8, 0], [0, 0, 5], [0, 7, 0]]]] startState = stateWiki fixedLocations = [] for xs in range(3): for ys in range(3): for x in range(3): for y in range(3): if startState[ys][xs][y][x] != 0: fixedLocations.append(Location(x + 3 * xs, y + 3 * ys)) showState(startState) Programmcode markieren
Für das Backtracking musst mit der Funktion getNeighbours(state) die Nachfolgeknoten eines Zustands bestimmen. Dabei wählst du mit getEmptyCell() eine leere Zelle aus und setzt dort der Reihe nach alle Zahlen ein, die zu einem legalen Spielzustand gehören. Mindestens eine davon wir sicher die richtige sein. In getNeighbours(state) kopierst du zuerst die gegebene Zustandsliste in einen Clone, da du den übergegebenen Zustand nicht verändern darfst. Nachher füllst du die leere Zelle nacheinander mit den Zahlen 1 bis 9 und fügst Kopien der erlaubten Zustände in die Nachbarliste, die du zuletzt zurückgibst [mehr... Im Modul copy gibt es die Funktion deepcopy(), welche einen Clone erstellt.] Die rekursive definierte Funktion search(), die das eigentliche Backtracking durchführt, kannst praktisch unverändert von anderen Backtracking-Problemen übernehmen. Du suchst hier nur nach einer einzigen Lösung und beendest mit dem Flag found den rekursiven Aufruf. from gamegrid import * def pressEvent(e): loc = toLocationInGrid(e.getX(), e.getY()) if loc in fixedLocations: setStatusText("Location fixed") return x = loc.x y = loc.y value = startState[y][x] value = (value + 1) % 10 startState[y][x] = value showState(startState) if isValid(startState): setStatusText("State valid") else: setStatusText("Invalid state") def getBlockValues(state, x, y): return [state[y][x], state[y][x + 1], state[y][x + 2], state[y + 1][x], state[y + 1][x + 1], state[y + 1][x + 2], state[y + 2][x], state[y + 2][x + 1], state[ y + 2][x + 2]] def showState(state): removeAllActors() for y in range(9): for x in range(9): loc = Location(x, y) value = state[y][x] if value != 0: if loc in fixedLocations: c = Color.black else: c = Color.red digit = TextActor(str(value), c, Color.white, Font("Arial", Font.BOLD, 20)) addActorNoRefresh(digit, loc) refresh() def isValid(state): # Check lines for y in range(9): values = [] for x in range(9): value = state[y][x] if value > 0 and value in values: return False else: values.append(value) # Check rows for x in range(9): values = [] for y in range(9): value = state[y][x] if value > 0 and value in values: return False else: values.append(value) # Check blocks for yblock in range(3): for xblock in range(3): values = [] li = getBlockValues(state, 3 * xblock, 3 * yblock) for value in li: if value > 0 and value in values: return False else: values.append(value) return True def getEmptyCell(state): emptyCells = [] for y in range(9): for x in range(9): if state[y][x] == 0: return [x, y] return [] def cloneState(state): li = [] for y in range(9): line = [] for x in range(9): line.append(state[y][x]) li.append(line) return li def getNeighbours(state): clone = cloneState(state) cell = getEmptyCell(state) validStates = [] for value in range(1, 10): clone[cell[1]][cell[0]] = value if isValid(clone): validStates.append(cloneState(clone)) return validStates def search(state): global found, solution if found: return visited.append(state) # state marked as visited # Check for solution if getEmptyCell(state) == []: solution = state found = True return for neighbour in getNeighbours(state): if neighbour not in visited: # Check if already visited search(neighbour) # recursive call visited.pop() makeGameGrid(9, 9, 50, Color.red, False, mousePressed = pressEvent) show() setTitle("Sudoku") addStatusBar(30) visited = [] setBgColor(Color.white) getBg().setLineWidth(3) getBg().setPaintColor(Color.red) for x in range(4): getBg().drawLine(150 * x, 0, 150 * x, 450) for y in range(4): getBg().drawLine(0, 150 * y, 450, 150 * y) startState = [ [0, 6, 0, 7, 9, 8, 0, 1, 2], [7, 9, 4, 1, 0, 5, 0, 6, 8], [2, 0, 1, 4, 0, 0, 9, 5, 7], [0, 0, 0, 2, 1, 0, 5, 0, 0], [0, 5, 6, 3, 0, 0, 2, 4, 1], [0, 1, 2, 5, 4, 0, 7, 3, 9], [6, 3, 0, 8, 7, 4, 0, 0, 0], [1, 0, 5, 6, 0, 2, 8, 0, 0], [4, 2, 8, 9, 0, 1, 0, 7, 0]] fixedLocations = [] for x in range(9): for y in range(9): if startState[y][x] != 0: fixedLocations.append(Location(x, y)) showState(startState) setStatusText("Press any key to search solution.") getKeyCodeWait(True) setStatusText("Searching. Please wait...") found = False solution = None search(startState) if solution != None: showState(solution) setStatusText("Solution found") else: setStatusText("No solution") Programmcode markieren
|
MEMO |
Du kannst auch Sudoku-Vorgaben verwenden, die du im Internet oder in einer Rätselecke gefunden hast. Wenn du genügend Geduld hast, so sollte dein Programm immer eine Lösung finden. Man kann die Rechenzeit drastisch senken, falls man Lösungsstrategien in das Programm einbeziehst, die man auch bei der Lösung von Hand anwendet. Es bleibt dir überlassen, den Algorithmus mit solchen heuristischen Verfahren zu verbessern. Das Lösungsverfahren lässt sich auch auf Sudoku-Vorgaben anwenden, die nicht-quadratische Blöcke aufweisen. Du musst dazu lediglich die Funktion getBlockValues() entsprechend anpassen. |
DIE EIFERSÜCHTIGEN EHEMÄNNER |
"Drei eifersüchtige Ehemänner befinden sich am Abend zusammen mit ihren Ehefrauen an einem Flussübergang, wo sie ein Boot ohne Bootsführer vorfinden. Das Boot ist so klein, dass darin nur zwei Personen Platz haben. Es stellt sich die Frage, wie die 6 Personen den Fluss überqueren können, ohne dass sich jemals eine der Frauen in Anwesenheit eines oder zwei Männer befindet, ohne dass ihr eigener Ehemann dabei ist, sei es auf der einen oder anderen Seite des Flusses, sei es im Boot." Das Problem löst du wieder schrittweise und erstellst als erstes die Benutzeroberfläche, wobei sich ein GameGrid gut eignet, da man die Personen leicht durch Spritebilder veranschaulichen kann. Du fügst die GameGrid-Actors in ein unsichtbares Gitter der Grösse 7x3, damit du im Mittelstreifen den Fluss einzeichnen kannst. Da es nur wesentlich ist, ob sich eine Person links oder rechts vom Fluss befindet, ist als Datenstruktur eine Dualzahl geeignet, bei der jedes Bit zu einer bestimmten Person gehört. Der Bitwert 0 sagt, dass sich die Person links, der Wert 1, dass sie sich rechts vom Fluss befindet. Für das Boot verwendest du das Bit mit der höchsten Wertigkeit. In der Simulation gibt es ein blaues, grünes und rotes Ehepaar, die du folgenden Stellen der Dualzahl zuordnest:
b0 ist das Bit mit der kleinsten Wertigkeit. Interpretierst du den Zustand im Dezimalsystem, so kommen also alle Zahlen zwischen 0 und 127 vor, d.h. es gibt 128 verschiedene Zustände.
Bereits hier baust du mit isStateAllowed(state) einen Test ein, ob die aktuelle Situation gemäss den Vorgaben legal ist. (Du könntest auch zuerst eine Vorversion ohne diesen Test erstellen.) from gamegrid import * def pressEvent(e): global state loc = toLocationInGrid(e.getX(), e.getY()) if loc in left_locations: actor = getOneActorAt(loc) if actor != None: x = 6 - actor.getX() y = actor.getY() actor.setLocation(Location(x, y)) if loc in right_locations: actor = getOneActorAt(loc) if actor != None: x = 6 - actor.getX() y = actor.getY() actor.setLocation(Location(x, y)) state = 0 for i in range(7): loc = right_locations[i] actor = getOneActorAt(loc) if actor != None: state += 2**(6 - i) showState(state) def stateToString(state): return str(bin(state)[2:]).zfill(7) def showState(state): sbin = stateToString(state) for i in range(7): if sbin[i] == "0": actors[i].setLocation(left_locations[i]) else: actors[i].setLocation(right_locations[i]) setTitle("State: " + str(state) + ", bin: " + stateToString(state)) if isStateAllowed(state): setStatusText("Situation allowed") else: setStatusText("Situation not allowed") refresh() def isStateAllowed(state): print(state) stateStr = stateToString(state) mred = stateStr[1] == "1" fred = stateStr[2] == "1" mgreen = stateStr[3] == "1" fgreen = stateStr[4] == "1" mblue = stateStr[5] == "1" fblue = stateStr[6] == "1" if mred and not fred or not mred and fred: # mred/fred not together if not fred and (not mgreen or not mblue) or fred and (mgreen or mblue): return False if mgreen and not fgreen or not mgreen and fgreen:#mgreen/fgreen not together if not fgreen and (not mred or not mblue) or fgreen and (mred or mblue): return False if mblue and not fblue or not mblue and fblue: # mblue/fblue not together if not fblue and (not mred or not mgreen) or fblue and (mred or mgreen): return False return True makeGameGrid(7, 3, 50, None, False, mousePressed = pressEvent) setBgColor(Color.white) addStatusBar(30) show() actors = [Actor("sprites/boat.png"), Actor("sprites/man_0.png"), Actor("sprites/woman_0.png"), Actor("sprites/man_1.png"), Actor("sprites/woman_1.png"), Actor("sprites/man_2.png"), Actor("sprites/woman_2.png")] left_locations = [Location(2, 0), Location(2, 1), Location(2, 2), Location(1, 1), Location(1, 2), Location(0, 1), Location(0, 2)] right_locations = [Location(4, 0), Location(4, 1), Location(4, 2), Location(5, 1), Location(5, 2), Location(6, 1), Location(6, 2)] for i in range(7): addActorNoRefresh(actors[i], left_locations[i]) for i in range(3): getBg().fillCell(Location(3, i), Color.blue) refresh() startState = 0 showState(startState) Programmcode markieren
Als nächstes implementierst du wieder den Backtracking-Algorithmus, wie du ihn aus Kapitel 10.3. bestens kennst. Dabei muss du als erstes in getNeighbours(state) zu einem bestimmen Zustand alle möglichen Folgezustände bestimmen. Dabei gehst du so vor: Zuerst unterscheidest du, ob sich das Boot links (state < 64) oder rechts (state >=64) befindet. Dann bestimmst du in den Listen li_one und li_two alle Personen, die für einen Transfer als Einzelperson oder im Zweierteam in Frage kommen. Dabei musst du noch mit removeForbiddenTransfers() berücksichtigen, dass eine Frau wirklich nie mit einem fremden Mann im Boot sitzt. In search() implementierst du das Backtracking in bekannter Form. Die Lösungen kopierst du in eine Liste solutions, auf die du nach Ende des Suchvorgangs zugreifen kannst, um die Lösungen zu untersuchen. Dabei schreibst du zuerst die Anzahl gefundener Lösungen aus und wählst dann die kürzeste aus, die du mit Tastenbetätigungen durchlaufen kannst. from gamegrid import * import itertools def pressEvent(e): global state loc = toLocationInGrid(e.getX(), e.getY()) if loc in left_locations: actor = getOneActorAt(loc) if actor != None: x = 6 - actor.getX() y = actor.getY() actor.setLocation(Location(x, y)) if loc in right_locations: actor = getOneActorAt(loc) if actor != None: x = 6 - actor.getX() y = actor.getY() actor.setLocation(Location(x, y)) state = 0 for i in range(7): loc = right_locations[i] actor = getOneActorAt(loc) if actor != None: state += 2**(6 - i) setTitle("State: " + str(state) + ", bin: " + stateToString(state)) if isStateAllowed(state): setStatusText("Situation allowed") else: setStatusText("Situation not allowed") refresh() def stateToString(state): return str(bin(state)[2:]).zfill(7) def showState(state): sbin = stateToString(state) for i in range(7): if sbin[i] == "0": actors[i].setLocation(left_locations[i]) else: actors[i].setLocation(right_locations[i]) refresh() def getTransferInfo(state1, state2): state1 = state1 & 63 state2 = state2 & 63 mod = state1 ^ state2 passList = [] for n in range(6): if mod % 2 == 1: if n // 2 == 0: couple = "blue" elif n // 2 == 1: couple = "green" elif n // 2 == 2: couple = "red" if n % 2 == 0: passList.append("f" + couple) else: passList.append("m" + couple) mod = mod // 2 return passList def getTransferSequence(solution): transferSequence = [] oldState = solution[0] for state in solution[1:]: transferSequence.append(getTransferInfo(oldState, state)) oldState = state return transferSequence def isStateAllowed(state): stateStr = stateToString(state) mred = stateStr[1] == "1" fred = stateStr[2] == "1" mgreen = stateStr[3] == "1" fgreen = stateStr[4] == "1" mblue = stateStr[5] == "1" fblue = stateStr[6] == "1" if mred and not fred or not mred and fred: # mred/fred not together if not fred and (not mgreen or not mblue) or fred and (mgreen or mblue): return False if mgreen and not fgreen or not mgreen and fgreen:#mgreen/fgreen not together if not fgreen and (not mred or not mblue) or fgreen and (mred or mblue): return False if mblue and not fblue or not mblue and fblue: # mblue/fblue not together if not fblue and (not mred or not mgreen) or fblue and (mred or mgreen): return False return True def removeForbiddenTransfers(li): forbiddenPairs = [(0, 3), (0, 5), (1, 2), (2, 5), (1, 4), (3, 4)] allowedPairs = [] for pair in li: if pair not in forbiddenPairs: allowedPairs.append(pair) return allowedPairs def getNeighbours(state): neighbours = [] li_one = [] # one person in boat bin = stateToString(state) if state < 64: # boat at left for i in range(6): if bin[6 - i] == "0": li_one.append(i) li_two = list(itertools.combinations(li_one, 2)) #two persons in boat li_two = removeForbiddenTransfers(li_two) else: # boat at right for i in range(6): if bin[6 - i] == "1": li_one.append(i) li_two = list(itertools.combinations(li_one, 2)) li_two = removeForbiddenTransfers(li_two) li_values = [] if state < 64: # boat at left, restrict to two persons transfer for li in li_two: li_values.append(2**li[0] + 2**li[1] + 64) else: # boat at right, one or two persons transfer for i in li_one: li_values.append(2**i + 64) for li in li_two: li_values.append(2**li[0] + 2**li[1] + 64) for value in li_values: v = state ^ value if isStateAllowed(v): # restrict to allowed states neighbours.append(v) return neighbours def search(state): visited.append(state) # state marked as visited # Check for solution if state == targetState: solutions.append(visited[:]) for neighbour in getNeighbours(state): if neighbour not in visited: # Check if already visited search(neighbour) # recursive call visited.pop() nbSolution = 0 makeGameGrid(7, 3, 50, None, False, mousePressed = pressEvent) addStatusBar(30) setBgColor(Color.white) setTitle("Searching...") show() visited = [] actors = [Actor("sprites/boat.png"), Actor("sprites/man_0.png"), Actor("sprites/woman_0.png"), Actor("sprites/man_1.png"), Actor("sprites/woman_1.png"), Actor("sprites/man_2.png"), Actor("sprites/woman_2.png")] left_locations = [Location(2, 0), Location(2, 1), Location(2, 2), Location(1, 1), Location(1, 2), Location(0, 1), Location(0, 2)] right_locations = [Location(4, 0), Location(4, 1), Location(4, 2), Location(5, 1), Location(5, 2), Location(6, 1), Location(6, 2)] for i in range(7): addActorNoRefresh(actors[i], left_locations[i]) for i in range(3): getBg().fillCell(Location(3, i), Color.blue) refresh() startState = 0 targetState = 127 solutions = [] search(startState) maxLength = 0 maxSolution = None minLength = 100 minSolution = None for solution in solutions: if len(solution) > maxLength: maxLength = len(solution) maxSolution = solution if len(solution) < minLength: minLength = len(solution) minSolution = solution setStatusText("#Solutions: " + str(len(solutions)) + ", Min Length: " + str(minLength) + ", Max Length: " + str(maxLength)) setTitle("Press key to cycle") oldState = startState for state in minSolution[1:]: getKeyCodeWait(True) showState(state) info = getTransferInfo(oldState, state) setTitle("#Transferred: " + str(info)) oldState = state setTitle("Done. #Boat Transfers: " + str((len(minSolution) - 1))) Programmcode markieren
|
MEMO |
Rätsel dieser Art haben die Eigenschaft, dass man zum vornherein nicht weisst, ob sie keine, genau eine oder mehrere Lösungen haben. Dabei zeigt sich, dass es für uns Menschen viel einfacher ist, eine Lösung zu finden, als zu beweisen, dass es keine Lösung gibt. Für den Computer, der mit der Erschöpfenden Suche systematisch nach allen Lösungen sucht, ist das Aufsuchen aller Lösungen grundsätzlich kein Problem, ausser dass der Lösungsprozess sehr lange dauern kann. Dies ist wegen der kombinatorischen Explosion leider schon bei relativ einfachen Spielanlagen der Fall, sodass wir auch hier wieder an Grenzen des Computereinsatzes stossen.
|
AUFGABEN |
|