Algoritmi e strutture dati con laboratorio

Note del corso di
Algoritmi e strutture dati con laboratorio
Modulo II
Gianluca Rossi
Ver. 2011-03-04
Indice
1
2
Grafi
1.1
1.2
1.3
1.4
1.5
1.6
I Grafi . . . . . . . . . . .
Rappresentazione dei grafi
Shortest-path . . . . . . .
Ricerca di cicli . . . . . .
Ordinamento topologico .
Grafi pesati . . . . . . . .
Intrattattabilità
2.1 Problemi decisionali . .
2.2 Le classi P ed NP . . .
2.3 Riducibilità polinomiale
2.4 La classe NP . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
6
7
11
14
17
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
18
18
19
20
21
1
Lista di algoritmi
1.1
1.2
1.3
1.4
Shortest path . . . .
Visita in profondità .
Grafo ciclico/aciclico
Topological Sort . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
7
11
13
16
Introduzione
Gli algoritmi presentati in queste note vengono descritti utilizzando un linguaggio (o pseudocodice) il più possibile vicino al linguaggio Python [1]. Nella maggior parte dei casi
l’algoritmo presentato è ricavato da una sua implementazione funzionante in Python. Altre
volte per motivi di legibilià e di spazio la descrizione dell’algoritmo potrebbe contenere dei
richiami a funzioni esterne che eseguono compiti elementari.
La scelta del linguaggio Python è dovuta principalmente alla chiarezza e compattezza
del codice. Inoltre il linguaggio mette a disposizione strutture “complesse” dati come ad
esempio liste e dizionari come strutture primitive del linguaggio. Infine altre utili strutture
sono disponibili nelle librerie standard (code doppiamente linkate, heap,. . . ).
Come esempio di algoritmo in Python viene mostrata una semplice funzione che crea e
restituisce una coppia di liste (L1 , L2 ), la prima contenente le coppie (x, x 2 ) e la seconda
le coppie (x, x 3 ) per tutti gli x tra 0 e n − 1.
d e f f (n) :
L = range ( n )
L2 = []
L3 = []
for x in L:
L2 . append ( (x, x 2 ) )
L3 . append ( (x, x 3 ) )
r e t u r n (L2 , L3 )
Attraverso la funzione Python range si memorizza in L la lista degli interi da 0 ad n − 1
dopo di che vengono create due liste vuote L2 ed L3 . Con il ciclo for si accedono a tutti gli
elementi della lista L, per ognuno di questi elementi x vengono create le coppie (x, x 2 ) e
(x, x 3 ) che vengono aggiunte in coda alle liste L2 ed L3 rispettivamente. Questa operazione
viene eseguita attraverso la funzione append.
Si noti che contrariamente ad altri linguaggi di programmazione, i blocchi nidificati
sono indicati con le indentazioni. Nell’esempio precedente le due operazioni append sono
indentate rispetto al for quindi fanno parte del blocco del for. Inoltre tutto il programma
è indentato rispetto alla prima operazione (def), quindi tutto il programma fa parte della
definizione della funzione f .
Altri caratteristiche del linguaggio Python saranno fornite di volta in volta quando
queste si renderanno utili.
3
Capitolo 1
Grafi
Supponiamo di dover risolvere il seguente problema: data una mappa vogliamo trovare il
percorso più veloce per andare da un punto s ad un punto t. La soluzione di questo
problema, sebbene non difficile dal punto di vista algoritmico, richiede l’introduzione di
strutture matematiche che permettono di definire la mappa e i punti su di essa, quindi l’input,
ed il percorso cercato, ovvero l’output. La mappa può essere rappresentata specificando
l’insieme dei siti, l’insieme di strade che connettono due siti e, opzionalmente, un costo o
peso associato ad ogni strada che può rappresentare la lunghezza della stessa oppure il
tempo di percorrenza. In Figura 1.1 è mostrata una rappresentazione schematica in cui si
mostrano i siti u0 , . . . , u6 , e le strade che li collegano con relativi pesi. Una struttura di
questo tipo è detta grafo.
La stessa struttura può essere utilizzata per rappresentare Internet, le ferrovie, il web,
i collegamenti aerei, le amicizie su Facebook, gli organigrammi aziendali, . . . , in generale
le reti. Una rete non è altro un insieme di oggetti tra i quali esiste una qualche relazione.
1.1
I Grafi
Come discusso precedentemente , le reti si prestano ad essere rappresentate per mezzo dei
grafi. Un grafo è costituito da un insieme finito di elementi chiamati nodi e da un insieme
che rappresenta le relazioni tra coppie di nodi. Se la relazione è simmetrica, questa è
rappresentata utilizzando un insieme di due elementi distinti, altrimenti, se la relazione è
asimmetrica viene rappresentata con una coppia ordinata di nodi. Nel primo caso il grafo
si dice non diretto mentre nel secondo caso si dice diretto.
Definizione 1.1. Un grafo non diretto G è definito da una coppia di insiemi (V , E ) dove
V = {v0 , ... , vn−1 } è un insieme finito di elementi detti nodi ed E = {e0 , ... , em−1 }
è un insieme finito di sottoinsiemi di V di cardinalià 2 detti archi. Ovvero per ogni
i ∈ 0, ... , m − 1, ei ⊆ E e |ei | = 2.
Un grafo G = (V , E ) è diretto se E = {e0 , ... , em−1 } e per ogni e ∈ E vale
e = (u, v ) con u, v ∈ V .
Il grafo non diretto illustrato in Figura 1.2 è definito da V = {u0 , u1 , u2 , u3 , u4 , u5 , u6 }
e
E = {{u0 , u1 }, {u0 , u4 }, {u0 , u6 }, {u1 , u2 }, {u2 , u6 }, {u3 , u5 }} .
Comunemente anche gli archi dei grafi non diretti vengono rappresentati come coppie
anziché come insiemi. L’ambiguità viene eliminata specificando di volta in volta se si tratta di
4
Gianluca Rossi
Note del corso di ASDL Mod. II
5
u1
4
u2
9
10
u3
3
11
u0
3
u4
1
7
6
u6
u5
Figura 1.1: La rappresentazione di una mappa con un grafo
u1
u2
u3
u4
u0
u6
u5
Figura 1.2: La rappresentazione di una mappa con un grafo
Gianluca Rossi
Note del corso di ASDL Mod. II
6
grafi diretti o non diretti. Anche in queste note useremo lo stesso sistema, quindi l’insieme
di archi del grafo in Figura 1.2 è
E = {(u0 , u1 ), (u0 , u4 ), (u0 , u6 ), (u1 , u2 ), (u2 , u6 ), (u3 , u5 )} .
Dato un nodo u di un grafo G = (V , E ), definiamo con N(u) l’insieme dei nodi di G che
sono collegati a u per mezzo di un arco in E . Ovvero N(u) = {v : ∃v ∈ V , (u, v ) ∈ E }.
L’insieme N(u) viene chiamato vicinato di u ed i suoi componenti sono i vicini di u. Un
cammino tra i nodi u e v di G è una sequenza di nodi puv = x0 , x1 , ... , xk tale che x0 ≡ u,
xk ≡ v e per ogni i = 0, ... , k − 1, (xi , xi+1 ) ∈ E . Quindi un cammino è una sequenza
di archi adiacenti che permettono di collegare due nodi che perciò vengono detti connessi.
Un cammino tra due nodi è detto semplice se non passa due volte per lo stesso nodo. La
lunghezza di un cammino p (indicata con |p|) è data dal numero di archi che lo compongono.
Nell’esempio di Figura 1.2 la sequenza u4 , u0 , u6 , u2 è un cammino semplice tra u4 e u2
di lunghezza 3 dunque questi due nodi sono connessi. Se ogni coppia di nodi di un grafo
è connessa il grafo si dice connesso se questo è non diretto altrimenti si dice fortemente
connesso se questo è diretto. Il solito grafo dell’esempio non è connesso in quanto non
esiste nessun cammino tra u3 e u4 .
1.2
Rappresentazione dei grafi
Un grafo di n nodi può essere rappresentato con una matrice E di dimensione n × n.
Assumendo senza perdita di generalità che i nodi siano interi da 0 a n − 1, E [i, j] > 0
se esiste l’arco tra il nodo i ed il nodo j, altrimenti E [i, j] = 0. La matrice E è nota
come matrice di adiacenza del grafo. Si osservi che tale metodo è utile per rappresentare
archi sia non diretti che diretti. Nel primo caso la matrice sarà simmetrica rispetto alla
diagonale principale. La seguente matrice rappresenta il grafo illustrato in Figura 1.2: la
corrispondenza nodi-indici della matrice è quella più naturale.


0 1 0 0 1 0 1
 1 0 1 0 0 0 0 


 0 1 0 0 0 0 1 


 0 0 0 0 0 1 0 


 1 0 0 0 0 0 0 


 0 0 0 1 0 0 0 
1 0 1 0 0 0 0
La rappresentazione attraverso matrici di adiacenza è poco compatta in quanto non ci
si limita a rappresentare solo gli archi ma occorre anche rappresentare i non archi. Quindi
anche se il grafo è sparso, ovvero composto da un numero O(n) di archi, la rappresentazione
di questo richiede una quantità di memoria quadratica in n.
Un modo più efficiente di rappresentare grafi è attraverso le liste di adiacenza: per
ogni nodo del grafo u utilizziamo una lista N[u] di tutti i vicini di u. Quindi il grafo G
è rappresentato da un vettore di liste. In particolare quello della Figura 1.2 può essere
rappresentato nel seguente modo:
Gianluca Rossi
Note del corso di ASDL Mod. II
7
Algoritmo 1.1: Shortest path
d e f shortestpath ( G , s , t ) :
n = len ( G )
Q = deque ( )
Q . append ( (None, s) )
X = [False]∗n
T = [None]∗n
w h i l e len ( Q ) > 0 :
(z, u) = Q . popleft ( )
i f X [u] == False :
X [u] = True
T [u] = z
i f u == t :
r e t u r n path ( T , s , t )
f o r v i n G [u] :
Q . append ( (u, v ) )
r e t u r n NULL
G = [
[ u1 ,
[ u0 ,
[ u1 ,
[ u5 ]
[ u0 ]
[ u3 ]
u4 , u6 ] ,
u6 ] ,
u6 ] ,
,
,
]
Alcune volte può essere più utile rappresentare il grafo con un’unica lista che contiene
tutti gli archi. In ogni caso, di volta in volta, utilizzeremo la rappresentazione più conveniente
anche in virtù del fatto che si puó passare da una forma ad un altra in modo efficiente.
1.3
Shortest-path
La ricerca del cammino più breve che connette una coppia di nodi è forse uno dei problemi più
naturali nell’ambito dei grafi. Le applicazioni sono innumerevoli e vanno dall’instradamento
dei pacchetti nella rete internet alla ricerca del cammino più breve che collega due punti
geografici.
Formalmente, dato un grafo G = (V , E ) e due nodi s e t si vuole trovare il cammino
più breve (minor numero di archi) che connette s con t. Nel caso i due nodi non siano
connessi viene restituito un cammino vuoto.
L’algoritmo proposto visita i nodi a distanza crescente da s fino ad incontrare t. I nodi
incontrati durante l’esplorazione del grafo vengono tenuti in una coda in modo che quelli
Gianluca Rossi
Note del corso di ASDL Mod. II
8
in testa siano i più vicini a s. Ad ogni passo viene estratto un nodo v dalla testa della
coda, nel caso in cui questo non sia stato già raggiunto in precedenza e non sia il nodo
che stiamo cercando vengono inseriti nella coda tutti i suoi vicini ed il processo continua.
Se un nodo v viene inserito in coda perché vicino del nodo u a distanza d da s, il nodo
v è a distanza d + 1 ed è raggiungibile da s attraverso u; memorizzando l’arco (u, v ) in
una qualche struttura saremo in grado di ricostruire il cammino che da s porta ad v . La
struttura utilizzata per memorizzare gli archi è fondamentalmente un albero: T [v ] = u se
e solo il cammino minimo da s ad v è dato dal cammino minimo da s a u seguito dall’arco
(u, v ). Affinché sia possibile costruire T è necessatio memorizzare nella coda ogni nodo
insieme con l’arco che ha portato alla scoperta del nodo stesso. Si veda l’Algoritmo 1.1 per
lo pseudo-codice.
L’input dell’algoritmo è formato dal grafo G di n nodi e dai due nodi s e t. Il grafo è
rappresentato mediante liste di adiacenza mentre i nodi sono interi che vanno da 0 ad n − 1.
Pertanto G [v ] è la lista di adiacenza del nodo v . La funzione len restituisce la dimensione
di G ovvero in numero di nodi. Q è una coda, quindi l’operazione append aggiunge un
elemento alla fine della coda mentre l’operazione popleft elimina e restituisce il primo
elemento della coda. Il vettore booleno X viene utilizzato per tener traccia dei nodi già
visitati e, come si è detto, il vettore T è l’albero utilizzato per memorizzare gli archi della
soluzione. Infine se viene raggiunto il nodo t si estrae da T il cammino da s a t utilizzando
la funzione path.
d e f path ( T , s , t ) :
P = []
P . append ( t ) ;
u = t;
w h i l e u != s :
u = T [u]
P . append ( u )
P . reverse ( )
return P
Da t si raggiunge s seguendo a ritroso gli archi in T . Ogni nodo incontrato si inserisce
in fondo (append) alla lista P inizialmente vuota. Prima di essere restituita in output la
lista viene invertita con reverse.
Si osservi che nel caso in cui da s non esiste alcun cammino per t termina perché la
coda è vuota, in questo caso viene restituito None.
Gianluca Rossi
Note del corso di ASDL Mod. II
9
Nota sulla complessita degli operatori su liste, code e pile in Python Il tipo di
dato lista è un tipo primitivo del Python. Con
L = []
creiamo una lista vuota. Tra gli operatori su liste sono da sottolineare l’operatore
append e pop.
L . append ( x )
Aggiunge x in fondo alla lista.
x = L . pop ( )
Rimuove l’ultimo elemento inserito nella lista e lo assegna alla variabile x.
La complessità di questi due operatori è costante. Esistono anche operatori che
aggiungono o estraggono elementi in posizione qualsiasi della lista, tuttavia la
complessita di questi operatori dipende dalla posizione in cui si interviene.
Per quanto detto il tipo lista è efficiente per la gestione di pile. Per implementare efficientemente una coda si deve ricorrere alla double-ended-queue
(deque). Questa struttura lineare permette di accedere (in lettura, scrittura e
cancellazione) agli estremi della lista in tempo costante.
L = deque ( )
Crea una deque vuota.
L . append ( x )
Aggiunge x all’estremità destra di L.
x=L . pop ( )
Rimuove l’elemento all’estremità destra di L e lo assegna ad x
L . appendleft ( x )
Aggiunge x all’estremità sinistra di L.
x=L . popleft ( )
Rimuove l’elemento all’estremità sinistra di L e lo assegna ad x.
La deque non è un tipo primitivo di Python ma è contenuto nella libreria
collections, quindi per poter essere utilizzata occorre importarla.
f r o m collections i m p o r t deque
Per dimostrare la correttezza dell’algoritmo abbiamo bisogno del concetto di distanza in
un grafo: sia G = (V , E ) un grafo e u e v due suoi nodi, la distanza tra u e v in G –
indicata con dG (u, v ) – è la lunghezza del cammino più breve in G che connette u con v .
Teorema 1.1. Sia G = (V , E ) un grafo con n nodi ed m archi e siano s e t due nodi di
G allora l’algoritmo shortestpath con input G , s e t restituisce il percorso più breve
tra s e t in G . Inoltre la sua complessità è O(n + m).
Gianluca Rossi
Note del corso di ASDL Mod. II
10
Dimostrazione. Innanzi tutto osserviamo che se t è connesso a s allora l’algoritmo crea un
cammino fino a t. Assumiamo per assurdo che l’algoritmo restituisce il cammino vuoto e che
esista un cammino P da s a t. Sia v il primo nodo in P a partire da s che non viene
raggiunto dall’algoritmo (v esiste, al massimo è proprio t). Inoltre sia u il nodo che precede
v in P. Poiché l’algoritmo crea un percorso fino ad u, v viene preso in considerazione in
quando vicino di u, quindi in coda compare l’arco (u, v ). Quando (u, v ) viene estratto dalla
coda v viene visitato e collegato in T ad u oppure scartato in quanto u risulta già visitato
(X [u] = True) e quindi collegato a qualche altro nodo.
Ora dimostriamo che i nodi più vicini ad s vengono visitati prima di quelli più lontani,
ovvero se dG (s, u) < dG (s, v ) = ` < dG (s, t) allora il nodo u viene visitato prima del
nodo v . Dimostriamo questo risultato per induzione su `. Se ` = 1 l’unico nodo a distanza
inferiore è s che viene visitato prima di tutti i nodi. Supponiamo ora che valga l’ipotesi
induttiva e siano v 0 e u 0 i nodi nel cammino più breve da s a v e da s a u che precedono,
rispettivamente, i nodi v ed u. Allora dG (s, u 0 ) < dG (s, v 0 ) = ` − 1. Poiché vale l’ipotesi
induttiva u 0 viene visitato prima di v 0 . Allora u viene inserito in coda prima di v quindi u
viene visitato prima di v .
Sia P il cammino da s a t restituito dall’algoritmo. Se P è vuoto allora t non è
raggiungibile da s. Assumiamo che P non sia vuoto e dimostriamo che questo è proprio il
cammino minimo da s a t. Ragioniamo per assurdo: sia v il primo nodo nel cammino P
tale che il sotto-cammino di P da s ad v non è minimo e sia u il nodo che precede v (u
e v esistono sempre, perché?); indichiamo con ` la lunghezza del sotto-cammino di P che
va da s ad v allora dG (s, v ) = d ≤ ` − 1 e dG (s, u) = ` − 1, inoltre esiste un nodo z
tale che dG (s, z) = d − 1 < ` − 1 e (z, v ) ∈ E allora z viene visitato prima di u quindi
v viene collegato a z e non a u.
Veniamo ora al calcolo della complessità. Si noti che ogni arco viene inserito in coda
al più una volta quindi il numero complessivo di popleft e append è O(m). Inoltre
l’inizializzazione delle strutture ha un costo O(n). Quindi la tesi.
L’Algoritmo 1.1 è un algoritmo di visita ovvero, se opportunamente modificato (eliminando
il test u = t) è in grado di raggiungere, o visitare, tutti i nodi connessi al nodo di partenza
(s), questo tipo di algoritmi ha una importanza strategica per la comprensione della topologia
del grafo.
L’Algoritmo 1.1 fa in modo che i nodi vengano visitati in ordine di distanza dal nodo
di partenza, questo strategia di visita viene detta Breadth-First Search (BFS) o visita in
ampiezza.
Esercizio 1.1. Si progetti un algoritmo che dato un grafo G non diretto stabilisca se questo
è connesso o meno. L’algoritmo deve avere complessità lineare (in n ed m).
Una componente connessa di un grafo non diretto G = (V , E ) è un sottoinsieme dei
nodi C connessi tra di loro e tale che ogni nodo in V − C non è connesso con nessun
nodo di C . I nodi di un grafo possono essere partizionati in componenti connesse, nel caso
in cui G sia connesso allora l’unica componente connessa coincide con V .
Esercizio 1.2. Si progetti un algoritmo che partizioni l’insieme dei nodi V di un grafo non
diretto in componenti connesse. In particolare l’algoritmo deve creare un vettore C di |V |
elementi tale che C [u] = k se e solo se u appartiene alla k-esima componente connessa
di G .
Gianluca Rossi
Note del corso di ASDL Mod. II
11
x
y
z
Figura 1.3: Un grafo diretto aciclico
Algoritmo 1.2: Visita in profondità
d e f dfs ( G , s ) :
n = len ( G )
S = []
S . append ( (None, s) )
X = [False]∗n
T = [None]∗n
w h i l e len ( S ) > 0 :
(z, u) = S . pop ( )
i f X [u] == False :
X [u] = True
T [u] = z
f o r v i n G [u] :
S . append ( (u, v ) )
return T
1.4
Ricerca di cicli
Il p = x0 , x1 , ... , x7 , x0 è detto ciclo in quanto è un cammino chiuso ovvero che inizia e
termia con lo stesso nodo. Il ciclo è semplice se non presenta nodi ripetuti interni. Vogliamo
progettare un algoritmo che verifica se un grafo G = (V , E ) contiene almeno un ciclo e
magari, in caso affermativo, restituisce il ciclo trovato.
Se G è non diretto è sufficiente modificare l’algoritmo di visita in ampiezza in modo tale
che se da u si raggiunge un un nodo v già visitato (in X ) allora la risposta è affermativa
in quanto sappiamo che in T esiste il cammino da s a u e da s a v ed inoltre esiste l’arco
(u, v ). Poiché il grafo è non diretto il cammino da u a v è anche un cammino da v a s.
Quindi utilizzando T è facile costruire il ciclo che mette insieme i due cammini con l’arco.
Se il grafo è diretto può essere raggiunto un nodo visitato senza che questo evento
corrisponda alla scoperta di un ciclo. La visita in ampiezza applicata al grafo in Figura 1.3
a partire da x raggiunge nell’ordine i nodi x, y e z. Arrivati a z si visita l’arco (z, y )
raggiungendo il nodo y già visitato. Tuttavia questo grafo non contiene cicli.
Se nell’algoritmo di visita in ampiezza utilizziamo una pila al posto di una coda otteniamo
un nuova strategia di visita, chiamata Depth-First Search (DFS) o visita in profondità. Il
nome è dovuto al fatto che l’algoritmo ha la tendenza a raggiungere nodi sempre più lontani.
L’Algoritmo 1.2 illustra questo tipo di visita.
Gianluca Rossi
Note del corso di ASDL Mod. II
12
L’albero di visita T generato dall’Algoritmo 1.2, chiamato albero DFS, ha delle proprietà
utili per la verifica dell’esistenza di cicli nel grafo.
Lemma 1.1. Sia T un albero DFS di G a partire dal nodo s e (u, v ) un arco di G . Se u
viene visitato prima di v dall’algoritmo di visita allora u è un antenato di v nell’albero T .
Dimostrazione. Con la visita di u l’arco (u, v ), insieme a tutti gli archi uscenti da u, viene
aggiunto in S. In questo momento la testa della pila è formata da un insieme di archi
(u, vk ) e tra questi troviamo anche (u, v ). Prima che quest’ultimo arco verrà estratto da S
saranno trattati altri archi (u, vk ) creando in T un certo numero di sotto-alberi tutti aventi
per radice qualche vicino di u. Al momento in cui viene estratto (u, v ), se questo risulterà
visitato allora v appartiene ad uno di questi sotto-alberi, altrimenti viene inserito in T
come figlio di u.
Dato un albero DFS T di G , diciamo che un arco (u, v ) di G −T è un arco all’indietro
di T se v è un antenato di u in T .
Teorema 1.2. G contiene un ciclo se e solo se un qualsiasi albero DFS T di G ha un
arco all’indietro
Dimostrazione. Se T ha un arco all’indietro allora necessariamente G deve essere ciclico.
Sia G ciclico, indichiamo con C un ciclo di G . Chiamiamo u il primo nodo di C raggiunto
dall’algoritmo di visita DFS quindi sia C = u, x1 , ... , xk , u. Per il Lemma 1.1 tutti i nodi
xi del ciclo sono discendenti di u in T quindi l’arco (xk , u) é un arco all’indietro.
Il Teorema 1.2 ci indica un metodo per decidere se il grafo è aciclico o meno: se durante
la visita dell’arco (u, v ) riscontriamo che v risulta già visitato e (u, v ) risulta un arco
all’indietro allora il grafo è ciclico. Se tale arco non viene trovato il grafo è aciclico.
L’Algoritmo 1.2 può essere modificato in modo naive nella seguente maniera: se il nodo
v dell’arco (u, v ) attualmente estratto dalla pila risulta essere visitato, da u risaliamo fino
alla radice attraverso T fino ad incontrare v oppure fino ad s. Solo nel primo caso (u, v )
risulterà essere un arco all’indietro. Questa verifica ha un costo O(n) nel caso peggiore,
vedremo come eseguire questo test in tempo costante.
Se (u, v ) è un arco all’indietro allora u è in un sotto-albero di T con radice v , ovvero
l’algoritmo scova l’arco all’indietro solo se il sotto-albero Tv di T con radice v non è stato
ancora completato. La costruzione di Tv termina quando è esplorato il suo sotto-albero che
ha per radice l’ultimo vicino di v presente nella pila. Dobbiamo fare in modo di individuare
il momento in cui questo ultimo vicino è esplorato completamente. Se riuscissimo a farlo
allora potremmo assegnare a v una etichetta speciale ed utilizzarla per il nostro scopo.
Questa etichetta indicata con E [v ] è definita come segue:

v aperto ovvero visitato e Tv non completo;
 A
C
v chiuso ovvero visitato e Tv completo;
E [v ] =

None
altrimenti.
Allora (u, v ) è all’indietro se e solo se E [v ] = A.
Il nodo v diventa aperto nel momento in cui viene visitato. Per individuare il momento
in cui avviene la chiusura inseriamo nella pila un arco finto (v , None) prima di inserire gli
Gianluca Rossi
Note del corso di ASDL Mod. II
Algoritmo 1.3: Grafo ciclico/aciclico
d e f findcycle ( G , s ) :
n = len ( G )
S = []
S . append ( (None, s) )
E = [None]∗n
X = [False]∗n
T = [None]∗n
w h i l e len ( S )>0 :
(z, u) = S . pop ( )
i f u == None :
E [z] = C
else :
i f X [u] == False :
X [u] = True
E [u] = A
T [u] = z
S . append ( (u, None) )
f o r v i n G [u] :
S . append ( (u, v ) )
e l i f E [u] == A :
L = path ( T , u , z )
return L
r e t u r n None
13
Gianluca Rossi
Note del corso di ASDL Mod. II
AM
CPS
FI
MD
ASDL
FO
CN
14
PR
BD
LT
IA
RLAC
LMD
SOR
IS
LIS
Figura 1.4: Schema delle propedeuticità degli insegnamenti del Corso di Laurea Triennale
in Informatica
altri archi (v , x) veri. In questo modo l’arco finto sarà l’ultimo arco di origine v estratto
dalla pila. Al momento dell’estrazione di questo impostiamo E [v ] = C .
L’Algoritmo 1.3 mostra la modifica dell’algoritmo di visita DFS che restituisce un ciclo
trovato dalla visita DFS a partire dal nodo s. Per prima cosa si verifica che l’arco (z, u)
estratto dalla pila non corrisponda ad un nodo finto (u = None), in questo caso il nodo
z viene chiuso. Nel caso il nodo u venga visitato allora questo viene etichettato aperto.
Infine nel caso u sia visitato ed aperto allora viene restituito il cammino da u a z che, con
l’arco (z, u), costituisce un ciclo.
La complessità di questo algoritmo è O(n + m) come l’algoritmo di visita.
Se G presenta un ciclo L non raggiungibile da s l’algoritmo fallisce. Tuttavia l’Algoritmo 1.3 può essere modificato in modo da riprendere la visita a partire da un eventuale nodo
non ancora visitato: la complessità può essere mantenuta lineare. I questo modo l’algoritmo
viene reso indipendente dal nodo s di inizio visita.
1.5
Ordinamento topologico
I grafi diretti aciclici (DAG acronimo di directed acyclic graph) sono utilizzati per rappresentare relazioni d’ordine tra oggetti. Per esempio in Figura 1.4 è rappresentato lo schema
delle propedeuticità degli esami nel corso di Informatica. Altre applicazioni sono la relazione
di ereditarietà tra classi nella programmazione a oggetti oppure l’ordine di esecuzione dei
processi elementari per la produzione di un bene.
Ritornando all’esempio delle propedeuticità degli insegnamenti, vogliamo trovare una
permutazione degli esami che rispetti i vincoli di propedeuticità del diagramma in Figura 1.4.
Questo problema è noto come ordinamento topologico del grafo: dato un grafo G =
(V , E ) di n nodi, un ordinamento topologico di G è una funzione biettiva π : V →
{0, ... , n − 1} dei nodi di V tale che per ogni arco (u, v ), π(u) < π(v ). In altre parole,
se disponiamo i nodi in orizzontale da sinistra a destra in base al valore di π, gli archi de
grafo vanno tutti da sinistra a destra.
Gianluca Rossi
Note del corso di ASDL Mod. II
15
Osserviamo che il problema è ben definito solo se il grafo è un DAG in quanto se avesse
un ciclo qualsiasi ordinamento π esisterebbe definisce un primo nodo (diciamo u) ed un
ultimo nodo del ciclo (diaciamo v ) rispetto π. Quindi per l’arco (v , u) si ha π(u) < π(v ).
Proposizione 1.1. Sia G = (V , E ) un DAG allora esiste un nodo u di V che non ha archi
uscenti.
Dimostrazione. Ragioniamo per assurdo supponendo che ogni nodo di G ha almeno un arco
uscente. Sia T un qualsiasi albero DFS di G , per il Teorema 1.2 poiché T è aciclico, gli
archi uscenti dai nodi di G non in T non possono essere all’indietro. In particolare ogni
foglia f di T deve avere un arco uscente che termina in un sotto-albero di T diverso da
quello contenente f . Questo tipo di archi vengono detti trasversali.
Partendo da una foglia qualsiasi di T seguiamo l’arco che da questa va in un altro
sotto-albero, quindi attraverso gli archi del sotto-albero raggiungiamo un altra foglia e così
via. In questo modo abbiamo definito un cammino che tocca alcune foglie di T attraverso
archi trasversali seguiti da cammini in T . Questo cammino prima o poi deve chiudersi
creando un ciclo, ovvero prima o poi deve raggiungere un sotto-albero di T già traversato.
Se così non fosse il numero di foglie deve essere inferiore al numero di sotto-alberi.
Quindi trovare un ordinamento topologico dei nodi del DAG è immediato in quanto è
sufficiente individuare un nodo u senza archi uscenti, metterlo in fondo all’ordinamento e
ripeter la procedura sul DAG ottenuto dal primo eliminando eliminando u e tutti i suoi
archi entranti. Questo algoritmo può essere implementato ricorrendo a tante visite quanti
sono i nodi del grafo. Tuttavia con qualche accorgimento possiamo abbassarne il costo
computazionale.
Sia T un albero DFS del DAG G e π : V → {0, 1, ... , n − 1} una biezione con la
seguente proprietà: per ogni coppia di nodi u e v
• se u ∈ Tv allora π(v ) < π(u);
• altrimenti si supponga u visitato prima di v allora π(v ) < π(u).
Teorema 1.3. La funzione π è un ordinamento topologico di G .
Dimostrazione. Sia e = (x, y ) un arco di G , consideriamo tutti i possibili casi. Se e è in
T allora y ∈ Tx quindi π(x) < π(y ). Se e non è in T allora x può essere un antenato di
y oppure e è un arco trasversale. Il primo caso è simile al caso e ∈ T . Per il secondo si
osservi che y deve essere visitato prima di x altrimenti y dovrebbe essere un discendente
di x in T : quindi π(x) < π(y ).
L’Algoritmo 1.4 utilizza lo stesso trucco utilizzato nell’Algoritmo 1.3 per segnare il momento in cui termina la visita di un sotto-albero Tv dell’albero DFS T : l’aggiunta di un
finto arco (v , None) come ultimo arco uscente da v . Quando questo arco viene estratto
deve risultare che a tutti i nodi in Tv (tranne lo stesso v ) sia assegnato un valore di π.
Quindi viene assegnato a π[v ] il valore preso da una variabile t inizializzata a n − 1 e
decrementata ogni volta il suo valore viene utilizzato per la funzione π. Dovrebbe essere
evidente che la funzione π calcolata dall’algoritmo è la stessa del Teorema 1.3.
Si noti che l’algoritmo continua ad eseguire visite in profondità partendo da un nodo
non visitato fintanto che questi esistono. La complessità dell’algoritmo è quella della visita
ovvero O(n + m).
Gianluca Rossi
Note del corso di ASDL Mod. II
Algoritmo 1.4: Topological Sort
d e f topologicalsort ( G ) :
n = len ( G )
V = range ( n )
S = []
t =n−1
π = [None]∗n
X = [False]∗n
for s in V :
i f X [s] == False :
S . append ( (None, s) )
w h i l e len ( S ) > 0 :
(z, u) = S . pop ( )
i f u == None :
π[z] = t
t =t −1
else :
i f X [u] == False :
X [u] = True
S . append ( (u, None) )
f o r v i n G [u] :
S . append ( (u, v ) )
return π
16
Gianluca Rossi
1.6
Note del corso di ASDL Mod. II
17
Grafi pesati
Un grafo a cui si associa ad ogni arco un valore numerico è detto grafo pesato. Più
precisamente è identificato da una tripla (V , E , w ) dove V ed E sono ancor al’insieme dei
nodi e l’insieme dagli archi; invece w : E → R è la funzione peso sugli archi. Sia e ∈ E ,
w (e) è detto peso oppure costo dell’arco e. Un grafo pesato G = (V , E , w ) può essere
diretto o non diretto, connesso o non connesso a seconda che lo sia il grafo non pesato
(V , E ).
Capitolo 2
Intrattattabilità
Abbiamo visto che il problema di trovare il percorso più breve (shortest-path) che connette
due nodi di un grafo non diretto può essere risolto efficientemente eseguendo una visita
in ampiezza a partire da un nodo radice. Il costo computazionale di questo algoritmo è
lineare nella dimensione del grafo. I problemi che ammettono un algoritmo polinomiale nella
dimensione dell’istanza sono generalmente considerati trattabili.
Se invece fossimo interessati al cammino semplice più lungo (longest-path) che congiunge due nodi di un grafo le cose cambierebbero: non è noto nessun algoritmo polinomiale per questo problema. Questo fatto non riguarda soltanto questo problema specifico ma
un’intera classe di problemi che per questo motivo vengono considerati intrattabili.
2.1
Problemi decisionali
Il problema del longest-path, come altri problemi di ottimizzazione può essere visto in tre
modi diversi.
Dato un intero k, ci potremmo accontentare di sapere se il grafo contiene un cammino
semplice tra i due nodi composto da almeno k archi. In questo caso ci aspettiamo soltanto
una risposta booleana (si/no) pertanto questa versione del problema è detta decisionale.
L’input sarà composto dal grafo, dalla coppia di nodi e dall’intero k, l’output sarà un valore
tra vero o falso (ma anche si/no, 0/1).
Nel caso non ci accontentassimo della semplice risposta si/no ma volessimo anche avere
una soluzione di lunghezza almeno k, allora parleremmo di problema di ricerca. In questo
caso l’output che ci si aspetta da un algoritmo per il problema del longest-path è un cammino
di lunghezza minore di k se questo esiste, altrimenti una qualche segnalazione che il tale
cammino non esiste per esempio restituendo il cammino vuoto.
Infine, nel caso volessimo la soluzione che sia la migliore possibile, ovvero, nel caso
del problema del longest-path il cammino più lungo tra i due nodi, allora parleremmo di
problema di ottimizzazione. In quest’ultimo caso dall’input sparirebbe l’intero k.
Un algoritmo A per la versione di ottimizzazione del problema longest-path può essere
usato per risolvere la versione di ricerca del problema: sia p il cammino più lungo restituito
da A, se questo è composto da almeno k archi allora può essere esibito come soluzione del
problema di ricerca, altrimenti l’output sarebbe il cammino vuoto. A sua volta un algoritmo
B per la versione di ricerca del problema può servire per risolvere la versione decisionale:
se l’output dell’algoritmo B è un cammino allora si restituisce vero altrimenti si restituisce
falso.
18
Gianluca Rossi
Note del corso di ASDL Mod. II
19
Da quanto detto si capisce che la versione decisionale di un problema è quella più
facile, quindi un risultato di intrattabilità su questa versione implica l’intrattabilità di tutta
la catena.
Esercizio 2.1. Si supponga che la versione di ricerca del problema del longest-path abbia
un algoritmo di complessitá f (n, m) dove n ed m denotano rispettivamente il numero di
nodi ed archi. Si dimostri che il problema di ottimizzazione può essere risolto in tempo
O(f (n, m) log n).
2.2
Le classi P ed NP
Concentriamoci per il momento sui problemi decisionali. Abbiamo detto per il problema del
longest-path non è noto alcun algoritmo polinomiale e per questo motivo tale problema è
considerato intrattabile. Il longest-path fa parte di una sotto-classe dei problemi intrattabili
molto ben caratterizzata. Sebbene per questi problemi non siano noti algoritmi efficienti
tuttavia, data una soluzione potenziale, siamo in grado di testare se questa rispetta i requisiti
o meno. Per esempio, per il problema del longest-path una soluzione è un cammino da u
a v ed in tempo polinomiale possiamo stabilire se questa è composta da almeno k archi
(in realtà potremmo testare in tempo polinomiale anche se la sequenza di archi costituisce
effettivamente un cammino semplice). La classe di problemi che rispetta questo requisito è
detta classe NP che di seguito verrà definita formalmente.
Sia P un problema decisionale ed x una sua istanza di lunghezza |x|, P appartiene alla
classe NP se esiste un algoritmo polinomiale A ed un polinomio p tali che: se x ammette
una soluzione (è una istanza si) allora esiste una soluzione potenziale y (o certificato
polinomiale) di lunghezza al più p(|x|) e tale per cui A(x, y ) = 1; se x è una istanza no
allora per ogni potenziale soluzione y di lunghezza al più p(|x|), A(x, y ) = 0.
Alla classe NP si contrappone la classe P dei problemi trattabili, ovvero che ammettono un algoritmo polinomiale. Un problema decisionale P è in P se esiste un algoritmo
polinomiale A che, data una istanza x di P, A(x) = 1 se e solo se x è una istanza si.
Proposizione 2.1. P ⊆ NP.
Dimostrazione. Sia P un problema in P ed x una sua istanza allora esiste un algoritmo
A1 tale che A1 (x) = 1 se e solo so x è una istanza si. Utilizzeremo l’algoritmo A1 come
funzione dell’algoritmo A2 che prende in input istanze di P e sue potenziali soluzioni.
d e f A2 (x, y ) :
r e t u r n A1 (x)
Se x è una istanza si allora per ogni y l’algoritmo A2 restituisce si, altrimenti restituisce
no, inoltre l’algoritmo è polinomiale. Questo dimostra che P è in NP.
Il problema del longest-path è intrattabile anche se il nodo iniziale e finale coincidono
e k = n − 1. In questo caso ci stiamo chiedendo se esiste un ciclo che tocca tutti i nodi del
grafo. Questo problema è meglio noto come circuito Hamiltoniano. Un grafo che contiene
un circuito Hamiltoniano è detto grafo Hamiltoniano.
Gianluca Rossi
2.3
Note del corso di ASDL Mod. II
20
Riducibilità polinomiale
La nozione di intrattabilità all’interno della classe NP si può far transitare da un problema
notoriamente intrattabile P1 ad un altro problema P2 se si riesce a dimostrare che l’esistenza
di un algoritmo polinomiale per P2 implica l’esistenza di un algoritmo polinomiale per P1 .
Vediamo un esempio considerando un nuovo problema, quello del commesso viaggiatore.
Dato un grafo G = (V , E , w ) completo e pesato di n nodi ed un intero k, ci si chiede se
esiste un ciclo semplice C che tocca tutti i nodi e tale che il costo totale del ciclo sia al
più k, ovvero
X
w (C ) =
w (e) ≤ k.
e∈C
Proposizione 2.2. Se il problema del commesso viaggiatore è in P lo è anche circuito
Hamiltoniano.
Dimostrazione. Sia Acv l’algoritmo polinomiale per il problema del commesso viaggiatore e
sia G = (V , E ) l’istanza del problema del circuito Hamiltoniano. A partire da G costruiamo
una istanza per il problema del commesso viaggiatore. Sia il G 0 il grafo completo e pesato
avente per nodi V e come funzione peso la seguente
1
se (u, v ) ∈ E ;
Per ogni u, v ∈ V con u 6= v w (u, v ) =
2
altrimenti.
Ora consideriamo il seguente algoritmo per il problema del circuito Hamiltoniano.
d e f Ach (G = (V , E )) :
costruisci G 0 e w da G
r e t u r n Acv (G 0 , w , |V | − 1)
L’algoritmo Ach restituisce vero se e solo se Acv ha trovato un ciclo semplice che tocca
tutti i nodi di G 0 (quindi di G ) che utilizza solo archi di lunghezze 1, ovvero archi in E .
Ovvero Ach restituisce vero se e solo se G è un grafo Hamiltoniano. La costruzione di G 0 è
polinomiale, l’algoritmo Acv è polinomiale per ipotesi quindi l’algoritmo Ach è polinomiale.
Quindi circuito Hamiltoniano è in P.
Poiché il problema del circuito Hamiltoniano è considerato intrattabile lo è anche il
problema del commesso viaggiatore. Per dimostrare questo risultato abbiamo trovato una
trasformazione polinomiale tra i due problemi tale che l’istanza d’arrivo è una istanza si se
e solo se lo è l’istanza di partenza. Tali trasformazioni vengono dette riduzioni polinomiali.
Una riduzione tra due problemi decisionali P1 e P2 è una funzione calcolabile in tempo
polinomiale f che trasforma istanze del problema P1 in istanze del problema P2 tale che x
è una istanza si di P1 se e solo se f (x) è una istanza si del problema P2 . In tal caso si
dice che P1 è riducibile polinomialmente (o solo riducibile) a P2 , in simboli P1 ≤P P2 .
Nella prova della Proposizione 2.2 è mostrata una riduzione polinomiale tra circuito
Hamiltoniano a commesso viaggiatore quindi circuito Hamiltoniano ≤P commesso viaggiatore.
Poiché P è contenuto in NP, la classe NP contiene sia problemi considerati intrattabili
(come longest-path, circuito Hamiltoniano e commesso viaggiatore) che problemi trattabili
(come shortest-path). L’intento è quello di distinguere gli uni dagli altri, ovvero isolare
i problemi trattabili da quelli intrattabili. I problemi intrattabili sono quelli più difficili
all’interno della classe NP. Se alla fine questi problemi devessero risultare trattabili allora
lo sarebbero tutti quelli della classe NP, ovvero le due classi coinciderebbero. Quindi
Gianluca Rossi
Note del corso di ASDL Mod. II
21
all’interno della classe NP distinguiamo una sotto-classe detta dei problemi NP-completi
o classe NP − C. Un problema P appartiene a NP − C se ogni problema P 0 di NP,
P 0 ≤P P e P è in NP. Invece se P è fuori da NP allora esso è NP-hard. Ovvero
un problema NP-hard è in NP − C se appartiene a NP. Si noti che la scoperta di un
algoritmo polinomiale per P implicherebbe P ≡ NP. Il seguente risultato afferma che la
classe NP − C non è vuota.
Teorema 2.1 (Cook-Levin [3, 4]). Il problema del circuito Hamiltoniano è NP-completo.
Chi fosse interessato ad una prova di questo risultato può consultare il libro [2].
La seguente proprietà della riduzione ci permetterà di semplificare dimostrazioni di
NP-completezza.
Proposizione 2.3 (Transitività di ≤P ). Dati tre problemi P1 , P2 e P3 , se P1 ≤P P2 e
P2 ≤P P3 allora P1 ≤P P3 .
Dimostrazione. Sia f la riduzione da P1 a P2 e g la riduzione da P2 a P3 allora la funzione
g ◦ f è una riduzione da P1 a P3 , infatti sia x una istanza di P1 , g (f (x)) è una istanza di
P3 . Inoltre x è una istanza si di P1 se e solo se f (x) è una istanza si di P2 se e solo se
g (f (x)) è una istanza si di P4 . Infine la composizione di due funzioni polinomiali è a sua
volta polinomiale.
Quindi per dimostrare che un problema è NP-hard è sufficiente ridurlo da un problema già notoriamente NP-hard. Per dimostrare la sua appartenenza a NP − C occorre
dimostrare anche la sua appartenenza a NP.
Proposizione 2.4. Il problema del commesso viaggiatore è NP-completo.
Dimostrazione. Dalla Proposizione 2.2 si ha che circuito Hamiltoniano é riducibile a commesso viaggiatore. Per il Teorema 2.1 e per la transitività della riduzione (Proposizione 2.3)
ogni problema in NP è riducibile a commesso viaggiatore. Quindi commesso viaggiatore è
NP-hard.
Esiste un algoritmo polinomiale che è in grado di stabilire se una soluzione potenziale
per il commesso viaggiatore è un ciclo che tocca tutti i nodi ed il cui costo è inferiore
all’intero dato in input. Quindi commesso viaggiatore appartiene a NP. Questo conclude
la prova.
2.4
La classe NP
La classe NP può essere definita anche utilizzando gli algoritmi non deterministici. Il
non deterministico in informatica è un concetto soltanto teorico e riguarda l’abilità di un
algoritmo o una macchina di trovarsi in più stati contemporaneamente. Possiamo vedere
la computazione di un algoritmo tradizionale come una sequenza di stati, il primo stato
o stato iniziale è comune a tutti gli input mentre i successivi sono conseguenza dello
stato attuale e della porzione dell’input che si sta analizzando. L’ultimo stato, o stato
finale, nel caso di algoritmi decisionali, è uno stato di accettazione o di rigetto. Un
algoritmo non deterministico può transitare da uno stato a piú stati contemporaneamente.
Un algoritmo non deterministico, tranne per il primo stato che è unico, puó trovarsi in più
stati contemporaneamente quindi una computazione non deterministica non è una sequenza
lineare di stati ma bensì un albero la cui radice è lo stato iniziale. Le foglie di questo
albero rappresentano gli stati finali che pertanto, contrariamente a quanto avviene per le
Gianluca Rossi
Note del corso di ASDL Mod. II
22
NP − C
NP
P
Figura 2.1: Se P ≡ NP le tre classi collassano in un unica classe.
computazioni deterministiche, possono essere molteplici. Una macchina non deterministica
accetta se esiste almeno uno stato finale di accettazione altrimenti se tutti gli stati finali
sono di rigetto, la macchina rigetta.
Mostriamo ora l’idea di algoritmo non deterministico per il problema del circuito Hamiltoniano. Una computazione di questo algoritmo su un grafo G = (V = {v1 , v2 , ... , vn }, E )
transiterà per una serie di stati ognuno dei quali rappresenta una soluzione parziale. In
particolare uno stato s a distanza k dallo stato iniziale rappresenterà una potenziale
soluzione parziale composta da una sequenza di k nodi (x1 , x2 , ... , xk ). Lo stato s ha
t = N(xk ) figli. Se N(xk ) = {y1 , ... , yt }, allora i figli di s rappresentano le sequenze
(x1 , ... , xk , y1 ), (x1 , ... , xk , y2 ), . . . (x1 , ... , xk , yt ). Le foglie dell’albero, che si troveranno a
distanza n − 1, rappresentano sequenze di n − 1 nodi del grafo, ovvero potenziali soluzioni.
Sia h = (x1 , x2 , ... , xn−1 ) una sequenza associata ad una foglia f , avremo che lo stato rappresentato dalla foglia f è uno stato di accettazione se e solo se: gli xi sono tutti distinti,
per ogni i = 1, ... , n − 2 (xi , xi+1 ) ∈ E e (xn−1 , x1 ) ∈ E . Ovvero h è un ciclo Hamiltoniano. Come passa l’algoritmo da uno stato a quello successivo? A questo punto interviene
il non determinismo dell’algoritmo. Da uno stato s l’algoritmo transita contemporaneamente
negli stati figli di s (Figura 2.2). La complessità di un algoritmo non deterministico è data
dall’altezza dell’albero delle computazioni in quanto si assume che una transizione non deterministica abbia costo costante. Quindi l’algoritmo per circuito Hamiltoniano ha un costo
non deterministico lineare.1
Il seguente risultato mette in relazione la classe NP con il non determinismo.
1 Il grado di non determinismo di un algoritmo è dato dal massimo grado dell’albero degli stati. In questo caso
è maxu∈V N(u). Per essere precisi si deve osservare che affinché un problema sia in NP il suo grado di non
determinismo deve essere costante. In questo caso possiamo ridurre il grado di non determinismo a 2 ottenendo
un algoritmo di complessità O(n log n). Infatti possiamo codificare ogni soluzione con n log n bit (ognuno degli
n nodi è codificato con una sequenza di log n bit) quindi l’albero degli stati sarà binario di altezza n log n che
genererà tutte sequenze binarie di lunghezza n log n.
Gianluca Rossi
a
Note del corso di ASDL Mod. II
23
d
a
c
b
b
c
d
a
a
b
c
d
a
d
a b
d
b
c
c
b
d
c
a d d
c
b
a
c
b
Figura 2.2: Con passo non deterministico la computazione transita dallo stato s agli stati
s1 , s2 , . . . , sn .
Teorema 2.2. Il problema P è in NP se e solo se esiste un algoritmo polinomiale non
deterministico per P.
Abbozzo di dimostrazione. Se il problema è in NP allora le soluzioni potenziali y di una
istanza x hanno lunghezza al più p(|x|) per un polinomio p. Con un algoritmo non deterministico che genera un albero di altezza p(|x|) è possibile creare uno stato finale per ogni
possibile soluzione. Ognuna di queste può essere verificata in tempo polinomiale. Questo
dimostra la prima parte del teorema.
Sia P un problema che ammette un algoritmo A non deterministico polinomiale. Se
x è una istanza si di P esiste un cammino di lunghezza polinomiale dallo stato iniziale
ad uno stato finale di accettazione, sia y la sequenza di stati toccati dal cammino. Ora
possiamo costruire un algoritmo B che prende in input l’istanza x di P ed una sequenza
y di stati di A, l’algoritmo B simula il comportamento di A su input x eseguendo scelte
deterministiche dettate da y . Sostanzialmente B(x, y ) esegue un ramo della computazione
di A(x) avendo come “mappa” la sequenza y e restituise 1 se e solo se termina in uno stato
di accettazione. Se x è una istanza no A non ha stati di accettazioni quindi per tutte le
sequenze y B(x, y ) = 0.
Questo risultato ci dice che possiamo usare come definizione alternativa di classe NP
la seguente: la classe NP è costituita da problemi decisionali che ammettono un algoritmo
polinomiale non deterministico.
Abbiamo visto che una computazione non deterministica polinomiale può essere vista
come un albero di altezza polinomiale nella lunghezza dell’input. Questo albero ha quindi
un numero esponenziale di nodi che può essere visitato da un algoritmo deterministico in
tempo lineare nel numero di nodi ovvero esponenziale nella lunghezza dell’input. Se la
visita incontra un nodo di accettazione esso restituisce 1 altrimenti 0. Quello che abbiamo
appena descritto à un algoritmo deterministico che simula il comportamento di un algoritmo
non deterministico. Quindi, tenendo conto anche del Teorema 2.2, abbiamo dimostrato il
seguente risultato.
Teorema 2.3. I problemi nella classe NP ammettono algoritmi di complessità esponenziali.
Gianluca Rossi
Note del corso di ASDL Mod. II
24
Si è detto che non è noto se P è propriamente contenuto in NP oppure le due classi
coincidono. Se fosse P ≡ NP il non determinismo non sarebbe più potente dal punto di
vista della complessità computazionale del determinismo: ogni algoritmo polinomiale non
deterministico potrebbe essere sostituito da un algoritmo polinomiale deterministico. Questa
ipotesi sembra poco verosimile ed è anche per questo che i ricercatori ritengono che sia più
plausibile che valga la congettura P 6= NP.
Bibliografia
[1] Python Programming Language – Official Website. www.python.org.
[2] D.P. Bovet and P. Crescenzi. Introduction to the theory of complexity. Prentice Hall, 1994.
Versine elettronica disponibile gratuitamente con licenza Creative Commons all’indirizzo
http://piluc.algoritmica.org/books/itlcc.
[3] S. Cook. The complexity of theorem proving procedures. In Proceedings of the Third
Annual ACM Symposium on Theory of Computing, pages 151–158, 1971.
[4] L. Levin. Universal’nye perebornye zadachi. Problemy Peredachi Informatsii, 9(3):265–
266, 1973. Rivista in lingua Russa.
25