892 lines
45 KiB
Markdown
892 lines
45 KiB
Markdown
|
# Algoritmi
|
||
|
|
||
|
Elenco dei principali algoritmi studiati a lezione, usando tecniche di programmazione
|
||
|
dinamica, branch and bound, algoritmi approssimati e algoritmi probabilistici.
|
||
|
|
||
|
## Note sulla Programmazione dinamica
|
||
|
|
||
|
La programmazione dinamica e' applicabile in casi in cui:
|
||
|
* Il numero di sottoproblemi e' polinomiale
|
||
|
* La soluzione del problema puo' essere facilmente calcolata dalle soluzioni ai sottoproblemi
|
||
|
* Vi e' un ordinamento naturale di sottoproblemi *dal piu' piccolo al piu' grande* con
|
||
|
sottoproblemi facili da calcolare.
|
||
|
|
||
|
### Caratteristiche
|
||
|
|
||
|
* Iterativa nell'implementazione, ricorsiva nell'espressione dell'*equazione alle ricorrenze*
|
||
|
* Bottom-up
|
||
|
|
||
|
## Note su Backtracking / Branch and Bound
|
||
|
|
||
|
### Backtracking
|
||
|
|
||
|
Il backtracking e' una tecnica che permette la **wicerca esaustiva** (brute-force) esplorando
|
||
|
sistematicamente lo spazio degli stati. A differenza della semplice ricerca esaustiva (che
|
||
|
prova tutte le possibilita'), il backtracking **riduce il numero di tentativi effettuati**
|
||
|
utilizzando un **vettore componente** e delle funzioni criterio (**bounding functions**) per
|
||
|
verificare se una soluzione parziale puo' generare una soluzione accettabile.
|
||
|
|
||
|
#### Caratteristiche
|
||
|
|
||
|
* Si applica a molti problemi che richiedono **un insieme di soluzioni** che soddisfino una
|
||
|
serie di **vincoli**. I vincoli espliciti identificano lo spazio delle soluzioni.
|
||
|
* Ricorsiva nell'implementazione, per generare **un albero degli stati**.
|
||
|
* Caratterizzata da una chiamata che esplora l'albero degli stati ricorsivamente **resettando
|
||
|
lo stato precedente quando ritorna**, di modo da considerare tutte le possibili soluzioni.
|
||
|
* Problema: la ricerca esaustiva spesso esplora stati **ridondanti**. Per questo si ricorre a
|
||
|
soluzioni di *potatura* dell'albero delle soluzioni, tra cui:
|
||
|
1. Rimozione di simmetrie (i.e. evitare che i cicli vengano percorsi in entrambe le
|
||
|
direzioni nel ciclo hamiltoniano) -> forzare un **ordine**
|
||
|
2. Individuare soluzioni *poco promettenti* (che certamente non portano a una soluzione)
|
||
|
Le **funzioni di bounding** sono utilizzate in questo contesto per ridurre lo spazio degli
|
||
|
stati.
|
||
|
|
||
|
#### Calcolo dell'efficienza
|
||
|
|
||
|
L'efficienza di un algoritmo backtracking dipende da:
|
||
|
* Il tempo per generare il valore successivo nel vettore soluzioni `s`.
|
||
|
* Il numero di valori di `s` che soddisfano i vincoli espliciti.
|
||
|
* Il tempo per il calcolo delle funzioni di bounding.
|
||
|
* Il numero di valori in `s` che soddisfano le funzioni di bounding.
|
||
|
|
||
|
Si nota che due diverse istanze dello stesso problema possono portare lo stesso algoritmo di
|
||
|
backtracking a una soluzione lineare O(n) e a una soluzione esponenziale / fattoriale.
|
||
|
Questo perche' la generazione dello spazio degli stati in alcuni problemi significa generare
|
||
|
tutte le combinazioni / permutazioni di un certo input, ottenendo complessita' elevate. Per questo si
|
||
|
utilizzano funzioni di bounding, che riducono tramite dei vincoli il problema.
|
||
|
|
||
|
#### Teorema
|
||
|
|
||
|
Se definiamo:
|
||
|
* h: altezza albero delle soluzioni
|
||
|
* O(f(n)): tempo richiesto dalla visita di un nodo interno all'albero delle soluzioni
|
||
|
* O(g(n)): tempo richiesto dalla visita di una foglia dell'albero (soluzione)
|
||
|
* D(n): numero di soluzioni
|
||
|
|
||
|
Possiamo dire che **visitare solo nodi nei cui sottoalberi e' presente una soluzione**
|
||
|
richiede tempo:
|
||
|
```
|
||
|
O(h*f(n)*D(n) + g(n)*D(n))
|
||
|
```
|
||
|
|
||
|
*Nota: questo teorema NON FUNZIONA se la funzione di bounding non assicura che un nodo venga
|
||
|
visitato solo se il sottoalbero contiene soluzioni.* vedi Ciclo Hamiltoniano.
|
||
|
|
||
|
### Branch and Bound
|
||
|
|
||
|
Generalizzazione del backtracking ai problemi di ottimizzazione. Tecnica che permette di
|
||
|
calcolare i limiti per le soluzioni parziali al fine di contenere il numero di soluzioni
|
||
|
complessive che derivano dall'esplorazione completa dell'albero delle soluzioni.
|
||
|
Si divide in due parti:
|
||
|
1. **Branch**: regola che determina il sottoalbero delle scelte da analizzare
|
||
|
2. **Bound**: regola che determina il **lower bound** per ogni nodo dell'albero, generando un
|
||
|
minimo sotto al quale i costi delle soluzioni non scendono. Questo permette di escludere
|
||
|
quelle che di sicuro portano a una soluzione peggiore dell'attuale.
|
||
|
|
||
|
#### Caratteristiche
|
||
|
|
||
|
* Come backtracking esplora tutto l'albero delle soluzioni, ma si applica a problemi **di
|
||
|
minimizzazione**
|
||
|
* Mantiene sempre la migliore soluzione ammissibile tra quelle esaminate
|
||
|
* Non genera i sottoalberi di scelte per i nodi che non possono migliorare la soluzione
|
||
|
ottima attuale.
|
||
|
* **Tecnica ulteriore di potatura**: Se si considera un percorso di costo `c`, non ha senso
|
||
|
continuare la ricerca su percorsi con costo **maggiore di** `c`.
|
||
|
* Come per il backtracking, il caso peggiore risulta avere complessita' nel tempo
|
||
|
**super-polinomiale**.
|
||
|
|
||
|
#### Lower bound
|
||
|
|
||
|
Il calcolo del lower bound dipende dalla sequenza di scelte fatte e deve garantire che tutte
|
||
|
le soluzioni ammissibili generabili da quella sequenza abbiano un costo maggiore a `LB(x)`.
|
||
|
|
||
|
Si assegna un rango ai nodi sulla base dello sforzo computazionale aggiuntivo per raggiungere
|
||
|
dal nodo considerato il nodo risposta. Questo significa considerare:
|
||
|
1. Il numero di nodi nel sottoalbero da generare per ottenere un nodo risposta
|
||
|
2. Il numero di livelli per cui un nodo risposta dista dal nodo
|
||
|
|
||
|
Queste sono funzioni di costo ideali, nel senso che **per determinare il rango esatto con
|
||
|
queste funzioni, e' necessario esplorare l'intero sottoalbero**. Dato che questo
|
||
|
vanificherebbe lo scopo del bounding, si utilizzano ranghi basati su una **stima** del costo.
|
||
|
|
||
|
Costo di un nodo `X`:
|
||
|
```
|
||
|
C[s](X) = f(h(X)) + g[s](X)
|
||
|
```
|
||
|
Dove:
|
||
|
* f() e' una funzione non decrescente
|
||
|
* h(X) e' il costo per raggiungere `X` dalla radice
|
||
|
* `g[s](X)` e' il costo **stimato** necessario a raggiungere un nodo risposta da X.
|
||
|
|
||
|
#### Least-Cost Search
|
||
|
|
||
|
La strategia di ricerca (FIFO / LIFO) si chiama **Least Cost Search**, perche' come nodo
|
||
|
successivo viene scelto quello *vivo* con `C[s]` minore.
|
||
|
* FIFO (Breadth-first): LC-s con `g[s](X) = 0` e `f(h(X)) = level(X)`
|
||
|
* LIFO (Depth-first): LC-s con `f(h(X)) = 0` e per ogni Y figlio di X, `g[s](X) >= g[s](Y)`
|
||
|
(dato che Y e' piu' vicino a un nodo risposta rispetto a X)
|
||
|
|
||
|
## Algoritmi di Approssimazione
|
||
|
|
||
|
Se garantire l'ottimalita' di una soluzione per un problema **NP-completo** richiede un tempo
|
||
|
di calcolo troppo elevato, si ricorre ad una soluzione **sub-ottima** calcolabile in tempo
|
||
|
polinomiale. Per questi casi si usano algoritmi di approssimazione che esprimono la distanza
|
||
|
tra soluzione teoricamente ottima e approssimata con un **rapporto limite**.
|
||
|
|
||
|
### Rapporto Limite
|
||
|
|
||
|
Se `I` e' un problema di ottimizzazione, allora `S(I)` e' l'insieme di soluzioni ammissibili:
|
||
|
|
||
|
* Ad ogni soluzione ammissibile S di I, si associa un **costo C(I,S)** che per i problemi di
|
||
|
minimizzazione sara' il minimo tra tutte le soluzioni ammissibili, viceversa per i
|
||
|
problemi di massimo.
|
||
|
|
||
|
Definiamo quindi il rapporto limite come **la massima distanza** tra il costo di una
|
||
|
soluzione prodotta dall'algoritmo approssimato e una prodotta dall'algoritmo ottimale.
|
||
|
(un po' come il competitive ratio)
|
||
|
|
||
|
Per gli algortmi approssimati, il rapporto limite e' sempre **maggiore di 1**. Piu' alto e'
|
||
|
il rapporto, peggiore e' la soluzione approssimata.
|
||
|
|
||
|
### Schemi di approssimazione
|
||
|
|
||
|
Tecniche che garantiscono di poter calcolare (e contenere entro un certo valore) il rapporto
|
||
|
limite.
|
||
|
|
||
|
1. Greedy: Si deve comunque trovare una regola che porti **vicini** alla soluzione ottima.
|
||
|
2. Pricing: si considera un costo da pagare per ogni vincolo del problema da rinforzare.
|
||
|
3. Programmazione lineare
|
||
|
4. Programmazione dinamica su una versione *arrotondata* dei dati in input.
|
||
|
|
||
|
### Appossimazione e Riduzione polinomiale
|
||
|
|
||
|
La teoria della complessita' dice che e' possibile ridurre un problema a un altro in NP,
|
||
|
(si pensi a Cook-Levin). Per i problemi di approssimazione bisogna pero' considerare che
|
||
|
alcune volte non e' possibile ridurre un algoritmo approssimato ad un altro. Questo perche' i
|
||
|
fattori di approssimazione possono cambiare tra un problema e l'altro.
|
||
|
|
||
|
## Scheduling di intervalli
|
||
|
|
||
|
In versione pesata e non pesata.
|
||
|
|
||
|
* Greedy (intro pr.din 1) solo non pesati: con gli intervalli pesati, l'algoritmo greedy non
|
||
|
funziona. **Funzionamento:** si riordinano gli intervalli per tempo di fine crescente. Da
|
||
|
questo set ordinato si prendono uno alla volta gli intervalli e li si inserisce all'interno
|
||
|
della soluzione. In questo la complessita' massima e' **O(nlogn)** a causa
|
||
|
dell'ordinamento.
|
||
|
|
||
|
* Dinamica (intro pr.din 1) - intervalli pesati: utilizzato per introdurre il paradigma di
|
||
|
programmazione dinamica, l'equazione alle ricorrenze e il memoizing.
|
||
|
Si utilizzano intervalli con peso numerico e si vuole massimizzare la somma dei pesi degli
|
||
|
intervalli selezionati tra i compatibili (*non sovrapposti*).
|
||
|
* **Sottoproblemi**: i sottoproblemi sono soluzioni ottime in un subset degli intervalli.
|
||
|
Definiamo un intervallo come appartenente a una soluzione ottima su un set di intervalli
|
||
|
se soddisfa l'equazione alle ricorrenze:
|
||
|
```
|
||
|
v[j] + OPT(p(j)) >= OPT(j-1) // j e' l'intervallo considerato
|
||
|
```
|
||
|
Questo significa che un intervallo appartiene alla soluzione ottima se il suo peso
|
||
|
migliora la soluzione ottima precedente `OPT(j-1)`. Si nota che in questo caso `p(j)` e'
|
||
|
l'indice dell'ultimo intervallo `i` tale che `i` e `j` sono disgiunti.
|
||
|
* **Complessita'**: L'algoritmo richiede tempo esponenziale nel caso peggiore (intervalli
|
||
|
ordinati in modo decrescente per tempo di fine, slides).
|
||
|
Questo e' dovuto al fatto che calcolare i sottoproblemi ricorsivamente genera un albero
|
||
|
di sottoproblemi ridondante.
|
||
|
* **Memoizing**: usato per risolvere il problema della ridondanza. Si utilizza un array M
|
||
|
che viene riempito iterativamente memorizzando i pesi ottimali. Questo significa che i
|
||
|
valori precedenti (OPT(j-1) in particolare) non vengono ricalcolati ma semplicemente
|
||
|
letti dall'array (accesso O(1) in quanto si conosce l'indice). Cosi' facendo, si ottiene
|
||
|
la soluzione ottima in un tempo **O(n)**.
|
||
|
Memoizing:
|
||
|
```
|
||
|
M[j] <-- max(v[j] + M[p(j)], M[j-1] //si nota come p(j) restituisca l'indice
|
||
|
dell'elemento compatibile
|
||
|
```
|
||
|
|
||
|
|
||
|
## Cammini minimi in un DAG / grafo con cicli positivi
|
||
|
|
||
|
* Dinamica (intro pr.din 2): questo algoritmo e' importante perche' illustra la strategia di
|
||
|
programmazione dinamica quando si presentano **scelte multiple**: ad ogni passo non abbiamo
|
||
|
piu' due possibilita' ma un numero **polinomiale** di esse.
|
||
|
Si consideri un grafo orientato, aciclico, pesato in cui si vogliono trovare i cammini di
|
||
|
lunghezza minima dato un nodo di origine.
|
||
|
* **Sottoproblemi**: Per ogni nodo `v`, la distanza minima dal nodo di origine sara' il
|
||
|
minimo tra le distanze minime dall'origine dei suoi predecessori.
|
||
|
```
|
||
|
dist(v) = min{ dist(u) + w(u,v) | (u,v) in E } // w e' la funzione di peso, dist(s) = 0
|
||
|
```
|
||
|
Se si considerano i nodi da sinistra a destra (vedi ordinamento topologico), quando si ha
|
||
|
un nodo `v` si dispone anche delle informazioni per calcolare `dist(v)`, essendo che per
|
||
|
i predecessori la distanza minima e' gia' stata calcolata.
|
||
|
* **Complessita'**: la complessita' nel caso peggiore e' data dalla somma della
|
||
|
complessita' dell'ordinamento topologico e dal numero di confronti per calcolare le
|
||
|
distanze. Si ottiene:
|
||
|
```
|
||
|
O(V+E) + O(V^2) --> O(V^2)
|
||
|
```
|
||
|
* Note: in questo caso non si usa la struttura dati del DAG, che rimane implicita: i nodi
|
||
|
del dag sono sottoproblemi e gli archi le dipendenze da essi.
|
||
|
|
||
|
* Dinamica (cammini minimi e prodotto di matrici): Variante. Trovare il peso dei cammini di
|
||
|
peso minimo tra tutte le coppie di vertici di un grafo orientato e pesato, supponendo che
|
||
|
non ci siano cicli di *peso negativo*. In questo caso i cicli positivi sono permessi.
|
||
|
* **Sottoproblemi**: si definiscono i cammini minimi in funzione della lunghezza dei
|
||
|
cammini, sapendo che:
|
||
|
1. Un cammino inesistente ha peso infinito.
|
||
|
2. Non esiste cammino lungo `0` archi tra due vertici diversi.
|
||
|
3. Un singolo nodo ha un cammino lungo `0` che porta a se stesso
|
||
|
4. Il cammino di peso minore tra due nodi distinti di lunghezza *al piu'* `m` e' un
|
||
|
cammino di lunghezza minore di `m`, oppure un cammino di lunghezza `m` ottenuto da un
|
||
|
cammino minimo di lunghezza `m-1` fino a un vertice predecessore, seguito dall'arco
|
||
|
che collega il predecessore al nodo destinazione.
|
||
|
```
|
||
|
D(0)[i,j] = 0 se i == j OPPURE infinito se i != j
|
||
|
|
||
|
D(m)[i,j] = min{ D(m-1)[i,k] + W[k,j] } for k in 1..n
|
||
|
|
||
|
D(1) = W
|
||
|
```
|
||
|
* **Memoizing**: Si utilizza una matrice di vertici x vertici (quadrata) in cui si
|
||
|
memorizzano i cammini di lunghezza minima da ogni vertice a ogni altro.
|
||
|
* **Complessita'**: **O(n^3)** dato che dobbiamo iterare su ogni cella della matrice e per
|
||
|
ognuna cercare il minimo cammino (O(n))
|
||
|
Se si vuole mostrare tutte le coppie e' necessaria un'ulteriore iterazione, ottenendo
|
||
|
complessita **O(n^4)**
|
||
|
* **Variante Efficiente**: Se `m` e' **pari**, un cammino minimo tra due vertici `i,j`
|
||
|
sara' scomponibile in due cammini di lunghezza al piu' `m/2`.
|
||
|
Possiamo quindi modificare l'equazione alle ricorrenze:
|
||
|
```
|
||
|
D(m)[i,j] = min{ D(m/2)[i,k] + D(m/2)[k,j] } per m > 1
|
||
|
|
||
|
D(1)[i,j] = W[i,j]
|
||
|
```
|
||
|
La procedura viene impostata in forma iterativa con l'algoritmo visto in precedenza, solo
|
||
|
che la divisione dell'intervallo abbassa la complessita': l'algoritmo e'
|
||
|
**O(n^3*log2(n))**
|
||
|
* **Floyd-Warshall**: Utilizza la numerazione dei vertici tra `1..n` e definisce il cammino
|
||
|
tra due vertici in funzione dei vertici attraversati dal cammino. In questo modo il
|
||
|
cammino tra `i,j` di peso minore **che attraversa al piu' i vertici** `1..k` e' un
|
||
|
cammino che attraversa al piu' i vertici `1..k-1` oppure un cammino che attraversa `k`
|
||
|
ottenuto concatenando un cammino minimo tra `i,k` e un cammino minimo tra `k,j`.
|
||
|
```
|
||
|
D(m)[i,j] = W[i,j] se m == 0
|
||
|
|
||
|
OPPURE
|
||
|
|
||
|
D(m)[i,j] = min{ D(m-1)[i,j], D(m-1)[i,m] + D(m-1)[m,j] } se m > 0
|
||
|
```
|
||
|
Questo algoritmo ha complessita' **O(n^3)**
|
||
|
|
||
|
## Lunghezza massima di una sottosequenza crescente
|
||
|
|
||
|
* Dinamica (intro pr.din. 2): Data una sequenza di numeri, determinare la sottosequenza
|
||
|
crescente piu' lunga al suo interno.
|
||
|
Questo problema puo' essere rappresentato da un grafo che associ ad ogni vertice un
|
||
|
elemento della sequenza, utilizzando un arco orientato per indicare se un elemento ne
|
||
|
precede un altro all'interno della sequenza. Si riconduce quindi il problema al DAG, in cui
|
||
|
si deve trovare il cammino **massimo**.
|
||
|
* **Sottoproblemi**: definendo il massimo su un insieme vuoto come `0`:
|
||
|
```
|
||
|
L(j) = max{ L(i) | i<j && X[i] < X[j]} + 1 // aggiungiamo 1 perche' si inserisce un
|
||
|
elemento alla volta
|
||
|
```
|
||
|
* **Complessita'**: L'algoritmo ricorsivo genera un albero di sottoproblemi ridondante con
|
||
|
numero di nodi esponenziale in n. Per questo si adotta l'algoritmo iterativo che si
|
||
|
avvale del memoizing (specialmente nel caso in cui si voglia salvare anche la
|
||
|
sottosequenza e non solo la sua lunghezza).
|
||
|
|
||
|
## Scheduling (variante di scheduling di intervalli)
|
||
|
|
||
|
Si consideri il problema in cui le richieste in uso di una risorsa sono specificate da una
|
||
|
durata e debbano essere soddisfatte entro un particolare intervallo di tempo.
|
||
|
|
||
|
* Dinamica (pr-dinamica-zaino): In questo problema si mostra come l'insieme ovvio dei
|
||
|
sottoproblemi alle volte non sia sufficiente e si debba creare una collezione di
|
||
|
sottoproblemi piu' *ricca*.
|
||
|
* **Sottoproblemi**: Come nel caso precedente, due casi:
|
||
|
1. Se la richiesta non appartiene alla soluzione ottima, `OPT(n) = OPT(n-1)`
|
||
|
2. Se la richiesta appartiene alla soluzione ottima, vogliamo trovare il miglior valore
|
||
|
tra tutte le soluzioni che la contengono come ultima richiesta.
|
||
|
Sono necessari piu' sottoproblemi che negli intervalli pesati semplici,
|
||
|
dato che si vuole conoscere la miglior soluzione nelle `n-1` richieste con *tempo*
|
||
|
`W - w[n]`
|
||
|
Si risolve un sottoproblema per ogni insieme di richieste e ogni possibile valore di
|
||
|
tempo restante.
|
||
|
```
|
||
|
OPT(i,w) = max_S{ sum in j of w[j] for j in S}
|
||
|
|
||
|
quindi:
|
||
|
|
||
|
if (w < w[i]) {
|
||
|
OPT(i,w) = OPT(i-1,w)
|
||
|
else
|
||
|
OPT(i,w) = max{ OPT(i-1, w), w[i] + OPT(i-1, w-w[i]) }
|
||
|
```
|
||
|
In questo caso e' necessario confrontare i valori ottenuti includendo ed escludendo il
|
||
|
nodo `i`. L'algoritmo e' chiamato **Subset-Sum** e fa uso di memoizing per i tempi.
|
||
|
* **Complessita'**: Subset-sum ha complessita' **O(nW)** per calcolare il valore ottimo, ma
|
||
|
data una tabella di valori ottimi dei sottoproblemi, questo si riduce a **O(n)**.
|
||
|
|
||
|
## Zaino 0/1
|
||
|
|
||
|
Knapsack problem classico con uno zaino di capacita' `C` e `n` oggetti `O[1..n]`. Dato ogni
|
||
|
oggetto con un peso e un valore, si deve selezionare un sottoinsieme degli oggetti da
|
||
|
inserire nello zaino per **massimizzare** il valore del contenuto. Molto simile a scheduling.
|
||
|
|
||
|
* Dinamica (pr-dinamica-zaino): Si suppone un vettore `p` di pesi e un vettore `v` di valori,
|
||
|
con i quali si considerano i seguenti sottoproblemi.
|
||
|
* **Sottoproblemi**: Se l'i-esimo oggetto non sta nello zaino, allora:
|
||
|
```
|
||
|
Val(i,k) = Val(i-1,k) se i,k != 0 and k < p[i] // k e' la capacita' attuale
|
||
|
```
|
||
|
Se invece entra nello zaino:
|
||
|
```
|
||
|
Val(i,k) = max{ Val(i-1,k), Val(i-1, k-p[i]) + v[i] } se i,k != 0 and k >= p[i]
|
||
|
```
|
||
|
Anche qui, come nello scheduling, e' necessario confrontare i valori ottenuti includendo
|
||
|
ed escludendo l'oggetto considerato. Questo e' dovuto al fatto che anche se un oggetto
|
||
|
entra nello zaino, non e' per forza la soluzione ottima assoluta del problema.
|
||
|
* **Memoizing**: in questo caso si utilizza una matrice di indici/capacita' per salvare i
|
||
|
valori corrispondenti.
|
||
|
* **Correttezza**: sappiamo che la definizione data e' corretta perche' applica una
|
||
|
proprieta' essenziale, ovvero che se uno zaino di dimensione `k` e' riempito nel modo
|
||
|
ottimo con l'oggetto `i`, allora lo zaino di dimensione `k-p[i]` e' riempito in modo
|
||
|
ottimo con gli oggetti `1..(i-1)`.
|
||
|
* **Complessita'**: **O(nC)**
|
||
|
|
||
|
* Backtracking (3.Zaino-0-1...): Si utilizza un vettore soluzione e un adiacente vettore
|
||
|
`Sol` per salvare la soluzione migliore attualmente trovata.
|
||
|
* **Bounding**: Si ipotizza un vettore peso e un vettore valore, entrambi **ordinati in
|
||
|
modo non crescente**. La funzione di bound determina un upper bound sulla migliore
|
||
|
soluzione ottenibilie in base ai vincoli:
|
||
|
1. **inserire** un oggetto nello zaino significa **potare i sottoalberi sinistri**:
|
||
|
si richiede che il peso degli oggetti nello zaino sommato a quello dell'oggetto
|
||
|
considerato non superi la capacita'.
|
||
|
2. **scartare** un oggetto dallo zaino significa **potare i sottoalberi destri**:
|
||
|
si cerca un limite superiore al valore raggiungibile senza considerare l'oggetto
|
||
|
stesso.
|
||
|
Si nota come questo algoritmo sia una ricerca in profondita' con vincoli aggiunti.
|
||
|
|
||
|
* Branch and bound (4.BB.2.1.pdf): Utilizzato per il confronto con l'algoritmo di
|
||
|
backtracking (sopra). Si mantiene l'ipotesi dei vettori ordinati in maniera **non
|
||
|
crescente**. Si deve pero' impostare il problema come un problema di minimo.
|
||
|
* Funzione obbiettivo da minimizzare:
|
||
|
```
|
||
|
(minus) sum for i in [1,n] of: V[i]*S[i] = C(X)
|
||
|
// somma negativa dei valori corrispondenti agli elementi della soluzione.
|
||
|
// si utilizza come funzione di costo per tutti i nodi foglia
|
||
|
|
||
|
Con vincolo:
|
||
|
|
||
|
sum for i in [1,n] of: P[i]*S[i] <= C
|
||
|
// la somma dei pesi degli elementi della soluzione non deve eccedere la capacita'
|
||
|
|
||
|
```
|
||
|
* Quindi e' possibile definire il costo come la funzione obbiettivo nel caso in cui la
|
||
|
soluzione sia accettabile (somma dei pesi minore di capacita'), mentre il costo e' infinito
|
||
|
se il nodo non appartiene a una soluzione accettabile.
|
||
|
Funzione di costo per i nodi interni:
|
||
|
```
|
||
|
C(X) = min{ C(left(X)), C(right(X)) }
|
||
|
```
|
||
|
* Questo e' il lower bound di C(X) per ogni nodo X, per cui puo' essere usata come
|
||
|
funzione di lower bound `C[S](X)`
|
||
|
* L'algoritmo utilizza la tecnica greedy per migliorare il valore attuale della soluzione
|
||
|
provvisoria. (sceglie l'elemento migliore dal punto di vista del valore).
|
||
|
* La funzione di upper bound puo' essere trovata considerando che se si riempie lo zaino
|
||
|
alla cieca si ottiene il risultato peggiore. L'alternativa semplice e' utilizzare la
|
||
|
funzione obbiettivo su ogni nodo a livello `j`.
|
||
|
* **Confronto con backtracking**
|
||
|
|
||
|
|
||
|
## Zaino con Ripetizione
|
||
|
|
||
|
Estensione dello zaino 0/1 in cui sono permesse ripetizioni di elementi. Il problema si
|
||
|
semplifica in quanto non serve l'informazione sugli oggetti usati. Aumenta pero' il numero di
|
||
|
scelte (da due -> scheduling a multiple)
|
||
|
|
||
|
* Dinamica (pr-dinamica-zaino): Si utilizzano sottoproblemi dipendenti unicamente dalla
|
||
|
capacita' dello zaino.
|
||
|
* **Sottoproblemi**: A differenza dello zaino 0/1, ora non consideriamo piu' il caso
|
||
|
`k < p[i]` perche' abbiamo potenzialmente un numero infinito di oggetti.
|
||
|
```
|
||
|
Val(k) = max{ Val(k-1), max{ Val(k-p[i]) + v[i] | p[i] <= k } } se k != 0
|
||
|
```
|
||
|
Per ogni oggetto, dobbiamo contollare il massimo valore ottenibile con gli oggetti a
|
||
|
disposizione, e poi controllare di aver migliorato la soluzione precendente, ossia quella
|
||
|
in cui la capacita' era ridotta di 1. Si nota come l'algoritmo iteri sulle capacita' di
|
||
|
modo da calcolare i casi migliori con le capacita' minori e procedere con memoizing.
|
||
|
* **Complessita'**: **O(nC)**
|
||
|
|
||
|
* Greedy (pr-dinamica-zaino), oggetti **continui**: in questo caso riempiamo lo zaino
|
||
|
partendo dagli elementi piu' preziosi. Solo algoritmo.
|
||
|
|
||
|
## Moltiplicazione di matrici
|
||
|
|
||
|
Date `n` matrici, determinare una **parentesizzazione** del prodotto A1\*A2\*..An che
|
||
|
minimizzi il numero di moltiplicazioni scalari.
|
||
|
|
||
|
* Dinamica (Cammini minimi e prodotto di matrici): Si comincia considerando quante e quali
|
||
|
sono le possibili parentesizzazioni di `n` matrici. La funzione `P(n)` indica il **numero
|
||
|
di parentesizzazioni** per `A1*A2*..An`. Si nota che l'ultima moltiplicazione puo'
|
||
|
occorrere per `n-1` posizioni diverse. Fissiamo quindi l'indice `k` dell'ultima
|
||
|
moltiplicazione, per considerare `P(k)` parentesizzazioni fino ad `Ak` e `P(n-k)`
|
||
|
parentesizzazioni per `A(k+1)..An`. Si ottiene:
|
||
|
```
|
||
|
P(n) = sum for k from 1 to n-1 of: P(k)*P(n-k) // definizione di numeri catalani
|
||
|
```
|
||
|
Possiamo quindi dire che **P(n) ha lower bound Ω(2^n)**.
|
||
|
L'approccio brute-force risulta quindi troppo costoso. Si cerca di ricavare dei
|
||
|
sottoproblemi per applicare la pr. din.
|
||
|
* **Sottoproblemi**: Se si considera una **sottostruttura (parentesizzazione) ottima** del
|
||
|
prodotto tra `Ai*..*Aj`, allora `A[i..k]` e `A[k+1..j]` sono parentesizzazioni ottime dei
|
||
|
rispettivi prodotti (`Ai*..*Ak` e `A(k+1)*..*Aj`). Questo puo' essere dimostrato per
|
||
|
assurdo. Si puo' quindi definire il numero minimo di prodotti scalari:
|
||
|
```
|
||
|
m(i,j) = m(i,k) + m(k+1,j) + c[i-1]*c[k]*c[j] // dove c.. sono il numero di prodotti
|
||
|
per moltiplicare le rispettive
|
||
|
matrici.
|
||
|
```
|
||
|
Dato che il valore di `k` non e' noto a prescindere ma si sa che e' compreso tra `i,j-1`,
|
||
|
allora li si prova tutti:
|
||
|
```
|
||
|
m(i,j) = min{ m(i,k) + m(k+1,j) + c[i-1]*c[k]*c[j] } per k in [i,j]
|
||
|
```
|
||
|
* **Complessita'**: se si utilizza l'approccio top-down, si ha complessita' **Ω(2^n)**.
|
||
|
Dato che l'albero delle soluzioni calcolato a brute-force risulta in molte computazioni
|
||
|
ridondanti, si applica la pr. dinamica. Questo permette di ridurre il numero di problemi
|
||
|
calcolati secondo la formula:
|
||
|
```
|
||
|
binomial(n, 2) + n = O(n^2) // binomial.. sono i sottoproblemi con i != j
|
||
|
n sono i sottoproblemi con i == j
|
||
|
```
|
||
|
L'algoritmo risultante (*Matrix-chain-order*) ha complessita' **O(n^3)**.
|
||
|
* **Memoizing**: Il calcolo bottom-up richiede una matrice nxn per ricordare i valori dei
|
||
|
sottoproblemi risolti. Il prodotto viene effettuato sempre tra una matrice di indice
|
||
|
inferiore e una di indice superiore, pertanto serve unicamente il triangolo superiore
|
||
|
della matrice.
|
||
|
* **Memorizzare la sequenza di moltiplicazioni**: si utilizza un algoritmo ricorsivo che
|
||
|
utilizza la matrice soluzione per memorizzare la posizione `k` della parentesizzazione.
|
||
|
|
||
|
## Distanza di matching (edit)
|
||
|
|
||
|
Misurare la **distanza** tra due stringhe. Uno dei modi piu' semplici e' *allinearle* e
|
||
|
controllare lettera per lettera se un carattere e' match oppure no. Il valore associato all'
|
||
|
allineamento (costo) e' **il numero di operazioni necessarie per allineare le stringhe**.
|
||
|
|
||
|
* Dinamica (distanza di edit): la distanza di edit si presenta quindi come **il costo del
|
||
|
miglior allineamento**. Essendo che controllare tutti i possibili allineamenti e' un
|
||
|
problema combinatorio (esponenziale in tempo), possiamo definire dei sottoproblemi per la
|
||
|
pr.din.
|
||
|
* **Sottoproblemi**: date due stringhe, si considerino due caratteri `a[i],b[j]`.
|
||
|
Per allinearle ci sono 3 opzioni (nell'allineamento **migliore**):
|
||
|
1. l'ultima lettera della stringa `a`, ovvero `a[n]`, e' successiva all'ultima lettera
|
||
|
della stringa `b`, ovvero `b[m]`. Costo: `C[a[1..n-1]] + C[b[1..m]] + delta`.
|
||
|
2. Il contrario di `1`, ossia `b[m] successiva ad a[n]`.
|
||
|
Costo: `C[a[1..n]] + C[b[1..m-1]] + delta`
|
||
|
3. `a[n],b[m]` coincidono. Costo: `C[a[1..n-1]] + C[b[1..m-1]] + alpha[n,m]` dove alpha
|
||
|
e' `0` se `a[n] == b[m]`
|
||
|
Questo porta alla seguente definizione di equazione alle ricorrenze:
|
||
|
```
|
||
|
E(i,j) = min{ delta + E(i-1,j), delta + E(i,j-1), alpha[i,j] + E(i-1,j-1) }
|
||
|
|
||
|
dove alpha[i,j] = 0 se a[i] == b[j], altrimenti != 0 se sono diverse.
|
||
|
|
||
|
Nota: E(i,j) e' il costo della distanza di edit tra a[1..i] e b[1..j]
|
||
|
```
|
||
|
* **Memoizing**: si utilizza una matrice (n+1)x(m+1) per memorizzare la distanza di edit
|
||
|
ottimale per ogni sottostringa `a[1..i],b[1..j]`. La risultante tabella puo' essere
|
||
|
ripercorsa all'indietro per ottenere un risultato ottimale. *(slides)*
|
||
|
* **Complessita'**: L'algoritmo iterativo ha complessita' **O(nm)**.
|
||
|
* **String matching approssimato**: se si considera il problema di trovare un'occorrenza di
|
||
|
una data stringa in un testo, ammettendo i seguenti errori:
|
||
|
1. Caratteri corrispondenti nella stringa e nel testo possono essere diversi.
|
||
|
2. Uno/piu' caratteri della stringa non compaiono nel testo.
|
||
|
3. Uno/piu' caratteri del testo non compaiono nella stringa.
|
||
|
Definiamo quindi un occorrenza **n-approssimata** quella in cui abbiamo un match preciso
|
||
|
a `n` caratteri diversi. (2-approssimato -> 2 caratteri diversi, etc).
|
||
|
L'algoritmo risultante e' simile alla distanza di edit, con la stessa complessita' nel
|
||
|
tempo.
|
||
|
|
||
|
## String Matching (KMP)
|
||
|
|
||
|
Data una stringa e un testo, si vogliono trovare tutte le occorrenze **esatte** della stringa
|
||
|
nel testo. A differenza della distanza di edit / matching approssimato, qui non si
|
||
|
considerano errori nel matching.
|
||
|
|
||
|
* Backtracking (KnuthMorrisPratt): il metodo piu' *naif* consiste nell'allineare i caratteri
|
||
|
di sinistra della stringa e del testo finche non si trova un'occorrenza della stringa / si
|
||
|
trovano due caratteri diversi (che negano l'occorrenza). Si ricomincia quindi con la
|
||
|
stringa fatta avanzare verso destra di un carattere.
|
||
|
* **Problema**: numero di confronti molto elevato nel caso di match parziali. **O(nm)**.
|
||
|
Questo puo' essere ridotto a **O(n+m)** spostando la stringa di piu' di un carattere in
|
||
|
presenza di un errore oppure riducendo i confronti saltando alcune parti dopo lo shift.
|
||
|
* Si nota che **solo il pattern aiuta a calcolare l'indice da cui ricominciare il
|
||
|
confronto mentre sul testo non e' necessario backtracking**.
|
||
|
* Se si tiene da conto il **numero di posizioni** da attraversare nel caso si incontri un
|
||
|
errore, possiamo utilizzare quest'informazione per ridurre il numero di iterazioni
|
||
|
necessarie. Questo significa che al momento dell'errore non si ricomincia con la stringa
|
||
|
da capo ma si torna all'ultimo carattere che potrebbe dare un match (l'ultimo uguale).
|
||
|
* Si tratta di un **automa che effettua transizioni all'indietro** per tutti i simboli che
|
||
|
non permettono un match del pattern, fino all'ultimo carattere uguale che potrebbe
|
||
|
permettere un match.
|
||
|
|
||
|
## Commesso Viaggiatore
|
||
|
|
||
|
Il commesso viaggiatore e' un problema che si presenta in questo modo:
|
||
|
Data una lista di citta' e distanze tra ogni coppia di citta', qual'e' la **strada piu' corta
|
||
|
possibile** per visitare tutte le citta' e tornare alla citta' di partenza?
|
||
|
Si identifica quindi come il problema di trovare **cicli di costo minimo** a partire da un
|
||
|
dato vertice.
|
||
|
|
||
|
Formulazione grafi: Dato un grafo non orientato, **completo**, e pesato, trovare un ciclo
|
||
|
semplice che contenga tutti i vertici del grafo e sia di costo minimo.
|
||
|
|
||
|
* Dinamica (CommViaggDin): si parte considerando `g(i,S)`, ossia una funzione che ritorna il
|
||
|
costo di un cammino minimo dal vertice `i` al vertice `1` che attraversa una sola volta i
|
||
|
vertici in `S`. Pertanto, il ciclo minimo sara' `g(1, V-{1})`, ovvero il cammino minimo
|
||
|
che torni al vertice di partenza (da 1 a 1) passando una sola volta da tutti i vertici a
|
||
|
parte 1.
|
||
|
* **Sottoproblemi**: per il principio di ottimalita', possiamo definire per ogni `i` non
|
||
|
appartenente al set `S` (a cui viene poi aggiunto all'iterazione successiva):
|
||
|
```
|
||
|
g(i,S) = min{ d[i,j] + g(j,S-{j}) } con S non vuoto e j in S.
|
||
|
|
||
|
Si nota che g(i, S=vuoto) = d[i,1] // la distanza tra i e 1 se non vi sono vertici
|
||
|
in mezzo.
|
||
|
```
|
||
|
La soluzione finale sara' quindi:
|
||
|
```
|
||
|
g(1,V-{1}) = min{ d[1,k] + g(k,V-{1,k}) } con k in [2..n]
|
||
|
```
|
||
|
* **Memoizing**: si utilizza una tabella per memorizzare le lunghezze minime di un cammino
|
||
|
semplice da `i` a `1` in `S` (su slides *gtab*).
|
||
|
* **Complessita'**: l'algoritmo in programmazione dinamica si basa sul calcolo di `g(i,S)`,
|
||
|
il che richiede un gran numero di confronti: `O(n^2*2^n)`. Si nota come questa
|
||
|
complessita' sia in realta' vantaggiosa rispetto alle implementazioni in branch&bound /
|
||
|
backtracking, che permettono di ottenere un caso peggiore `O(n!)`.
|
||
|
La complessita' nello spazio delle strutture dati e' critica nel caso della prog.din:
|
||
|
`O(n*2^n)`.
|
||
|
|
||
|
* Branch and Bound (4.BB.2.1): Si comincia definendo **il costo dei nodi**. La funzione
|
||
|
`C(X)` avra' quindi due differenti significati:
|
||
|
* Se X e' **foglia**, allora C(X) e' il costo del cammino ciclico individuato (soluzione)
|
||
|
* Se X e' **interno**, allora C(X) e' il costo della foglia di costo minimo nel
|
||
|
sottoalbero di X
|
||
|
Si cercano quindi due funzioni per cui `C[s](X) <= C(X) <= u(X)`.
|
||
|
* `C[s](X)` puo' essere definita considerando che `C(X)` per la soluzione parziale dalla
|
||
|
radice al vertice X e' dato almeno da:
|
||
|
```
|
||
|
C(X) = sum for j in [1..(i-1)] of: d(S[j], S[j+1])
|
||
|
```
|
||
|
Sappiamo che ogni cammino ciclico che inizia con S[1..i] non puo' avere peso minore di:
|
||
|
```
|
||
|
C[s](X) = C(X) + floor( A + B + sum for h not in S of: D[h])/2 // se X non foglia
|
||
|
|
||
|
C[s](X) = C(X) + d[S(i), S(1)] // se X e' una foglia
|
||
|
|
||
|
- dove:
|
||
|
A = min{ d[S[i], h] } con h not in S
|
||
|
// l'arco di lunghezza minima da un nodo di S[1..i] a h
|
||
|
|
||
|
B = min{ d[S[1], h] } con h not in S
|
||
|
// l'arco di lunghezza minima da un vertice h non in S al nodo di origine
|
||
|
|
||
|
D[h] = min{ d[h,p] + d[h,q] } con h != p != q, h not in S ma p,q possono appartenere
|
||
|
// ogni vertice in h not is S dovra' essere attraversato, quindi estremo di due archi
|
||
|
(h,p) e (h,q)
|
||
|
|
||
|
A + B + somma per h non in s di: D[h] = peso di 2+2k = 2(k+1) archi
|
||
|
// k e' il numero di vertici non in S
|
||
|
|
||
|
- A questi archi ne vanno aggiunti ancora k+1 per chiudere il ciclo, ma il loro peso non
|
||
|
e' inferiore alla meta' di quello dei 2(k+1) archi
|
||
|
|
||
|
Quindi C[s](X) e' definita come sopra in quanto valore minimo per C(X)
|
||
|
```
|
||
|
Se si utilizza una variabile globale per salvare il costo della soluzione minima, si
|
||
|
ottiene l'algoritmo.
|
||
|
|
||
|
|
||
|
## Permutazioni
|
||
|
|
||
|
Generare tutte le possibili permutazioni di un dato valore in input, in cui i numeri dispari
|
||
|
precedono i numeri pari. Esempio utilizzato per illustrare il principio di backtracking con
|
||
|
vincoli.
|
||
|
|
||
|
* Backtracking (2.1.Backtracking): L'algoritmo e' ottenuto verificando che nella prima meta'
|
||
|
del vettore soluzioni devono comparire solo numeri dispari e nella seconda solo numeri
|
||
|
pari. Questo e' il vincolo esplicito che ci permette di ridurre lo spazio delle soluzioni.
|
||
|
* **Complessita'**: Senza vincoli, **O(n!)**, con vincolo pari/dispai invece proporzionale
|
||
|
al numero di permutazioni da generare.
|
||
|
|
||
|
## Sottoinsiemi
|
||
|
|
||
|
Dati due interi `n,c`, generare tutti i sottinsiemi di `{1..n}` di cardinalita' `c`.
|
||
|
|
||
|
* Backtracking (2.1.Backtracking): La ricerca della soluzione avviene esaminando tutti i
|
||
|
sottinsiemi di `{1..n}` e verificando quali hanno cardinalita' `c`. Applicazione diretta
|
||
|
della ricerca esaustiva.
|
||
|
* **Complessita'**: Il tempo di esecuzione e' **O(n2^n)**, dato che la generazione dello
|
||
|
spazio degli stati richiede tempo esponenziale in `2^n` e il calcolo del numero di
|
||
|
elementi al piu' `n`. Si nota come gli insiemi da generare sono **binomial(n,c)** e
|
||
|
quindi **O(n^c)**, per cui quanto piu' `c` e' piccolo rispetto a `n` tanto piu'
|
||
|
l'algoritmo e' inefficiente. Sono necessarie funzioni di bounding:
|
||
|
* **Bounding**: Per potare l'albero delle scelte, si eliminano i seguenti sottoalberi:
|
||
|
1. Quelli in cui sono stati inseriti piu' di `c` elementi
|
||
|
2. Quelli in cui il numero degli elementi della soluzione parziale piu' il numero di
|
||
|
elementi non considerati e' **minore di c**.
|
||
|
Le funzioni di bounding decideranno quindi:
|
||
|
1. Se escludere l'elemento i-esimo non preclude la possibilita' di ottenere una
|
||
|
soluzione, allora si visita il sottoalbero destro
|
||
|
2. Se aggiungere l'elemento i-esimo non genera una soluzione parziale con piu' di `c`
|
||
|
elementi, allora si visita il sottoalbero sinisto.
|
||
|
Questo significa che **ogni nodo viene visitato solo se nel suo sottoalbero e' presente
|
||
|
almeno una soluzione**. Dal teorema sopra citato, si ottiene costo in tempo:
|
||
|
```
|
||
|
O(n*n^c) = O(n^(c+1))
|
||
|
```
|
||
|
|
||
|
## Cicli Hamiltoniani
|
||
|
|
||
|
Dato un grafo non orientato, esiste un ciclo semplice che contiene tutti i nodi? Nota:
|
||
|
semplice => ogni nodo una volta sola.
|
||
|
|
||
|
* Backtracking (1.Backtracking-intro): Si considera un vettore di nodi i cui posti `!=0`
|
||
|
indicano che il nodo e' stato visitato, 0 altrimenti. L'algoritmo fa uso delle funzioni di
|
||
|
bounding per potare l'albero delle soluzioni da **simmetrie** (cicli in un senso o
|
||
|
nell'altro) ed escludere i percorsi che dividono il grafo di modo che un ciclo semplice non
|
||
|
sia piu' possibile.
|
||
|
|
||
|
## Somma di sottoinsieme
|
||
|
|
||
|
Dati un insieme e un intero, trovare tutti i sottoinsiemi la cui somma degli elementi sia
|
||
|
l'intero.
|
||
|
|
||
|
* Backtracking (2.2.Backtracking): I sottoinsiemi sono rappresentati da un vettore i cui
|
||
|
elementi possono assumere il valore 0 per l'elemento presente e 1 per il valore assente.
|
||
|
* **Bounding**: la funzione di bounding e' analoga a quella per i sottoinsiemi di
|
||
|
cardinalita' fissa:
|
||
|
```
|
||
|
B(X[1]..X[i]) = Sum for k in [1..i] of W[k]*X[k] +
|
||
|
Sum for k in [i+1..|W|] of W[k] >= M
|
||
|
|
||
|
Dove M e' l'intero considerato.
|
||
|
```
|
||
|
Questo assicura che non si considerino sottoalberi che **non possono portare a M**.
|
||
|
Inoltre se si considera vettori in ordine **non decrescente** si puo' utilizzare la
|
||
|
seguente funzione di bounding:
|
||
|
```
|
||
|
Sum for k in [1..i] of W[k]*X[k] + W[i+1] <= M
|
||
|
```
|
||
|
Questo assicura che non si considero sottoalberi che superino M.
|
||
|
|
||
|
## Colorabilita' dei grafi
|
||
|
|
||
|
Dato un grafo trovare tut i i modi in cui e' possibile colorare i vertici di esso in modo che
|
||
|
i vertici adiacenti abbiano colori diversi.
|
||
|
|
||
|
* Backtracking (2.2.Backtracking): L'algoritmo utilizza un vettore X per associare colori a
|
||
|
vertici. La procedura prova ad assegnare al nodo i-esimo ogni colore supponendo che siano
|
||
|
tutti compatibili, richiamandosi sul nodo successivo ricorsivamente. I controlli fanno si'
|
||
|
che le combinazioni che non portano a soluzione siano escluse in quanto non vanno a buon
|
||
|
fine. Si nota come in questo algoritmo non siano presenti procedure di reset ma check che
|
||
|
a priori escludono soluzioni (combinazioni di colori) non valide.
|
||
|
* **Complessita'**: La verifica di ogni scelta richiede controlli di complessita'
|
||
|
trascurabile, per cui se il numero di chiamate ricorsive di `Color(i)` e' `C(i)`:
|
||
|
```
|
||
|
C(1) = m*C(2) = m*m*C(3) = ... m^n --> O(m^n) // m = numero di colori, n = numero nodi
|
||
|
```
|
||
|
|
||
|
## N Regine
|
||
|
|
||
|
Data una scacchiera NxN, si posizionino su di essa N regine di modo che nessuna di esse sia sotto scacco. (una per riga, una per colonna, una per diagonale).
|
||
|
|
||
|
* Backtracking (2.2.Backtracking): in questo caso il vincolo **esplicito** e' che nessuna
|
||
|
regina sia sulla stessa riga, mentre il vincolo implicito e' che nessuna sia sotto scacco.
|
||
|
Per questo si ricorre sulle righe utilizzando come insieme delle scelte le colonne 1..N. Si
|
||
|
ricorre unicamente se la posizione `i, X[i]` e' sicura, di modo da generare solo le
|
||
|
disposizioni **ammesse** e stamparle.
|
||
|
* **Verifica**: La parte complessa dell'algoritmo qui risiede nella verifica di una
|
||
|
diagonale libera, in quanto le colonne libere sono facili da verificare. Si nota che la
|
||
|
**differenza** degli indici di una diagonale discendente e' costante, mentre la **somma**
|
||
|
degli indici di una diagonale discendente e' anch'essa costante. Questo permette di
|
||
|
verificare le colonne. Ci sono formulazioni alternative.
|
||
|
|
||
|
## Load Balancing
|
||
|
|
||
|
Dato un insieme finito di programmi, tali che ogni programma richiede un tempo intero di
|
||
|
esecuzione, e `m` processori, eseguire tutti i programmi sui processori *nel minor tempo
|
||
|
possibile.*
|
||
|
|
||
|
* Approssimata/Greedy (algoritmi-di-approssimazione-I): il problema richiede di **minimizzare
|
||
|
il massimo carico su ogni macchina (makespan)**. L'algoritmo piu' semplice considera i
|
||
|
lavori in ordine qualunque e si assegna un lavoro alla volta alla macchina piu' scarica in
|
||
|
quel momento. Non si arriva alla soluzione ottima (greedy), quindi si analizza il makespan.
|
||
|
* **rapporto limite**: Se chiamiamo `T` il makespan greedy, dobbiamo calcolare anche `T*`,
|
||
|
ovvero il *migliore makespan possibile*. Per trovarlo, si cerca un limite inferiore alla
|
||
|
soluzione ottima, imponendo che ogni macchina svolga almeno `1/m` del lavoro totale.
|
||
|
Questo limite, aggiunto alla condizione che `T* >= max[j] of t[j]` (un lavoro molto piu'
|
||
|
grande degli altri).
|
||
|
Sappiamo che prima dell'assegnamento di un lavoro, la macchina scelta ha il minor carico
|
||
|
tra tutte le macchine. Prima dell'assegnamento: `T[i] = T[i] - t[j]`
|
||
|
```
|
||
|
Si sommano tutti i carichi:
|
||
|
|
||
|
Sum for k in 1..n of T[k] >= m(T[i] - t[j])
|
||
|
|
||
|
Che significa:
|
||
|
|
||
|
T[i] - t[j] <= 1/m * (sum for k of T[k])
|
||
|
```
|
||
|
Questa sommatoria equivale al **carico totale dovuto alle attivita'
|
||
|
`Sum of T[k] = Sum of t[j]`
|
||
|
Pertanto, per la prima funzione di lower bound:
|
||
|
```
|
||
|
T[i] - t[j] <= 1/m * (sum for j of t[j]) <= T*
|
||
|
```
|
||
|
Considerando la parte restante del carico su `M[i]`, ovvero quello finale `t[j]`,
|
||
|
utilizziamo la seconda funzione di lower bound:
|
||
|
```
|
||
|
t[j] <= T*
|
||
|
```
|
||
|
Unendo le due disuguaglianze:
|
||
|
```
|
||
|
T[i] = (T[i] - t[j]) + t[j] <= 2T*
|
||
|
```
|
||
|
Che prova un **rapporto limite = 2** (2T*/T*). L'algoritmo si avvicina a questo valore
|
||
|
al crescere del numero di macchine. Questo puo' essere migliorato considerando i lavori
|
||
|
**ordinati in ordine decrescente di tempo**. Si ottiene un rapporto limite di 3/2. Questo
|
||
|
algoritmo ha complessita' **O(nlogn)** ed e' chiamato Largest Processing Time.
|
||
|
|
||
|
## Center Selection
|
||
|
|
||
|
Si vogliono selezionare `k` centri dove costruire grossi centri commerciali e un insieme di
|
||
|
`S` siti. Ogni individuo, residente in un sito, andra' in uno dei `k` centri (il piu'
|
||
|
vicino).
|
||
|
|
||
|
Bisogna bilanciare la collocazione dei centi di modo da non sfavorire nessun sito. I siti
|
||
|
sono collocati su di un piano e su qualsiasi punto di questo piano e' possibile collocare i
|
||
|
centri.
|
||
|
|
||
|
* Approssimata/Greedy (algoritmi-di-approssimazione-I): Definiamo `C` un **r-cover** se:
|
||
|
```
|
||
|
dist(s,C) <= r for every s in S
|
||
|
```
|
||
|
Il massimo valore di `r` per cui `C` e' un r-cover e' il raggio di copertura di C ed e'
|
||
|
indicato con r(C). Questo rappresenta il percorso piu' lungo che ciascuno deve fare per
|
||
|
giungere al centro commerciale piu' vicino.
|
||
|
|
||
|
Bisogna quindi **minimizzare r(C)**.
|
||
|
|
||
|
La soluzione greedy propone di selezionare ogni sito senza considerare dove andranno
|
||
|
collocati gli altri, cioe' per ogni centro ottimizzare la distanza tra tutti i siti.
|
||
|
Questo pero' risulta in assegnamenti poco efficaci. Si sfrutta allora la conoscenza di
|
||
|
`r(C*)` da una soluzione ottima `C*` per costruire una soluzione approssimata con:
|
||
|
```
|
||
|
r(C) <= 2*r(C*)
|
||
|
```
|
||
|
Nel caso in cui `r(C*)` non sia noto, si puo' procedere per tentativi effettuando una
|
||
|
ricerca binaria che peo' aumenta di un fattore logaritmico la complessita'. Questa
|
||
|
soluzione costruisce i centri commerciali nei siti stessi e non in punti qualunque del
|
||
|
piano. La complessita' e' O(n\*k).
|
||
|
|
||
|
## Set Cover
|
||
|
|
||
|
Dati un insieme finito U e una famiglia di sottoinsiemi F di U tale che ogni elemento di U
|
||
|
appartiene almeno ad un sottoinsieme di F.
|
||
|
|
||
|
Si vuole determinare una collezione di sottoinsiemi di F definita come **copertura** C se
|
||
|
l'unione di tutti gli insiemi in C restituisce U. Si vuole inoltre trovare una collezione C
|
||
|
di cardinalita' minima. Il costo di una soluzione ammissibile C e' |C|.
|
||
|
|
||
|
* Approssimazione/Greedy (algoritmi-di-approssimazione-I): Costruire la copertura un insieme
|
||
|
alla volta, scegliendo ad ogni passo l'insieme che sembra migliore, ovvero quello che
|
||
|
contiene il massimo numero di elementi non ancora coperti. Questa soluzione ha complessita'
|
||
|
`O(|U|*|F|*min{|U|,|F|}`.
|
||
|
* **Rapporto Limite**: Si distribuisce il costo unitario di ogni sottoinsieme della
|
||
|
soluzione C sugli elementi che esso aggiunge alla copertura. (Se un set aggiunge 6
|
||
|
elementi, il peso di ogni elemento sara' 1/6)
|
||
|
Si puo'quindi considerare:
|
||
|
```
|
||
|
|C| = Sum for j in U of p[j]
|
||
|
```
|
||
|
Da cui il costo dell'elemento `j` coperto da `S[i]` e':
|
||
|
```
|
||
|
p[j] = 1 / |S[i] - (S[1] U S[2] ... U S[i-1])|
|
||
|
```
|
||
|
La soluzione ottima comprendera' quindi tutti gli elementi di U almeno una volta.
|
||
|
```
|
||
|
|C| = Sum for j in U of p[j] <= Sum for S in C* of (Sum for j in S of p[j])
|
||
|
```
|
||
|
Essendo che uno stesso elemento in U puo' appartenere a piu' di un set in `C*`.
|
||
|
Se S e' un qualsiasi elemento di F, il peso introdotto dalla soluzione C dell'algoritmo
|
||
|
soddisfa:
|
||
|
```
|
||
|
Sum for j in S of p[j] <= H(|S|) // H e' la serie armonica arrestata ad |S|
|
||
|
```
|
||
|
*(vedi slides per catena disuguaglianze)*
|
||
|
Rapporto finale: `H(max{ |S|: S in F }) = H(S*)`. Questo tempo di approssimazione non e'
|
||
|
migliorabile in tempo polinomiale, a meno che P = NP.
|
||
|
|
||
|
* Approssimazione/Pricing (approssimati II): l'algoritmo greedy per i set **pesati** puo'
|
||
|
anche essere visto come algoritmo di pricing: il costo di ogni sottoinsieme e' un costo di
|
||
|
**condivisione**. Pertanto la definizione di greedy / pricing porta allo stesso problema in
|
||
|
questo caso.
|
||
|
|
||
|
## Copertura di vertici
|
||
|
|
||
|
Versione pesata. Trovare un insieme di vertici di un grafo che copra tutti gli archi del
|
||
|
grafo, con peso minimo.
|
||
|
|
||
|
Il peso puo' essere definito come:
|
||
|
```
|
||
|
W(S) = Sum for i in S of w[i]
|
||
|
```
|
||
|
|
||
|
* Approssimati/Pricing (approssimati II): Per ogni vertice selezionato, si coprono tutti gli
|
||
|
archi incidenti in quel vertice. Se carichiamo questi archi di un costo totale **maggiore**
|
||
|
del costo del vertice, allora la procedura diventa **unfair**, mentre il prezzo `p[e]` puo'
|
||
|
essere definito **fair** se il costo totale degli archi incidenti in `i` e' al massimo il
|
||
|
costo del vertice stesso.
|
||
|
```
|
||
|
Sum for e=(i,j) of p[e] <= w[i] // Se la somma e' == w[i], il nodo e' tight
|
||
|
```
|
||
|
Per ogni copertura di vertici S con prezzi positivi e fair, possiamo dire:
|
||
|
```
|
||
|
Sum for e in E of p[e] <= Sum for i in S of (Sum for e=(i,j) of p(e)) <= W(S)
|
||
|
```
|
||
|
L'algoritmo trova una copertura e attribuisce i costi, usandoli per guidare la selezione
|
||
|
dei vertici. Da questo punto di vista l'algoritmo puo' essere considerato greedy.
|
||
|
* **Rapporto limite**: L'insieme dell'algoritmo e' una copertura di vertici per cui:
|
||
|
```
|
||
|
Sum for e in E of p[e] <= W(S)
|
||
|
```
|
||
|
Questo vale anche per la copertura di costo minimo `S*`.
|
||
|
Se consideriamo che tutti i nodi in S sono **tight**, ovvero la sommatoria e' esattamente
|
||
|
`w[i]` per ogni nodo `i` in S e ogni arco puo' pagare al massimo due volte:
|
||
|
```
|
||
|
W(S) = Sum for i in S of (Sum for e=(i,j) of p[e]) <= 2 * (Sum for e in E of p[e])
|
||
|
```
|
||
|
Si ottiene che `W(S) <= 2 W(S*)`, per cui il rapporto limite dell'algoritmo pricing e' 2.
|
||
|
|
||
|
## Cammini disgiunti
|
||
|
|
||
|
Confronto greedy / pricing.
|
||
|
Dato un grafo orientato e un valore intero `c` detto capacita' si vuole considerare ciascuna
|
||
|
coppia di nodi come una richiesta di passaggio dal nodo s[i] al nodo t[i] come sorgente /
|
||
|
destinazione. Si vogliono trovare i cammini che permettono di soddisfare il maggior numero
|
||
|
possibile di richieste di passaggio.
|
||
|
|
||
|
Problemi: percorsi che condividono archi non sono ottimali, e troppi cammini che condividono
|
||
|
uno stesso arco portano a disfunzioni nella maggior parte delle applicazioni.
|
||
|
|
||
|
Si consideri il caso in cui `c==1`, ovvero ogni arco porta solo una richiesta e i cammini
|
||
|
**devono per forza essere disgiunti**.
|
||
|
|
||
|
* Approssimati/Greedy (algoritmi II): L'algoritmo greedy tende a selezionare cammini piu'
|
||
|
brevi rispetto a cammini piu' lunghi. Se il grafo e' connesso, allora l'algoritmo seleziona
|
||
|
almeno un cammino.
|
||
|
(*vedi slides*)
|