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()