INTRODUCTION |
Les jeux logiques comportant un aspect mathématique sont très populaires et répandus. Le but de ce chapitre n’est pas de vous ôter le plaisir de résoudre ce genre de jeux à la main mais bien plutôt de mettre en évidence que l’ordinateur peut en trouver la solution de manière systématique à l’aide du retour sur trace (backtracking en anglais). Cela se fait néanmoins avec deux différences significatives. D’une part, en l’absence de stratégie particulière et de limite claire, l’exploration systématique par l’ordinateur peut prendre un temps colossal, même sur un ordinateur très puissant. D’autre part, l’ordinateur est capable de trouver toutes les solutions possibles, ce qui n’est souvent pas faisable à la main. Ceci permet d’exploiter l’ordinateur pour vérifier qu’une certaine solution est la meilleure et la plus courte. |
SUDOKU |
Le Sudoku a subi un gros boom depuis 1980 et n’a cessé de se répandre depuis pour devenir très populaire de nos jours et se retrouver dans pratiquement tous les quotidiens ou hebdomadaires. Il s’agit d’un jeu numérique dont les règles sont très simples. Dans la version standard, les nombres de 1 à 9 doivent être placés dans une grille 9x9 de sorte que chaque nombre n’apparaisse qu’une seule fois dans chaque ligne et chaque colonne. De plus, la grille est divisée en neuf sous-grilles de 3x3 cellules dans lesquels chaque nombre doit apparaître exactement une seule fois. La donnée du jeu consiste en une grille partiellement remplie. Idéalement, une grille initiale ne devrait comporter qu’une et une seule solution. Selon la configuration initiale, le jeu peut être plus ou moins difficile à résoudre. Un joueur expérimenté use de certaines stratégies bien connues et d’autres plus personnelles pour trouver la solution. Dans l’algorithme du retour sur trace naïf effectué par l’ordinateur, aucune stratégie n’est mise en place: les cellules vides sont remplies une à une avec les nombres de 1 à 9 de telle sorte qu’il n’y ait pas de conflit selon les règles du jeu. Si l’algorithme se retrouve dans une impasse, le dernier essai est annulé.
startState = [ \ Comme d’habitude, développons le programme étape après étape. La première tâche consiste à afficher un état donné dans la GameGrid et permettre à l’utilisateur de placer un nombre dans une cellule vide à l’aide de la souris. Bien que cette fonctionnalité ne soit pas nécessaire dans une résolution automatique du jeu, elle permet d’influencer le jeu de manière interactive, ce qui peut se révéler très utile dans la phase de test. Cela permet également de résoudre la grille à l’écran au lieu d’utiliser le papier et le crayon. 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) Sélectionner le code
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) Sélectionner le code
Pour effectuer le retour sur trace (backtracking), il est nécessaire de déterminer les sommets adjacents dans le graphe de transition des états du jeu avec la fonction getNeighbours(state). Pour ce faire, on choisit une cellule vide avec getEmptyCell() et on insère dans l’ordre tous les nombres appartenant à un état du jeu autorisé. Au moins l’un de ces nombres sera à coup sûr le bon. Dans la fonction getNeighbours(state), on commence par cloner la grille partielle reçue en paramètre dans une deuxième liste clone à l’aide de cloneState(state). Cela est nécessaire pour pouvoir travailler sans danger dans cette grille puisqu’elle est passée par référence. Ne pas la cloner reviendrait à modifier la liste originale dans la partie appelante, ce qu’il faut à tout prix éviter dans ce cas (Voir la section « Modèle mémoire en Python » de ce même chapitre pour de plus amples explications à ce sujet). Ensuite, on choisit avec getEmptyCell(state) la prochaine cellule vide dans l’ordre de parcours de la grille choisi (en l’occurrence ligne après ligne, de haut en bas et de gauche à droite, selon la boucle imbriquée dans getEmptyCell). Chacun des neufs nouveaux états ainsi générés est ensuite cloné dans la liste d’états possibles validStates si isValid() indique qu’il est valide. La fonction récursive search() qui effectue la recherche par backtracking peut être reprise pratiquement telle quelle des précédents problèmes résolus par backtracking puisque la partie dépendante du problème réside dans la fonction getNeighbours(state). Dans le cas présent, on ne cherche qu’une seule solution (les grilles de Sudoku bien formées ne possèdent qu’une solution) et on termine la récursion avec le fanion found puisqu’il est vain de poursuivre la recherche. 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") Sélectionner le code
|
MEMENTO |
Il est possible d’utiliser ce programme pour résoudre n’importe quel Sudoku publié sur le Web ou dans un journal. Avec suffisamment de patience, le programme finira toujours par trouver la solution. On pourrait diminuer le temps de résolution en incluant les stratégies de résolution utilisées lors d’une résolution à la main. À vous d’améliorer l’algorithme de recherche en y incluant de telles heuristiques de recherche. La méthode de résolution utilisée peut très bien servir également à résoudre des Sudokus dont les cellules ne sont pas carrées. Il suffit pour cela d’adapter la fonction getBlockValues() en conséquence. |
LES VILAINS MARIS JALOUX |
Pour résoudre ce problème, on procède à nouveau étape par étape. On commence par créer l’interface graphique (une grille GameGrid fera l’affaire) puisque les personnes impliquées peuvent facilement être représentées par des images de sprite. On ajoute les acteurs à une grille invisible 7x3, ce qui permet de dessiner une rivière sur la bande du milieu. Puisque la seule chose qui importe est de savoir si une personne donnée est à gauche ou à droite de la rivière, on choisit comme structure de données un nombre binaire dont chacun des bits représente l’état d’une personne bien précise. Un bit à 0 signifie que la personne est à gauche et un bit à 1 qu’elle est à droite de la rivière. On utilise le bit de poids fort (celui ayant la plus grande valeur) pour représenter la position du bateau. Dans le programme de simulation, il y a un couple de chaque couleur (rouge, vert, bleu) ainsi que le bateau qui sont associés de la manière suivante aux bits d’état:
où b0 est le bit de poids faible (ayant la plus faible valeur numérique). En interprétant l’état du jeu dans le système décimal, l’ensemble des nombres entre 0 et 127 représentent les 128 états possibles au niveau des positions.
You already build a test here with isStateAllowed(state) to check if the current situation is legal according to the rules. (You could also first create a prototype without this test.) 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) Sélectionner le code
Dans une deuxième étape de développement, on implémente l’algorithme de recherche par backtracking bien connu du chapter 10.3. Encore une fois, il faut déterminer tous les états possibles atteignables à partir de l’état courant state au sein d’une fonction getNeighbours(state) qui fonctionne de la manière suivante
La recherche par backtracking est implémentée de manière familière dans la fonction search(). On copie ensuite les solutions possibles dans une liste solutions qu’il sera possible d’examiner une fois le processus de recherche terminé. Ceci permet de compter le nombre de solutions possibles et de déterminer celle qui implique le moins de traversées et qui peut être visualisée pas à pas avec n’importe quelle touche du clavier. 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))) Sélectionner le code
|
MEMENTO |
Avec de tels casse-têtes, on ne sait jamais à l’avance s’ils possèdent une ou plusieurs solutions. Il se trouve qu’il est bien plus facile, pour nous humains, de trouver une solution en tâtonnant que de prouver qu’il n’existe pas de solution. Par contre, pour un ordinateur qui effectue une recherche exhaustive de manière systématique, l’énumération de toutes les configurations possibles et de ce fait la recherche de l’ensemble des solutions possibles n’est pas un problème, même si celle-ci peut nécessiter un temps énorme. Cette situation peut malheureusement déjà survenir avec des casse-têtes relativement simples en raison de l’explosion combinatoire, de sorte que l’on touche à nouveau aux limites de la calculabilité et de l’utilisation de l’ordinateur. |
EXERCICES |
|