Schema del backtracking: scegliere, esplorare, annullare la scelta
Implementi lo scheletro del backtracking in tre passaggi, lo segua su un esempio semplice e individui dove inserire le condizioni di potatura.
Schema del backtracking: scegliere, esplorare, annullare la scelta è una lezione Coding Interview Prep gratuita su CoddyKit. Questa è la lezione 1 di 4. Puoi leggere la lezione completa qui gratuitamente — poi esercitati direttamente nel browser con un editor di codice integrato e un tutor IA disponibile 24/7. Fa parte del percorso di apprendimento Coding Interview Prep, e i tuoi progressi si sincronizzano tra il web e l'app CoddyKit. Il corso Coding Interview Prep include 4 lezioni in totale.
Che cos'è il backtracking?
Il backtracking è un metodo sistematico per trovare tutte (o alcune) le soluzioni, esplorando progressivamente ogni candidato e abbandonando (tramite la potatura) un ramo non appena si determina che non può produrre una soluzione valida. È l'algoritmo alla base della risoluzione del Sudoku, della generazione delle permutazioni e della ricerca di tutte le combinazioni valide. Lo si può considerare una ricerca in profondità su un albero decisionale.
# 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')Il modello in tre passaggi
Ogni funzione di backtracking segue tre passaggi: Scegli — selezioni il candidato successivo tra le opzioni disponibili. Esplora — ricorra con quella scelta, scendendo di un livello nell'albero decisionale. Annulla la scelta — annulli la scelta dopo il ritorno dalla ricorsione, per ripristinare lo stato in vista del candidato successivo. A seconda del contesto, questo modello è chiamato anche add/recurse/remove oppure mark/recurse/unmark.
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 TrueEsempio più semplice: tutti i sottoinsiemi
Generi tutti i sottoinsiemi di [1, 2, 3]. A ogni indice, scelga se includere o escludere l'elemento. L'indice di partenza avanza dopo ogni chiamata, così non vengono rivisitati gli elementi precedenti. Non è necessario verificare alcun vincolo: ogni stato parziale è valido. Si ottengono 2ⁿ sottoinsiemi. Il passaggio di annullamento della scelta è path.pop() dopo la chiamata ricorsiva.
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]]Individuare la condizione di potatura
Il punto di forza del backtracking rispetto alla forza bruta sta nella potatura: riconoscere tempestivamente che un percorso parziale non può portare a una soluzione valida. Nel problema della somma delle combinazioni (somma obiettivo con un limite), non appena la somma corrente supera l'obiettivo, ogni ramo più profondo può solo aumentare: si pota il ramo restituendo immediatamente. Per le N-regine, se una regina attacca quelle già posizionate, si salta quella colonna. La potatura trasforma alberi esponenziali in ricerche gestibili.
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]]Il ripristino dello stato è fondamentale
Un errore comune nel backtracking consiste nel non ripristinare completamente lo stato prima dell'iterazione successiva. Se si usa una struttura dati mutabile (lista, insieme, griglia), ogni modifica apportata durante il passaggio Scegli deve essere annullata durante Annulla la scelta. Ad esempio, quando si modifica una griglia (come nel Sudoku o nella ricerca di parole), si imposti la cella come vuota dopo la chiamata ricorsiva. Dimenticarlo lascia lo stato corrotto per i rami fratelli.
# 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')) # TrueSeguire l'albero decisionale
Per la somma delle combinazioni con [2, 3, 6, 7] e obiettivo 7, si segua l'albero: alla radice si prova 2. Da 2, si prova di nuovo 2 (remaining=3). Da 2+2, si prova ancora 2 (remaining=1). 2>1, quindi si pota. Si prova 3: 3>1, si pota. Si torna indietro. Da 2+2 si prova 3 (remaining=3). 3 corrisponde al valore rimanente: si registra [2,2,3]. Si torna indietro e si continua. Questa traccia mostra come la potatura elimini i rami prima che producano risultati non validi.
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)Backtracking e forza bruta
La forza bruta prova tutte le possibili soluzioni complete e poi verifica ciascuna di esse. Il backtracking pota durante la costruzione, senza mai completare i percorsi non validi. Per N=8 nel problema delle N-regine, la forza bruta verifica 8^8 = 16 milioni di disposizioni. Il backtracking riduce il numero a circa 2.057 chiamate ricorsive. La differenza cresce rapidamente con N: per N=12, la forza bruta prova 8,9 miliardi di disposizioni, mentre il backtracking esplora solo una frazione dell'albero.
# 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]}')Raccolta dei risultati e restituzione anticipata
I problemi di backtracking rientrano in due categorie: enumerare tutte le soluzioni (raccogliere ogni percorso completo) oppure trovare una qualunque soluzione (restituire True non appena un percorso ha esito positivo). Per l'enumerazione, aggiunga sempre ogni risultato a un elenco. Per trovare una sola soluzione, restituisca immediatamente True dalla chiamata ricorsiva e propaghi il valore verso l'alto. Restituire any(backtrack(...)) oppure usare if backtrack(...): return True implementa il comportamento di short-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), pathMemoizzazione con backtracking
Il backtracking puro esplora ogni percorso senza memorizzazione nella cache, il che va bene quando servono tutte le soluzioni. Tuttavia, alcuni problemi di backtracking presentano sotto-problemi sovrapposti. Ad esempio, Word Break II può essere risolto con backtracking + memoizzazione: si memorizza nella cache l'elenco delle frasi possibili a partire da ogni indice iniziale. In questo modo il backtracking esponenziale nel caso peggiore diventa un algoritmo in tempo polinomiale. Riconosca quando i sotto-problemi si ripetono per applicare questo approccio ibrido.
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']Complessità temporale del backtracking
La complessità temporale del backtracking dipende dal numero di foglie dell'albero decisionale moltiplicato per il lavoro per nodo. Per i sottoinsiemi: O(n × 2ⁿ). Per le permutazioni: O(n × n!). Per la somma delle combinazioni: O(target/min_candidate ^ n) nel caso peggiore. La potatura riduce la costante, ma non l'ordine asintotico. Quando Le viene chiesta la complessità in un colloquio, indichi la dimensione dell'albero nel caso peggiore e specifichi che la potatura di solito rende l'algoritmo molto più veloce nella pratica.
# 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')Individuare i problemi di backtracking
Segnali che indicano la necessità del backtracking: (1) trovare tutte o generare tutte le combinazioni, permutazioni o sottoinsiemi; (2) il problema richiede di posizionare elementi o persone sotto determinati vincoli (N-regine, Sudoku); (3) lo spazio delle soluzioni è esponenziale, ma i vincoli eliminano presto la maggior parte dei rami; (4) è necessario esplorare percorsi in un grafo o in una griglia che potrebbero rivisitare stati. Quando riconosce questi segnali, ricorra al modello scegli-esplora-annulla la scelta.
# 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 TrueVerifica rapida
Metta alla prova la Sua comprensione dei concetti di Data Structures & Algorithms — Coding Interview Prep trattati in questa lezione.
Riepilogo della lezione
In questa lezione ha imparato che: il modello del backtracking ha tre passaggi — scegli, esplora, annulla la scelta — che corrispondono rispettivamente all'aggiunta di una scelta, alla ricorsione e alla sua rimozione, le condizioni di potatura eliminano presto i rami e sono ciò che rende il backtracking pratico rispetto alla forza bruta e lo stato deve essere completamente ripristinato dopo ogni chiamata ricorsiva per evitare di corrompere i rami fratelli. Ora applicheremo il modello per generare tutti i sottoinsiemi e l'insieme delle parti.
Domande Frequenti
La lezione «Schema del backtracking: scegliere, esplorare, annullare la scelta» è gratuita?
Sì — il testo completo di «Schema del backtracking: scegliere, esplorare, annullare la scelta» è gratuito qui sul web. Per esercitarvi in modo interattivo (un editor di codice integrato e un tutor IA 24/7) e sbloccare il resto del corso Coding Interview Prep, passa a CoddyKit PRO. Il corso Coding Interview Prep include 4 lezioni in totale.
Cosa imparerò in «Schema del backtracking: scegliere, esplorare, annullare la scelta»?
Implementi lo scheletro del backtracking in tre passaggi, lo segua su un esempio semplice e individui dove inserire le condizioni di potatura. Eserciti Coding Interview Prep con codice pratico che esegui direttamente nel browser, e un tutor IA 24/7 risponde alle tue domande mentre lavori sulla lezione.
Ho bisogno di esperienza per iniziare Coding Interview Prep?
Non è richiesta alcuna esperienza precedente. Coding Interview Prep su CoddyKit è strutturato per principianti e studenti avanzati, quindi puoi iniziare da qui o dall'inizio e procedere al tuo ritmo. Questa è la lezione 1 di 4.
Quanto tempo richiede la lezione «Schema del backtracking: scegliere, esplorare, annullare la scelta»?
La maggior parte delle lezioni CoddyKit richiede circa 5–10 minuti. Ogni lezione è breve e interattiva, quindi fai progressi costanti e riprendi esattamente da dove hai lasciato su web e app.
Posso scrivere ed eseguire codice in questa lezione Coding Interview Prep?
Sì. Ogni lezione Coding Interview Prep include un editor di codice integrato, quindi scrivi ed esegui codice reale direttamente nel tuo browser e ricevi feedback istantaneo dall'IA — nessuna configurazione locale necessaria.
Tutte le lezioni di questo corso
- Schema del backtracking: scegliere, esplorare, annullare la scelta
- Sottoinsiemi e insieme delle parti
- Permutazioni e combinazioni
- N-regine e propagazione dei vincoli