Capitolo 7: Il Backtracking Backtracking Il backtracking è una tecnica non molto astuta ma spesso molto efficace. Il termine “to back track” si traduce in “tornare sui propri passi”, infatti la politica del backtracking è appunto la seguente: “Prova a fare qualcosa e, se non funziona, disfala e riprova a fare qualcos’altro”. Un tipico esempio dell’utilizzo di questa tecnica è l’algoritmo della ricerca delle permutazioni di n numeri. Esso è realizzato con una procedura ricorsiva nella quale si utilizza un insieme S che contiene la soluzione e un insieme E che indica se l’elemento i-esimo è attualmente presente nella soluzione parziale. La procedura si arresta quando il numero k di elementi dell’insieme S è uguale al numero n di elementi considerati; inoltre è necessario che tutti gli elementi presenti in S siano differenti: in caso contrario si prova ad aggiungere un elemento diverso alla soluzione parziale. Riportiamo di seguito l’algoritmo delle permutazioni: la chiamata iniziale è PERMUTAZIONI(S, 0, E, n); procedure permutazioni(S,E:vettore; k,n:integer) begin if k=n stampa l’insieme S else begin for i := 1 to n do if E[i] = 0 then begin S[k+1] := i; E[i] :=1; PERMUTAZIONI(S, k+1, E, n) E[i] := 0 endif endfor endelse end. Un’applicazione che combina la tecnica di backtracking e l’algoritmo delle permutazioni, è la ricerca di un ciclo hamiltoniano all’interno di un grafo. 59 Capitolo 7: Il Backtracking Cicli Hamiltoniani I cicli o circuiti hamiltoniani prendono il nome dal famoso matematico irlandese Sir William Rowan Hamilton che, nel 1859, pose il problema di trovare un cammino chiuso sul bordo di un dodecaedro. Definizione 1: Dato un grafo G = (N,A) con N insieme dei nodi ed A insieme degli archi, si definisce cammino hamiltoniano un cammino p= <n1, n2, …, nk> tale per cui: * k è pari a |N| (cardinalità di N) * ni ≠ nj ∀ i, j Più semplicemente un cammino hamiltoniano tocca tutti i vertici del grafo uno ed una sola volta! Definizione 2: Dato un grafo G = (N,A) e un insieme S A, S è l’insieme di archi di un ciclo hamiltoniano se e solo se: - in ogni nodo di G incidono esattamente due archi di S - S non contiene cicli di cardinalità inferiore a |N| Un ciclo o circuito hamiltoniano è quindi un cammino hamiltoniano in cui l’ultimo vertice coincide con il primo, come quello rappresentato in figura. E’ possibile rappresentare i cicli hamiltoniani come permutazioni degli n nodi del grafo G. Ovviamente non tutte le permutazioni degli n nodi sono 60 Capitolo 7: Il Backtracking necessariamente un ciclo. Un grafo completo avrà (n−1)! cicli hamiltoniani mentre un grafo sparso potrebbe non averne neanche uno (si pensi ad esempio ad un albero che, appunto è un grafo aciclico). In particolare le soluzioni possibili non sono altro che le permutazioni di tutti i nodi connessi del grafo in cui, però, ci sia un arco di collegamento tra l’ultimo elemento della soluzione ed il primo. Il teorema di Dirac fornisce una condizione sufficiente (ma non necessaria) affinché un grafo con n vertici sia hamiltoniano: se per ogni nodo ci sono almeno n/2 archi, allora il grafo è hamiltoniano. Il Problema del Commesso Viaggiatore Uno dei classici problemi legati ai cicli hamiltoniani è il Problema del Commesso Viaggiatore. Il Problema del Commesso Viaggiatore (Traveling Salesman Problem, o TSP) è definito come segue: Dato un grafo pesato G=(N,A), trovare un ciclo hamiltoniano tale per cui la lunghezza del cammino (ossia la somma dei pesi degli archi che lo compongono) è minima. Il commesso deve partire dall’azienda in cui lavora e fornire con i suoi prodotti diverse città. Il percorso deve essere, però, pianificato in modo che egli possa fornire tutte le città percorrendo il numero minimo di Km. A fine giornata il commesso deve ritornare nella azienda di cui è dipendente. Il problema proposto quindi è molto simile all’esempio precedente con la differenza dei pesi sugli archi. Nella figura seguente si riporta un albero di ricerca delle soluzioni, dove l’uso della tecnica di Backtracking garantisce il taglio del sottoalbero con radice = 4, poiché la distanza dal nodo 1 al 4 (dist=33) è già superiore alla distanza minima trovata (dist=24). 61 Capitolo 7: Il Backtracking Stampa dei cicli Hamiltoniani Perchè una permutazione sia un ciclo Hamiltoniano per G, tra un nodo e quello che segue nella permutazione deve esserci un arco in A e lo stesso deve valere anche per l’ultimo nodo ed il primo nodo della permutazione. Il Backtracking entra in gioco con una funzione di taglio che evita la generazione di permutazioni parziali in cui fra un nodo e il successivo non sia presente un arco in A (permutazioni di questo genere non potranno completarsi in soluzioni ammissibili). Inoltre, uno stesso ciclo hamiltoniano può essere rappresentato da n diverse permutazioni in funzione del nodo da cui si parte fa partire il ciclo. Per evitare di stampare più volte uno stesso ciclo, consideriamo che il primo elemento della permutazione sia il vertice 1 del grafo (vale a dire S[1] = 1). CICLI-HAMILTONIANI(S, k, E, n, G) 62 Capitolo 7: Il Backtracking Begin if k = n and {S[n],S[1]} Є A then stampa l’insieme S else for i := 2 to n do if E[i] = 0 and {S[k],i} Є A then S[k+1] := i E[i]:= 1 CICLI-HAMILTONIANI(S, k+1, E, n, G) E[i] := 0 end if end for end. L’algoritmo descritto in sostanza è uguale a quello descritto per le permutazioni ma vi è una piccola differenza nella condizione di Backtracking. La procedura infatti si ferma se e solo se il numero di elmenti della soluzione e quello degli elementi dell’insieme coincidono e inoltre ci deve essere un arco di collegamento tra il primo e l’ultimo elemento della soluzione. La chiamata iniziale è: CICLI-HAMILTONIANI(S, 1,E, n, G); Segue la risoluzione in Pascal del problema “La tavola di Camelot” delle finali italiane delle Olimpiadi di I nformatica del 2004. La tavola rotonda di Camelot Il problema A Camelot, nel periodo di maggior splendore, ogni amicizia è corrisposta. Ogni cavaliere è amico di oltre la metà dei suoi compagni d'arme, ossia, se N è il numero dei cavalieri, ciascuno di essi può contare su almeno N/2 validi amici. La tavola rotonda è disposta in un enorme salone, circondata da N sedie, con una sedia per cavaliere. Com'è facilmente immaginabile, ogni cavaliere pretende di avere amici ai suoi due lati, altrimenti rifiuta di sedersi al tavolo. Tutti contano su Mago Merlino per disporre i cavalieri intorno al tavolo nella soddisfazione generale di tutti. Ma il poverino ha perso la formula magica per ottenere ciò in men che non si dica! 63 Capitolo 7: Il Backtracking Aiuta Merlino, nei limiti delle tue possibilità, a disporre i cavalieri intorno al tavolo in modo che ogni cavaliere abbia degli amici ai suoi lati. Dati di input Il file input.txt contiene nella prima riga l'intero N, il numero dei cavalieri della tavola rotonda. I cavalieri sono numerati da 1 a N. Ognuna delle successive N righe contiene una sequenza di N valori 0 oppure 1, separati da uno spazio. La sequenza contenuta nell'i-esima di tali righe rappresenta le relazioni di amicizia del cavaliere numero i. In particolare, il jesimo valore in tale riga indica se i cavalieri i e j sono amici (valore = 1) o meno (valore = 0). Poiché l'amicizia è sempre corrisposta, se il cavaliere i è amico del cavaliere j allora il cavaliere j è amico del cavaliere i. Dati di output Il file output.txt deve contenere una sola riga contenente una sequenza di N numeri separati da uno spazio, per rappresentare la disposizione dei cavalieri attorno alla tavola rotonda. Il primo numero è sempre 1, a rappresentare il cavaliere numero 1. Il resto della sequenza è una permutazione dei numeri 2, 3, ..., N (cioè una disposizione in un qualche ordine, senza ripetizioni). Per ogni coppia i e j di numeri adiacenti nell'intera sequenza, i cavalieri i e j devono essere amici. Inoltre, il cavaliere alla fine della sequenza deve essere amico del cavaliere numero 1, in quanto vicini di posto. Assunzioni • 1 < N < 4000 64 Capitolo 7: Il Backtracking program camelot; type grafo=array[1..100,1..100] of integer; vettore=array[1..100] of integer; var g:grafo; inp,oup:text; nn:integer; sol,elem:vettore; fine:boolean; procedure acqgrafo(var inp:text; var g:grafo; var nn:integer); var na,r,c,u1,u2,peso:integer; begin for r:=1 to nn do begin for c:=1 to nn do begin read(inp,g[r,c]); end; readln(inp); end; close(inp); end; procedure stampa(var sol:vettore; nodi:integer); var i:integer; begin for i:=1 to nodi do begin write(oup,sol[i]); end; writeln(oup); end; 65 Capitolo 7: Il Backtracking function arco(n1,n2:integer; var g:grafo):boolean; var temp:boolean; begin if (g[n1,n2]=1) then temp:=true else temp:=false; arco:=temp; end; procedure cicli(var sol,elem:vettore; nodi:integer; var g:grafo; k:integer); var i:integer; begin if (k=nodi) and (arco(sol[nodi],sol[1],g)) then begin stampa(sol,nodi) ; fine:=true; end else begin for i:=2 to nodi do begin if (elem[i]=0) and (arco(sol[k],i,g)) then begin sol[k+1]:=i; elem[i]:=1; cicli(sol,elem,nodi,g,k+1); {se è stato trovato un ciclo hamiltoniano, l'esecuzione termina} if fine then begin exit; end; elem[i]:=0; end; end; end; end; 66 Capitolo 7: Il Backtracking procedure azzera(var v:vettore; nodi:integer); var i:integer; begin for i:=1 to nodi do begin v[i]:=0; end; end; { ***** MAIN ***** } begin assign(inp,'input.txt'); assign(oup,'output.txt'); reset(inp); readln(inp,nn); azzera(sol,nn); azzera(elem,nn); sol[1]:=1; elem[1]:=1; acqgrafo(inp,g,nn); rewrite(oup); cicli(sol,elem,nn,g,1); close(oup); end. 67 Capitolo 7: Il Backtracking Bibliografia - Alan Bertossi, Algoritmi e strutture di dati Casa editrice UTET Libreria 2004 - Massimo Sciarra, Romeo Pruno Università di Camerino, Dipartimento di Matematica e Informatica Traveling Salesman’s Problem Dispensa: http://www.marconivr.it/attivita_olimpinfo/testi/hamiltoniano.ppt 68