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