Note della lezione 1
Transcript
Note della lezione 1
Algoritmi e Strutture Dati II: Parte B Anno Accademico 2004-2005 Lezione 1 Docente: Ugo Vaccaro Dalla Teoria della NP-Completezza abbiamo appreso che esiste una classe di problemi (NPhard) per cui non è noto alcun algoritmo di soluzione di complessità polinomiale ed è inoltre molto improbabile che per tali problemi possa essere mai trovato un algoritmo di soluzione di complessità polinomiale. Tuttavia, la classse dei problemi NP completi contiene molti problemi di grande importanza pratica. Per essi è molto importante dover produrre una “qualche soluzione”, independentemente dal fatto che la soluzione “migliore” sia difficile da trovare o meno.1 Tipicamente, ci sono tre differenti approcci che si possono seguire quando ci si trova di fronte alla necessità di trovare soluzioni accettabili in pratica per problemi NP completi . 1. Se l’input al problema dettato dalla specifica situazione pratica in cui il problema sorge è piccolo, allora si può provare a trovare la “migliore” soluzione mediante una ricerca esaustiva, che essenzialmente genera tutte le possibili soluzioni associate all’istanza di input e poi se ne sceglie la migliore. Tale approccio genera algoritmi con complessità esponenziale. Tuttavia, se l’input è piccolo, in pratica può produrre soluzioni in un tempo ragionevole. 2. Il fatto che un problema sia NP completo essenzialmente implica che è molto improbabile che esista un algoritmo di complessità polinomiale che produce la soluzione desiderata in corrispondenza ad ogni possibile istanza di input. Può capitare, però , che per “specifiche” particolari istanze di input, un tale algoritmo possa essere trovato. Pertanto, una analisi accurata delle possibili istanze di input che possono “ragionevolmente” accadere in pratica, può portare alla elaborazione di algoritmi efficienti. 3. Il terzo approccio consiste nel progettare algoritmi che producano una soluzione in tempo polinomiale, per ogni possibile istanza di input, non necessariamente di valore ottimo, ma che si discosti dal valore ottimo per “poco”, e che tale “poco” sia stimabile in funzione della taglia dell’input. In questo corso seguiremo il terzo approccio. Precisiamo i concetti sopra esposti. Innanzitutto diciamo che ci occuperemo di problemi di ottimizzazione. Informalmente un problema di ottimizzazione Π è definito da: • un insieme IΠ di possibili istanze di input; • ad ogni possibile istanza di input i ∈ I Π è associato un insieme di possibili soluzioni S Π (i). É definita inoltre una funzione costo che in corrispondenza ad ogni possibile istanza di input i ∈ IΠ associa ad ogni soluzione s ∈ SΠ (i) un costo c(s) ∈ R+ . 1 Per il momento la discussione è da intendersi ad un livello molto informale, successivamente daremo un senso preciso ai termini “migliore soluzione” e “qualche soluzione” per i problemi NP completi che considereremo. 1 In linea di principio, l’obiettivo sarebbe di trovare in maniera efficiente, per ogni istanza di input i ∈ IΠ , una soluzione di minimo costo (o massimo costo, a seconda che il problema sia di minimo o di massimo, rispettivamente), associata alla istanza i. Sia OPT Π (i) il costo di una tale soluzione. Per i problemi di ottimizzazione che studieremo in questo corso, calcolare OPT Π (i) è difficile, ovvero il corrispondente problema di decisione è NP completo (vedi ASD2 parte A). Esempio 1 Un classico esempio di problema di ottimizzazione è il problema del Minimum Spanning Tree . L’insieme delle possibili istanze di input a Minimum Spanning Tree consiste di tutte le possibili coppie (G, w), dove G = (V, E) è un grafo con insieme di vertici V ed insieme di archi E, e w : E → R+ è una funzione peso che associa numeri non negativi agli archi nell’insieme E. Per ogni possibile istanza di input i = (G, w), l’insieme delle possibili soluzioni S(i) consiste di tutti i sottoalberi di G che contengono tutti i vertici in V . Ad ogni possibile soluzione s ∈ S(i) (ovvero ad ogni possibile sottoalbero T di G che contiene tutti i vertici di G) è associato come costo il valore c(s) pari alla somma dei pesi secondo la funzione w degli archi di T . Il problema di ottimizzazione consiste nel trovare per ogni i = (G, w) un albero di minimo costo in S(i). Esempio 2. Un altro classico problema di ottimizzazione è il problema del Vertex Cover . L’insieme delle possibili istanze di input a Vertex Cover consiste di tutti i possibili grafi G = (V, E). Per ogni grafo G, l’insieme delle possibili soluzioni ad esso associato consiste di tutti i sottoinsiemi S ⊆ V tali che per ogni arco e ∈ E, almeno uno dei due vertici incidenti su e appartiene a S. Ogni tale S viene chiamato vertex cover di G. Il costo di una soluzione S ⊆ V associata al grafo G corrisponde alla sua cardinalità |S|. Il problema di ottimizzazione consiste nel trovare per ogni grafo G un vertex cover di minima cardinalità . Informalmente, un algoritmo di approssimazione A per un problema di ottimizzazione Π produce, in tempo polinomiale ed in corrispondenza ad ogni istanza di input di Π, una soluzione di valore (costo) “prossimo” all’ottimo, dove per prossimo intendiamo che differisce dal valore ottimo per un certo fattore, sperabilmente piccolo. Precisiamo maggiormente questi concetti, differenziando tra problemi di ottimizzazione di minimo e problemi di ottimizzazione di massimo. Definizione. Sia Π un problema di ottimizzazione di minimo, e sia ρ : N → R + , con ρ(n) ≥ 1, per ogni n ∈ N . Un algoritmo A è detto essere un algoritmo di approssimazione con fattore di approssimazione ρ per il problema di ottimizzazione Π se, per ogni istanza di input i di Π l’algoritmo A produce in tempo polinomiale nella taglia dell’input |i| una soluzione s ∈ S Π (i) tale che il valore della funzione costo sulla soluzione s soddisfi c(s) ≤ ρ(|i|)OP TΠ (i) Chiaramente, più prossimo a 1 è ρ(|i|) e migliore è l’approssimazione prodotta dall’algoritmo. Analoga è la definizione di algoritmo di approssimazione per problemi di ottimizzazione di massimo. Definizione. Sia Π un problema di ottimizzazione di massimo, e sia ρ : N → R + , con ρ(n) ≥ 1, per ogni n ∈ N . Un algoritmo A è detto essere un algoritmo di approssimazione con fattore di approssimazione ρ per il problema di ottimizzazione Π se, per ogni istanza di input i di Π l’algoritmo A produce in tempo polinomiale in |i| una soluzione s ∈ S Π (i) tale che il valore della 2 funzione costo sulla soluzione s soddisfi c(s) ≥ OP TΠ (i) ρ(|i|) Per semplicità , denoteremo con SOL il valore della soluzione prodotta dagli algoritmi di approssimazione che esamineremo, e con OP T il valore della soluzione ottima. Dalla definizione di algoritmi di approssimazione discende che per provare che un algoritmo A abbia un fattore di approssimazione ρ occorre provare che il costo della soluzione trovata da A si discosta dal valore ottimo OP T della soluzione al più per un fattore ρ. Si pone il problema di come dimostrare questo fatto, visto che per i problemi che studieremo non conosciamo ovviamente a priori OP T né riusciamo a calcolarlo. Il “trucco” consiste nello stimare OP T con quantità di più facile calcolo. Vediamo come farlo nel seguente problema. VERTEX COVER • Input: – grafo G = (V, E) • Output: sottoinsieme S ⊆ V di minima cardinalità |S| tale che ∀(u, v) ∈ E valga {u, v} ∩ S 6= ∅. L’intuizione alla base di un algoritmo di approssimazione per il problema del Vertex Cover è la seguente. Se nel grafo G abbiamo un certo numero k di archi disgiunti, ovvero di archi in cui nessuno di essi è adiacente all’altro, allora occorreranno almeno k vertici per coprirli tutti. Ora, un insieme M ⊆ E di archi disgiunti viene chiamato matching. L’osservazione appena fatta ci permette di trarre la seguente conclusione: ogni Vertex Cover per G deve essere grande almeno quanto ogni matching di G. Ciò vale ovviamente anche per il Vertex Cover ottimo (ovvero quello di cardinalità minima). Sia OP T la cardinalità del Vertex Cover di taglia minima. Abbiamo quindi appena dimostrato che OP T ≥ |M |, per ogni M matching in G. (1) Quindi la (1) ci ha permesso di ottenere una prima stima di OP T che useremo per valutare il fattore di approssimazione del nostro algoritmo. Diremo ora che un matching M è massimale se per ogni arco e ∈ E vale che M ∪ {e} non è più un matching. L’osservazione chiave è che l’insieme A dei vertici che incidono sugli archi di un matching massimale formano un Vertex Cover per G. Infatti, se ciò non fosse, ovvero se esistesse un arco e ∈ E non coperto dai vertici di A, ciò vorrebbe dire che l’arco e non è adiacente ad alcun arco in M , ovvero M ∪ {e} è ancora composto da archi non adiacenti, quindi M ∪ {e} è ancora un matching, contraddicendo il fatto che M è un matching massimale. Quindi, il seguente algoritmo produce un Vertex Cover . 3 • Costruisci un matching massimale M in G • Output tutti i vertici che toccano gli archi di M Valutiamo ora il fattore di approssimazione dell’algoritmo. Sia SOL il valore della soluzione ritornata dall’algoritmo, ovvero il numero di vertici nell’insieme output dell’algoritmo. Abbiamo che SOL = 2|M | ≤ 2OP T per la (1) e quindi l’algoritmo ha un fattore di approssimazione costante pari a 2. Resta da vedere come costruire in tempo polinomiale un matching massimale in G. Ciò è semplicemente fatto mediante il seguente algoritmo. Algoritmo per la costruzione di un matching massimale • M ← e, (e arco qualunque di E) • Cancella nel grafo G tutti gli archi adiacenti a e • Itera nel grafo rimanente fin a quando non diventa vuoto • Output M É istruttivo chiedersi se è possibile migliorare il fattore di approssimazione 2, mediante un’analisi più accurata dell’algoritmo. La risposta è no. Si consideri il seguente esempio di grafo in cui l’algoritmo da noi proposto produrrebbe una soluzione dal valore esattamente pari a 2OP T . Il grafo è un grafo bipartito con 2n nodi, in cui ogni nodo del lato sinistro è connesso ad ogni noto del lato destro. Un Vertex Cover di cardinalità minima è dato da uno dei due lati 4 dei vertici, quindi OP T = n. D’altra parte, l’algoritmo considererebbe un matching massimale di cardinalità n (rappresentato ad esempio dagli archi di maggior spessore nella figura di sopra) e di conseguenza la soluzione prodotta dall’algoritmo avrebbe cardinalità SOL = 2n. Inoltre, esiste una classe di grafi in cui ogni matching massimale ha cardinalità esattamente pari alla metà di un Vertex Cover ottimale. Tale classe di grafi è composta da tutti i grafi completi 2 su n nodi, con n dispari. Da ciò si evince che se si insiste ad usare la tecnica del matching, non si potrà mai migliorare il fattore di approssimazione 2 appena provato. A tutt’oggi, algoritmi di approssimazione per Vertex Cover con fattore di approssimazione costante inferiore a 2 non sono noti. Esercizio 1 Presentiamo un altro algoritmo di approssimazione per il problema del Vertex Cover con fattore di approssimazione pari a 2. L’algoritmo consiste dei seguenti passi: Algoritmo alternativo per il calcolo del Vertex Cover • Esegui una DFS sul grafo G • Output: l’insieme S costituito da tutti i vertici dell’albero DFS tranne le foglie. Mostriamo innanzitutto che l’insieme S output dall’algoritmo è un Vertex Cover del grafo. Supponiamo per assurdo che non lo sia. Ciò implica che esiste un arco e = (u, v) ∈ E, con u e v entrambi foglie dell’albero DFS. Senza perdita di generalità , supponiamo che u sia il primo tra i due vertici u, v ad essere stato visitato durante la visita DFS. Allora, per come la DFS opera, v diventerà figlio di u nell’albero. Ciò contraddice il fatto che u è una foglia dell’albero DFS. Sia S l’insieme output dell’algoritmo sopra esposto. Ci resta da provare che |S| ≤ 2OP T , dove OP T rappresenta la cardinalità di un Vertex Cover di taglia minima di G. Proviamo innanzitutto che nell’albero DFS (e quindi anche in G) vi è un matching M di cardinalità almeno |S|/2. Costruiamo il matching M nel seguente modo, partendo dal nodo di livello 0 dell’albero, ovvero dalla radice r. Scegliamo un figlio arbitrario u di r e poniamo (r, u) in M . Iterando, al livello 1 prendiamo tutti i nodi non incidenti ad archi già posti in M , se esistono, e per ciascuno di essi scegliamo un figlio arbitrario e poniamo gli archi dei nodi al livello 1 ai loro figli cosi scelti nel matching M . Iteriamo fin quando tutti i nodi interni dell’albero DFS non sono stati considerati. Abbiamo quindi costruito un matching M i cui archi incidono su ognuno degli |S| nodi in S. Ogni arco di M può incidere su al più due nodi di S, quindi |M | ≥ |S|/2. D’altra parte, sappiamo che la cardinalità OP T di un minimo Vertex Cover per G soddisfa OP T ≥ |M |, quindi otteniamo che |S| ≤ 2OP T . Fino ad adesso abbiamo considerato problemi di minimo. Consideriamo ora un esempio di problema di ottimizzazione di massimo Esercizio 2 Consideriamo il seguente problema 2 Si ricordi che un un grafo è completo se esiste un arco tra ogni coppia di vertici. 5 Massimo Sottografo Aciclico • Input: – grafo diretto G = (V, E) • Output: sottoinsieme A ⊆ E di massima cardinalità |A| tale che il grafo H = (V, A) non contiene cicli. Il problema è NP completo . Un algoritmo di approssimazione può essere il seguente. Scriviamo i vertici V = {1, 2, . . . , n} da sinistra a destra su di una linea. Poniamo in A tutti gli archi di G della forma (i, j) con i < j, ed in B tutti gli archi della forma (i, j) con i > j. Ovviamente, A ∪ B = E, e A ∩ B = ∅. Di conseguenza, |A| + |B| = |E|, e quindi o A oppure B ha cardinalità almeno |E|/2. Tale insieme di cardinalità almeno |E|/2 sarà l’output del nostro algoritmo. Esso è sicuramente una soluzione valida (ovvero è un insieme di archi senza cicli in G, in quanto gli archi vanno tutti in una stessa direzione). Detta SOL la sua cardinalità , avremo che SOL ≥ |E|/2 ≥ OPT /2, dato che ovviamente OP T ≤ |E|. Pertanto, il nostro algoritmo ha un fattore di approssimazione pari a 2. Esercizio 3 Consideriamo il seguente problema Maximum Cut • Input: – grafo non diretto G = (V, E) • Output: partizione dell’insieme dei vertici V in due sottoinsiemi S 1 , S2 , con S1 ∪ S2 = V , S1 ∩ S2 = ∅, in modo tale che il numero di archi c(S 1 , S2 ) tra S1 e S2 sia massimizzato. Il problema è NP completo . Un algoritmo di approssimazione per tale problema può essere ottenuto mediante la tecnica delle ricerca locale, che consiste, data una possibile soluzione, nel tentare di migliorarla mediante “piccole” modifiche di tipo locale. Nel nostro caso, si parta con una arbitraria partizione di V nei due insiemi S 1 e S2 . Fin quando esiste un vertice che portato da S1 a S2 (o da S2 a S1 ) aumenta il corrente valore c(S1 , S2 ), lo si faccia. Si termina quando non esiste alcun vertice con questa proprietà. 6 Algoritmo per Maximum Cut Sia (S1 , S2 ) una partizione arbitraria di V While esiste un vertice x ∈ V tale che c(S1 ∪ {x}, S2 − {x}) > c(S1 , S2 ) (oppure c(S1 − {x}, S2 ∪ {x}) > c(S1 , S2 )) do S1 ← S1 ∪ {x}, S2 ← S2 − {x} (oppure S1 ← S1 − {x}, S2 ← S2 ∪ {x}) • Output la partizione (S1 , S2 ) Il numero di iterazioni dell’algoritmo è chiaramente al più pari a |E|, visto che ogni iterazione del ciclo while aumenta il valore di c(·, ·), e questo non può superare |E|. Inoltre, alla terminazione dell’algoritmo, ogni vertice x ∈ V avrà almeno deg(x)/2 vertici nell’altro lato della partizione, dove deg(x) è il grado di x nel grafo G. Infatti, se ciò se non fosse, allora sarebbe possibile portare x dal suo lato della partizione all’altro lato della partizione ed aumentare il P valore di c(S1 , S2 ), contro l’ipotesi. Quindi, usando il fatto che x∈V deg(x) = 2|E|, otteniamo che 2c(S1 , S2 ) = X (numero di archi che vanno da x all’altro lato della partizione) (2) x∈V ≥ = X deg(x) x∈V (3) 2 1 2|E| = |E|. 2 (4) Pertanto, il valore della soluzione S restituita dall’algoritmo soddisfa SOL = c(S1 , S2 ) ≥ OP T |E| ≥ . 2 2 Può essere istruttivo generalizzare l’esercizio al caso in cui si desideri trovare la partizione di V in k classi con il massimo numero di archi tra vertici appartenenti a classi differenti. Concludiamo la lezione con la prova del fatto che X deg(x) = 2|E|. (5) u∈V La (5) è ovvia, una volta che si osservi che ogni arco (u, v) viene conteggiato due volte nella somma che appare al membro sinistro della (5), la prima volta quando si somma deg(u), la seconda quando si somma deg(v). 7