Il backtracking è una tecnica algoritmica utilizzata tipicamente per risolvere problemi in cui devono essere soddisfatti dei vincoli. Costruisce progressivamente una soluzione, scartando i percorsi che violano i vincoli e tornando indietro quando una scelta porta a un vicolo cieco o quando una soluzione parziale è stata completata.
il backtracking ha una complessità esponenziale, quindi è poco efficiente nell’affrontare problemi che non siano NP-completi (problemi NP-completi)
esercizi
stringhe binarie
stringhe binarie (senza vincoli)
Dato un parametro intero n, stampare tutte le stringhe binarie lunghe n.
osservazioni
le stringhe da stampare sono 2n
stampare una stringa lunga n costa Θ(n)
quindi, il meglio che ci si può aspettare da un algoritmo è Ω(2n⋅n)
def strbin(n, sol = []): if len(sol) == n: print(''.join(sol)) return sol.append('0') strbin(n, sol) sol.pop() sol.append('1') strbin(n, sol) sol.pop()
La tecnica del backtracking si vede nell’uso del pop() - infatti, prima si aggiunge 0 alla soluzione parziale e si esplorano tutte le stringhe che iniziano con quel prefisso, e poi si rimuove l’ultima scelta (dello 0) con un pop() e si esplorano tutte le stringhe con un 1. Anche dopo questa chiamata si esegue un altro pop() per ripristinare lo stato iniziale della lista prima di tornare ancora indietro.
questo algoritmo genera un albero di chiamate ricorsive alto n
l’albero ha quindi ∑i=oh2i=2h+1−1 nodi, che ∈Θ(2n)
ogni nodo esegue solo operazioni in Θ(1), tranne per il print(), che costa Θ(n) e viene eseguito solo dalle foglie
quindi, i nodi interni lavorano per Θ(1)⋅Θ(2n)=Θ(2n), mentre i nodi foglia per 2n⋅Θ(n), per un totale di:
Θ(2n)+2n⋅Θ(n)=Θ(2n⋅n)
vincolo sugli zeri
Si vogliono stampare solo le stringhe che contengono massimo k zeri, con k≤n.
soluzione ottimizzabile
Una soluzione in Θ(2n⋅n) è quella per cui si creano tutte le stringhe, ma si stampano solo quelle ammissibili: