Raccolta di esercizi svolti con la tecnica backtracking, si basa sull’osservare l’albero che produciamo con il nostro algoritmo e “tagliare” i rami che non ci interessano per ridurre la complessità
Esercizio 1
Progettare un algoritmo che prende come parametro un intero e stampa tutte le stringhe binarie lunghe .
Ad esempio per l’algoritmo deve stampare stringhe.
Notiamo che:
- Le stringhe da stampare sono
- Per stampare una stringa lunga dobbiamo pagare
- Quindi il meglio che possiamo ottenere da un algoritmo simile è
L’albero delle scelte possibili è il seguente:
Scriviamo un algoritmo per risolvere il problema:
def bk(n, sol = None):
if sol is None:
sol = []
if len(sol) == n:
print(''.join(sol))
return
sol.append('0')
bk(n, sol)
sol.pop()
sol.append('1')
bk(n, sol)
sol.pop()
Abbiamo ottenuto un algoritmo efficiente, infatti è uguale alla soglia minima descritta prima:
- L’albero ha nodi interni e foglie
- Ciascun nodo richiede
- Ciascuna foglia richiede per essere stampata
- In totale abbiamo
Esercizio 2
Progettare un algoritmo che prende come parametro due interi e con e stampa tutte le stringhe binarie lunghe che contengono al più 1.
Ad esempio per delle stringhe lunghe dobbiamo stampare soltanto le seguenti 11 strighe:
- 0000, 0001, 0010, 0100, 1000, 0011, 0101, 1001, 0110, 1010, 1100
Analizziamo l’albero delle scelte possibili e vediamo quali rami tagliare se e :
In verde sono indicate le foglie che vanno stampate
In generale possiamo non percorrere i seguenti percorsi:
Analizziamo il tetto minimo per la complessità di un algoritmo che risolve il problema, in generale un buon algoritmo dovrebbe avere una complessità proporzionale al numero delle stringhe da stampare quindi , dove con indichiamo il numero di stringhe da stampare.
Nel caso di si ha che:
- indica le stringhe che hanno 2 uni al loro interno
- le stringhe che hanno un solo 1
- 1 è la stringa senza 1
Vediamo una soluzione che non fa uso del backtracking:
def bk1(n, k, sol = None):
if sol is None:
sol = []
if len(sol) == n:
if sol.count('1') <= k:
print(''.join(sol))
return
sol.append('0')
bk1(n, k, sol)
sol.pop()
sol.append('1')
bk1(n, k, sol)
sol.pop()
Questa soluzione ha una complessità di perché non tagliamo i rami che non ci servono, esploriamo tutto l’albero e ad ogni foglia controlliamo se va bene stamparla oppure no.
Vediamo invece come evitare di controllare rami inutili alla soluzione, ovvero quelli che contengono più di 1:
def bk2(n, k, tot1 = 0, sol = None):
if sol is None:
sol = []
if len(sol) == n:
print(''.join(sol))
return
sol.append('0')
bk2(n, k, tot1, sol)
sol.pop()
if tot1 < k:
sol.append('1')
bk2(n, k, tot1 + 1, sol)
sol.pop()
In questo modo evitiamo di percorrere i rami inutili visti nell’immagine prima.
Attenzione
Ci serve una variabile per contare gli 1, non possiamo fare
count('1')
altrimenti ogni stringa avrebbe costo
Tempi delle due soluzioni a confronto con :
Perché andiamo a risparmiare così tanto tempo?
Si consideri un algoritmo di enumerazione basato sul backtracking dove l’albero di ricorsione ha altezza , il costo di una foglia è e il costo di un nodo interno è di .
Se l’algoritmo gode della seguente proprietà:
- Un nodo viene generato solo se ha la possibilità di portare ad una foglia da stampare
Allora la complessità dell’algoritmo è proporzionale al numero di cose da stampare , più precisamente:
- Il costo totale dei nodi foglia sarà perché verranno generate soltanto le foglie da stampare
- I nodi interni dell’albero che verranno generati saranno perché se un nodo esiste allora appartiene ad un percorso che porta ad una foglia, quindi abbiamo foglie altezza
Esercizio 3
Progettare un algoritmo che, presi due interi e con stampi tutte le stringhe che contengono esattamente uni.
Ad esempio per delle stringhe lunghe bisogna stampare soltanto le seguenti 20:
def bk3(n, k, tot1 = 0, sol = None):
if sol is None:
sol = []
if len(sol) == n:
print(''.join(sol))
return
#remain = n - len(sol) - 1 caratteri che mi rimangono da aggiungere
if n - len(sol) - 1 + tot1 >= k:
sol.append('0')
bk3(n, k, tot1, sol)
sol.pop()
if tot1 < k:
sol.append('1')
bk3(n, k, tot1 + 1, sol)
sol.pop()
Esercizio 4 (settembre 2020)
Progettare un algoritmo che, dato un intero , stampi tutte le stringhe sull’alfabeto dei 3 simboli in cui il numero delle supera quello di ciascuno degli altri due simboli.
Ad esempio per l’algoritmo deve stampare la sola stringa mentre per le stringhe da stampare sono:
def bk4(n, na = 0, nc = 0, sol = None):
if sol is None:
sol = []
if len(sol) == n:
print(''.join(sol))
return
sol.append('b')
bk4(n, na, nc, sol)
sol.pop()
# Caratteri che mi mancano da aggiungere sono maggiori del numero della
# stringa con un carattere a aggiunto
if n - (na + nc + 1) > na + 1:
sol.append('a')
bk4(n, na + 1, nc, sol)
sol.pop()
# Caratteri che mi mancano da aggiungere sono maggiori del numero della
# stringa con un carattere c aggiunto
if n - (na + nc + 1) > nc + 1:
sol.append('c')
bk4(n, na, nc + 1, sol)
sol.pop()
Esercizio - Marzo 2023
Dati tre interi positivi con definiamo valida una sequenza di lunghezza con interi tra e la somma di cui elementi è .
Ad esempio per allora la sequenza è valida mentre non è valida.
Progettare un algoritmo che dati gli interi stampi tutte le sequenze valide.
L’algoritmo deve avere complessità dove è il numero di sequenze valide esistente.
def bk(n, k, T, somma = 0, sol=[]):
if len(sol) == n:
print(''.join(sol))
return
for new_char in range(k + 1):
if new_char + somma + (n - len(sol)-1) * k >= T and somma + new_char <= T:
sol.append(str(new_char))
bk(n, k, T, somma + new_char, sol)
sol.pop()
- Con la prima condizione dell’if sto controllando se aggiungendo il nuovo elemento posso comunque raggiungere il valore T con i restanti elementi.
- La seconda mi permette di non superare T
Esercizio
Progettare un algoritmo che data una stringa X lunga sull’alfabeto ternario stampi tutte le stringhe che è possibile ottenere da X sostituendo il simbolo ad alcuni dei suoi caratteri in modo che i caratteri rimanenti risultino in ordine strettamente crescente.
Ad esempio:
- X = “021” allora dobbiamo stampare:
***, 0**, 02*, 0*1, *2*, **1
- X = “2110” allora stampiamo:
****, 2***, *1**, **1*, ***0
L’algoritmo deve avere una complessità di dove è il numero di stringhe da stampare.
# Sol Professore
def esAstMonti(X, p = '*', sol = []):
if len(sol) == len(X):
print(''.join(sol))
return
sol.append('*')
esAstMonti(X, p, sol)
sol.pop()
i = len(sol)
if p == '*' or X[i] > p:
sol.append(X[i])
esAstMonti(X, X[i], sol)
sol.pop()
# Sol mia più brutta perchè non ho pensato al fatto che abbiamo solo 3 caratteri (in teoria funziona anche con numeri da 0 a 9 (?))
def esAst(seq, max='-1', sol=None):
if sol is None:
sol = []
if len(sol) == len(seq):
print(''.join(sol))
return
sol.append('*')
esAst(seq, max, sol)
sol.pop()
i = len(sol)
if max < seq[i]:
sol.append(seq[i])
esAst(seq, seq[i], sol)
sol.pop()
Esercizio Ottobre 2020
Progettare un algoritmo che, data una stringa X di lunghezza n, sull’alfabeto dei 3 simboli 0, 1 e 2, stampa tutte le stringhe di lunghezza n sullo stesso alfabeto che differiscono da X in ciascuna posizione. L’algoritmo proposto deve avere complessità dove è il numero di stringhe da stampare.
Ad esempio se X=“020” l’algoritmo deve stampare le 8 stringhe: 101, 102, 111, 112, 201, 202, 211, 212
def esOtt2020(X, sol = []):
if len(sol) == len(X):
print(''.join(sol))
return
for char in ['0','1','2']:
if char != X[len(sol)]:
sol.append(char)
esOtt2020(X, sol)
sol.pop()
Esercizio Gennaio 2021
Progettare un algoritmo che, dato un intero n, stampa tutte le stringhe binarie
lunghe in cui non appaiono mai 3 simboli uguali consecutivi.
L’algoritmo proposto deve avere complessità dove è il numero
di stringhe da stampare.
Ad esempio per l’algoritmo dve stampare, non necessariamente nello
stesso ordine, le seguenti 6 stringhe: 001, 010, 011, 100, 101, 110
def esGenn2021(n, sol = []):
if len(sol) == n:
print(''.join(sol))
return
i = len(sol)
for char in ['0', '1']:
if i >= 2:
if sol[i-1] != char or sol[i-2] != char:
sol.append(char)
esGenn2021(n, sol)
sol.pop()
else:
sol.append(char)
esGenn2021(n, sol)
sol.pop()
Esercizio es
Progettare un algoritmo che, dato un intero , stampa tutte le stringhe binarie lunghe per le quali la somma dei primi bit è uguale alla somma degli ultimi bit.
Ad esempio per , deve stampare:
0101, 0110, 1010, 1001, 0000, 1111
def esEsame(n, sum=0, sol = None):
if sol is None:
sol = []
if len(sol) == 2*n:
print(''.join(sol))
return
for char in [0,1]:
if len(sol) < n:
sol.append(str(char))
esEsame(n, sum + char, sol)
sol.pop()
else:
if sum - char <= 2*n - len(sol) - 1 and sum-char >= 0:
sol.append(str(char))
esEsame(n, sum - char, sol)
sol.pop()
Esercizio Gennaio 2023
Data una stringa binaria , la differenza tra il numero di 1 e il numero di 0 nelle prime posizioni di è detto vantaggio alla posizione in .
Ad esempio per il vantaggio nelle dodici posizioni è rispettivamente: -1, 0, -1, -2, -1, -2, -1, 0, 1, 0, -1, -2
Dati due interi positivi ed con definiamo con l’insieme di stringhe binarie di lunghezza il cui vantaggio in ogni posizione cade nell’intervallo che va da ad .
Ad esempio con
L’algoritmo, dati ed deve stampare tutte le stringhe in .
def esGenn2023(n, a, diff = 0, sol = None):
if sol is None:
sol = []
if len(sol) == n:
print(''.join(sol))
return
for char in [0,1]:
d = -1 if char == 0 else 1
if -a < diff < a:
sol.append(str(char))
esGenn2023(n, a, diff + d, sol)
sol.pop()