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