Algoritmi/Tipologie di problemi
- problemi di ricerca: ricerca di una soluzione valida (= che soddisfa certi criteri), se esistente (ho o non ho trovato la soluzione?):
- es.: ciclo hamiltoniano: dato un grafo non orientato, esiste un cammino semplice[1] chiuso (ciclico) che collega tutti i vertici?
- problemi di ottimizzazione: ricerca della soluzione ottima (= che minimizzi un determinato costo o massimizzi un determinato vantaggio):
- es.: cammini minimi: data una rete di città modellata come un grafo, a ogni arco è associata una funzione distanza (peso). Si cerca di minimizzare la funzione distanza. Se il cammino minimo non esiste, è come se ci fosse un cammino di peso infinito;
- problemi ibridi (ricerca + ottimizzazione): ricerca della soluzione valida minima. Non si conoscono tuttora algoritmi polinomiali per risolvere questo tipo di problemi → algoritmi euristici:
- es.: commesso viaggiatore: ricerca del cammino semplice chiuso minimo.
S spazio delle soluzioni (= insieme di tutte le possibilità) → V soluzioni valide → M soluzioni migliori
- problemi di ricerca: S ⊂ V → verificare che V ≠ ∅ e trovare una soluzione valida qualsiasi;
- problemi di ottimizzazione: S = V (tutte le soluzioni sono valide) → trovare la soluzione ottima;
- problemi ibridi: S ⊂ V → trovare la soluzione di minimo costo/massimo vantaggio all'interno di S.
Trovare la soluzione ottima significa esaminare tutto lo spazio delle soluzioni → non è sempre possibile:
- soluzione ottima globalmente: massimo assoluto della funzione di ottimizzazione nell'intero dominio;
- soluzione ottima localmente: massimo relativo della funzione di ottimizzazione in un sottoinsieme del dominio → meno costo.
Soluzione ottima globalmente
[modifica | modifica sorgente]Il problema dello zaino discreto (approccio divide et impera di tipo ricorsivo)
[modifica | modifica sorgente]Un ladro con uno zaino piccolo di volume deve rubare oggetti appartenenti a un insieme ; l'oggetto -esimo, di peso e valore , vale se viene preso oppure se lasciato. Il ladro deve minimizzare il volume occupato () e massimizzare il valore complessivo (), verificando ogni volta la capacità residua disponibile, cioè il volume ancora libero. Il problema si dice discreto perché ogni oggetto è preso o lasciato, non si può spezzare in più parti. Le soluzioni possono essere enumerate esaustivamente tramite un albero, in modo da poterle esplorare tutte ricorsivamente. Si arriva a una foglia quando non è più possibile andare avanti (non è detto che lo zaino è pieno). L'obiettivo è trovare la foglia per cui il valore degli oggetti è massimo, tenendo presente che ci possono essere eventualmente più foglie con lo stesso valore. Questo approccio ha un costo elevato, ma la soluzione è ottima globalmente.
Il paradigma greedy
[modifica | modifica sorgente]Si segue un solo cammino, scegliendo quella che appare di volta in volta la soluzione migliore, secondo una funzione di appetibilità (= che misura la convenienza locale) → non è detto che si trovi la soluzione ottima globalmente, però si riduce il costo. Nell'algoritmo greedy non c'è il backtrack, ossia non si ritorna mai sui passi precedenti. Il procedimento termina quando la configurazione non permette più l'esecuzione di alcuna scelta.
- Tipi di appetibilità
- appetibilità fisse: le appetibilità sono costanti → una volta ordinate in un vettore, a ogni passo si sceglie l'appetibilità massima;
- appetibilità modificabili: a ogni passo si ricalcolano le appetibilità → le code a priorità permettono di mantenere l'ordinamento.
Esempi
[modifica | modifica sorgente]Il cambiamonete
[modifica | modifica sorgente]Il problema consiste nel trovare il resto con numero minimo di monete. A ogni passo, si sceglie la moneta di valore più grande (cioè avente appetibilità massima) tra quelle compatibili sul resto, quindi si considera il resto residuo. Le appetibilità sono fisse, perché dipendono unicamente dal sistema di monetazione corrente → se la monetazione è particolare, la soluzione data dall'algoritmo greedy può però essere non ottima.
Il problema dello zaino discreto (approccio greedy)
[modifica | modifica sorgente]Si estrae un cammino dallo spazio di ricerca, cioè a ogni passo si segue un solo cammino. Questo approccio ha un costo inferiore, ma non è detto che si riesca a trovare la strada ottima globalmente.
Si cerca l'oggetto ad appetibilità massima (= massimo valore), con la limitazione che sia compatibile con la funzione volume.
Il problema dello zaino continuo
[modifica | modifica sorgente]è una variabile reale che misura quanta parte di un oggetto (es. polvere d'oro) è stata presa.
Per ogni oggetto non si considera il valore complessivo, ma si considera il valore specifico: → soluzione migliore.
Codici
[modifica | modifica sorgente]Come effettuare codifiche (da simbolo a parola di codice) e decodifiche (da parola di codice a simbolo)? Una serie di parole di codice si dice codice.
- simboli isofrequenti: la probabilità di trovare simboli è uniforme (c'è la stessa probabilità di trovare un simbolo piuttosto che un altro)
- simboli non isofrequenti: es. lingue (più vocaliche o più consonantiche)
Le parole di codice possono essere:
- a lunghezza fissa: dati simboli, sono possibili parole di codice costituite da bit. La decodifica di un codice è più facile, perché basta suddividerlo nella unità di lunghezza;
- a lunghezza variabile: ogni parola di codice ha una lunghezza variabile → la decodifica è difficoltosa, però si può usare per la compressione delle informazioni perché, assegnando meno bit ai simboli più frequenti, si può limitare il numero di bit necessari.
stringa di ordine k (k-prefisso) = stringa contenente i primi k bit
Nel caso a lunghezza variabile, è necessario costruire un codice (libero da) prefisso per evitare le ambiguità, in modo che nessuna parola di codice sia a sua volta prefisso di un'altra parola di codice.
Codici di Huffman
[modifica | modifica sorgente]I codici di Huffman sono buoni codici a lunghezza variabile. Come costruire un codice di Huffman tramite un albero binario:
- i simboli si ordinano per frequenza crescente in una coda a priorità;
- la coppia di simboli a frequenza meno elevata (costituita dal primo minimo e dal secondo minimo) si fonde in un aggregato;
- si associa a un simbolo il bit 0 e all'altro simbolo il bit 1;
- si assegna all'aggregato una frequenza pari alla somma delle loro frequenze;
- l'aggregato si reinserisce nella coda a priorità;
- si riparte al punto 1, considerando l'aggregato come un semplice simbolo ma con la nuova frequenza.
Condizione di terminazione: la coda a priorità è vuota (a ogni passo da due simboli si passa a un solo simbolo). È un algoritmo greedy perché ogni volta si scelgono le appetibilità massime (in questo caso: le frequenze minime).
Il costo è linearitmico perché si usano le operazioni sulle code a priorità:
Percorrendo questo albero binario, si possono effettuare le operazioni di codifica/decodifica.
Note
[modifica | modifica sorgente]- ↑ cammino semplice = che non passa mai due volte per lo stesso nodo (in questo caso vertice).