Préparez-vous pour votre prochaine interview – Real Python

By | septembre 21, 2020

Formation Python

Il s’agit d’un problème plus vaste et plus complexe que celui que vous avez abordé jusqu’ici dans ce didacticiel. Vous parcourrez le problème étape par étape, en terminant par une fonction récursive qui résout le puzzle. Voici un aperçu des étapes à suivre:

  • Lis le puzzle en forme de grille.
  • Pour chaque cellule:
    • Pour chaque nombre possible dans cette cellule:
      • Endroit le nombre dans la cellule.
      • Retirer ce nombre de la ligne, de la colonne et du petit carré.
      • Bouge toi à la position suivante.
    • S'il ne reste aucun nombre possible, déclarez le puzzle insoluble.
    • Si toutes les cellules sont remplies, alors retourner la solution.

La partie délicate de cet algorithme est de suivre la grille à chaque étape du processus. Vous utiliserez la récursivité, en créant une nouvelle copie de la grille à chaque niveau de la récursivité, pour conserver ces informations.

Avec ce plan à l'esprit, commençons par la première étape, la création de la grille.

Générer une grille à partir d'une ligne

Pour commencer, il est utile de convertir les données du puzzle dans un format plus utilisable. Même si vous souhaitez finalement résoudre le casse-tête dans le format SDM donné, vous progresserez probablement plus rapidement en travaillant sur les détails de votre algorithme avec les données sous forme de grille. Une fois que vous avez une solution qui fonctionne, vous pouvez la convertir pour qu'elle fonctionne sur une structure de données différente.

À cette fin, commençons par quelques fonctions de conversion:

    1 # sudokusolve.py
    2 def line_to_grid(valeurs):
    3     la grille = []
    4     ligne = []
    5     pour indice, carboniser dans énumérer(valeurs):
    6         si indice et indice % 9 == 0:
    7             la grille.ajouter(ligne)
    8             ligne = []
    9         ligne.ajouter(int(carboniser))
dix     # Ajouter la dernière ligne
11     la grille.ajouter(ligne)
12     revenir la grille
13 
14 def grid_to_line(la grille):
15     ligne = ""
16     pour rangée dans la grille:
17         r = "".joindre(str(X) pour X dans rangée)
18         ligne + = r
19     revenir ligne

Votre première fonction, line_to_grid (), convertit les données d'une seule chaîne de quatre-vingt-un chiffres en une liste de listes. Par exemple, il convertit la chaîne ligne à une grille comme début:

ligne = "0040060790000006020560923000780 ... 90007410920105000000840600100"
début = [[[[
    [[[[ 0, 0, 4,   0, 0, 6,   0, 7, 9],
    [[[[ 0, 0, 0,   0, 0, 0,   6, 0, 2],
    [[[[ 0, 5, 6,   0, 9, 2,   3, 0, 0],
    [[[[ 0, 7, 8,   0, 6, 1,   0, 3, 0],
    [[[[ 5, 0, 9,   0, 0, 0,   4, 0, 6],
    [[[[ 0, 2, 0,   5, 4, 0,   8, 9, 0],
    [[[[ 0, 0, 7,   4, 1, 0,   9, 2, 0],
    [[[[ 1, 0, 5,   0, 0, 0,   0, 0, 0],
    [[[[ 8, 4, 0,   6, 0, 0,   1, 0, 0],
]

Chaque liste intérieure représente ici une ligne horizontale dans votre puzzle sudoku.

Vous commencez avec un vide la grille et un vide ligne. Vous construisez ensuite chacun ligne en convertissant neuf caractères du valeurs chaîne en entiers à un chiffre, puis en les ajoutant au courant ligne. Une fois que vous avez neuf valeurs dans un ligne, comme indiqué par index% 9 == 0 à la ligne 7, vous insérez cela ligne dans le la grille et commencez un nouveau.

La fonction se termine par l'ajout du dernier ligne à la la grille. Vous en avez besoin car le pour la boucle se terminera par le dernier ligne toujours stocké dans la variable locale et pas encore ajouté à la grille.

La fonction inverse, grid_to_line (), est légèrement plus courte. Il utilise une expression de générateur avec .joindre() pour créer une chaîne de neuf chiffres pour chaque ligne. Il ajoute ensuite cette chaîne à l'ensemble ligne et le renvoie. Notez qu'il est possible d'utiliser des générateurs imbriqués pour créer ce résultat avec moins de lignes de code, mais la lisibilité de la solution commence à diminuer considérablement.

Maintenant que vous avez les données dans la structure de données souhaitée, commençons à travailler avec.

Génération d'un petit itérateur carré

Votre fonction suivante est un générateur qui vous aidera à rechercher le plus petit carré trois par trois dans lequel se trouve une position donnée. Étant donné les coordonnées x et y de la cellule en question, ce générateur produira une liste de coordonnées qui correspondent le carré qui le contient:

Une grille de Sudoku avec l'un des petits carrés en surbrillance.

Dans l'image ci-dessus, vous examinez la cellule (3, 1), votre générateur produira donc des paires de coordonnées correspondant à toutes les cellules légèrement ombrées, en ignorant les coordonnées qui ont été passées:

(3, 0), (4, 0), (5, 0), (4, 1), (5, 1), (3, 2), (4, 2), (5, 2)

Mettre la logique de détermination de ce petit carré dans une fonction utilitaire distincte permet de garder le flux de vos autres fonctions plus lisible. En faire un générateur vous permet de l'utiliser dans un pour boucle pour parcourir chacune des valeurs.

La fonction pour ce faire implique l'utilisation des limitations des mathématiques entières:

# sudokusolve.py
def petit carré(X, y):
    supérieurX = ((X + 3) // 3) * 3
    supérieurY = ((y + 3) // 3) * 3
    lowerX = supérieurX - 3
    inférieurY = supérieurY - 3
    pour subX dans gamme(lowerX, supérieurX):
        pour subY dans gamme(inférieurY, supérieurY):
            # Si subX! = X ou subY! = Y:
            si ne pas (subX == X et subY == y):
                rendement subX, subY

Il y a beaucoup de trois dans deux de ces lignes, ce qui fait des lignes comme ((x + 3) // 3) * 3 semble déroutant. Voici ce qui se passe lorsque X est 1.

>>>

>>> X = 1
>>> X + 3
4
>>> (X + 3) // 3
1
>>> ((X + 3) // 3) * 3
3

L'utilisation de l'arrondi des nombres entiers vous permet d'obtenir le multiple suivant le plus élevé de trois au-dessus d'une valeur donnée. Une fois que vous avez cela, soustraire trois vous donnera le multiple de trois en dessous du nombre donné.

Il y a quelques autres fonctions utilitaires de bas niveau à examiner avant de commencer à construire dessus.

Passer à l'endroit suivant

Votre solution devra parcourir la structure de grille une cellule à la fois. Cela signifie qu'à un moment donné, vous devrez déterminer quelle devrait être la prochaine position. compute_next_position () à la rescousse!

compute_next_position () prend les coordonnées x et y actuelles comme entrée et renvoie un tuple contenant un fini drapeau avec les coordonnées x et y de la position suivante:

# sudokusolve.py
def compute_next_position(X, y):
    suivantY = y
    suivantX = (X + 1) % 9
    si suivantX < X:
        suivantY = (y + 1) % 9
        si suivantY < y:
            revenir (Vrai, 0, 0)
    revenir (Faux, suivantX, suivantY)

le fini L'indicateur indique à l'appelant que l'algorithme est sorti de la fin du puzzle et a terminé toutes les cases. Vous verrez comment cela est utilisé dans une section ultérieure.

Suppression des nombres impossibles

Votre dernier utilitaire de bas niveau est assez petit. Il prend une valeur entière et un itérable. Si la valeur est différente de zéro et apparaît dans l'itérable, la fonction la supprime de l'itérable:

# sudokusolve.py
def test_and_remove(valeur, possible):
    si valeur ! = 0 et valeur dans possible:
        possible.retirer(valeur)

En règle générale, vous ne transformez pas cette petite fonctionnalité en une fonction. Cependant, vous utiliserez cette fonction plusieurs fois, il est donc préférable de suivre le principe DRY et de le faire passer à une fonction.

Vous avez maintenant vu le niveau inférieur de la pyramide des fonctionnalités. Il est temps d’intensifier et d’utiliser ces outils pour créer une fonction plus complexe. Vous êtes presque prêt à résoudre le puzzle!

Trouver ce qui est possible

Votre fonction suivante utilise certaines des fonctions de bas niveau que vous venez de parcourir. Étant donné une grille et une position sur cette grille, il détermine les valeurs que cette position pourrait encore avoir:

Une grille de Sudoku montrant un point de départ pour indiquer les valeurs possibles pour une cellule spécifique.

Pour la grille ci-dessus, à la position (3, 1), les valeurs possibles sont [1, 5, 8] car les autres valeurs sont toutes présentes, soit dans cette ligne ou cette colonne, soit dans le petit carré que vous avez regardé plus tôt.

C'est la responsabilité de detect_possible ():

# sudokusolve.py
def detect_possible(la grille, X, y):
    si la grille[[[[X][[[[y]:
        revenir [[[[la grille[[[[X][[[[y]]

    possible = ensemble(gamme(1, dix))
    # Test horizontal et vertical
    pour indice dans gamme(9):
        si indice ! = y:
            test_and_remove(la grille[[[[X][[[[indice], possible)
        si indice ! = X:
            test_and_remove(la grille[[[[indice][[[[y], possible)

    # Test en petit carré
    pour subX, subY dans petit carré(X, y):
        test_and_remove(la grille[[[[subX][[[[subY], possible)

    revenir liste(possible)

La fonction commence par vérifier si la position donnée à X et y a déjà une valeur différente de zéro. Si tel est le cas, c’est la seule valeur possible et elle renvoie.

Sinon, la fonction crée un ensemble de nombres de un à neuf. La fonction vérifie les différents numéros de blocage et les supprime de cet ensemble.

Il commence par vérifier la colonne et la ligne de la position donnée. Cela peut être fait avec une seule boucle en alternant simplement les changements d'indice. la grille[x][index] vérifie les valeurs dans la même colonne, tandis que la grille[index][y] vérifie ces valeurs dans la même ligne. Vous pouvez voir que vous utilisez test_and_remove () ici pour simplifier le code.

Une fois que ces valeurs ont été supprimées de votre possible set, la fonction passe au petit carré. C'est là que le petit carré() générateur que vous avez créé avant est utile. Vous pouvez l'utiliser pour parcourir chaque position dans le petit carré, à nouveau en utilisant test_and_remove () pour éliminer toutes les valeurs connues de votre possible liste.

Une fois que toutes les valeurs de blocage connues ont été supprimées de votre ensemble, vous avez la liste de toutes possible valeurs pour cette position sur cette grille.

Vous vous demandez peut-être pourquoi le code et sa description indiquent que la position est «sur cette grille». Dans votre prochaine fonction, vous verrez que le programme fait de nombreuses copies de la grille en essayant de la résoudre.

Résoudre le problème

Vous avez atteint le cœur de cette solution: résoudre()! Cette fonction est récursive, donc une petite explication initiale pourrait vous aider.

La conception générale de résoudre() est basé sur le test d'une seule position à la fois. Pour la position d'intérêt, l'algorithme obtient la liste des valeurs possibles, puis sélectionne ces valeurs, une à la fois, pour être dans cette position.

Pour chacune de ces valeurs, il crée une grille avec la valeur supposée dans cette position. Il appelle ensuite une fonction pour tester une solution, en passant dans la nouvelle grille et la position suivante.

Il se trouve que la fonction qu'elle appelle est elle-même.

Pour toute récursivité, vous avez besoin d'une condition de terminaison. Cet algorithme en a quatre:

  1. Il n'y a pas de valeurs possibles pour cette position. Cela indique que la solution testée ne peut pas fonctionner.
  2. Il a marché jusqu'au bout de la grille et a trouvé une valeur possible pour chaque position. Le puzzle est résolu!
  3. Une des suppositions à cette position, lorsqu'elle est renvoyée au solveur, renvoie une solution.
  4. Il a essayé toutes les valeurs possibles à cette position et aucune d’entre elles ne fonctionnera.

Examinons le code pour cela et voyons comment tout cela se passe:

# sudokusolve.py
importer copie

def résoudre(début, X, y):
    temp = copie.copie profonde(début)
    tandis que Vrai:
        possible = detect_possible(temp, X, y)
        si ne pas possible:
            revenir Faux

        fini, suivantX, suivantY = compute_next_position(X, y)
        si fini:
            temp[[[[X][[[[y] = possible[[[[0]
            revenir temp

        si len(possible) > 1:
            Pause
        temp[[[[X][[[[y] = possible[[[[0]
        X = suivantX
        y = suivantY

    pour devine dans possible:
        temp[[[[X][[[[y] = devine
        résultat = résoudre(temp, suivantX, suivantY)
        si résultat:
            revenir résultat
    revenir Faux

La première chose à noter dans cette fonction est qu'elle fait un .deepcopy () de la grille. Il fait un copie profonde car l'algorithme doit garder une trace de l'endroit exact où il se trouvait à tout moment de la récursivité. Si la fonction ne faisait qu'une copie superficielle, alors chaque version récursive de cette fonction utiliserait la même grille.

Une fois la grille copiée, résoudre() peut travailler avec la nouvelle copie, temp. Une position sur la grille a été transmise, c'est donc le numéro que cette version de la fonction résoudra. La première étape consiste à voir quelles valeurs sont possibles dans cette position. Comme vous l'avez vu plus tôt, detect_possible () renvoie une liste de valeurs possibles qui peuvent être vides.

S'il n'y a pas de valeurs possibles, vous avez atteint la première condition de fin de la récursivité. La fonction renvoie Faux, et la routine d'appel continue.

S'il y a sont valeurs possibles, alors vous devez passer à autre chose et voir si l'une d'entre elles est une solution. Avant de faire cela, vous pouvez ajouter un peu d'optimisation au code. S'il n'y a qu'une seule valeur possible, vous pouvez insérer cette valeur et passer à la position suivante. La solution présentée le fait dans une boucle, vous pouvez donc placer plusieurs nombres dans la grille sans avoir à se reproduire.

Cela peut sembler une petite amélioration, et j'admets que ma première mise en œuvre ne l'a pas inclus. Mais certains tests ont montré que cette solution était considérablement plus rapide que simplement récurrente ici au prix d'un code plus complexe.

Parfois, bien sûr, il y aura plusieurs valeurs possibles pour la position actuelle, et vous devrez décider si l'une d'entre elles mènera à une solution. Heureusement, vous avez déjà déterminé la position suivante dans la grille, vous pouvez donc renoncer à placer les valeurs possibles.

Si la position suivante est en dehors de la fin de la grille, la position actuelle est la dernière à remplir. Si vous savez qu’il existe au moins une valeur possible pour ce poste, vous avez trouvé une solution! La position actuelle est remplie et la grille complétée est renvoyée jusqu'à la fonction appelante.

Si la position suivante est toujours sur la grille, vous parcourez chaque valeur possible pour le spot actuel, remplissez la supposition de la position actuelle, puis appelez résoudre() avec le temp grille et la nouvelle position à tester.

résoudre() ne peut renvoyer qu'une grille complétée ou Faux, donc si l’une des hypothèses possibles renvoie un résultat qui n’est pas Faux, puis un résultat a été trouvé, et cette grille peut être renvoyée dans la pile.

Si toutes les suppositions possibles ont été faites et qu'aucune d'entre elles n'est une solution, alors la grille qui a été transmise est insoluble. S'il s'agit de l'appel de niveau supérieur, cela signifie que le casse-tête est insoluble. Si l’appel est plus bas dans l’arbre de récursivité, cela signifie simplement que cette branche de l’arbre de récursivité n’est pas viable.

Mettre tous ensemble

À ce stade, vous avez presque terminé la solution. Il ne reste qu’une dernière fonction, sudoku_solve ():

# sudokusolve.py
def sudoku_solve(chaîne_entrée):
    la grille = line_to_grid(chaîne_entrée)
    répondre = résoudre(la grille, 0, 0)
    si répondre:
        revenir grid_to_line(répondre)
    autre:
        revenir "Insoluble"

Cette fonction fait trois choses:

  1. Convertit la chaîne d'entrée en une grille
  2. Appels résoudre() avec cette grille pour obtenir une solution
  3. Renvoie la solution sous forme de chaîne ou "Insoluble" s'il n'y a pas de solution

C'est tout! Vous avez parcouru une solution au problème du solveur de sudoku.

Sujets de discussion d'entrevue

La solution de solveur de sudoku que vous venez de parcourir est une bonne quantité de code pour une situation d'entrevue. Une partie du processus d'entrevue consisterait probablement à discuter d'une partie du code et, plus important encore, de certains des compromis de conception que vous avez faits. Examinons quelques-uns de ces compromis.

Récursion

La plus grande décision de conception concerne l'utilisation de la récursivité. Il est possible d’écrire une solution non récursive à tout problème ayant une solution récursive. Pourquoi choisir la récursivité plutôt qu'une autre option?

C'est une discussion qui dépend non seulement du problème, mais également des développeurs impliqués dans l'écriture et la maintenance de la solution. Certains problèmes se prêtent à des solutions récursives plutôt nettes, et d’autres non.

En général, les solutions récursives prendront plus de temps à s'exécuter et utiliseront plus de mémoire que les solutions non récursives. Mais ce n’est pas toujours vrai et, plus important encore, ce n’est pas toujours le cas important.

De même, certaines équipes de développeurs sont à l'aise avec les solutions récursives, tandis que d'autres les trouvent exotiques ou inutilement complexes. La maintenabilité doit également jouer dans vos décisions de conception.

Une bonne discussion à avoir sur une décision comme celle-ci concerne la performance. À quelle vitesse cette solution doit-elle s'exécuter? Sera-t-il utilisé pour résoudre des milliards d'énigmes ou juste une poignée? Sera-t-il exécuté sur un petit système embarqué avec des contraintes de mémoire, ou sera-t-il sur un gros serveur?

Ces facteurs externes peuvent vous aider à décider quelle est la meilleure décision de conception. Ceux-ci sont génial sujets à aborder lors d'une interview lorsque vous travaillez sur un problème ou discutez de code. Un seul produit peut avoir des endroits où les performances sont essentielles (faire du lancer de rayons sur un algorithme graphique, par exemple) et des endroits où cela n'a aucune importance (comme l'analyse du numéro de version lors de l'installation).

Le fait de soulever des sujets comme celui-ci lors d'une interview montre que vous ne pensez pas seulement à résoudre un problème abstrait, mais que vous êtes également disposé et capable de passer au niveau supérieur et de résoudre un problème spécifique auquel l'équipe est confrontée.

Lisibilité et maintenabilité

Parfois, il vaut la peine de choisir une solution plus lente afin de créer une solution plus facile à utiliser, à déboguer et à étendre. La décision dans le défi du solveur de sudoku de convertir la structure de données en grille est l'une de ces décisions.

Cette décision de conception ralentit probablement le programme, mais à moins que vous n'ayez mesuré, vous ne savez pas. Même si tel est le cas, mettre la structure de données sous une forme qui est naturelle pour le problème peut faciliter la compréhension du code.

Il est tout à fait possible d’écrire un solveur qui opère sur les chaînes linéaires qui vous sont données en entrée. C'est probablement plus rapide et prend probablement moins de mémoire, mais petit carré(), entre autres, sera beaucoup plus difficile à écrire, lire et maintenir dans cette version.

Faux pas

Une autre chose à discuter avec un intervieweur, que vous soyez en train de coder en direct ou de discuter du code que vous avez écrit hors ligne, ce sont les erreurs et les faux virages que vous avez pris en cours de route.

Ceci est un peu moins évident et peut être légèrement préjudiciable, mais surtout si vous codez en direct, prendre une mesure pour refactoriser un code qui n'est pas correct ou qui pourrait être meilleur peut montrer comment vous travaillez. Peu de développeurs peuvent écrire du code parfait du premier coup. Heck, peu de développeurs peuvent écrire bien codez la première fois.

Les bons développeurs écrivent le code, puis reviennent en arrière et le refactorisent et le corrigent. Par exemple, ma première implémentation de detect_possible () ressemblait à ceci:

# sudokusolve.py
def first_detect_possible(X, y, la grille):
    impression(F"position[[[[X][[[[y]= la grille[[[[X][[[[y]")
    possible = ensemble(gamme(1, dix))

    # Test horizontal
    pour indice, valeur dans énumérer(la grille[[[[X]):
        si indice ! = y:
            si la grille[[[[X][[[[indice] ! = 0:
                possible.retirer(la grille[[[[X][[[[indice])
    # Test vertical
    pour indice, rangée dans énumérer(la grille):
        si indice ! = X:
            si la grille[[[[indice][[[[y] ! = 0:
                possible.retirer(la grille[[[[indice][[[[y])

    impression(possible)

Ignorant qu'il ne considère pas le petit carré() informations, ce code peut être amélioré. Si vous comparez cela à la version finale de detect_possible () ci-dessus, vous verrez que la version finale utilise une seule boucle pour tester à la fois l'horizontale et les dimensions verticales.

Emballer

C'est votre visite guidée d'une solution de solveur de sudoku. Il y a plus d'informations disponibles sur les formats de stockage des puzzles et une énorme liste de puzzles de sudoku sur lesquels vous pouvez tester votre algorithme.