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