0Pricing
DSA Interview Prep · Leçon

Modèle de retour sur trace : choisir, explorer, annuler

Mettez en œuvre le squelette de retour arrière en trois étapes, suivez son exécution sur un petit exemple et repérez où s’insèrent les conditions d’élagage.

Modèle de retour sur trace : choisir, explorer, annuler est une leçon DSA Interview Prep gratuite sur CoddyKit. Ceci est la leçon 1 sur 4. Tu peux lire la leçon complète ci-dessous gratuitement — puis la pratiquer en direct dans le navigateur avec un éditeur de code intégré et un tuteur IA 24/7. Elle fait partie du parcours d'apprentissage DSA Interview Prep, et ta progression se synchronise sur le web et l'application CoddyKit. Le cours DSA Interview Prep comprend 4 leçons au total.

Qu’est-ce que le retour arrière ?

Le retour arrière est une méthode systématique permettant de trouver toutes les solutions, ou certaines d’entre elles, en explorant progressivement chaque candidat et en abandonnant (élaguant) une branche dès qu’il est établi qu’elle ne peut pas produire de solution valide. C’est l’algorithme utilisé pour résoudre le Sudoku, générer des permutations et trouver toutes les combinaisons valides. Vous pouvez le voir comme une recherche en profondeur dans un arbre de décision.

# Mental model: backtracking explores a decision tree
# At each node you make a choice, go deeper, then undo it
#
# Tree for generating subsets of [1,2,3]:
#        []
#      /    \
#    [1]   []
#   / \    / \
# [1,2][1][2] []
# ...

# Every leaf is a potential solution
# Pruning cuts branches early based on constraints
print('Backtracking = DFS on decision tree with pruning')

Le gabarit en trois étapes

Toute fonction de retour arrière suit trois étapes : Choisir — sélectionner le prochain candidat parmi les options disponibles. Explorer — effectuer un appel récursif avec ce choix pour descendre d’un niveau dans l’arbre de décision. Annuler — défaire le choix après le retour de la récursion afin de restaurer l’état pour le candidat suivant. Selon le contexte, ce modèle est également appelé ajouter/récursivité/supprimer ou marquer/récursivité/démarquer.

def backtrack(current_state, choices, results):
    # Base case: is current_state a complete solution?
    if is_complete(current_state):
        results.append(list(current_state))  # record solution
        return
    
    for choice in choices:
        if is_valid(choice, current_state):    # pruning condition
            # 1. CHOOSE
            current_state.append(choice)
            # 2. EXPLORE
            backtrack(current_state, choices, results)
            # 3. UNCHOOSE (backtrack)
            current_state.pop()

# Placeholder functions — filled per problem
def is_complete(state): return True
def is_valid(choice, state): return True

Exemple le plus simple : tous les sous-ensembles

Générez tous les sous-ensembles de [1, 2, 3]. À chaque indice, choisissez d’inclure ou d’exclure l’élément. L’indice de départ avance après chaque appel afin de ne pas revisiter les éléments précédents. Aucune vérification de contrainte n’est nécessaire : tout état partiel est valide. Cela produit 2ⁿ sous-ensembles. L’étape d’annulation consiste à exécuter path.pop() après l’appel récursif.

def subsets(nums):
    result = []
    def backtrack(start, path):
        result.append(list(path))  # every state is a valid subset
        for i in range(start, len(nums)):
            path.append(nums[i])    # CHOOSE
            backtrack(i + 1, path)  # EXPLORE
            path.pop()              # UNCHOOSE
    backtrack(0, [])
    return result

print(subsets([1, 2, 3]))
# [[], [1], [1,2], [1,2,3], [1,3], [2], [2,3], [3]]

Identifier la condition d’élagage

La puissance du retour arrière par rapport à la force brute réside dans l’élagage : reconnaître rapidement qu’un chemin partiel ne peut pas mener à une solution valide. Pour la somme de combinaisons (somme cible avec un budget), dès que la somme courante dépasse la cible, toute branche plus profonde ne pourra que croître davantage : élaguez-la en retournant immédiatement. Pour le problème des N reines, si une reine attaque des reines déjà placées, ignorez cette colonne. L’élagage transforme des arbres exponentiels en recherches gérables.

def combination_sum(candidates, target):
    result = []
    candidates.sort()  # sort enables early termination
    def backtrack(start, path, remaining):
        if remaining == 0:
            result.append(list(path))
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining: break   # PRUNE: sorted, so rest are bigger too
            path.append(c)            # CHOOSE
            backtrack(i, path, remaining - c)   # EXPLORE (reuse allowed)
            path.pop()                # UNCHOOSE
    backtrack(0, [], target)
    return result

print(combination_sum([2, 3, 6, 7], 7))  # [[2,2,3],[7]]

La restauration de l’état est essentielle

Une erreur fréquente dans le retour arrière consiste à ne pas restaurer complètement l’état avant l’itération suivante. Si vous utilisez une structure de données modifiable (liste, ensemble, grille), chaque modification effectuée pendant Choisir doit être annulée pendant Annuler. Par exemple, lorsque vous modifiez une grille (comme dans le Sudoku ou la recherche de mots), videz la cellule après l’appel récursif. Oublier cette étape laisse l’état corrompu pour les branches sœurs.

# Bug: forgetting to unmark in word search
# Correct pattern for grid backtracking:
def word_search(board, word):
    m, n = len(board), len(board[0])
    def dfs(r, c, k):
        if k == len(word): return True
        if not (0<=r<m and 0<=c<n): return False
        if board[r][c] != word[k]: return False
        temp, board[r][c] = board[r][c], '#'  # CHOOSE (mark visited)
        found = any(dfs(r+dr, c+dc, k+1)
                    for dr, dc in [(0,1),(0,-1),(1,0),(-1,0)])
        board[r][c] = temp  # UNCHOOSE (restore cell)
        return found
    return any(dfs(r, c, 0) for r in range(m) for c in range(n))

board = [['A','B','C','E'],['S','F','C','S'],['A','D','E','E']]
print(word_search([row[:] for row in board], 'ABCCED'))  # True

Parcourir l’arbre de décision

Pour une somme de combinaisons avec [2, 3, 6, 7] et une cible de 7, parcourez l’arbre : à la racine, essayez 2. À partir de 2, essayez encore 2 (reste=3). À partir de 2+2, essayez encore 2 (reste=1). 2>1, donc élaguez. Essayez 3 : 3>1, élaguez. Backtrack. À partir de 2+2, essayez 3 (reste=3). 3 correspond au reste : enregistrez [2,2,3]. Backtrack, puis continuez. Ce parcours montre comment l’élagage élimine les branches avant qu’elles ne produisent des résultats invalides.

def combination_sum_trace(candidates, target):
    result = []
    candidates.sort()
    def backtrack(start, path, remaining, depth):
        indent = '  ' * depth
        print(f'{indent}explore({path}, remaining={remaining})')
        if remaining == 0:
            result.append(list(path))
            print(f'{indent}FOUND: {path}')
            return
        for i in range(start, len(candidates)):
            c = candidates[i]
            if c > remaining:
                print(f'{indent}PRUNE at {c}')
                break
            path.append(c)
            backtrack(i, path, remaining - c, depth + 1)
            path.pop()
    backtrack(0, [], target, 0)
    return result

combination_sum_trace([2, 3, 6, 7], 7)

Retour arrière ou force brute

La force brute essaie toutes les solutions complètes possibles, puis valide chacune d’elles. Le retour arrière élague pendant la construction et ne termine jamais les chemins invalides. Pour le problème des N reines avec N=8, la force brute vérifie 8^8 = 16 millions de placements. Le retour arrière réduit ce nombre à environ 2 057 appels récursifs. L’écart augmente considérablement avec N : pour N=12, la force brute essaie 8,9 milliards de placements, tandis que le retour arrière n’explore qu’une fraction de l’arbre.

# Compare call counts: brute force vs backtracking for permutations
import sys
calls_brute = [0]
calls_back = [0]

def brute_force_perms(nums):
    from itertools import permutations
    return list(permutations(nums))

def backtrack_perms(nums):
    result = []
    used = [False] * len(nums)
    def bt(path):
        calls_back[0] += 1
        if len(path) == len(nums):
            result.append(list(path))
            return
        for i, n in enumerate(nums):
            if not used[i]:
                used[i] = True
                path.append(n)
                bt(path)
                path.pop()
                used[i] = False
    bt([])
    return result

backtrack_perms([1,2,3,4])
print(f'Backtrack calls for 4 items: {calls_back[0]}')

Tout collecter ou retourner rapidement

Les problèmes de retour arrière appartiennent à deux catégories : énumérer toutes les solutions (collecter chaque chemin complet) ou trouver une seule solution (retourner True dès qu’un chemin réussit). Pour l’énumération, utilisez toujours append pour ajouter les résultats à une liste. Pour la recherche d’une solution quelconque, retournez immédiatement True depuis l’appel récursif et propagez cette valeur vers le haut. Retourner any(backtrack(...)) ou utiliser if backtrack(...): return True implémente le comportement de court-circuit.

# Enumerate all: collect in results list
def all_solutions(candidates):
    results = []
    def bt(path, remaining):
        if remaining == 0:
            results.append(list(path))
            return
        for c in candidates:
            if c <= remaining:
                path.append(c); bt(path, remaining - c); path.pop()
    bt([], 5)
    return results

# Find any one: return True on first success
def any_solution(candidates, target):
    def bt(path, remaining):
        if remaining == 0: return True
        for c in candidates:
            if c <= remaining:
                path.append(c)
                if bt(path, remaining - c): return True  # short-circuit
                path.pop()
        return False
    path = []
    return bt(path, target), path

Mémorisation avec le retour arrière

Le retour arrière pur explore chaque chemin sans mise en cache, ce qui convient lorsque toutes les solutions sont nécessaires. Cependant, certains problèmes de retour arrière comportent des sous-problèmes qui se chevauchent. Par exemple, le problème de segmentation de mots II peut être résolu avec du retour arrière et de la mémorisation : mettez en cache la liste des phrases possibles à partir de chaque indice de départ. Cela transforme le retour arrière exponentiel dans le pire cas en un algorithme de complexité polynomiale. Repérez les sous-problèmes qui se répètent afin d’appliquer cette approche hybride.

from functools import lru_cache

def word_break_all(s, wordDict):
    words = set(wordDict)
    
    @lru_cache(maxsize=None)
    def bt(start):
        if start == len(s): return ['']  # empty suffix
        result = []
        for end in range(start + 1, len(s) + 1):
            word = s[start:end]
            if word in words:
                for rest in bt(end):
                    result.append(word if not rest else word + ' ' + rest)
        return result
    
    return bt(0)

print(word_break_all('catsanddog', ['cat','cats','and','sand','dog']))
# ['cat sand dog', 'cats and dog']

Complexité temporelle du retour arrière

La complexité temporelle du retour arrière dépend du nombre de feuilles de l’arbre de décision multiplié par la quantité de travail par nœud. Pour les sous-ensembles : O(n × 2ⁿ). Pour les permutations : O(n × n!). Pour la somme de combinaisons : O(target/min_candidate ^ n) dans le pire cas. L’élagage réduit la constante, mais pas la borne asymptotique. Lorsque l’on vous demande la complexité pendant un entretien, donnez la taille de l’arbre dans le pire cas et précisez que l’élagage accélère généralement beaucoup l’exécution en pratique.

# Complexity quick reference:
# Subsets of n elements:     O(n * 2^n)  - 2^n subsets, each copied in O(n)
# Permutations of n:          O(n * n!)   - n! perms, each copied in O(n)
# Combination sum (target T): O(T^n / n!) worst case without pruning
# N-Queens:                   O(n!)       - prune reduces practical count

# For n=10 permutations: 10! = 3,628,800 paths
import math
n = 10
print(f'n={n}: n!={math.factorial(n):,} paths')
print(f'n={n}: 2^n={2**n:,} subsets')

Identifier les problèmes de retour arrière

Voici les signes indiquant qu’un problème nécessite du retour arrière : (1) trouver toutes les combinaisons, permutations ou sous-ensembles, ou tous les générer ; (2) le problème consiste à placer des éléments ou des personnes sous certaines contraintes (problème des N reines, Sudoku) ; (3) l’espace des solutions est exponentiel, mais les contraintes éliminent rapidement la plupart des branches ; (4) vous devez explorer des chemins dans un graphe ou une grille qui peuvent revisiter certains états. Lorsque vous observez ces signes, utilisez le gabarit choisir-explorer-annuler.

# Common backtracking problem types:
# 1. Subsets / Power set
# 2. Permutations (with/without duplicates)
# 3. Combinations (k from n, combination sum)
# 4. Grid path finding (word search, unique paths with visited tracking)
# 5. Constraint satisfaction (N-queens, Sudoku solver)
# 6. String partitioning (palindrome partition, word break all)

# Template reminder:
def backtrack(start, path):
    # base case: add to results or return True
    for choice in get_choices(start):
        if is_valid(choice, path):   # prune
            path.append(choice)      # choose
            backtrack(start+1, path) # explore
            path.pop()               # unchoose

def get_choices(start): return []
def is_valid(c, p): return True

Vérification rapide

Évaluez votre compréhension des concepts de structures de données & algorithmes — préparation aux entretiens de programmation présentés dans cette leçon.

Récapitulatif de la leçon

Dans cette leçon, vous avez appris que : le gabarit du retour arrière comporte trois étapes — choisir, explorer, annuler — qui correspondent à l’ajout d’un choix, à la récursion et à sa suppression, les conditions d’élagage éliminent rapidement les branches et rendent le retour arrière pratique par rapport à la force brute, et l’état doit être entièrement restauré après chaque appel récursif afin d’éviter de corrompre les branches sœurs. Nous allons maintenant appliquer ce gabarit pour générer tous les sous-ensembles et l’ensemble des parties.

Questions Fréquemment Posées

La leçon « Modèle de retour sur trace : choisir, explorer, annuler » est-elle gratuite ?

Oui — le texte complet de « Modèle de retour sur trace : choisir, explorer, annuler » est gratuit à lire ici sur le web. Pour la pratiquer de manière interactive (un éditeur de code intégré et un tuteur IA 24/7) et déverrouiller le reste du cours DSA Interview Prep, passe à CoddyKit PRO. Le cours DSA Interview Prep comprend 4 leçons au total.

Qu'est-ce que j'apprendrai dans « Modèle de retour sur trace : choisir, explorer, annuler » ?

Mettez en œuvre le squelette de retour arrière en trois étapes, suivez son exécution sur un petit exemple et repérez où s’insèrent les conditions d’élagage. Tu pratiques DSA Interview Prep avec du code pratique que tu exécutes directement dans le navigateur, et un tuteur IA 24/7 répond à tes questions au fur et à mesure que tu avances dans la leçon.

Dois-je avoir de l'expérience pour commencer DSA Interview Prep ?

Aucune expérience préalable n'est requise. DSA Interview Prep sur CoddyKit est structuré pour les débutants jusqu'aux apprenants avancés, donc tu peux commencer ici ou depuis le début et avancer à ton rythme. Ceci est la leçon 1 sur 4.

Combien de temps prend la leçon « Modèle de retour sur trace : choisir, explorer, annuler » ?

La plupart des leçons CoddyKit prennent environ 5–10 minutes. Chacune est courte et interactive, tu progresses régulièrement et tu repiques exactement où tu t'es arrêté sur le web et l'app.

Peux-tu écrire et exécuter du code dans cette leçon DSA Interview Prep ?

Oui. Chaque leçon DSA Interview Prep inclut un éditeur de code intégré, tu écris et exécutes du vrai code directement dans ton navigateur et tu reçois des retours IA instantanés — aucune configuration locale requise.

Toutes les leçons de ce cours

  1. Modèle de retour sur trace : choisir, explorer, annuler
  2. Sous-ensembles et ensemble des parties
  3. Permutations et combinaisons
  4. Problème des N reines et propagation des contraintes
← Retour à DSA Interview Prep