Si conclude oggi la definizione dei concetti di base per descrivere gli algoritmi. Rimaneva da definire un criterio formale per descrivere i sottoprogrammi, e regolarne il loro scambio con "l'esterno", cioè con i programmi che chiamano procedure dedicate a compiti specifici. Già gli algoritmi di ordinamento introdotti hanno quella caratteristica: prendendo in input il vettore da ordinare (e il numero dei suoi elementi) procedono a effettuare quel compito, modificando la memoria dove si trovano i dati di input trasformandoli in dati di output).
Si è convenuto di scrivere descrivere questa struttura così:
ORDINA (N, dati)
{.....
}
Qindi quando ci troviamo di fronte a una serie di dati che vogliamo ordinare, ci è sufficiente richiamare ORDINA perché questi dati risultino ordinati.
A ben guardare un esempio di sottoprocedura l'avevamo già vista negli algoritmi di ordinamento presentati: lo SCAMBIA.
Scambiare due elementi è un'operazione complessa che però si può scomporre in passi semplici. Descriviamo l'operazione:
SCAMBIA (a, b, dati)
dove a e b sono gli indici degli elementi da scambiare, e dati è il vettore che contiene gli elementi.
La procedura SCAMBIA è scomponibile in passi semplici in questo modo:
SCAMBIA (a, b, dati)
{
temporaneo = dati[a]
dati[a] = dati[b]
dati[b] = temporaneo
}
L'argomento principale di questa lezione è però una questione teorica. Molto teorica. Riguarda il fatto che possiamo riconoscere almeno due categorie di problemi nel mondo: ci sono problemi "facili" e problemi "difficili". I problemi "facili" sono quelli che ammettono algoritmi risolutivi efficienti e in classi di complessità al più polinomiali.
Le classi di complessità (semplificando molto) si possono sostanzialmente dividere secondo questa gerarchia di complessità crescente
C=1 (tempo costante, per esempio trovare il valore minimo di un vettore ordinato: è sempre il primo elemento! Il numero di operazioni quindi non dipende dalla dimensione dell'input)
C=Log N (Tempo logaritmico: per esempio la ricerca dicotomica -- vedere le lezioni precedenti)
C=N (tempo lineare: per esempio la scansione di un vettore alla ricerca di un elemento)
C=N Log N (tempo semilineare, o loglineare: per esempio il quicksort)
C=N^2 (per esempio il prodotto di matrici, o l'ordinamento poco efficiente che avevamo visto nelle prime lezioni)
Le complessità comprese in queste categorie si chiamano POLINOMIALI e sono caratteristiche di problemi che ammettono delle "buone" soluzioni, cioè delle soluzioni efficienti. Tutti i problemi che abbiamo visto finora hanno la caratteristica di ammettere algoritmi "veloci" che li risolvano. Possiamo perciò dire che il problema di ordinare un elenco di valori è un problema che rientra nella categoria dei problemi facili. Questa categoria è detta P (che sta per "problemi che ammettono un algoritmo risolutivo di complessità al più polinomiale").
Esistono quindi problemi di natura diversa, che non ammettono algoritmi risolutivi efficienti? Sì, esistono. E sono problemi alla prova dei fatti molto comuni, per esempio trovare il percorso migliore per fare un tour di varie città, o pesare un kg di mele, o aspettarci che un navigatore satellitare non ci faccia fare strade assurde. Questi possono apparire problemi facili, ma sono tali solo per via della quantità normalmene ristretta di informazioni che si devono gestire. (Infatti quando la dimensione diventa più complessa, come per i tragitti stradali, i risultati spesso sono tutt'altro che buoni). I problemi che non ammettono soluzioni "efficienti" appartengono alla cosiddetta categoria NP (Nondeterministicamente polinomiali).
Quali sono questi problemi? e quali sono le categorie di complessità che li caratterizzano?
Il problema con cui si è affrontta la questione è il seguente: data una funzione binaria che opera su n valori binari, esiste almeno una combinazione di questi valori per cui il risutlato di questa funzione sia 1? Cioè, la funzione è soddisfacibile per almeno una delle posibili combinazioni di input? Per rispondere a questa domanda è necessario provarle tutte, e come è noto dalle prime lezioni il numero di combinazioni di una numero binario di N elementi è 2^N. Lo stesso principio si applica alle password: solo una password può farci passare. Se qualcuno vuole entrare nella nostra posta senza conoscere la password dovrà provare tutte le possibili combinazioni di caratteri (fino a che una e una sola lo farà entrare). Se supponiamo che i caratteri utilizzabili siano lettere maiuscole, minuscole e numeri, per una password di 8 caratteri, il numero di tentativi è 218 miliardi (= 62^8). Mettendo anche di poterne fare 1000 al secondo, per esaurire tutte le combinazioni ci vorrebbero oltre 6923 anni! L'affidabilità del sistema a password si basa sulla natura "difficile" del compito.
Altri problemi apparentemente facili:
- La cricca (CLIQUE): dato un insieme di persone, per cui ogni persona è amico di qualcuno ma non di altri, trovare la cricca di questo gruppo, cioè il sottoinsieme di persone di dimensioni massime per cui tutti i componenti si conoscono tra loro. Determinare quale sia questa cricca è un problema DIFFICILE.
- Il dramma del fruttivendolo (SUBSET-SUM): data una cassetta di mele, trovare la particolare combinazione di mele che pesa esattamente (per esempio) un kg. È necessario provare TUTTE le combinazioni, per scoprire se ne esiste una. Determinare l'esistenza di queta combinazione di mele è un problema DIFFICILE.
- Il ciclo hamiltoniano (HAM-CYCLE): dato un insieme di città e di strade che le collegano, trovare un percorso che parte da una certa città, tocca tutte le altre solo una volta, e torna "a casa". Determinare l'esistenza di questo percorso è un problema DIFFICILE.
- Il dramma del navigatore satellitare (o "del commeso viaggiatore", Travelling-Salesman Problem, TSP): dato un insieme di città e di strade che le collegano, e sapendo la lunghezza di ogni strada (o il costo del biglietto aereo, o della benzina...) trovare il percorso che da una certa città permetta di visitare tutte le altre una volta sola percorrendo la minor strada possibile. Questo è un problema DIFFICILE. (in particolare il numero di casi da esaminare è N fattoriale, N! = N*(N-1)*(N-2)... *3*2*1 che è certamente > 2^N)
Questa è la ragione di fondo per cui i computer NON POSSONO fare cose incredibili, impossibili, inimmaginabili: possono solo fare in modo molto efficinte quel che NOI gli diciamo di fare. Ma se noi non possediamo l'intelligenza sufficiente (o il mondo non offre appigli nella sua struttura) siamo perduti: i computer non possono risolvere nulla che non sia già stato affrontato da noi in modo efficiente. I navigatori satellitari che devono esaminare centinaia di combinazioni di strade e stradine per decidere qual è la migliore sono condannati al fallimento perenne, le compagnie di trasporti aerei, di spedizione, lo smistamento di telefonate, persino le coincidenze dei treni, affrontano tutti problemi minati da questa difficoltà insuperabile di fondo. Ovviamente esistono sistemi per ottenere soluzioni "abbastanza buone", ma LA soluzione migliore è al di là della potenza di qualsiasi computer (o meglio, richiederebbe talmente tanti millenni da essere di fatto inutile). Questo genere di problemi si applica anche a giochi, per esempio il master-mind, il nonogramma, il sudoku, il tetris, il campo minato (o campo fiorito) di Windows, e la creazione di parole crociate (perdonate i refusi e qualche definizione un po' forzata):
La prossima volta che ve la prendete col vostro stupido navigatore satellitare, o che trovate scandaloso un disservizio di una rete telefonica, che il sito delle ferrovie vi dà coincidenze assurde, riflettete su questo fatto: è con gli inrinseci limiti dell'intelligenza umana (e della complessità del mondo) che vi state scontrando.