Algoritmi e Strutture Dati Autunno 01 Algoritmi di Ricerca Dip. Informatica ed Appl. Prof. G. Persiano Università di Salerno 1 Ricerca esaustiva 1 2 Backtrack 3 2.1 Backtrack per enumerazione...................................... 3 2.2 Backtrack per ottimizzazione..................................... 4 2.3 Algoritmo backtrack per il problema dello zaino........................... 4 3 Branch and Bound 5 3.1 Il puzzle del 15............................................. 5 3.2 Branch and Bound per ottimizzazione................................ 6 3.3 Lower bounding............................................. 6 1 Ricerca esaustiva Consideriamo problemi computazionali le cui soluzioni ammissibili sono descritte da vettori (x 1,, x n ) dove ciascun x i appartiene ad un insieme S i. A secondo dei casi, vorremo risolvere un problema di ottimizzazione: trovare un vettore (x 1,, x n ) che minimizza (o massimizza) un certo criterio P (x 1,, x n ); oppure un problema di enumerazione: enumerare tutti i vettori (x 1,, x n ) che soddisfano il criterio P (x 1,, x n ). La tecnica della ricerca esaustiva semplicemente considera tutti i possibili Π n S i vettori. La discussione degli algoritmi di ricerca è facilitata se consideriamo lo spazio delle soluzioni organizzato ad albero. La radice (posta al livello 1) rappresenta la soluzione parziale in cui nessuna delle n componenti è stata ancora specificata. Un nodo a livello i ha S i figli: uno per ciascun possibile valore di x i e per comodità consideriamo questo valore come etichetta dell arco. In tal modo la soluzione parziale rappresentata dal nodo v è ottenuta leggo le etichette degli archi che costituiscono il cammino dalla radice al nodo v. Un nodo foglia è una soluzione. Esempio 1 [Subset Sum]. Sono dati in input n interi positivi w 1,, w n ed un intero target M. Desideriamo trovare un sottoinsiemi degli interi w i la cui somma è M. Se abbiamo n = 4 interi positivi (11, 13, 24, 7) ed M = 31, il problema Subset Sum ammette 2 soluzione (11, 13, 7) e (24, 7). Possiamo descrivere le soluzioni del problema Subset Sum mediante un vettore (x 1,, x k ) ove 1. ogni intero x i è compreso tra 1 ed n; 2. gli interi sono in ordine crescente: x i < x i+1 per i = 1, k 1; 3. la somma degli interi il cui indice appare in (x 1,, x k ) è M: w xj = M. j=1 Alternativamente, una soluzione può essere descritta da un vettore binario: se l i-esima componente è uguale a 0, l i-esimo elemento non appartiene al sottoinsieme soluzione; se invece l i-esima componente è uguale a 1, l i-esimo elemento appartiene al sottoinsieme soluzione (vedi Figura 1). In generale, differenti rappresentazioni dello spazio delle soluzioni sono possibili per lo stesso problema. Una volta definito l albero che rappresenta lo spazio delle soluzioni, lo stesso può essere visitato parto dalla radice e generando ad ogni passo un nodo dell albero. Un nodo che è stato generato ma i cui figli non sono
Algoritmi di Ricerca 2 6 x1=0 x1=1 6 6 x2=0 x2=1 x2=0 x2=1 6 6 6 6 x3=0 x3=1 x3=0 x3=1 x3=0 x3=1 x3=0 x3=1 6 6 6 6 6 6 6 6 x4=1 x4=1 x4=1 x4=1 x4=1 x4=1 x4=1 x4=1 x4=0 x4=0 x4=0 x4=0 x4=0 6 6 6 6 6 x4=0 x4=0 x4=0 6 6 6 6 6 6 6 6 6 6 6 Figura 1: L albero degli stati per il problema SubsetSum con 4 interi. La topologia dell albero non dipe dai valori input. stati tutti generati è detto vivo. Il nodo vivo di cui si sta attualmente generando un figlio è detto l E-nodo. Un nodo v invece è detto morto se tutti i suoi figli sono stati generati o abbiamo deciso che non è utile espandere v ulteriormente. Differenti algoritmi di ricerca si ottengono specificando: Criterio di espansione. Come scegliere il prossimo E-nodo tra tutti i nodi vivi; Criterio di taglio. Come decidere se è inutile espandere un nodo vivo. La ricerca esaustiva ad esempio non taglia mai un nodo e può decidere di espandere i nodi in un ordine qualsiasi. Ad esempio, un espansione in profondità è ottenuta seguo la seguenti regole: 1. ogni volta che un nodo x figlio dell E-nodo corrente y è generato, x diviene immediatamente il nuovo E-nodo; 2. se il corrente E-nodo invece non ha figli che non siano stati ancora generati, allora il padre di y diviene il prossimo E-nodo; 3. se y non ha padre (cioé y è la radice) e tutti i suoi figli sono stati generati, allora la ricerca termina. La ricerca esaustiva può essere espressa usando il seguente pseudocodice. PEsaustivaTutte() stampa tutte le soluzioni al problema. PEsaustivaMinima invece restituisce nella variabile la soluzione S di costo minimo (P contiene il costo di S è deve essere inzializzato a + ). PEsaustivaTutte(x 1,, x k 1 ) print (x 1,, x k ); PEsaustivaTutte (x 1,, x k ) PEsaustivaMinima(x 1,, x k 1 ) if P (x 1,, x k ) < P ; P = P (x 1,, x k ); S = (x 1,, x k ); PEsaustivaMinima(x 1,, x k )
Algoritmi di Ricerca 3 2 Backtrack La tecnica del backtrack ci permette, per alcune istanze del problema, di non esaminare tutte le soluzioni utilizzando una funzione di bounding per decidere il taglio dei nodi. 2.1 Backtrack per enumerazione Consideriamo prima il problema di enumerazione. Il backtrack genera i nodi eseguo una visita in profondità ed utilizza una funzione di bounding B con la seguente proprietà: Bounding per enumerazione. se B(x 1,, x k ) restituisce Falso allora non esiste nessun cammino con prefisso (x 1,, x k ) che porta ad un nodo soluzione. Pertanto, se durante l esplorazione dello spazio delle soluzioni giungiamo ad un nodo v corrispondente ad una soluzione parziale (x 1,, x k ) per cui B vale Falso, è inutile esplorare il sottoalbero radicato in v. Di seguito riportiamo una descrizione in pseudocodice del backtrack. BacktrackTutte(x 1,, x k 1 ) print (x 1,, x k ); if B(x 1,, x k ) BacktrackTutte(x 1,, x k ) Torniamo al problema SubsetSum e consideriamo la rappresentazione delle soluzioni mediante vettori binari (come in Figura 1). Supponiamo senza perdita di generalità che gli interi w i siano ordinati in ordine crescente. Per definire la funzione B, facciamo le due seguenti osservazioni. Supponiamo di avere una soluzione parziale descritta dal vettore (x 1, x k ). 1. Se w i x i + i=k+1 w i < M allora anche sommando tutti i rimanenti interi w k+1,, w n otteniamo un valore minore di M. Pertanto il vettore (x 1, x k ) non può essere esteso ad una soluzione. In altre parole, giunti al nodo v specificato dal cammino (x 1, x k ) è inutile esplorare l albero radicato in v in quanto non vi troveremo alcuna soluzione. 2. Se w i x i + w k+1 > M aggiungo un qualsiasi intero tra quelli ancora da considerare w k+1,, w n otterremo certamente un valore maggiore di M (nota che w k+1 è il minore degli elementi ancora da considerare). Come prima, l albero radicato nel nodo v specificato dal cammino (x 1, x k ) non contiene alcuna soluzione. Con queste osservazioni in mente, scriviamo il seguente algoritmo Backtrack per il problema SubsetSum. Lo stato corrente è memorizzato nell array globale (x 1,, x n ). Gli argomenti s, k e r hanno il seguente significato: s contiene il valore della somma corrente; k è l indice del prossimo elemento da considerare; r, invece, contiene la somma degli elementi ancora da considerare. La chiamata iniziale è SubsetSum(0, 1, n w i). Assumiamo che w 1 M e che n w i M. SubsetSum(s, k, r) 01. r = r w k ; 02. x k = 0; 03. if s + r M and s + w k+1 M 04. SubsetSum(s, k + 1, r);
Algoritmi di Ricerca 4 05. x k = 1; 06. if s + w k = M 07. print (x 1,, x k ); 08. return; 09. 10. s = s + w k ; 11. if s + w k+1 M 12. SubsetSum(s, k + 1, r); Notiamo che alla linea 11 non è necessario controllare che Infatti, vale certamente che k 1 i=k+1 w i M. w i M altrimenti non avremo effettuato la chiamata corrente e, poiché x k = 1, abbiamo k 1 2.2 Backtrack per ottimizzazione w i = i=k i=k Se intiamo utilizzare la tecnica del backtrack per risolvere un problema di massimizzazione la funzione di bounding deve soddisfare la seguente condizione: Bounding per massimizzazione. B(x 1,, x k ) restituisce un limite superiore (upper bound) al costo della migliore soluzione ottenuta espando la soluzione parziale (x 1,, x k ). Supponiamo che la migliore soluzione trovata finora abbia costo u. Allora se B(x 1,, x k ) u, è inutile esplorare l albero radicato nel nodo (x 1,, x k ). Possiamo pertanto scrivere il seguente pseudocodice. BacktrackMassima(x 1,, x k 1 ) if P (x 1,, x k ) > u u = P (x 1,, x k ); S = (x 1,, x k ); if B(x 1,, x k ) > u BacktrackMassima(x 1,, x k ) La variabile u è inizializzata uguale a. i=k+1 2.3 Algoritmo backtrack per il problema dello zaino Consideriamo il problema di massimizzazione dello zaino intero. Riceviamo in input n pesi positivi w 1,, w n, n profitti positivi p 1,, p n ed un intero M. Vogliamo trovare una sequenza 0/1 tale che w i x i M w i.
Algoritmi di Ricerca 5 e il profitto p i x i sia massimo. Lo spazio delle soluzioni consiste di tutti i vettori binari con n componenti. Osserviamo che la soluzione ottima dello zaino intero ha un profitto che è certamente non maggiore del profitto della soluzione ottima dello zaino frazionario. Quindi se i valori di x i per i = 1,, k sono stati già determinati un limite superiore si ottiene calcolando la soluzione ottima per il problema dello zaino in cui la condizione x i {0, 1} è rilassata a x i [0, 1] per i = k + 1,, n. Come è noto con il vincolo di interezza rilassato il problema dello zaino può essere risolto efficientemente. 3 Branch and Bound Algoritmi branch-and-bound sono caratterizzati da un diverso criterio di espansione: un nodo resta E-nodo finché non muore (cioé tutti i suoi figli sono stati generati). Una volta che tutti i figli dell E-nodo corrente sono stati generati, il prossimo E-nodo può essere scelto in vari modi: il nodo vivo che è stato generato da più tempo (disciplina FIFO) o il nodo vivo che è stato generato da meno tempo (disciplina LIFO). Un modo più sofisticato consiste nell assegnare ad ogni nodo vivo x un valore c(x) e scegliere ad ogni passo come prossimo E-nodo, il nodo vivo con il valore c(x) più piccolo. Ad esempio, nel caso in cui è cercata una qualsiasi soluzione, c(x) potrebbe essere definito come la distanza dalla radice della piú vicina soluzione che si trova nell albero radicato in x. Ovviamente, valutare c( ) su un nodo risulta essere tanto difficile quanto risolvere il problema stesso. Dobbiamo pertanto accontentarci di una funzione ĉ( ) che approsimi la funzione c. Tipicamente si scegli una funzione ĉ( ) della forma ĉ(x) = f(h(x)) + ĝ(x) ove h è la distanza di x dalla radice, f è una funzione non decrescente e ĝ(x) è una stima della distanza di x da una soluzione. La ricerca che si ottiene in tal modo viene detta ricerca LeastCost e può essere descritta nel modo seguente. LeastCost(T, ĉ) if T soluzione then print T ; return; E T ; Q ; while (1) foreach X child of E if X soluzione then print cammino da T a X; return; Q = Q {X}; P (X) = E; if Q = then print Nessuna soluzione ; E ExtractMin(Q); 3.1 Il puzzle del 15 Il puzzle del 15 è un puzzle molto diffuso è si gioca su un riquadro 4 per 4 con 15 tessere numerate che lasciano uno spazio vuoto. Il gioco consiste nel sistemare parto da una configurazione arbitrarie le tessere numerate da 1 a 15 in ordine crescente. Le uniche mosse consentite sono quelle di scambiare di posto lo spazio vuoto con una tessera numerata adiacente. In questo caso ogni nodo dell albero dello spazio delle soluzioni consiste in una configurazione (permutazione) delle tessere ed ha come figli le configurazione ottenibili effettuando una mossa. Per questo problema un buon candidato per la funzione ĉ è la funzione h(x) + ĝ(x) ove h(x) è la profondità del nodo x e ĝ(x) è il numero di tessere che si trovano fuori posto.
Algoritmi di Ricerca 6 Figura 2: Un istanza per cui LeastCost non restituisce la soluzione ottima. 3.2 Branch and Bound per ottimizzazione In questa sezione consideriamo problemi di minimizzazione ed in tal caso la funzione c(x) restituisce il costo minimo di una soluzione presente nell albero radicato in x. Ovviamente, calcolare c(x) è tanto difficile quanto risolvere il problema input e pertanto dobbiamo accontentarci di una sua approssimazione ĉ. La ricerca LeastCost non necessariamente restituisce la soluzione di costo minimo: si veda l esempio in Figura 2. Il problema in questo caso è causato dal fatto che per due nodi x e y abbiamo che ĉ(x) < ĉ(y) sebbene c(x) > c(y). Proviamo ora che se nessuna inversione si verifica allora la ricerca LeastCost raggiunge una soluzione di costo minimo. Teorema 1 Sia la funzione ĉ tale che, per ogni x e y, ĉ(y) < ĉ(x) se e solo se c(y) < c(x). Allora la ricerca LeastCost su un albero finito raggiunge un nodo di costo minimo. Dim.: È facile convincersi che la LeastCost trova sempre una soluzione su un albero finito. Supponiamo invece che la ricerca LeastCost restituisca una soluzione g di costo c(g) > c, ove c è il costo di una soluzione ottima. Sia u l antenato più vicino a g il cui sottoalbero contiene una soluzione g di costo minimo e siano α 1,, α k e β 1,, β l i nodi che si trovano lungo il camino tra u e g e u e g, rispettivamente. Per definizione di u, l albero radicato in β 1 non contiene nessuna soluzione di costo c. Per definizione della funzione c abbiamo che mentre invece Per la proprietà di ĉ, ciò implica che c(u) = c(α 1 ) = c(α 2 ) = = c(α k ) = c(g ) c(β 1 ),, c(β l ), c(g) > c(u). ĉ(α 1 ),, ĉ(α k ) > ĉ(β 1 ). Pertanto β 1 verrà espaso dopo che α 1,, α k sono stati espasi e pertanto LeastCost incontrerà prima la soluzione g. 3.3 Lower bounding Il teorema precedente ci dà una condizione sotto la quale la ricerca LeastCost ci conduce alla soluzione ottima. Purtroppo è difficile definire funzioni ĉ che soddisfano la proprietà. Pertanto consideriamo una ricerca Least- Cost leggermente modificata che però garantisce la soluzione ottima sotto assunzioni meno stringenti. Infatti è sufficiente che la funzione ĉ sia, per ogni x, un lower bound alla c(x). Teorema 2 Sia ĉ(x) c(x) per tutti i nodi e ĉ(x) = c(x) per tutti i nodi soluzione. LeastCostMod resituisce sempre una soluzione di costo minimo. Allora la ricerca Dim.: Nel momento in cui viene trovata una soluzione E, abbimao che ĉ(e) ĉ(l) per tutti i nodi vivi L. Per assunzione ĉ(e) = c(e) e ĉ(l) c(l). Pertanto c(e) c(l) per tutti i nodi vivi e quindi E è una soluzione di costo minimo. LeastCostMod(T, ĉ) E T ; Q ; while (1) if E soluzione then print cammino da T a E; return; foreach X child of E Q = Q {X}; P (X) = E; if Q = then print Nessuna soluzione ; E ExtractMin(Q); La versione aggiornata di questo documento si trova all url http://www.dia.unisa.it/ giuper/asdi/note/search.pdf.