Uploaded by mario esposito (Mariolim)

MetodiFormaliInformatica

advertisement
UNIVERSITÀ DEGLI STUDI DI TORINO
Facoltà di Scienze Matematiche Fisiche e Naturali
Corso di Laurea Magistrale in Informatica
Riassunto di
Metodi Formali dell’Informatica
Enrico Mensa
Basato sulle lezioni di: Simona Ronchi Della Rocca
ii
Indice
1
Introduzione
1.1 Calcolabilità e complessità . . . . . . . . . . . . . . . . . . .
1.2 Da Babbage a Turing: la nascita della calcolabilità . . . . . .
1
1
2
2
Calcolabilità
2.1 La macchina di Turing . . . . . . . . . . . . . . . . . . . . .
2.1.1 Definizione della macchina di Turing . . . . . . . . . .
2.1.2 Un primo esempio . . . . . . . . . . . . . . . . . . . .
2.2 Le funzionalità della macchina di Turing . . . . . . . . . . . .
2.2.1 Dai linguaggi ai problemi . . . . . . . . . . . . . . . .
2.2.2 Calcolare con la macchina di Turing . . . . . . . . . .
2.3 Un nuovo concetto di cardinalità: insiemi numerabili . . . . .
2.3.1 Reminder: il concetto di funzione . . . . . . . . . . .
2.3.2 Definizione di insieme numerabile . . . . . . . . . . .
2.3.3 Esempio: l’insieme dei numeri pari è numerabile? . . .
2.3.4 Se lo posso numerare lo posso calcolare . . . . . . . .
2.3.5 La diagonalizzazione di Cantor . . . . . . . . . . . . .
2.3.6 La numerabilità effettiva . . . . . . . . . . . . . . . .
2.4 Codifica delle macchine di Turing . . . . . . . . . . . . . . . .
2.4.1 La numerazione di Göedel . . . . . . . . . . . . . . .
2.4.2 Dalla numerazione di Göedel alla codifica delle MdT .
2.4.3 Codificare le funzioni calcolabili . . . . . . . . . . . .
2.4.4 Perché codificare MdT e non funzioni? . . . . . . . .
2.5 Halt: una funzione non calcolabile . . . . . . . . . . . . . . .
2.6 Macchine di Turing a più nastri . . . . . . . . . . . . . . . . .
2.6.1 Definizione della MdT a più nastri . . . . . . . . . . .
2.6.2 Più nastri, stessa potenza . . . . . . . . . . . . . . .
2.7 Equivalenza fra algoritmi: non calcolabile . . . . . . . . . . .
2.7.1 Le funzioni totali calcolabili non sono effettivamente
numerabili . . . . . . . . . . . . . . . . . . . . . . . .
2.7.2 L’equivalenza con l’identità non è calcolabile . . . . .
2.7.3 Passo di riduzione . . . . . . . . . . . . . . . . . . . .
2.7.4 Il teorema Snm (cenni) . . . . . . . . . . . . . . . . .
7
7
7
9
10
11
12
13
14
15
16
17
17
19
22
23
25
27
27
28
31
31
32
32
iii
33
35
36
37
iv
INDICE
2.8
3
La macchina di Turing universale . . . . . . . . . . . . . . . .
2.8.1 La completezza delle macchine di Turing . . . . . . .
2.9 Interleaving su esecuzioni parallele . . . . . . . . . . . . . . .
2.9.1 La funzione STEP . . . . . . . . . . . . . . . . . . .
2.9.2 Macchine parallele . . . . . . . . . . . . . . . . . . .
2.10 La nozione di semidecidibilità . . . . . . . . . . . . . . . . . .
2.10.1 Definizioni equivalenti di semidecidibilità . . . . . . . .
2.10.2 Proprietà relative alla decidibilità e alla semidecidibilità
2.10.3 Il teorema di Post . . . . . . . . . . . . . . . . . . . .
2.10.4 Un esempio di funzione semidecidibile e non decidibile
2.10.5 Ricapitolando . . . . . . . . . . . . . . . . . . . . . .
2.11 Due teoremi fondamentali . . . . . . . . . . . . . . . . . . .
2.11.1 Il teorema di Kleene . . . . . . . . . . . . . . . . . .
2.11.2 Il teorema di Rice . . . . . . . . . . . . . . . . . . . .
2.12 Completezza funzionale e completezza algoritmica . . . . . .
2.13 Calcolabilità secondo Kleene . . . . . . . . . . . . . . . . . .
2.13.1 Le funzioni di base . . . . . . . . . . . . . . . . . . .
2.13.2 La composizione . . . . . . . . . . . . . . . . . . . .
2.13.3 La ricorsione . . . . . . . . . . . . . . . . . . . . . .
2.13.4 Le funzioni primitive ricorsive . . . . . . . . . . . . . .
2.13.5 Definire funzioni calcolabili totali secondo Kleene . . .
2.13.6 La minimalizzazione . . . . . . . . . . . . . . . . . .
2.13.7 Definire funzioni calcolabili alla Kleene . . . . . . . . .
2.14 L’equivalenza fra le MdT ed il modello di Kleene . . . . . . .
2.14.1 Il modello delle MdT è potente almeno quanto quello
di Kleene . . . . . . . . . . . . . . . . . . . . . . . .
2.14.2 Il modello di Kleene è potente almeno quanto quello
delle MdT . . . . . . . . . . . . . . . . . . . . . . . .
2.15 Il linguaggio WHILE (cenni) . . . . . . . . . . . . . . . . . .
2.15.1 La sintassi . . . . . . . . . . . . . . . . . . . . . . . .
2.15.2 I/O, variabili e vettore di stato . . . . . . . . . . . . .
2.15.3 Una semantica per la sintassi: SOS . . . . . . . . . .
2.15.4 Definizione di calcolabilità secondo il linguaggio WHILE
38
38
39
39
40
43
44
47
47
48
49
49
49
52
57
58
58
58
59
60
61
63
64
65
Complessità
3.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . .
3.1.1 Il modello di calcolo . . . . . . . . . . . . . . . . . . .
3.1.2 Complessità sui problemi . . . . . . . . . . . . . . . .
3.1.3 La tesi di Cook . . . . . . . . . . . . . . . . . . . . .
3.2 Misurare la complessità . . . . . . . . . . . . . . . . . . . . .
3.2.1 La funzione di complessità . . . . . . . . . . . . . . .
3.2.2 Esempi di funzioni di complessità . . . . . . . . . . .
3.2.3 La notazione o-grande . . . . . . . . . . . . . . . . .
75
75
76
76
76
77
77
78
78
65
66
68
69
70
71
74
INDICE
v
3.2.4 Le classi di complessità standard . . . . . . . . . . . . 82
3.2.5 Un esempio introduttivo: MCD . . . . . . . . . . . . 84
3.3 La complessità di problemi . . . . . . . . . . . . . . . . . . . 87
3.4 La classe di complessità polinomiale . . . . . . . . . . . . . . 88
3.4.1 Il problema 2COL . . . . . . . . . . . . . . . . . . . . 89
3.4.2 Il problema 2SAT . . . . . . . . . . . . . . . . . . . . 90
3.4.3 Riduzione polinomiale fra problemi . . . . . . . . . . . 95
3.4.4 Riduzione di 2COL a 2SAT . . . . . . . . . . . . . . . 96
3.4.5 Riduzione di 2SAT a 2COL . . . . . . . . . . . . . . . 99
3.4.6 Un problema interessante: il test di primalità . . . . . 104
3.4.7 La classe dei problemi co-P . . . . . . . . . . . . . . 106
3.4.8 Note finali in merito alla classe polinomiale . . . . . . 107
3.5 La classe di complessità non deterministica polinomiale . . . . 108
3.5.1 Non c’è il due senza il tre: da 2SAT a 3SAT . . . . . 108
3.5.2 Considerare il problema di verifica . . . . . . . . . . . 109
3.5.3 Prima definizione di N P . . . . . . . . . . . . . . . . 109
3.5.4 3SAT è N P secondo la prima definizione . . . . . . . 110
3.5.5 Seconda definizione di N P . . . . . . . . . . . . . . . 111
3.5.6 3SAT è N P secondo la seconda definizione . . . . . . 115
3.5.7 L’equivalenza fra le due definizioni . . . . . . . . . . . 115
3.5.8 P è incluso in N P . . . . . . . . . . . . . . . . . . . 117
3.5.9 Alcuni esempi di problemi N P . . . . . . . . . . . . . 118
3.6 L’annoso problema: P = N P? . . . . . . . . . . . . . . . . . 118
3.6.1 Conseguenze dirette dell’equivalenza fra P ed N P . . 119
3.6.2 Come si dimostra P = N P? . . . . . . . . . . . . . . 119
3.7 Classi N P-hard ed N P-complete . . . . . . . . . . . . . . . 119
3.7.1 La classe N P-complete e le conseguenze sul problema
P = N P . . . . . . . . . . . . . . . . . . . . . . . . 120
3.7.2 SAT è N P-complete: il teorema di Cook-Levin . . . . 121
3.7.3 3SAT è N P-complete . . . . . . . . . . . . . . . . . 126
3.7.4 3COL è N P-complete . . . . . . . . . . . . . . . . . 130
3.8 Classi N P-intermediate e co-N P . . . . . . . . . . . . . . . 139
3.8.1 Esistono problemi non N P-complete? . . . . . . . . . 140
3.8.2 La classe co-N P: il solito problema aperto . . . . . . 140
3.9 Classi EX P e N EX P . . . . . . . . . . . . . . . . . . . . . . 141
3.9.1 Rapporti fra le classi di complessità temporale . . . . . 142
3.10 Complessità in spazio: una breve overview . . . . . . . . . . . 142
3.10.1 Il modello di calcolo: MdT a tre nastri . . . . . . . . . 142
3.10.2 Tempo e spazio: una relazione esponenziale . . . . . . 143
3.10.3 Le classi di complessità temporali . . . . . . . . . . . 145
3.10.4 PATH, il tipico problema N L . . . . . . . . . . . . . 146
3.10.5 La classe dei giochi: N PSPACE . . . . . . . . . . . 147
vi
INDICE
3.10.6 Riflessioni finali in merito alle classi di complessità spaziale . . . . . . . . . . . . . . . . . . . . . . . . . . . 147
4
Esercizi
149
4.1 Esercizi sulla calcolabilità . . . . . . . . . . . . . . . . . . . . 149
4.1.1 Guida rapida alle metodologie di dimostrazione . . . . 149
4.1.2 Dimostrazioni di numerabilità e non numerabilità . . . 150
4.1.3 Creazione di macchine di Turing . . . . . . . . . . . . 151
4.1.4 Dimostrazioni di calcolabilità . . . . . . . . . . . . . . 154
4.1.5 Dimostrazioni relative alla decidibilità e alla semidecidibilità . . . . . . . . . . . . . . . . . . . . . . . . . . 160
4.1.6 Modello di calcolo di Kleene . . . . . . . . . . . . . . 161
4.1.7 Altro . . . . . . . . . . . . . . . . . . . . . . . . . . 165
4.2 Esercizi sulla complessità . . . . . . . . . . . . . . . . . . . . 166
4.2.1 Dimostrazioni di appartenenza a P . . . . . . . . . . 166
4.2.2 Dimostrare l’appartenenza a P (1) . . . . . . . . . . . 166
Bibliografia
167
Capitolo 1
Introduzione
1.1
Calcolabilità e complessità
La calcolabilità è quella disciplina che ci dice che, dato un problema, ci dice
se questo è calcolabile oppure no. Un problema calcolabile è un problema per
il quale è possibile ottenere una soluzione mediane l’impiego di un algorimo
eseguito in maniera automatica.
Questo tipo di nozione risulta quindi essere assoluta ed indipendente dalla macchina (o dall’umano) sulla quale l’algoritmo viene eseguito. Perciò, se un certo
problema viene definito indecidibile 1 , nessuna miglioria tecnologica potrà mai
permetterne la calcolabilità.
La complessità, invece, è una disciplina che studia quanto costa eseguire un
certo algoritmo. Il costo viene quantificato in termini di tempo e di spazio.
Sebbene possa sembrare strano, anche la complessità è calcolabile in termini
assoluti. Infatti, definito un certo algoritmo per un determinato problema,
macchine più o meno veloci possono fornire risultati computazionali diversi fra
loro, ma sempre all’interno della stessa classe di complessità.
A contorno di queste due discipline vi sono i metodi formali, che cercano di
risolvere problemi indecidibili limitandone alcune caratteristiche (cioè, di fatto,
semplificando il problema).
Una delle grande applicazioni dei metodi formali consta nella verifica di programmi2 che è però per natura indecidibile. Sotto certe condizioni, però, la
verifica di programmi è possibile e molto importante.
1
2
Un problema indecidibile è un problema non calcolabile.
Un programma verificato è un programma del quale si può garantire la correttezza.
2
1.2
Capitolo 1. Introduzione
Da Babbage a Turing: la nascita della calcolabilità
Il concetto stesso di calcolabilità nasce molto tardi. Per centinaia di anni i
matematici hanno calcolato senza mai chiedersi cosa significasse calcolare.
Questo accadde perché la matematica è stata sempre consierata una scienza
‘’perfetta‘’, comprovata dalla realtà e definita da Galileo ‘’la lingua del mondo‘’.
Vediamo dunque come la storia abbia portato alla messa in discussione della
matematica, portandola a riflettere in merito a sé stessa e quindi a definire il
concetto di calcolabilità.
Babbage e la prima macchina per calcolare
Nei primi anni del 1800 Babbage disegna la prima macchina in grado di calcolare. La grande intuizione dello scienziato sta nell’idea di impiegare variabili,
implementate come pile di dischi con 10 suddivisioni (per le cifre da 0 a 9).
Leggendo i dischi verticalmente, è possibile leggere il valore della variabile.
L’idea di Babbage però non prende piede a causa dell’assenza di un software
per questa macchina: apparentemente ogni problema deve avere un proprio
software per poter ‘’girare‘’ sulla macchina.
L’introspezione del mondo matematico
Babbage non riscuote successo anche perché il concetto stesso di calcolo non è
ancora preciso. È nel 1850 che la matematica inizia a porsi dubbi su sé stessa,
grazie al contributo di Reimann. Lo scienziato inventa infatti una geometria
alternativa a quella Euclidea (rimuovendo il quinto postulato di Euclide sulle
rette parallele) che sembra essere coerente e sensata.
Da qui il grande interrogativo: come possono coesistere due geometrie che
non si contraddicono al loro interno? La comunità matematica si attiva, e
dimostra che ciò che risulta corretto nella geometria Euclidea risulta essere
corretto anche nella geometria di Reimann e viceversa.
Questo avvenimento ha sconvolto profondamente la comunità matematica,
dando vita a tutta una serie di riflessioni e pubblicazioni. La più importante
è indubbiamente quella di Gödel, del 1936, nella quale lo scienziato formula il
suo teorema di incompletezza.
La sfida di Hilbert
Prima di arrivare al teorema di incompletezza, dobbiamo parlare dei grandi
informatici che hanno ideato i primi modelli di macchine per la calcolabilità.
Nel 1900 il matematico Hilbert interviene ad un importante convengo, ponendo una serie di problemi sui quali la comunità matematica avrebbe dovuto
1.2. Da Babbage a Turing: la nascita della calcolabilità
3
concentrarsi nel secolo a venire. Fra questi, il problema di determinare se una
frase espressa nella logica del prim’ordine sia un teorema o meno3 . Questo
problema si rivelerà indecidibile e sarà Turing a scoprirlo.
Dal canto suo Hilbert propone un’approccio finitistico alla risoluzione dei problemi, ovvero, trattare problemi infiniti (nella logica del prim’ordine, ad esempio, vi sono infine proporzioni generabili) da un punto di vista finito. Questo
approccio, come scopriremo, si rivelerà fallimentare.
La tesi di Church-Turing
Un docente di Turing gli chiede di trovare un problema che non sia indecidibile.
Per portare a termine la richiesta, Turing sviscera il problema della calcolabilità
fino a ridurlo ai suoi minimi termini e ideando così la macchina di Turing. Il
primo problema di cui Turing dimostra l’indecidibilità è proprio quello proposto
da Hilbert che abbiamo citato poco fa, ovvero verificare se una frase nella
logica del prim’ordine sia o meno un teorema.
La macchina di Turing è un modello astratto che Turing utilizza per sapere se
un problema è calcolabile o meno. Ma non c’è prova che la macchina di Turing
sia sempre in grado di dare la risposta corretta... eppure ci fidiamo. Possiamo
contare sul fatto che la macchina di Turing ci dia risposte coerenti perché,
nello stesso periodo e indipendentemente fra loro, anche Kleene4 e Church5
sviluppano due modelli diversi dalla macchina di Turing ma che portano agli
stessi risultati.
Negli anni a venire la comunità scientifica dimostrerà che i tre modelli forniscono sempre le stesse risposte e che quindi qualunque cosa la macchina di
Turing non risolva, non è risolvibile neanche mediante il modello ricorsivo di
Kleen o il lambda calcolo di Church.
Nasce così la tesi di Church-Turing secondo la quale affermiamo che se un
certo problema è calcolabile all’interno della macchina di Turing allora è definito calcolabile, altrimenti è definito non calcolabile.
Pertanto se si vuole dimostrare la calcolabilità di un problema possiamo scegliere di usare la macchina di Turing o il lambda calcolo o un sistema di calcolo
equivalente e se abbiamo esito positivo allora possiamo definire ufficialmente
3
La logica del prim’ordine è costituita dagli operatori della logica classica, dalle variabili
e dai quantificatori. Una certa frase della logica del primordine è un teorema solamente se
è sempre vero, mentre tipicamente una frase della logica del prim’ordine è vera o falsa a
seconda del valore che le sue variabili assumono
4
Kleene sviluppa un modello basato sulla ricorsione, ovvero sulla composizione di funzioni
calcolabili (partendo da semplicissime funzioni calcolabili).
5
Church sviluppa il lambda calcolo, un semplice linguaggio basato sulla sola regola di
sostituzione (in pratica il passaggio di parametri).
4
Capitolo 1. Introduzione
il problema calcolabile.
La tesi non è un teorema, dunque non ha dimostrazione ma è soltanto asserita
(anche se in ottant’anni nessuno ha mai trovato un controesempio).
L’equivalenza fra i modelli di Turing, Kleene e Church vale anche per quanto
riguarda la complessità (ci può essere al massimo una differenza polinomiale,
dunque in caso di complessità logaritmica potremmo perderla).
Enunciamo dunque formalmente la tesi Church-Turing:
Tesi di Church-Turing. Se un problema è intuitivamente calcolabile, allora
esisterà una macchina di Turing (o un dispositivo equivalente, come il computer) in grado di risolverlo (cioè di calcolarlo).
Enunciamo anche la forma estesa, che sostiene che qualsiasi funzione calcolabile mediante la macchina di Turing è calcolabile anche mediante un modello
equivalente (lambda calcolo, modello di Kleene, ecc.) mantendo la stessa classe di complessità (al massimo la differenza è polinomiale).
Tesi di Church-Turing estesa. Dato un programma risolvibile con un formalismo e con complessità limitata da un polinomio, ossia Tπ = O(nk ), compilando
il programma in un altro formalismo si può notare che la complessità non cambia.
Si noti che mentre la tesi di Church-Turing è una tesi, dunque non è dimostrata, l’equivalenza fra i modelli di Kleene, Church e Turing è stata dimostrata
formalmente (ne vedremo anche una in seguito).
Il teorema di incompletezza di Gödel
È nel 1936 che Gödel formula il suo teorema di completezza. Ne parliamo
perché il suo teorema assomiglia molto a quanto fatto da Turing, tant’è che
ad oggi alcune delle tecniche adottate durante le dimostrazioni di calcolabilità
sono state utilizzate da Göedel durante lo sviluppo del suo teorema.
L’aritmetica è molto simile alla logica del prim’ordine, infatti, c’è capacità di
inferenza, le quattro operazioni, i quantificatori, ecc. Göedel si chiede se sia
possibile o meno, dato il modello concreto dell’aritmetica, dimostrare tutte
le proprietà di cui dispone il modello astratto dell’aritmetica (ad esempio le
proprietà dei numeri primi).
Il risultato di Göedel è in linea con quello di Turing: così come nella logica del
prim’ordine non è possibile dire se una frase sia o meno un teorema, a partire
dal mero modello dell’aritmetica non è possibile ottenere tutte le proprietà di
cui gode il modello artimetico astratto.
1.2. Da Babbage a Turing: la nascita della calcolabilità
5
In altre parole, Göedel dimostra che in ogni formalizzazione coerente della
matematica che sia sufficientemente potente da poter assiomatizzare la teoria
elementare dei numeri naturali (vale a dire, sufficientemente potente da definire
la struttura dei numeri naturali dotati delle operazioni di somma e prodotto)
è possibile costruire una proposizione sintatticamente corretta che non può
essere né dimostrata né confutata all’interno dello stesso sistema.
6
Capitolo 1. Introduzione
Capitolo 2
Calcolabilità
2.1
La macchina di Turing
Entriamo nel vivo della materia parlando della macchina di Turing. Stando a
quanto detto fin’ora, utilizzare le lambda expressions piuttosto che la macchina
di Turing risulta del tutto equivalente dal punto di vista di ciò che riescono a
fare. Volendo, sarebbe anche possibile utilizzare un linguaggio ad alto livello,
ma ciò sarebbe controproducente poiché i modelli sono pensati proprio al fine
di utilizzare linguaggi privi di orpelli e di funzionalità inutili al mero fine della
dimostrazione della calcolabilità di un certo problema.
Scegliamo di utilizzare la macchina di Turing poiché risulta la più semplice
fra i modelli esposti, oltre che avere un’importante valore storico. Inoltre, la
scelta risulta propedeutica: utilizzeremo la macchina di Turing anche parlando
di complessità degli algoritmi (il tempo di esecuzione di una certa istruzione
sulla macchina di Turing è ben definito).
2.1.1
Definizione della macchina di Turing
La macchina di Turing è un modello astratto, con una struttura meccanica
abbastanza semplice.
Osserviamo la Figura 2.1: si ha un nastro infinito suddiviso in slot, entro i quali
sono scritti dei simboli appartenti ad un certo alfabeto A (oppure ∗, il simbolo
vuoto). La macchina dispone poi di una testina che è piazzata, in ogni istante,
su uno e un solo slot. Grazie alla testina è possibile leggere quale simbolo si
trova nello slot osservato. Sebbene il nastro sia infinito, in ogni istante si ha
un numero finito di simboli all’interno della macchina. È anche presente una
nozione di stato (indicata con q in figura) che ci sarà utile per capire ‘’cosa si
deve fare‘’.
Sono poi definite delle transizioni di stato che, a partire dal simbolo letto sulla
testina e dallo stato attuale ci diranno cosa deve fare la macchina. Infine, ad
8
Capitolo 2. Calcolabilità
ogni transizione di stato abbiamo che il nastro si può muovere a destra o a
sinistra.
…
*
*
*
a
*
*
*
…
q
Figura 2.1: Una rappresentazione della macchina di Turing. Lo stato attuale è q e sulla testina è
possibile leggere il carattere a ∈ A.
Risulta quindi evidente che la macchina di Turing sia fondamentalmente un
manipolatore di stringhe 1 .
La definizione formale
La definizione che abbiamo dato fin’ora è intuitiva ma poco formale. Andiamo
quindi a definire più precisamente la macchina di Turing, introducendo anche
un po’ di notazione.
La macchina dispone di:
• Un’alfabeto A ∪ {∗}, dove con ∗ intendiamo lo spazio vuoto.
• Un’insieme di stati Q ∪ {q0 }, dove q0 è lo stato iniziale.
• Le operazioni della macchina sono definite mediante una serie di funzioni
di transizione δ.
Una funzione di transizione è definita come segue:
δ : A ∪ {∗} × Q ∪ {q0 } → A ∪ {∗} × Q ∪ {q0 } × {+1, −1}
Stiamo quindi dicendo che, dato un simbolo a appartenente all’alfabeto A ed
un certo stato q appartenente all’insieme Q possiamo ottenere una tripletta
che ci dice quale letterale scrivere nella posizione attuale della testina (che
quindi andrà a sostituire a), il nome del nuovo stato, e in che direzione spostare la testina (con +1 intendiamo a destra mentre con −1 intendiamo a
sinistra).
Per comodità possiamo rappresentare l’intera funzione di transizione come una
cinquina, ad esempio, δ(q0 , 1, q1 , 2, +1). Vediamo in Figura 2.2 un esempio
d’applicazione di questa δ. Si noti che le funzioni di transizione sono ovviamente funzioni, se ne deduce che non possano esistere due funzioni che abbiano
gli stessi argomenti di input ma diversi valori di output. Questo è vero poiché
stiamo trattando la macchina di Turing deterministica, che quindi definisce
1
Una stringa è un insieme finito di caratteri appartenenti ad un determinato alfabeto A.
2.1. La macchina di Turing
*
*
*
1
*
9
*
*
*
q0
*
*
2
*
*
*
q1
Figura 2.2: Esempio di applicazione della funzione di transizione δ(q0 , 1, q1 , 2, +1).
soltanto funzioni di transizione deterministiche.
L’insieme delle funzioni di transizione è, in un certo senso, il ‘’programma‘’ che
gira sulla macchina. La definizione non è comunque precisa, poiché secondo
la visione di Turing ogni macchina esegue un solo programma (quello appunto
definito dalle funzioni di transizione che sono cablate nella macchina).
Una rappresentazione più immediata
Ora che abbiamo compreso cosa sia la macchina di Turing capiamo bene che
utilizzare una rappresentazione grafica risulterebbe assolutamente impraticabile.
Introduciamo perciò una semplice notazione: possiamo rappresentare l’intera
macchina di Turing con una quadrupla siffatta:
(stringa sx, stato, simbolo sulla testina, stringa dx)
Perciò, la macchina in Figura 2.1 è rappresentabile mediante la semplice dicitura (λ, q0 , a, λ). Chiaramente, con λ indichiamo una sequenza infinita di
∗.
2.1.2
Un primo esempio
Tanto per impratichirci, cerchiamo di descrivere una macchina di Turing che
esegua l’operazione di successore per numeri rappresentati in unario2 .
Il nostro alfabeto A è quindi costituito dal solo simbolo 1. Il task è quindi
abbastanza semplice: partendo ad esempio da una macchina configurata come
(λ, q0 , 1, 11) vogliamo ottenere una configurazione finale con quattro uni di
seguito. Per convenzione, ci aspettiamo che la testina sia posizionata sulla
cifra più a sinistra della stringa mantenuta nella macchina.
Le funzioni di transizione che dobbiamo definire sono soltanto due:
δ(q0 , 1, q0 , 1, +1)
δ(q0 , ∗, q1 , 1, +1)
2
La rappresentazione unaria sfrutta soltanto la cifra 1. Lo zero è rappresentato da un
uno, e un generico numero n è rappresentato da una sequenza di n + 1 uni.
10
Capitolo 2. Calcolabilità
In sostanza con la prima funzione di transizione andiamo avanti fino a fine numero, dopodiché, appena leggiamo un ∗ lo sostituiamo con un uno ed andiamo
in un nuovo stato q1 per il quale non c’è funzione di attivazione (e quindi terminiamo l’esecuzione).
Chiamiamo la macchina che abbiamo appena definito M1 e supponiamo di
partire dallo stato (λ, q0 , 1, 11), ovvero il numero 2. Siamo ora in grado di
scrivere un’esecuzione della macchina:
(λ, q0 , 1, 11) `M1 (1, q0 , 1, 1)
`M1 (11, q0 , 1, λ)
`M1 (111, q0 , ∗, λ)
(2.1)
`M1 (111, q1 , 1, λ)
Evidentemente, il risultato è quello sperato: abbiamo ottenuto 1111 (cioè 3).
2.2
Le funzionalità della macchina di Turing
Ora che ci è chiaro il funzionamento della macchina di Turing interroghiamoci
su cosa siamo in grado di fare con questo gingillo.
La macchina di Turing può fondamentalmente fare due cose: riconoscere un
linguaggio (anche detto decidere) e accettare un linguaggio3 .
Riconoscere un linguaggio
Supponiamo di aver codificato in qualche modo il messaggio sì ed il messaggio
no all’interno della macchina di Turing M, e supponiamo anche di avere un
linguaggio L definito su A e che A sia l’alfabeto di M. Infine, poniamo una
stringa ω sul nastro di M.
Definizione 2.1 (Riconoscere/decidere un linguaggio). Se è sempre vero che:
• M converge a sì se ω ∈ L
• M converge a no altrimenti.
allora possiamo dire che M decide L e quindi che L è decidibile.
Accettare un linguaggio
Prendiamo una macchina di Turing M, e supponiamo anche di avere un linguaggio L definito su A e che A sia l’alfabeto di M. Infine, poniamo una
stringa ω sul nastro di M.
3
Un linguaggio è una serie finita o infinita di stringhe definite su un certo alfabeto
2.2. Le funzionalità della macchina di Turing
11
Definizione 2.2 (Accettare un linguaggio). Se è sempre vero che:
• M converge se ω ∈ L
• M diverge su ω se ω ∈
/ L.
allora possiamo dire che M accetta L e L è semidecidibile.
Chiaramente, la nozione di decidibilità è più forte di quella di semidecidibilità
(ogni linguaggio decidibile è anche semidecidibile). La semidecidibilità ci dice
in sostanza che siamo certi quando diamo una risposta positiva, ma non siamo
in grado di dare una risposta negativa sicura. Ritoneremo su questi concetti in
futuro, parlando del fatto che una funzione calcolabile è dal punto di vista delle
MdT un problema decidibile mentre una funzione non calcolabile risulta essere
indecidibile dal punto di vista delle MdT. Solamente dopo esserci impratichiti
col concetto di calcolabilità andremo poi a parlare della semidecidibilità e di
ciò che significhi relativamente alle funzioni.
Una piccola specifica notazionale: quando una macchina di Turing M converge
avendo un determinato input ω, possiamo dire che ‘’M converge su ω‘’, e
rappresentarlo in simboli come: M ↓ ω.
Se invece M diverge avendo ω come input allora possiamo dire che ‘’M diverge
su ω‘’, rappresentato in simboli come: M ↑ ω.
2.2.1
Dai linguaggi ai problemi
Riconoscere un linguaggio non è tanto distante dal risolvere un problema. Ad
esempio, se volessimo riconoscere tutti i numeri pari scritti in unario, potremmo
definire un linguaggio che comprenda solo i numeri pari e poi scrivere una
macchina di Turing che lo riconosca. Facciamolo.
Per prima cosa codifichiamo sì e no come segue:
• sì: 1
• no: 11
Questo significa che al termine dell’esecuzione, sul nastro dovrà rimanere solamente 1 oppure 11, a indicare l’output dell’esecuzione.
La soluzione risulta dunque essere:
[1] δ(q0 , 1, q1 , ∗, +1)
[2] δ(q1 , 1, q0 , ∗, +1)
[3] δ(q0 , ∗, q2 , 1, +1)
[4] δ(q1 , ∗, q0 , 1, +1)
12
Capitolo 2. Calcolabilità
In pratica abbiamo codificato con q0 lo stato in cui il numero è pari e q1 lo
stato in cui il numero è dispari. Abbiamo dunque un’alternanza delle funzioni
[1] e [2] che ‘’mangiano l’input‘’. Dopodiché, una volta finita la stringa, a
seconda dello stato in cui troviamo verrà eseguita [3] oppure [4]. Nel primo
caso avremo un numero pari (e infatti grazie a [3] scriviamo 1), nel secondo
abbiamo un numero dispari e quindi prima scriviamo un 1 grazie a [4] e il
secondo uno lo lasciamo scrivere a [3].
Verificare la soluzione
Per verificare la soluzione dobbiamo scrivere un’esecuzione generica e dimostrare che la nostra macchina risponda correttamente. Partiamo ad esempio
da questa configurazione:
(λ, q0 , 1, 1...1)
Come ultimo componente della quadrupla abbiamo 2n uni, quindi una quantità pari di simboli. Ma considerando anche l’1 sotto la testina abbiamo un
numero dispari di simboli, quindi, un numero pari (nel linguaggio unario lo zero
è esplicito ergo con un numero dispari di simboli rappresento un numero pari).
Ci aspettiamo dunque di ottenere un uno sul nastro a fine esecuzione. Ecco
l’esecuzione:
(λ, q0 , 1, 1...1) ` (λ, q1 , 1, 1...1)
` (λ, q0 , 1, 1...1)
` (λ, q1 , 1, 1...1)
` (λ, q0 , 1, 1...1)
(2.2)
?
` (λ, q1 , 1, λ)
` (λ, q0 , ∗, λ)
` (λ, q2 , ∗, λ)
Con `? abbiamo sottointeso tutte le applicazioni per consumare i 2n uni (abbiamo esplicitato i primi passi), infatti al passo successivo abbiamo ∗ sulla
testina. Sul nastro rimane un solo 1, quindi il numero pari è riconosciuto. Si
lascia al lettore il compito di verificare la lettura di un numero dispari.
2.2.2
Calcolare con la macchina di Turing
Cerchiamo ora di comprendere come è possibile calcolare una funzione sfruttando la macchina di Turing.
Partiamo dalla codifica: tendenzialmente vorremo lavorare con numeri naturali, dunque, useremo il binario (0 ed 1) per rappresentarli. Poi, come sappiamo,
le funzioni possono avere più variabili: dunque separiamo ogni valore in input
con un’asterisco.
2.3. Un nuovo concetto di cardinalità: insiemi numerabili
13
Prendiamo dunque una macchina M che gode delle caratteristiche sopra esposte e consideriamo questa definizione:
Definizione 2.3 (Calcolare una funzione unaria). Avendo ω come input e f (ω)
definita, allora M convergerà ed il risultato sul nastro sarà f (ω) (cioè il valore
della funzione). Se invece f (ω) è indefinita, M divergerà su ω.
Se questa definizione è valida, allora si dice che M calcola f .
Chiaramente, è facile estendere la stessa definizione per funzioni n-arie (usando
la convenzione di cui abbiamo parlato poco fa):
Definizione 2.4 (Calcolare una funzione n-aria). Avendo ω1 ∗ω2 ∗...∗ωn come
input e f (ω1 , ω2 , ..., ωn ) definita, allora M convergerà ed il risultato sul nastro
sarà f (ω1 , ω2 , ..., ωn ). Se invece f (ω1 , ω2 , ..., ωn ) è indefinita, M divergerà su
ω1 ∗ ω2 ∗ ... ∗ ωn .
2.3
Un nuovo concetto di cardinalità: insiemi numerabili
Siamo ora in grado di calcolare mediante la macchina di Turing. Come proseguiamo il nostro studio? Ciò che arriveremo a dire è che ognuna delle infinite
macchine di Turing è numerabile (identificabile con un numero), quindi lo sono
tutti gli algoritmi ad esse associate4 ; ma giacché le funzioni che le macchine di
Turing implementano sono tutte e sole le funzioni calcolabili5 , stiamo dicendo
che l’insieme delle funzioni calcolabili è numerabile. Per ora questa frase può
sembrare nebulosa, ma ha delle implicazioni di enorme portata e potremo capirne appieno il significato solo dopo aver appreso alcune nozioni di base.
Iniziamo dunque della nozione fondamentale di insieme numerabile, nozione
che sta alla base di quasi tutto ciò che si fa nella teoria della calcolabilità.
Il concetto di cardinalità per insiemi finiti risulta banale: la cardinalità di un
insieme è la quantità di elementi che quell’insieme contiene. Quando però si
parla di insiemi infiniti, il concetto di cardinalità deve essere in qualche modo
potenziato poiché possiamo incappare in paradossi. Ad esempio, l’insieme dei
numeri naturali e quello dei numeri pari sono entrambi infiniti... ma paradossalmente il primo include il secondo. Esistono dunque‘’infiniti diversi‘’ ? Stiamo
per rispondere a questa domanda.
4
Nella definizione di Turing, una macchina risolve un determinato algoritmo poiché non
c’è distinzione fra software ed hardware.
5
Vero per la tesi di Church-Turing.
14
Capitolo 2. Calcolabilità
2.3.1
Reminder: il concetto di funzione
Parleremo molto di funzioni, si rende dunque necessario ricordare alcuni concetti elementari.
In matematica, una funzione, anche detta applicazione, mappa o trasformazione, è definita dai seguenti oggetti:
• Un insieme X detto dominio della funzione.
• Un insieme Y detto codominio della funzione.
• Una relazione f : X → Y che ad ogni elemento dell’insieme X associa
uno ed un solo elemento dell’insieme Y ; l’elemento assegnato a x ∈ X
tramite f viene abitualmente indicato con f (x) e viene detta immagine
di x.
Perciò, una funzione si può pensare come un insieme di coppie del tipo:
{(x1 , y1 ), (x2 , y2 ), ...}
e si differenzia da una generica relazione dal fatto che non possono esistere due
o più coppie che presentino come elemento di sinistra lo stesso xi e che abbiano
diverso yi associato (in altre parole, la funzione è deterministica mentre una
generica relazione non lo è).
Funzioni parziali e funzioni totali. La definizione che abbiamo appena dato
è la definizione di funzione parziale. Infatti, non è garantito che la funzione
sia definita sia tutto il dominio, ovvero, è possibile che vi siano x ∈ X per cui
f (x) non ha valore nel codominio. Quando una funzione parziale è definita su
tutto il suo dominio è detta funzione totale. Se ne deduce che l’insieme delle
funzioni totali sia un sottoinsieme proprio delle funzioni parziali.
Nell’uso comune si tende spesso a sottointendere che una funzione sia totale,
ma nel campo della calcolabilità e della complessità ci interessiamo ad entrambe le tipologie di funzioni, dunque, quando parleremo di funzioni in senso
generico intenderemo funzioni parziali.
Un accorgimento: si intende per dominio di una funzione l’insieme dei punti per cui la funzione converge, anche se quando si parla di funzioni parziali
potrebbe non essere noto a priori il dominio/codominio e dunque ci riferiamo
ad un sovrainsieme (i numeri naturali, ad
√ esempio). Pertanto, secondo questa
definizione possiamo dire che f (x) = x ha come dominio e codominio l’insieme dei numeri naturali N. In realtà, però, f (x) è definito solamente se x è
un quadrato perfetto (e non un naturale qualsiasi). Tendenzialmente useremo
questo tipo di definizione e in caso contrario espliciteremo che ci riferiamo al
2.3. Un nuovo concetto di cardinalità: insiemi numerabili
15
dominio come l’insieme dei punti per cui la funzione converge.
Funzioni iniettive e funzioni suriettive. Una funzione è detta iniettiva quando su ogni punto in cui è definita vale la regola:
x1 6= x2 ⇒ f (x1 ) 6= f (x2 )
(2.3)
Si ha dunque che per differenti valori del dominio si hanno differenti valori del
codominio, ergo ogni valore di dominio ha un’immagine distinta nel codominio.
Una funzione è detta suriettiva quando ogni elemento del codominio è immagine di un x del dominio, ovvero:
∀ y ∈ Y ∃x ∈ X tale che f (x) = y
(2.4)
Si ha dunque che per differenti valori del dominio si hanno differenti valori del
codominio, ergo ogni valore di dominio ha un’immagine distinta nel codominio.
Può essere interessante notare che le nozioni di iniettività e suriettività riguardano condizioni sul codominio, mentre la definizione di funzione (parziale e
totale) concerne limitazioni sul dominio.
Argomenti e parametri. Una funzione del tipo f (x) si dice avere argomento
x (talvolta chiamato erroneamente parametro). Il parametro è invece un valore
fisso all’interno della funzione, che se astratto permette di identificare una
famiglia di funzioni. Spesso i parametri di una funzione sono indicati a pedice.
Ad esempio la funzione:
fk (x) = k + 3 + x
ha x come argomento, 3 come costante e k come parametro. fk (x) identifica
quindi una famiglia di funzioni che differisce per il valore k. La funzione
f2 (x) = 5 + x
è una precisa funzione appartenente alla famiglia di fk (x).
2.3.2
Definizione di insieme numerabile
La definizione di insieme numerabile è alquanto immediata:
Definizione 2.5 (Insieme numerabile). A è un insieme numerabile o di cardinalità numerabile se esiste una biezione da A ad N.
16
Capitolo 2. Calcolabilità
Ecco quindi che abbiamo trovato una nuova definizione di cardinalità per insiemi infiniti. Avere una funzione biettiva da A ad N può essere visto intuitivamente come il fatto che A ed N abbiano ‘’lo stesso numero di elementi‘’.
È ovviamente banale il fatto che N sia numerabile: un esempio di funzione
biettiva da N in N è la funzione identità f (x) = x, che per ogni numero naturale restituisce il numero naturale stesso.
È ovvio che aver definito gli infiniti numerabili implichi che ve ne siano anche
di non numerabili, altrimenti avremmo una definizione sostanzialmente inutile.
Cantor dimostra che esistono soltanto due tipologie di infinito, l’infinito degli
insiemi numerabili e l’infinito degli insiemi non numerabili, anche detto infinito
nel continuo. Non trattiamo questo tipo di infinito, ma possiamo dire che,
intuitivamente, se un insieme con cardinalità numerabile è un insieme che ha
tanti elementi quanti i naturali, un insieme con cardinalità non numerabile ha
tanti elementi quanti i numeri reali.
2.3.3
Esempio: l’insieme dei numeri pari è numerabile?
Iniziamo ad utilizzare il concetto di insieme numerabile cercando di capire se
l’insieme dei numeri pari P sia o meno numerabile. Al fine di risolvere questo
problema, introduciamo un interessante teorema che ci sarà utile in diversi
casi:
Teorema 2.1 (Esistenza di una funzione biettiva). Dati gli insiemi A e B,
esiste una funzione biettiva da A in B se esistono le seguenti due funzioni: una
funzione iniettiva da A in B e una funzione iniettiva da B in A.
Non dimostriamo questo teorema, ma almeno da un punto di vista intuitivo
non stiamo dicendo niente di sconvolgente: avere una funzione iniettiva A → B
è un po’ come dire che B ha un numero di elementi che è almeno quello di A;
analogamente avere una funzione iniettiva B → A ci dice che A ha un numero
di elementi che è almeno quello di B. Se entrambe le funzioni esistono allora
possiamo immaginare che A abbia tanti elementi quanti ne ha B.
Grazie al teorema appena enunciato sarà sufficiente trovare le due funzioni
iniettive:
f1 : P → N
f2 : N → P
per dimostrare che l’insieme dei pari è numerabile.
Trovare f1 è semplicissimo: la funzione identità fa proprio al caso nostro!
Qualsiasi numero pari ha un’immagine nei numeri naturali, che è proprio se
stesso. f2 è invece leggermente più difficile da trovare: d’altronde, il doppio
2.3. Un nuovo concetto di cardinalità: insiemi numerabili
17
di ogni numero naturale è un numero pari, dunque possiamo usare f (x) = 2x.
Ecco quindi le nostre due funzioni:
f1 (x) = x
f2 (x) = 2x
Avendo trovato queste due funzioni, possiamo dire grazie al Teorema 2.1 che
esiste una funzione biettiva da P a N e dunque grazie alla Definizione 2.5 che
l’insieme dei numeri pari è numerabile.
2.3.4
Se lo posso numerare lo posso calcolare
Aver definito il concetto di cardinalità numerabile sfruttando l’insieme dei numeri naturali ha un vantaggio intrinseco: i numeri naturali godono infatti di
una nozione di ordinamento implicito. In altre parole, se sappiamo che un certo
insieme è numerabile siamo in grado di identificarne gli elementi e di metterli
in ordine, semplicemente associando ad ogni elemento dell’insieme un determinato numero naturale. Nel caso dell’insieme P possiamo quindi associare a
0 il naturale 0, a 2 il naturale 1, a 4 il naturale 2, ecc.
Questa capacità di numerare sarà di fondamentale importanza in molte dimostrazioni. In particolare, le funzioni calcolabili sono infiniti numerabili, mentre
non è possibile calcolare funzioni su infiniti non numerabili.
2.3.5
La diagonalizzazione di Cantor
Introduciamo ora una tecnica di dimostrazione fondamentale, la diagonalizzazione di Cantor. È una tecnica che funziona molto bene per dimostare la non
calcolabilità di una funzione.
Usiamo la diagonalizzazione di Cantor per dimostrare l’insieme dei numeri reali
non è numerabile.
Esempio: i numeri reali fra 0 ed 1 non sono numerabili
Per dimostrare che l’insieme dei numeri reali non è calcolabile dimostriamo
qualcosa di più semplice, ovvero che già i numeri reali fra 0 ed 1 non lo sono.
Identifichiamo questo insieme come R0,1 .
Giacché vogliamo dimostrare un risultato negativo procediamo per assurdo, assumendo che l’insieme R0,1 sia numerabile. Ma grazie alla definizione di insieme
numerabile, sappiamo che deve esistere una determinata funzione h : N → R0,1
che sia biettiva (Definizione 2.5). Possiamo dunque numerare gli elementi di
R0,1 sfruttando h. Avremo cioè che h(0) restituisce il primo numero reale,
h(1) il secondo, h(2) il terzo, e così via.
18
Capitolo 2. Calcolabilità
Possiamo rappresentare in forma tabellare tutti questi infiniti numeri (indichiamo solo i numeri dopo la virgola, dato che sono gli unici a variare):
h(0)
h(1)
h(2)
h(3)
h(4)
..
.
0
c0
c0
c0
c0
c0
..
.
1
c1
c1
c1
c1
c1
..
.
2
c2
c2
c2
c2
c2
..
.
3
c3
c3
c3
c3
c3
..
.
4
c4
c4
c4
c4
c4
..
.
···
···
···
···
···
···
..
.
abbiamo rappresentato con ci la i -esima cifra decimale, pertanto nella cella
(i , j) abbiamo la j-esima cifra decimale dell’i -esimo numero naturale, ovvero
h(i ).
Dato che h è una funzione iniettiva ogni riga della tabella è diversa dalle altre
(ovvero differisce a tutte le altre righe per almeno un elemento) e dato che h
è suriettiva sappiamo che la tabella contiene tutti i numeri reali.
Se dunque riuscissimo a scrivere un reale che non è contenuto nella tabella
possiamo dimostrare l’insistenza della funzione h e dunque il fatto che R0,1 sia
non numerabile.
Costriamo quindi un certo reale r ? percorrendo la diagonale della tabella usando
k come indice e impostando:
• L’elemento k-esimo di r ? a 5, se l’elemento della diagonale ck è diverso
da 5.
• L’elemento k-esimo di r ? a 3, se l’elemento della diagonale ck è uguale
a 5.
Quindi, la k-esima cifra di r ? sarà necessariamente diversa da dalla k-esima
cifra di h(k). Così facendo r ? sarà per costruzione diverso da h(0) poiché avrà
il suo c0 diverso dal c0 di h(0), diverso da h(1) poiché avrà il suo c1 diverso
dal c1 di h(1), e così via per ogni elemento della tabella.
Avendo noi trovato un numero reale che non è incluso nella tabella, possiamo
asserire che non può esistere la funzione h e dunque che R0,1 non è numerabile.
Esempio: ordinare le coppie di naturali
Consideriamo l’insieme delle coppie di naturali N × N; come potremmo ordinarne gli elementi? Se usassimo la prima cifra otterremmo qualcosa del
genere:
(0, 0), (0, 1), (0, 2), . . . , (1, 0), (1, 1), . . .
2.3. Un nuovo concetto di cardinalità: insiemi numerabili
19
Notiamo dunque che fra la prima coppia che inizia con zero (0, 0) e la prima
coppia che inizia con uno (1, 1) abbiamo infiniti elementi. Lo stesso discorso
vale se ordinassimo basandoci sulla seconda cifra anziché la prima. Almeno
intuitivamente l’insieme sembrerebbe essere non numerabile, mentre invece lo
è. Dimostriamolo.
Sfruttiamo il Teorema 2.1 e cerchiamo le due funzioni iniettive:
f :N×N→N
g :N→N×N
Se riusciamo a trovarle entrambe, abbiamo dimostrato che il nostro insieme è
numerabile, poiché se esistono due funzioni iniettive come quelle sopra definite
allora esiste una funzione biettiva da N × N a N, e quindi l’insieme è numerabile.
La funzione g(n) è banale, dobbiamo semplicemente restituire una coppia qualsiasi purché con n diversi si restituisca una coppia diversa. Quindi, potremmo
restituire la coppia (n, 0) o (n, n). Meno immediata è invece la definizione di
f , che deve combinare la coppia in input (n, m) fornendo un valore univoco.
Possiamo sfruttare il fatto che ogni numero abbia una e una sola scomposizione in fattori primi e quindi definire f (n, m) come 2n · 3m . In questo modo il
valore restituito sarà certamente univoco poiché fissati n ed m esiste un solo
numero che ha scomposizione 2n · 3m .
Riassumendo, ecco le nostre funzioni:
f (n, m) = 2n · 3m
g(n) = (n, 0)
Abbiamo scritto le noste due funzioni iniettive, quindi l’insieme delle coppie
dei numeri naturali è numerabile.
2.3.6
La numerabilità effettiva
Con l’esercizio precedente siamo riusciti a dimostrare che l’insieme delle coppie
dei numeri naturali è numerabile, ma non abbiamo ottenuto una funzione biettiva che possiamo ‘’usare‘’, ovvero, non abbiamo una vera e propria funzione di
codifica (e una di decodifica) per poter mappare e demappare le nostre coppie.
Se si è in grado di definire un vero e proprio algoritmo che ordini gli elementi
di un insieme numerabile, quell’insieme viene detto effettivamente numerabile.
Funzione per la codifica
Riprendiamo dunque il problema di prima cercando una nuova f (n, m) di
codifica. Iniziamo scriviamo tutte le nostre coppie in tabella:
20
Capitolo 2. Calcolabilità
0
(0, 0)
(1, 0)
(2, 0)
(3, 0)
..
.
0
1
2
3
..
.
1
(0, 1)
(1, 1)
(2, 1)
(3, 1)
..
.
2
(0, 2)
(1, 2)
(2, 2)
(3, 2)
..
.
3
(0, 3)
(1, 3)
(2, 3)
(3, 3)
..
.
···
···
···
···
···
..
.
Per quanto abbiamo già detto in precedenza non possiamo numerare (quindi
ordinare) per riga né per colonna, poiché sono infinite. Possiamo invece usare
le diagonali inverse, che sono finite.
Notiamo due interessanti proprietà:
1. Se una coppia (n, m) si trova sulla i -esima diagonale inversa, allora la
somma dei suoi elementi è i , ovvero n + m = i .
2. Il numero di coppie dell’i -esima diagonale inversa è i + 1.
Vediamo in Figura 2.3 una scrittura lineare delle prime tre diagonali invertite
della tabella delle coppie dove possiamo osservare la veridicità di queste due
proprietà.
Diagonale
0
1
2
3
(0,0) (0,1) (1,0) (0,2) (1,1) (2,0) (0,3) (1,2) (2,1) (3,0)
f(n,m)
0
1
2
3
4
5
6
7
8
9
Figura 2.3: Rappresentazione lineare delle prime tre diagonali invertite della tabella delle coppie.
Possiamo dunque asserire che la coppia (n, m) si trova nella diagonale n +
m. Inoltre, per costruzione, sulle varie coppie della diagonale inversa il primo
elemento della coppia andrà da 0 ad i mentre il secondo elemento andrà da i
a 0 (infatti vi sono in totale i + 1 coppie, dove il +1 è dato dalla presenza dello
zero). Questo significa che la coppia (n, m) si troverà nella posizione n-esima
della sua diagonale (contando a partire da 0).
Non resta che formalizzare tutto questo in una semplice funzione di calcolo.
Per prima cosa dobbiamo spostarci fino alla diagonale corretta, e, rivedendo
Figura 2.3 possiamo notare che si tratta semplicemente di saltare 1 elemento
dalla diagonale 0, 2 elementi dalla diagonale 1, 3 elementi dalla diagonale 2 e
così via. Si hanno quindi un numero di elementi da saltare pari a:
1 + 2 + 3 + · · · + (n + m)
Si noti che n + m è il numero di elementi della diagonale, dunque abbiamo
raggiunto la diagonale di indice n + m − 1 (questo per la seconda proprietà
2.3. Un nuovo concetto di cardinalità: insiemi numerabili
21
che abbiamo trovato prima), ovvero la diagonale precedente a quella in cui si
trova la nostra coppia (n, m). Ma questa è la sommatoria di Gauss, che quindi
equivale a:
(n + m)(n + m + 1)
2
Non resta che aggiungere gli n elementi della diagonale e arrivare finalmente
all’elemento desiderato. Ecco quindi la funzione di codifica f (n, m):
f (n, m) =
(n + m + 1)(n + m)
+n
2
Funzione per la decodifica
Dato l’ordinamento così come l’abbiamo definito possiamo anche scrivere
un’algoritmo per la decodifica, ovvero delinare una certa funzione g(i ) che
dato l’indice della coppia restituisca la coppia (n, m) costituente la coppia
stessa.
Un banale esempio può essere scorrere la tabella per righe mediante l’indice k
e per colonne mediante l’indice j. Per ogni coppia trovata si compiono questi
semplici passi:
• Codifica la coppia in posizione (k, j).
• Confronta la codifica con il valore di input i :
– Se i è minore della codifica, significa che si è oltrepassata la codifica
per la coppia desiderata e quindi si può passare ad un altra riga:
k++, j=0 .
– Se i è maggiore della codifica si prosegue sulla stessa riga j++ (non
si è ancora superata la coppia).
– Se i è uguale alla codifica si restituisce la coppia in posizione (k, j).
Ovviamente questa soluzione è funzionante ma estremamente inefficente, poiché scandisce molte coppie inutili.
Si noti che in realtà non si utilizza
che ritorna una coppia di
Y una funzione
Y
valori, bensì due funzioni diverse
(i ) e
(i ) che eseguono tutti i passi
1
Y2
che abbiamo appena descritto ma alla fine
(i ) restituirà il primo elemento
1
Y
della coppia mentre
(i ) restituirà il secondo.
2
Esercitazione: si veda l’esercizio 4.1.3.
22
2.4
Capitolo 2. Calcolabilità
Codifica delle macchine di Turing
In questa sezione ci apprestiamo a costruire una pietra miliare sulla quale fonderemo praticamente tutte le dimostrazioni che faremo: la codifica delle MdT.
Come sappiamo l’insieme delle funzioni calcolabili è un insieme numerabile,
dunque siamo in grado di identificare ogni elemento al suo interno. D’altronde, sappiamo anche che ogni funzione calcolabile è tale poiché esiste una
macchina di Turing che la calcola (in realtà infinite, poiché la macchina di Turing codifica un determinato algoritmo e vi sono infiniti algoritmi per calcolare
la stessa funzione). Ma se sappiamo enumerare le funzioni calcolabili, allora
sappiamo anche enumerare le MdT e questo significa che siamo anche in grado
di codificarle. In altre parole, siamo in grado di trovare una funzione che dato
un certo valore intero i ci restituisca una MdT il cui codice è i . Ovviamente questa funzione di codifica deve essere iniettiva (ma non necessariamente
suriettiva come stiamo per imparare, anche se ci farebbe comodo), ovvero un
certo codice i deve essere il codice di una e una sola MdT.
Un primo esercizio di codifica
Prima di risolvere il problema della codifica delle macchine di Turing alleniamoci
con un esercizio più semplice. Supponiamo di avere un alfabeto A così definito:
A = {a0 , . . . , aN }
La nostra funzione di codifica f : A? → N potrebbe parsificare la stringa e,
per ogni letterale a incontrato, sostituirlo col suo indice. Il risultato sarebbe
quindi un numero in base N + 1. Possiamo così codificare una certa stringa:
a3 a2 a5
come il numero 325 in base N + 1.
La funzione di codifica f è iniettiva? Se il codice usato è interpretato come
valore numerale, usare direttamente l’indice (con cifre da 0 a 9) fa sì che f
non sia iniettiva. Infatti, le stringhe:
a1
a0 a1
a0 a0 a1
hanno tutte tutte codifica 1 (tecnicamente 1, 01 e 001 che però sono tutte
lo stesso numerale 1).
Il problema è facilmente risolvibile: f dovrà, anziché usare direttamente l’indice dei letterali che parsifica, aumentarlo di 1. Così, le nostre tre stringhe
2.4. Codifica delle macchine di Turing
23
avrebbero codifica:
f (a1 ) = 2
f (a0 a1 ) = 12
f (a0 a0 a1 ) = 112
Ora f è iniettiva ed è quindi una perfetta funzione di codifica per le stringhe
sull’alfabeto A. Si noti che f non è suriettiva, infatti il numero 10 non è
immagine di nulla.
2.4.1
La numerazione di Göedel
Passiamo ora al nostro problema: codificare le MdT. La codifica ideata da
Göedel è un ottimo sistema per la codifica di stringhe poiché ha una definizione
gerarchica, dunque, è possibile codificare lettere, stringhe, stringhe di stringhe,
ecc.
Consideriamo il solito alfabeto:
A = {a0 , . . . , aN }
Definiamo ora tre codifiche per trattare letteral, parole e frasi.
Definiamo la codifica #0 per i letterali appartenenti ad A nel seguente modo:
#0 (aj ) = 2 · j + 1
Perciò la codifica #0 restituirà sempre valori dispari.
Consideriamo poi una certa parola wj = a1j . . . anj , ovvero una parola con n letterali qualsiasi al suo interno (abbiamo preso quelli da a1 ad an per comodità),
identificati con j ad apice per non confonderli con letterali di altre parole).
Definiamo la codifica #1 per le parole appartenenti ad A? nel seguente modo:
j
j
j
# (anj )
#1 (wj ) = 2#0 (a1 ) · 3#0 (a2 ) · 5#0 (a3 ) · . . . · pn 0
dove pn è l’n-esimo numero primo (ed n è la lunghezza della parola).
Definiamo separatamente il caso per la stringa vuota:
#1 (λ) = 0
La codifica #1 è decisamente interessante: si sfrutta il teorema per il quale
ogni numero ha una scomposizione in fattori primi diversa, dunque, ogni stringa passata in input a #1 avrà una codifica differente. Ciò è immediatamente
intuibile dal fatto che se due stringhe hanno lunghezza diversa allora la loro
24
Capitolo 2. Calcolabilità
scomposizione userà un numero di numeri primi differente (e quindi la moltiplicazione darà un risultato diverso); se invece due stringhe hanno lunghezza
uguale ma sono diverse almeno uno degli esponenti dei numeri primi sarà diverso e quindi #0 fornirà un esponente diverso (ergo la moltiplicazione darà
un risultato diverso).
Si noti infine che #1 restituisce sempre numeri pari (è certamente divisibile
per 2 poiché ha 2 nella sua scomposizione in fattori primi).
Attualmente le funzioni di codifica che abbiamo definito hanno queste caratteristiche:
#0 : A → N iniettiva e con codominio dispari
#1 : A? → N iniettiva e con codominio pari
Perciò, dato un certo valore intero che è codifica di una stringa su A sappiamo
immediatamente se tale codifica è codifica di una parola o di un letterale: basta
vedere se il codice è pari o dispari. Ovviamente nessuna delle due funzioni è
suriettiva, ad esempio 7 non è immagine di nessun codice per #1 (per avere 7
dovremmo avere anche tutti i primi fino a 7) e un numero pari qualsiasi non è
immagine di nessun codice per #0 .
Proseguiamo il nostro lavoro studiando come codificare le frasi (stringhe di
stringhe).
Data una certa frase W = w1 , w2 , . . . wq possiamo codificarla semplicemente
iterando l’uso di #1 :
# (wq )
#2 = 2#1 (w1 ) · 3#1 (w2 ) · 5#1 (w3 ) · . . . · pq 1
Abbiamo seguito lo stesso principio già sfruttato per definire #1 . Notiamo
che la codifica per le frasi restituisce sempre numeri pari con scomposizione in
fattori primi nella quale ogni primo ha esponente pari.
Un piccolo algoritmo di decodifica
Volendo possiamo scrivere un piccolo algoritmo di decodifica che, dato un
certo codice, ci restituisce la stringa ad esso associata:
1
2
3
4
5
6
7
8
//N = numero d i e l e m e n t i d e l l ’ a l f a b e t o #0
decode ( i ) {
i f ( i d i s p a r i && i < ( 2N +1) ) { // c o d i f i c a d e l l a l e t t e r a
r e t u r n ( i −1/2) ; // e q u a z i o n e i n v e r s a d i #0
} e l s e { // p a r o l a o f r a s e
smp = s c o m p o s i z i o n e i n f a t t o r i p r i m i d i i ;
i f ( smp ha una s e q u e n z a d i n u m e r i p r i m i a p a r t i r e da 2 s e n z a
interruzioni ) {
i f ( smp ha e s p o n e n t i d e i p r i m i t u t t i d i s p a r i )
2.4. Codifica delle macchine di Turing
r e t u r n e q u a z i o n e i n v e r s a d i #1 ;
e l s e i f ( smp ha e s p o n e n t i d e i p r i m i
r e t u r n e q u a z i o n e i n v e r s a d i #2 ;
e l s e r e t u r n ERROR ;
9
10
11
12
}
else
13
14
tutti
pari )
r e t u r n ERROR ;
}
15
16
25
}
Listing 2.1: Algoritmo di decodifica per le stringhe sull’alfabeto A.
Chiaramente l’algoritmo può anche restituire errore, giacché esistono codici
che non hanno alcuna immagine (cioè alcuna stringa associata).
2.4.2
Dalla numerazione di Göedel alla codifica delle MdT
Come sappiamo una MdT è un insieme di quintuple costruito sull’alfabeto
A con l’aggiunta del simbolo ?. Ma giacché la MdT lavora su stringhe ed
è costituta da stringhe possiamo usare la codifica proposta da Göedel per
identificarle. Chiaramente all’interno della codifica della MdT dobbiamo anche
includere gli stati (che sono finiti, quindi non è un problema) e i simboli ? e
+1 e −1 per identificare il movimento della testina a destra o a sinistra.
Codifica per i simboli delle MdT
Consideriamo dunque una MdT con alfabeto A = {a1 , . . . , aN } ∪ {?} e con
stati Q = {q1 , . . . , qN }. Definiamo la nostra funzione:
#0 : A ∪ Q ∪ {?} ∪ {−1, +1} → N
mediante la quale possiamo usare #0 per codificare tutti i simboli coinvolti
nell’algoritmo:
Simbolo
-1
+1
?
a1
···
aN
q0
q1
···
qn
Codifica #0
3
5
7
9
···
2N + 7
2N + 9
2N + 11
···
2N + 2n + 9
26
Capitolo 2. Calcolabilità
Codifica per le transizioni delle MdT
La MdT che stiamo esaminando, come ogni MdT, definisce il suo algoritmo
mediante una serie di quintuple. Tali quintuple sono quindi parole su tutti i
simboli che abbiamo appena codificato, dunque, possiamo chiaramente sfruttare #1 per codificarle.
Definiamo quindi #1 come segue:
#1 : (A ∪ Q ∪ {?} ∪ {−1, +1})∗ → N
Consideriamo ora una certa quintupla generica ω:
ω = (qi , s, qj , s 0 , P )
dove con P intendiamo la direzione di spostamento della testina che può essere
+1 o -1. La possiamo codificare come segue:
0
#1 (ω) = 2#0 (qi ) · 3#0 (s) · 5#0 (qj ) · 7#0 (s ) · 11#0 (P )
Codifica per l’insieme delle transizioni delle MdT
Perfetto: sappiamo codificare i simboli e grazie a quella codifica abbiamo
definito la codifica per le quintuple. Rimane solo più un piccolo problema:
sappiamo codificare una quintupla e non un insieme di quintuple. Chiaramente
possiamo utilizzare #2 per codificare le parole della MdT (cioè la sequenza di
quintuple), con l’accortezza di ordinare lessicograficamente l’insieme di quintuple della MdT prima di codificarle. Tale passaggio è necessario poiché #2
lavora su sequenze di parole e non su insiemi: se non ordiniamo le transizioni
due MdT identiche (cioè la stessa MdT) con le funzioni di transizione scritte
in ordine diverso avrebbero due codici diversi.
In definitiva, presa la sequenza Ω = ω1 , . . . , ωk costituente tutte le quintuple
della MdT ordinate lessicograficamente, possiamo usare #2 come segue per
codificarle:
# (ωk )
#2 (Ω) = 2#1 (ω1 ) · 3#1 (ω2 ) · 5#1 (ω3 ) · . . . · pk 1
Perfetto! Le MdT sono numerabili ed ora abbiamo anche una funzione #2 che
ne codifica il codice (le quintuple). Essendo una MdT un oggetto sintattico,
ogni MdT avrà una ed una sola codifica che è costruita proprio dal codice che
la definisce. D’ora in poi useremo la notazione:
Mj
per identificare la macchina di Turing per la quale la codifica #2 (M) restituisce
l’intero j.
2.4. Codifica delle macchine di Turing
27
Ottenere una funzione di codifica suriettiva
Sappiamo che la nostra codifica è iniettiva (per costruzione), ma sappiamo anche che non è suriettiva. Perciò, se ci chiedessimo ‘’qual è la terza macchina
di Turing¿’ non otterremmo M3 bensì una certa Mk che è la terza macchina
di Turing nella sequenza delle MdT. Questo fatto non è di per se un problema;
ciò che non ci piace è che esistano dei codici (ergo numeri naturali) che non
sono associati ad alcuna MdT.
La soluzione più banale può essere quella di enumerare tutte le MdT (avendo
quindi dei ‘’buchi‘’ fra gli indici) e creare una funzione g che associ al valore 1 la prima MdT della sequenza, 2 alla seconda, 3 alla terza e così via.
Evidentemente questo passaggio sarebbe trasparente verso l’esterno, si tratta
solo di aggiungere un passaggio di codifica fra la codifica effettiva k ottenuta
mediante #2 e il valore restituito in output da g. In sostanza è sempre #2
a fare gran parte del lavoro, mentre g si occupa solo di rimappare il risultato
su una sequenza di interi ‘’senza salti‘’. Alternativamente, sempre al fine di
avere una numerazione ‘’senza buchi‘’ (e di avere sempre una MdT associata
ad un certo codice), potremmo considerare la funzione sempre indefinita: tale
funzione, come ogni altra, avrà infinite macchine di Turing che la calcolano.
Potremmo dunque prendere quelle infinite MdT ed associarle manualmente a
tutti i codici che non hanno una MdT associata.
Quale che sia l’approccio scelto, alla fine otteniamo una funzione di codifica
che è sia iniettiva che suriettiva, in grado di identificare ed enumerare tutte le
macchine di Turing.
2.4.3
Codificare le funzioni calcolabili
Siamo finalmente giunti al punto cruciale: se sappiamo enumerare tutte le
MdT e ogni funzione calcolabile ha infinite MdT che la calcolano, allora siamo
anche in grado di enumerare ed identificare tutte le funzioni calcolabili. Perciò, quando ci riferiamo alla MdT M1 possiamo direttamente far riferimento
alla funzione ϕ1 che quella MdT calcola. Chiaramente ϕ1 avrà altre infinite
macchine di Turing che la calcolano, ma a noi questo non interessa: tendenzialmente siamo interessati a sapere se ϕ1 ha almeno una MdT che la calcola
(e quindi infinite).
2.4.4
Perché codificare MdT e non funzioni?
Fermiamoci un attimo a riflettere sul perché abbiamo scelto (si fa per dire,
noi non abbiam scelto nulla) di codificare le MdT e quindi gli algoritmi anziché codificare direttamente le funzioni come insieme di coppie di elementi del
dominio-elementi del codominio.
28
Capitolo 2. Calcolabilità
Mentre un algoritmo è finito, il numero di coppie definita da una funzione è
possibilmente infinito. Questo significa che l’insieme delle funzioni, considerate come insieme di coppie, è un insieme di insiemi infiniti: tale insieme sarebbe
di cardinalità continua e dunque non potremmo farci sostanzialmente nulla.
Al contrario, usando gli algoritmi possiamo identificare quali sono le funzioni calcolabili, che sono un insieme numerabile, e dunque lavorarci in maniera
effettiva.
2.5
Halt: una funzione non calcolabile
Abbiamo formalizzato con precisione il concetto di funzione calcolabile, ma
tale formalizzazione sarebbe semplicemente inutile se non esistessero funzioni
non calcolabili. In questa sezione impareremo a conoscere la funzione che
formalizza il problema dell’halt, che ritroveremo spesso in futuro (talvolta sotto
forme inaspettate).
Definiamo H(n, m) la funzione che restituisce 1 se la macchina di Turing Mn
termina (converge) con input m e che restituisce 0 se Mn non termina (diverge)
con input n. Ecco quindi la nostra H che calcola il problema dell’halt:
(
1 se Mn ↓ m
H(n, m) =
0 se Mn ↑ m
Dal punto di vista delle funzioni stiamo dicendo che H(m, n) deve valere 1 se
ϕn (m) è definita e 0 se ϕn (m) è indefinita.
Ci approcciamo alla dimostrazione di non calcolabilità con una tecnica classica
che consta di due passi:
1. Dimostriamo la non calcolabilità di una funzione più semplice di quella
che vogliamo dimostrare non calcolabile.
2. Effettuiamo una riduzione e dimostriamo che la non calcolabilità della funzione più semplice implica la non calcolabilità della funzione più
complessa.
Passo 1.
Consideriamo la funzione h(n):
(
1 se Mn ↓ n
h(n) =
0 se Mn ↑ n
che è evidentemente un caso particolare di H(n, m), precisamente quello in
cui m = n, ovvero abbiamo che l’indice della MdT è uguale all’input sul quale
questa deve lavorare.
2.5. Halt: una funzione non calcolabile
29
Dimostrazione per assurdo dell’incalcolabilità di h. Assumiamo per assurdo che
h sia calcolabile.
Grazie a tale assunzione siamo in grado di scrivere la tabella:
M0
M1
M2
M3
..
.
0
sì
?
?
?
..
.
1
?
no
?
?
..
.
2
?
?
sì
?
..
.
3
?
?
?
sì
..
.
···
···
···
···
···
..
.
Abbiamo semplicemente scritto per ogni MdT (ogni riga) il valore restituito da
h (quindi abbiamo potuto popolare solo la diagonale). Anziché 1 e 0 abbiamo
scritto sì e no per maggiore leggibilità, ma ovviamente non cambia nulla).
Costruiamo ora una macchina di Turing M ? che sfrutta la calcolabilità di h per
calcolare:
(
1 se h(n) = 0
h? (n) =
⊥ se h(n) = 1
Con ⊥ indichiamo il fatto che la funzione diverge. h? (n) si comporta in un
certo senso in maniera inversa rispetto a h(n) (anche se qui abbiamo una
divergenza anziché 0).
M ? è una MdT e quindi deve essere inclusa nella tabella che abbiamo scritto
poco fa. Ma d’altronde la riga che dovremmo scrivere è necessariamente
diversa da ogni altra riga contenuta nella tabella poiché per costruzione il
primo elemento sulla riga di M ? sarà diverso dal primo elemento sulla riga di
M1 , il secondo elemento sulla riga di M ? sarà diverso dal secondo elemento
sulla riga di M2 e così via.
In sostanza, usando la diagonalizzazione abbiamo dimostrato che M ? non fa
parte della tabella di tutte le macchine di Turing (dando quindi origine ad un
assurdo) ergo h? non è calcolabile. Ma giacché h? si basa solamente su h per
definire il suo valore, allora h non è calcolabile.
Quella appena proposta non è l’unica dimostrazione di non calcolabilità di h
possibile. Vediamone un’altra, tanto per impratichirci.
Dimostrazione per assurdo dell’incalcolabilità di h (2). Assumiamo per assurdo che h sia calcolabile.
Grazie a tale assunzione siamo in grado di definire una certa funzione:
(
1 se h(n) = 0
h? (n) =
⊥ se h(n) = 1
30
Capitolo 2. Calcolabilità
h? (n) è calcolabile, dunque avrà un’indice ereditato da una delle infinite macchine di Turing che la calcola. Possiamo quindi attuare un semplice cambio
notazionale:
h? (n) = ϕe (n)
Vediamo come si comporta ϕe sul particolare valore e:
(
1 se h(e) = 0
ϕe (e) =
⊥ se h(e) = 1
Ciò che abbiamo scritto è un assurdo. Infatti, h(e) = 0 bimplica il fatto che
ϕe (e) non sia definito (è la definizione di h) e analogamente h(e) = 1 bimplica
il fatto che ϕe (e) sia definito. Abbiamo quindi scritto che una funzione deve
essere definita quando la stessa funzione è indefinita e viceversa.
Dato che abbiamo ottenuto l’assurdo possiamo asserire che h? non è calcolabile. Ma giacché h? si basa solamente su h per definire il suo valore, allora
anche h non è calcolabile.
Passo 2 (riduzione). Concentriamoci ora sul secondo passo della dimostrazione: la riduzione. Vogliamo dimostrare che H non è calcolabile sfruttando il
fatto che h non è calcolabile. Ciò si traduce in formule logiche come:
¬calcolabilità h ⇒ ¬calcolabilità H
Che è equivalente6 a dire:
calcolabilità H ⇒ calcolabilità h
Dobbiamo perciò in qualche modo dimostrare che la calcolabilità di H implica
la calcolabilità di h, dopodiché usando il fatto che h non è calcolabile avremo
trovato un assurdo e potremo asserire che H non è calcolabile.
Dimostrazione per assurdo della non calcolabilità di H. Assumiamo per assurdo H calcolabile.
Se H è calcolabile siamo in grado di calcolarla anche il caso particolare H(m, n)
con n = m. Evidentemente calcolare questa funzione implica saper calcolare
h(n) che noi abbiamo già dimostrato essere non calcolabile.
Aver dimostrato che il problema dell’halt non è calcolabile non è una bella
notizia. Infatti, abbiamo appena provato che non è possibile verificare staticamente se un programma termini o meno. Brutta notizia per chi costruisce
6
A ⇒ B ≡ ¬B ⇒ ¬A
2.6. Macchine di Turing a più nastri
31
compilatori per lavoro, eh?
D’altro canto, se limitiamo il problema dell’halt ad un caso meno generale di
quello che abbiamo studiato potremmo riuscire ad implementare una verifica
di terminazione statica.
Pensiamo ad esempio all’uso di tipi7 : in linguaggi ben tipati (ad esempio Haskell) è possibile proprio grazie ai tipi garantire che il programma termini o
meno a tempo di compilazione.
Esercitazione: si veda l’esercizio 4.1.4, l’esercizio 4.1.4 e l’esercizio 4.1.4.
2.6
Macchine di Turing a più nastri
Prima di proseguire sui nostri discorsi relativi alla calcolabilità andiamo a ‘’potenziare‘’ la macchina di Turing descrivendone una versione che utilizza un
numero arbitrario di nastri. Il termine potenziare è in realtà improprio, poiché,
come stiamo per imparare, la potenza di una MdT a n nastri è identica a quella
di una MdT con un nastro solo.
2.6.1
Definizione della MdT a più nastri
Consideriamo dunque di avere n nastri di cui il primo è il nastro di I/O, adoperato solamente per leggere l’input e riporvi l’output al termine della computazione. Gli altri n − 1 nastri fungono invece da nastri di lavoro sui quali avviene
la vera e propria computazione.
Per poter implementare un sistema del genere dobbiamo complicare leggermente la funzione di transizione. Infatti, dobbiamo leggere contemporaneamente
da tutti i nastri, scrivere contemporaneamente su tutti i nastri e spostare la testina su tutti i nastri. Perciò la funzione di transizione passa da essere definita
come:
δ : Q × A? → Q × A? × {+1, −1}
ad essere definita come:
δ : Q × A? × . . . × A? → Q × A? × . . . × A? × {+1, −1}
dove A? è ripetuto n volte nel dominio (un simbolo letto per per ogni nastro)
ed n volte nel codominio (un simbolo scritto per per ogni nastro). La nostra
quintupa diventerà quindi una sequenza di 2n + 3 valori nella forma:
(q, a0 , . . . , an , q 0 , b0 , . . . , bn , P )
dove P definisce lo spostamento e vale +1 o -1.
7
Un tipo è un’annotazione semantica su un elemento sintattico.
32
2.6.2
Capitolo 2. Calcolabilità
Più nastri, stessa potenza
Come abbiamo accennato poco fa, aumentare il numero di nastri impiegabili
da una MdT non aumenta la potenza del sistema di calcolo. In altre parole,
tutti gli algoritmi rappresentabili con MdT a n nastri sono rappresentabili da
MdT con un solo nastro (in maniera evidentemente molto più scomoda).
Esiste di fatto un teorema che ci assicura che quanto detto sia vero:
Teorema 2.2. Per ogni MdT M = (Q, A, δ, q0 ) su n nastri ausiliari esiste una
MdT M 0 = (Q0 , A0 , δ 0 , q0 ) con A ⊆ A0 tale che ciò che è calcolabile mediante
M è calcolabile anche mediante M 0 .
Questo teorema non ha in realtà una dimostrazione rigorosa ma sfrutta la tesi
di Church-Turing per la quale qualsiasi linguaggio di programmazione con formalismi standard ha la stessa potenza della macchina di Turing.
Possiamo anche dare una dimostrazione intuitiva (non formale) del fatto che
usare più nastri non modifichi la potenza del sistema di calcolo. Infatti, ragionare su n nastri è come ragionare su un nastro solo suddiviso in n sezioni
separate da un simbolo speciale, ad esempio ∇, aggiunto appositamente ad
A. A questo punto potremo computare su diversi nastri sfruttando particolari transizioni definite ad hoc che ci spostino nelle ‘’zone‘’ diverse dell’unico
nastro di cui disponiamo. Attenzione: giacché i nastri sono fra loro collegati,
lo spostamento o l’eliminazione di un elemento in un nastro potrebbe influenzare anche gli altri nastri. Pensiamo ad esempio all’aggiunta di un elemento:
dovremmo traslare la sezione di nastro sulla quale stiamo lavorando e tutti i
nastri successivi in avanti per fare spazio al nuovo simbolo.
Dal punto di vista della complessità, come afferma la tesi di Church-Turing, la
differenza fra le complessità di macchine ad un nastro e macchine a n nastri
(posto ovviamente che calcolino la stessa funzione) è al massimo polinomiale.
2.7
Equivalenza fra algoritmi: non calcolabile
Torniamo a parlare di funzioni non calcolabili e dimostriamo la non calcolabilità
dell’equivalenza fra algoritmi.
Formalmente, due algoritmi sono equivalenti quando calcolano la stessa funzione mentre sono identici quando sono uguali dal punto di vista sintattico.
È ovviamente calcolabile l’identicità fra algoritmi, mentre, come stiamo per
apprendere, non ne è calcolabile l’equivalenza. La capacità di definire equivalenti due algoritmi sarebbe interessante per molte applicazioni, ad esempio per
verificare la proprietà di trasparenza referenziale (anche detta sostituibilità),
ovvero la capacità di sostituire un algoritmo con un’altro equivalente all’interno
2.7. Equivalenza fra algoritmi: non calcolabile
33
di un determinato codice senza modificare l’output finale della computazione.
Osserviamo che dimostrare la non calcolabilità dell’equivalenza fra algoritmi è
come dimostrare la non calcolabilità dell’equivalenza tra funzioni parziali calcolabili.
Due funzioni parziali ϕ e ψ sono equivalenti quando:
dom(ϕ) = domdef (ϕ) = domdef (ψ) ∧ ∀ x ∈ domdef (ϕ) (ϕ(x) = ψ(x))
ovvero, due funzioni parziali sono equivalenti quando il dominio dei valori sui
quali sono definite è lo stesso e per i punti sui quali sono definiti il valore di output è lo stesso. In altre parole due funzioni parziali sono uguali
quando definiscono esattamente lo stesso insieme di coppie elementoDominoelementoCodominio.
In definitiva vogliamo dimostrare la non calcolabilità della funzione:
(
1 se Mx ∼ My ⇔ ϕx ∼ ϕy
EQ(x, y ) =
0 altrimenti
dove ∼ è il simbolo che adoperiamo per indicare l’equivalenza fra algoritmi/funzioni parziali.
La dimostrazione di non calcolabilità che stiamo per affrontare è piuttosto
complessa, eccone quindi un’outline introduttiva:
1. Mediante l’uso della diagonalizzazione dimostreremo che non è possibile
(non calcolabile, indecidibile, ecc.) dare una numerazione effettiva delle
funzioni calcolabili totali.
2. Dimostreremo che non è calcolabile la funzione:
(
1 se Mx ∼ i d ⇔ ϕx ∼ i d
eq(x) =
0 altrimenti
dove i d è la funzione identità (avremmo potuto sceglierne una qualsiasi).
L’idea è quella di prendere un algoritmo fisso (i d, appunto) e dimostrare
che non è possibile verificare che Mx sia equivalente a quell’algoritmo
fisso.
3. Passo di riduzione: la non calcolabilità di eq implica la non calcolabilità
di EQ.
2.7.1
Le funzioni totali calcolabili non sono effettivamente numerabili
Una funzione totale, come sappiamo, è una funzione definita su tutto il suo
dominio e che quindi non diverge mai. Noi siamo in grado di identificare e
34
Capitolo 2. Calcolabilità
numerare le funzioni parziali, delle quali le funzioni totali sono un sottoinsieme.
Vogliamo dimostrare in questa sede che non è possibile fornire una numerazione
effettiva per le funzioni totali.
Ricordiamo che avere una funzione di numerazione effettiva significa avere una
funzione del tipo:
f :N→N
tale per cui f (i ) = i-esima funzione nella numerazione generale delle funzioni.
Pertanto, supponendo che ϕ1 sia totale, ϕ2 e ϕ3 non lo siano e ϕ4 lo sia, ci
aspettiamo che il valore di f (2) sia 4 (l’indice della seconda funzione totale:
ϕ4 ).
Dimostrazione per assurdo della non esistenza di f . Assumiamo per assurdo che
f sia calcolabile e dunque che l’insieme delle funzioni totali calcolabili sia effettivamente numerabile. Grazie ad f possiamo definire l’insieme delle funzioni
totali calcolabili:
Φ = {ϕf (0) , ϕf (1) , ϕf (2) , . . . }
Assumiamo poi di avere una certa funzione g(x) totale calcolabile così definita:
g(x) = ϕf (x) + 1
Abbiamo quindi scritto che l’output di g(x) è il risultato della computazione
della funzione ϕf (x) (quindi un numero naturale) aumentato di 1. Non abbiamo
specificato il valore di input di ϕf (x) perché in realtà non ci interessa.
Ci apprestiamo ora a dimostrare che g(x) non è inclusa nell’insieme Φ e quindi
ad ottenere un’assurdo.
Diciamo innanzi tutto che essendo g totale e calcolabile avrà un certo indice
f (i ) all’interno di Ψ. Perciò, possiamo adottare il cambio notazionale:
g = ϕf (i)
Andiamo ora a vedere il valore che assume g (cioè ϕf (i) ) proprio su i :
g(i ) = ϕf (i) = ϕf (i) + 1
Guardando l’ultima equivalenza l’assurdo è lampante: abbiamo appena asserito
che un naturale è uguale se stesso aumentato di 1.
Abbiamo ottenuto un assurdo, quindi f non è calcolabile, quindi non è possibile
avere una numerazione effettiva delle funzioni totali calcolabili.
Il risultato che abbiamo appena ottenuto è abbastanza intuitivo dato ciò che
sappiamo in merito alla funzione dell’halt: per discernere una funzione totale
da una che diverge almeno in un punto dovremmo essere in grado di sapere
quando una funzione diverge, ovvero, risolvere il problema dell’halt (che sappiamo essere non calcolabile).
2.7. Equivalenza fra algoritmi: non calcolabile
35
Possiamo anche compiere un’altra importante riflessione: l’insieme delle funzioni totali e calcolabili è un sottoinsieme delle funzioni calcolabili; ma nonostante il secondo sia effettivamete numerabile il primo non lo è (anche se
ovviamente ha cardinalità numerabile). Questo accade per via del fatto che
siamo in grado di dare una definizione del sottoinsieme delle funzioni totali (mediante la nostra f ) ma non siamo in grado di estrarre la caratteristica
saliente (in questo caso il fatto che una funzione diverga o meno in almeno un punto) dagli elementi del sovrainsieme per numerare effettivamente il
sottoinsieme.
2.7.2
L’equivalenza con l’identità non è calcolabile
Procediamo ora col secondo passo della dimostrazione, nel quale useremo
quanto abbiamo appreso nel primo passo per arrivare all’assurdo. Consideriamo
la funzione:
(
1 se Mx ∼ i d ⇔ ϕx ∼ i d
eq(x) =
0 altrimenti
Come già detto, l’idea è quella di prendere un algoritmo fisso (la funzione
identità in questo caso) e dimostrare che non è possibile verificare che Mx sia
equivalente a quell’algoritmo fisso. Ricordiamo che la funzione identità è una
funzione che si comporta nel seguente modo:
i d(x) = x
ovvero è una funzione totale e calcolabile che restituisce come output l’input
che gli viene dato.
Dimostrazione per assurdo della non calcolabilità di eq. Assumiamo per assurdo che eq sia calcolabile.
Consideriamo poi la funzione:
(
y se Mx ↓ y ⇔ ϕx (y ) è definita
ξ(x, y ) =
⊥ altrimenti
ξ è una funzione parziale e calcolabile8 . Trasformiamo ora ξ in una funzione
unaria rendendo il suo argomento x un parametro. Otteniamo così infinite
funzioni ψx definite come segue:
(
y se ϕx (y ) è definita
ψx (y ) =
⊥ altrimenti
Non abbiamo fatto niente di straordinario: anziché usare x come argomento
di una funzione lo usiamo come parametro e dunque lo cabliamo all’interno
8
È semplice scrivere un algoritmo che calcoli ξ. Si riveda l’Esercizio 4.1.4 per maggiori
chiarimenti.
36
Capitolo 2. Calcolabilità
di ψ. Il valore di x che cabliamo dipende ovviamente dal valore indicato a
pedice della funzione, x, appunto. Ecco quindi che da ξ abbiamo ottenuto una
famiglia di funzioni ψx che si comportano in maniera identica rispetto a ξ.
Aver trasformato x da argomento a parametro ci permette di fare un ragionamento importantissimo: ogni funzione ψx che abbiamo ottenuto specificherà
nel suo codice x (cosa che non accadeva quando x era argomento), dunque, possiamo dire che la codifica del codice di ψx è funzione di x. Abbiamo
appena applicato il teorema Snm di Kleene (per maggiori dettagli si veda la
Sezione 2.7.4).
Ricapitolando, essendo ψx (y ) calcolabile (lo era ξ) avrà un certo codice nella
numerazione delle funzioni. Tale codice è però funzione di x, il parametro che
figura all’interno del codice. Possiamo dunque attuare un semplice cambio
notazionale:
ψx = ϕf (x)
Si osservi che f è la funzione di codifica usata per numerare le MdT (era
indicata come # nella numerazione di Göedel che abbiamo introdotto noi).
Rileggiamo ora la definizione di ϕf (x) con la nuova notazione:
ϕf (x) =
(
y
⊥
se ϕx (y ) è definita
altrimenti
È facile vedere che se ϕf (x) converge allora si comporta proprio come la funzione identità. Il problema è che ϕf (x) è una funzione parziale e dunque non
abbiamo garanzia di convergenza.
Possiamo però dire che se ϕf (x) è una funzione totale, allora si comporta come la funzione identità, cioè ϕf (x) (y ) = y . Se ne deduce che chiedersi se
ϕf (x) ∼ i d è uguale a chiedersi se ϕf (x) è totale e viceversa. Ma noi sappiamo
da quanto appreso al passo 1 che non possiamo calcolare se una funzione è
totale o meno, dunque l’equivalenza fra ϕf (x) (y ) e i d non è calcolabile.
In altre parole, saper dire che ϕf (x) ∼ i d implicherebbe saper dire che ϕf (x) è
totale, cosa che abbiamo dimostrato essere impossibile da calcolare.
2.7.3
Passo di riduzione
Non ci resta che effettuare l’ultimo passo di riduzione. Ricordiamo la definizione di EQ:
(
1 se ϕx ∼ ϕy
EQ(x, y ) =
0 altrimenti
2.7. Equivalenza fra algoritmi: non calcolabile
37
Come già effettuato in precedenza, vogliamo dimostrare che:
¬calcolabilità eq ⇒ ¬calcolabilità EQ
e quindi che:
calcolabilità EQ ⇒ calcolabilità eq
Dimostrazione per assurdo della non calcolabilità di EQ. Assumiamo per assurdo EQ calcolabile.
Se EQ è calcolabile siamo in grado di calcolare anche il caso particolare
EQ(x, i ) dove i è l’indice della funzione identità all’interno della numerazione
delle funzioni. Evidentemente calcolare questa funzione implica saper calcolare
eq (ϕy sarebbe la funzione identità) che noi abbiamo già dimostrato essere
non calcolabile.
2.7.4
Il teorema Snm (cenni)
Il teorema Snm , opera del matematico Stephen Kleene, è uno dei teoremi fondamentali utilizzati nelle dimostrazioni di calcolabilità; noi stessi l’abbiamo usata
e la useremo diverse volte.
Non è di nostro interesse studiarne nel dettaglio la dimostrazione, ma, data
la sua importanza, proviamo a fornire delle basi formali che ne spieghino la
potenza.
Come sappiamo il teorema Snm ci permette di ‘’trasformare‘’ l’input di una
funzione in un parametro. Nel nostro caso tale operazione è stata compiuta
aal fine di dire che una MdT M che calcola una funzione ϕx parametrica in
x ha una codifica che è g(x) dove g è la funzione di codifica delle MdT che
stiamo utilizzando (e.g., la codifica di Gödel). Perciò, possiamo identificare
la nostra MdT col nome Mg(x) . Chiariamo quanto appena detto adottando
approccio più formale.
Il teorema Snm asserisce la seguente uguaglianza fra numeri naturali:
ϕz (x1 , . . . , xn , y1 , . . . yn ) = ϕSnm (z,y1 ,...,ym ) (x1 , . . . xn )
Come da convenzione standard, a pedice abbiamo rappresentato i parametri
di ϕ. Possiamo osservare che siamo passati da una funzione di arità (m + n)
ad una funzione di arità n preservandone l’uguaglianza sul risultato. Tutto
ciò è possibile grazie ad S, una funzione che ci restituisce la codifica della
funzione di arità n (la seconda). In definitiva, Kleene col suo teorema ci dice è
che possibile ‘’congelare‘’ un certo numero di input di una funzione e renderli
parametri, creando così una famiglia di infinite funzioni che differiscono fra loro
sul valore dei parametri. Si osservi che l’uguaglianza dimostrata da Kleene è
38
Capitolo 2. Calcolabilità
fra numeri naturali (i.e., i valori calcolati dalle due funzioni), ovviamente le
due funzioni sono diverse fra loro.
Il teorema Snm si può anche descrivere dicendo che, considerata la funzione
ϕz (x1 , . . . , xn , y1 , . . . yn ), esiste una funzione ricorsiva primitiva Snm di m + 1
input che fornisce il numero di Gödel di una funzione delle restanti variabili
(i.e., x1 , . . . ,) che dia lo stesso risultato.
2.8
La macchina di Turing universale
Introduciamo in questa sezione l’importantissima nozione di macchina di Turing
universale. La macchina di Turing universale è una macchina di Turing in grado
di simulare ogni altra macchina di Turing (compresa se stessa).
Ciò che stiamo dicendo è che esiste una MdT U tale che:
U(x, y ) = ϕx (y )
La dimostrazione dell’esistenza di questa macchina è in realtà piuttosto banale
banale: informalmente, ci basta, per ogni scelta di x ed y , prima decodificare
x per ottenere la MdT x-esima Mx , e poi far lavorare Mx su y .
Una formalizzazione un po’ meno nebulosa può consistere nel descrivere come
è fatta µ.
U può essere pensata come una macchina con due nastri ausiliari (sappiamo
infatti che questo modello è equivalente a quello di una MdT con un solo
nastro). Dapprima U controlla che il suo input sia una coppia (M, y ), per
qualche macchina di Turing M = Mx e qualche input y per M. Poi U passa
a simulare M sull’input y . In particolare, U adopera il primo nastro ausiliario
per mantentenere traccia degli stati e della posizione della testina di M e il
secondo nastro per memorizzare le istruzioni di M. Così, U copia, di volta in
volta, una configurazione (α, q, a, β) di M dal nastro di input/output sul primo
nastro ausiliario; poi per determinare la regola di transizione (q, a, q 0 , a0 , b) di
M da usare, U estrae q ed a, quindi determina (q 0 , a0 , b) cercando nel secondo
nastro l’istruzione per (q, a). A quel punto si comporta come dettto dalla
terna (q 0 , a0 , b) per sviluppare sul nastro di input/output la sua computazione.
2.8.1
La completezza delle macchine di Turing
Perché U è tanto importante? A parte per la sua comodità (è spesso utilizzata
nelle dimostrazioni), il fatto che U esista è paradossale ma allo stesso tempo
necessario.
Infatti, U è detto interprete semi-circolare poiché se come argomento x passiamo proprio l’indice di U (uno degli infiniti indici di U per essere precisi) allora
2.9. Interleaving su esecuzioni parallele
39
avremo una macchina che interpreta sé stessa. D’altronde, l’assenza di questo
paradosso proverebbe in un certo senso che le macchine di Turing non sono
complete poiché esiterebbe qualcosa che non possono calcolare ma che è in
realtà calcolabile.
2.9
Interleaving su esecuzioni parallele
Affrontiamo in questa sezione un nuovo problema, ovvero come formalizzare
l’interleaving di più macchine di Turing. Più precisamente, vogliamo eseguire
in parallelo diverse MdT su uno stesso input dato.
2.9.1
La funzione STEP
È intuitivo pensare che se vogliamo implementare l’interleaving fra MdT sia
necessario in qualche modo ‘’maneggiare‘’ i passi9 delle MdT. A tal scopo
definiamo la funzione STEP:
(
1 se Mx (y ) si ferma su un numero di passi ≤ z
STEP(x, y , z) =
0 altrimenti
Ciò che vogliamo dimostrare è che questa funzione è calcolabile e lo facciamo
dimostrando che, di fatto, si tratta di una lieve modifica della macchina Mx in
grado di contare i passi di esecuzione. Almeno da un punto di vista intuitivo
dovrebbe essere chiaro che ogni qualvolta si esegua una funzione di transizione
di Mx si deve in realtà transire in una serie di stati che permettano l’incremento
del counter dei passi, per poi eseguire effettivamente la transizione di partenza. Se chiamiamo M c (z) la macchina di Turing in grado di incrementare un
contatore e che raggiunto il limite z restituisce 1, allora possiamo dire di voler
comporre Mx e M c affinché per ogni transizione effettuata da Mx M c esegua
per intero.
Vediamo dunque comporre due generiche MdT che calcolano due funzioni
qualsiasi f e g, dopodiché applicheremo un ragionamento simile per comporre
le nostre macchine Mx ed M c .
Composizione di macchine di Turing
Prendiamo dunque in considerazione due funzioni calcolabili siffatte:
f,g : N → N
Vogliamo dimostrare che f ◦ g è calcolabile, ricordando che la composizione di
funzioni si effettua come segue:
f ◦ g = f (g(x))
9
Intendiamo per passo l’applicazione di una funzione di transizione.
40
Capitolo 2. Calcolabilità
Iniziamo considerando le due MdT:
• M f che calcola la funzione f .
• Mg che calcola la funzione g.
La macchina M f ◦g che calcola la composizione di f e g dovrà evidentemente
contenere tutte le quintuple di M f e tutte le quintuple di M g . Possiamo
costruire tale macchina seguendo questi semplici passi:
1. Gli stati delle MdT che vogliamo comporre devono avere tutti gli stati
disgiunti, quindi per prima cosa andiamo a rinominarli affinché abbiano
nomi distinti. Chiamiamo gli stati di M g q0 , . . . , qn e gli stati di M f
qn+1 , . . . , qk .
2. Data la definizione di composizione vogliamo che M g esegua prima di
M f . Perciò, se qi è lo stato finale di M g e qj è lo stato iniziale di M f
non dovremo far altro che far sì che qi e qj siano coincidenti (cioè lo
stesso stato). Grazie a questa semplice modifica abbiamo fatto sì che
al termine dell’esecuzione di M g inizi l’esecuzione di M f .
Possiamo sfruttare questa tecnica anche per calcolare la funzione STEP.
Consideriamo una transizione qualsiasi di Mx , diciamo (q, s, q 0 , s 0 , P ). Se supponiamo che lo stato iniziale della macchina M c sia un certo qi , sarà sufficiente
modificare la transizione considerata in (q, s, qi , s 0 , P ) e impostare a q 0 lo stato
finale di M c . Prima di compiere tale operazione sarà chiaramente necessario
verificare che Mx e M c abbiano tutti gli stati disgiunti.
La tecnica appena descritta deve essere effettuata per ogni transizione di Mx ,
affinché ogni operazione (quindi ogni passo) venga correttamente conteggiato
grazie all’esecuzione di M c .
Avendo noi mostrato un’algoritmo per il calcolo di STEP, possiamo asserire
che tale funzione è calcolabile.
2.9.2
Macchine parallele
Adesso che disponiamo della funzione STEP possiamo finalmente programmare la macchina parallela, ovvero una macchina che porta avanti mediante
interleaving più esecuzioni di macchine di Turing diverse.
A seconda di quante macchine di Turing eseguono parallelamente abbiamo un
diverso grado di parallelismo.
2.9. Interleaving su esecuzioni parallele
41
Macchina parallela con grado di parallelismo pari a 2
Definiamo la funzione: INTER(x, y , z) la funzione che esegue in parallelo il
codice di Mx ed My sull’input z. Nel momento in cui una delle due macchine
termina anche INTER termina. Se né Mx né My terminano allora INTER non
termina. È facile scrivere un algoritmo che calcoli questa funzione:
1
2
INTER ( x , y , z ) {
i = 0;
3
w h i l e (STEP( x , z , i ) == 0 ∧ STEP( y , z , i ) == 0 ) {
i ++;
}
4
5
6
7
i f (STEP( x , z , i ) == 1 ) {
U(x , z)
} else {
U(y , z)
}
8
9
10
11
12
13
}
Listing 2.2: Algoritmo per il calcolo di INTER con grado di parallelismo pari a 2.
L’algoritmo è semplicissimo: facciamo eseguire Mx ed My mediante la funzione
STEP per un numero crescente di passi (grazie all’impiego di i ). Finché le due
STEP restituiscono 0 sappiamo che nessuna delle due macchine ha terminato
entro i -passi. Se nessuna delle due macchine termina allora il while si potrarrà
all’infinito, ma appena una delle due macchine termina il loop si interrompe. Il
controllo che segue permette di capire quale delle due macchine ha terminato
per prima e, una volta identificata, ne fa partire l’esecuzione sfruttando U (così
da avere l’output). È chiaro che questa soluzione sia terribilmente inefficiente.
Avendo noi mostrato un’algoritmo per il calcolo di INTER, possiamo asserire
che tale funzione è calcolabile. Ergo, l’interleaving con grado di parallelismo
pari a 2 è possibile.
Macchina parallela con grado di parallelismo k
È immediato costruire una macchina parallela con grado di parallelismo k a
partire dal codice che abbiamo appena visto in Listing 2.2. Sarà infatti sufficiente avere una INTER con arietà k + 1 (k indici di macchine più l’input),
k controlli su k esecuzioni di STEP all’interno del while. Infine, k rami if-else
per identificare qualche macchina sia terminata per prima fra le k che hanno
eseguito.
42
Capitolo 2. Calcolabilità
Macchina parallela con grado di parallelismo infinito
Dimostrare che è possibile calcolare una funzione che implementi il parallelismo
infinito non è così banale.
Iniziamo a trattare il problema in un caso semplificato, ovvero supponiamo
di voler mettere in parallelo tutte le macchine di Turing possibili. In seguito
invece vedremo come creare una macchina che esegua in parallelo su un numero
infinito di macchine definite da un insieme di funzioni infinito scelto a monte.
L’idea più banale che viene in mente per risolvere il problema posto consiste
nel creare due cicli annidati che scorrono sui passi e sulle MdT così da eseguire
ogni MdT per ogni numero di passi possibile. Apprestandoci alla scrittura di
tale codice, però, ci renderemmo conto che così facendo incapperemmo nel
rischio di una derivazione infinita senza la garanzia di aver provato tutte le
MdT con tutti i passi possibili. Vi sono infatti due casi:
• Se il ciclo esterno fissa le MdT ed il ciclo interno fissa i passi, eseguiamo
una singola MdT per tutti i passi possibili prima di passare alla MdT
successiva. Ma, se una MdT dovesse non terminare mai, il loop interno
sui passi non volgerebbe mai al termine e quindi ci impalleremmo sulla
MdT considerata senza aver mai neanche provato le successive.
• Se il ciclo esterno scorre sui passi ed il ciclo interno scorre sulle MdT,
al primo loop esterno eseguiremmo tutte le MdT su un passo: essendo
però le MdT infinite, tale loop non terminerebbe mai.
La difficoltà che abbiamo appena riscontrato potrebbe aver risvegliato in qualche lettore il sentore di qualcosa di già visto quando abbiamo cercato di ordinare
tutte le coppie di numeri naturali. Il problema che vogliamo risolvere risulta
infatti del tutto analogo al problema di ordinare le coppie di numeri naturali,
dato che vogliamo eseguire ogni macchina di Turing (identificata da un naturale) per ogni numero di passi (un altro naturale). Possiamo
dunque sfruttare
Y
Y
il prezioso lavoro di Cantor ed adoperare le funzioni
(i ) e
(i ) che, dato
1
2
un certo indice ci restituiscono la coppia di naturali associata (rispettivamente
restituiscono il primo elemento ed il secondo elemento della coppia). Ecco
dunque il codice per la macchina di Turing che implementa l’interleaving con
grado di parallelismo infinito:
1
2
3
4
INTERinf ( z ) {
i = 0 ; // i n d i c e c o p p i a
Y
x =
(i) ; // m a c c h i n a
Y1
p =
(i) ; // p a s s i
2
5
6
7
8
w h i l e (STEP( x , z , p ) == 0 ) {
i ++;
Y
x =
(i) ;
Y1
p =
(i) ;
2
2.10. La nozione di semidecidibilità
43
}
9
10
U(x , z)
11
12
}
Listing 2.3: Algoritmo per il calcolo di INTER con grado di parallelismo infinito.
Abbiamo quindi dimostrato la calcolabilità della funzione:
(
ϕx (z) se x è il primo valore k per cui ϕk (z) è definito
INTERinf (z) =
⊥
altrimenti
Come abbiamo già appurato, l’interleaving che abbiamo costruito lavora su
tutte le MdT. Se invece avessimo un insieme infinito di funzioni (ma non tutte)
che vogliamo calcolare all’interno della macchina con parallelismo infinito, non
dovremmo far altro che verificare, nel momento in cui estriamo l’indice di una
macchina di Turing, verificare se quell’indice è associato ad una delle funzioni
che ci interessa calcolare o meno. Se lo è, proseguiamo come abbiamo appena
visto, se non lo è passiamo alla coppia successiva.
2.10
La nozione di semidecidibilità
Fin’ora abbiamo sempre suddiviso le funzioni in due insiemi: funzioni calcolabili
e funzioni non calcolabili. Abbiamo poi affermato che, dal punto di vista delle
MdT, se una funzione è calcolabile allora è detta decidibile mentre se non è
calcolabile è detta indecidibile. Ma osservando la definizione della MdT (Sezione 2.2) noteremo che esiste anche la nozione di semidecidibilità in relazione
alla capacità della MdT di riconoscere un linguaggio.
Prima di proseguire ricordiamo un po’ di definizioni. Consideriamo un linguaggio L ⊆ N:
1. Un linguaggio L è decidibile se esiste una MdT che lo decide, ovvero che
risponde sì se l’input appartiene ad L e no altrimenti.
2. Un linguaggio L è semidecidibile se esiste una MdT che lo accetta, ovvero
termina su tutti e soli i valori di input che appartengono ad L.
3. Un linguaggio è per definizione un insieme, dunque, possiamo anche dire
che un insieme L è decidibile se la sua funzione caratteristica:
(
1 se x ∈ L
f (x) =
0 altrimenti
è calcolabile.
44
Capitolo 2. Calcolabilità
4. Un problema P è decidibile se è calcolable la funzione:
(
1 se P (x) ha soluzione
f (x) =
0 altrimenti
Vi sono dunque tre punti di vista diversi secondo i quali considerare la decidibilità: linguaggi, insiemi e problemi. Risulta naturale portare la definizione
di decidibilità dai linguaggi agli insiemi e ai problemi, mentre è più sottile
effettuare la stessa operazione con la definizione di semidecidibilità.
2.10.1
Definizioni equivalenti di semidecidibilità
Studiamo ora un teorema fondamentale che asserisce l’equivalenza fra la definizione di semidecidibilità e due definizioni relative alle funzioni (che possiamo
quindi usare quando parliamo di insiemi e di problemi).
Teorema 2.3. Le tre proposizioni:
(a) L è semidecidibile.
(b) L è dominio di una funzione calcolabile.
(c) L o è vuoto o è l’immagine di una funzione totale calcolabile.
sono fra loro equivalenti.
Andiamo ora a dimostrare le implicazioni che abbiamo appena enunciato.
(a) implica (b). Dimostrare l’implicazione fra la proposizione (a) e la proposizione (b) è immediato.
Infatti, se è vero che il linguaggio L è semidecidibile dovrà esistere una MdT
M L che calcola la funzione:
(
1 se x ∈ L
ψ(x) =
⊥ altrimenti
Utilizzando la definizione classica di dominio (il dominio di una funzione è
l’insieme dei punti per cui la funzione converge), è evidente che ψ abbia per
definizione L come domino.
(b) implica (a). Anche il lato inverso della dimostrazione è di immediata dimostrazione.
Infatti, se disponiamo di una funzione calcolabile il cui dominio è L (anche qui
usiamo la definizione classica per cui il dominio di una funzione è l’insieme dei
punti per cui la funzione converge) allora potremo semplicemente usare quella stessa funzione per costruire la MdT che accetta L. Infatti, è sufficiente
2.10. La nozione di semidecidibilità
45
richiamare ϕL (la funzione calcolabile con dominio L) sull’input fornito: se la
computazione termina significa l’elemento passato in input apparteneva ad L,
poiché il dominio di ϕL sono tutti e soli i valori di L e quindi i valori per cui
ϕL termina sono solo quelli appartenenti ad L. Possiamo dunque restituire
sì. Se invece la computazione di ϕL non termina significa che l’input passato
non appartiene ad L e quindi il comportamento divergente sarà appropriato per
l’implementazione della MdT (stiamo solo accettando L e non decidendolo!).
(a) implica (c). Iniziamo la dimostrazione supponendo che L non sia vuoto,
dunque deve essere l’immagine di una funzione totale calcolabile (siamo nel
secondo caso della definizione (c)). Poiché L è semidecidibile disponiamo di
M L , la macchina che accetta L. Non resta che interrogarsi sul comportamento
di M L , ovvero capire per quali input l’ouput di M L è pari ad 1 e mostrare che
tali input (che sono quindi gli elementi di L) sono immagine di una funzione
totale calcolabile.
Possiamo immaginare quindi una funzione totale f (x) che restituisca l’x-esimo
input della macchina M L per il quale la macchina risponde 1: se riusciamo a
scrivere il codice della macchina che calcola f (x) avremo trovato una funzione
totale calcolabile che ha come immagine L.
Ecco dunque il codice della macchina che calcola f (x):
1
// s i a k l ’ i n d i c e
d e l l a m a c c h i n a M L , q u i n d i Mk = M L
2
3
4
5
f (x) {
f o u n d = 0 ; // i n d i c e d i q u a n t i e l e m e n t i d i L a b b i a m o t r o v a t o
i = 0 ; // i n d i c e c o p p i a
6
7
8
Y
input =
(i) ; // i n p u t
1
Y
p =
(i) ; // p a s s i
2
9
10
11
12
while ( true ) {
i f (STEP( k , i n p u t , p ) == 1 ) {
//Mk
restituisce 1 quindi
input
a p p a r i t i e n e ad L
13
14
f o u n d ++; // n u o v o e l e m e n t o d i L t r o v a t o
15
16
17
// e r a l ’ x−e s i m o e l e m e n t o d i L : l o
i f ( f o u n d == x )
return input
18
19
20
21
// n u o v i i n p u t e p a s s i
i ++;
Y
input =
(i) ;
1
restituiamo
46
Capitolo 2. Calcolabilità
p =
22
Y
2
(i) ;
}
23
}
24
25
26
27
}
Listing 2.4: Codice della macchina che calcola la funzione totale che ha come immagine L.
Per scrivere il codice della macchina che calcola f (x) abbiamo sfruttato la numerazione delle coppie di Cantor, in modo da ottenere tutte le possibili coppie
valore di input-passi. Ogni valore di input deve infatti essere testato per tutti
i possibili passi dato che non possiamo sapere se e quando M L ci restituirà 1.
Ricapitolando, abbiamo scritto il codice di una MdT che restituisce gli input
per i quali la macchina che accetta L restituisce 1 (ergo gli elementi di L), così
facendo abbiamo calcolato una funzione totale che ha come immagine proprio
gli elementi di L dimostrando quanto asserito da (c).
L’unico caso in cui f (x) non rispetta la definizione di (c) è quando L è vuoto,
caso che infatti viene esplicitato direttamente nella definizione. Semplicemente, M L calcolerà la funzione che non converge mai e noi non possiamo dire
nulla di interessante.
(c) implica (a). Dimostriamo ora l’altro verso dell’implicazione esaminando
separatamente le due casistiche espresse da (c).
Se L è vuoto, allora L è banalmente semidecidibile poiché possiamo trovare
facilmente la MdT che accetta L: basta considerare la MdT che calcola la
funzione sempre divergente (non dobbiamo mai rispondere 1 poiché non esiste
alcun x ∈ L).
Per quanto riguarda l’altro caso (L non è vuoto), se è vero (c) ed f è il nome
della funzione totale calcolabile che ha come immagine L, possiamo scrivere
L come:
L = {f (0), f (1), f (2), . . . }
Il nostro intento è quello di mostrare il codice della MdT che accetta L così
da provare che L è semidecidibile.
Giacché f è totale e calcolabile il compito è abbastanza semplice: dato un
certo input x dovremo verificare se esiste un certo f (i ) che vale x, ovvero se
per un qualche i la funzione f ci restituisce x. Se ciò è vero, allora abbiamo
garanzia che x ∈ L poiché f ha come codominio tutti e soli i valori di L. Se x
non dovesse appartenere ad L avremmo chiaramente un loop infinito ma ciò
non è un problema poiché vogliamo soltanto accettare L e dunque è corretto
2.10. La nozione di semidecidibilità
47
che la funzione diverga.
Vediamo il codice della MdT che accetta L:
1
//f è l a
funzione
totale
c a l c o l a b i l e c h e ha L come i m m a g i n e
2
3
4
5
6
7
8
9
10
11
accept_L ( x ) {
i = 0 ; \\ c o n t a t o r e p e r
o u t p u t = f (0) ;
w h i l e ( o u t p u t != x ) {
i ++
o u t p u t = f (i) ;
}
return 1;
}
verificare
o g n i f (i)
Listing 2.5: Codice della MdT che accetta L.
2.10.2
Proprietà relative alla decidibilità e alla semidecidibilità
Oltre a quanto detto fin’ora, ci sono ulteriori proprietà interessanti da considerare:
• La decidibilità è chiusa per unione ed intersezione, ovvero se L1 è decidibile ed L2 è decidibile allora L1 ∪ L2 e L1 ∩ L2 sono decidibili (dimostrato
nell’Esercizio 4.1.5).
• Se L è decidibile allora N − L è decidibile. Possiamo costruire la MdT
che decide N − L semplicemente fornendo in output l’inverso dell’output
ottenuto dalla macchina che decide L. Si noti che questa implicazione
non si applica alla semidecidibilità (si veda il teorema di Post subito
sotto).
• Se un linguaggio L è decidibile allora è anche semidecidibile. La MdT che
accetta L può infatti sfruttare la MdT che decide L per leggere l’output
ed eventualmente tradurre il valore 0 nell’esecuzione di una macchina
che diverge sempre.
2.10.3
Il teorema di Post
Concludiamo la nostra trattazione sulla decidibilità e sulla semidecidibilità
enunciando e dimostrando il teorema di Post:
Teorema 2.4. Il fatto che un linguaggio L ⊆ N sia decidibile bimplica che sia
L che N − L siano semidecidibili.
48
Capitolo 2. Calcolabilità
La dimostrazione di tale teorema è semplice:
• Implicazione da sinistra verso destra: se L è decidibile, possiamo dire
che L è semidecidibile usando la terza proprietà enunciata nello scorso paragrafo, mentre possiamo dire che N − L è semidecidibile usando
rispettivamente la seconda e la terza proprietà enunciate nello scorso
paragrafo.
• Implicazione da destra verso sinistra: se sia L che N − L sono semidecidibili devono esistere due MdT M L ed M N−L che accettano rispettivamente L ed N − L. Costruiamo dunque la MdT che decide L lanciando
in esecuzione parallela M L e M N−L sull’input x. Una ed una sola delle
due macchine convergerà poiché x appartiene necessariamente ad uno
ed un solo degli insiemi L,N − L. Se la macchina che converge è M L
allora x sarà stato accettato e dunque appartiene ad L (restituiamo sì).
Al contrario, se converge M N−L avremo garantito che x è in N − L e
dunque non è in L, perciò, possiamo restituire no.
2.10.4
Un esempio di funzione semidecidibile e non decidibile
Ciò che abbiamo detto in merito alla semidecidibilità sarebbe semplicemente
inutile se non esistessero insiemi non decidibili che sono però semidecidibili.
Vediamo dunque un esempio di insieme non decidibile ma semidecidibile:
K = {n ∈ N|Mn ↓ n}
K definisce l’insieme degli indici delle MdT che terminano avendo come input
il loro stesso indice.
K non è decidibile poiché la sua funzione caratteristica è h, la funzione dell’halt:
(
1 se Mn ↓ n
h(n) =
0 se Mn ↑ n
In altre parole, dato un certo intero n non siamo in grado di sapere se questo
appartenga oppure no all’insieme K.
In compenso, siamo in grado di calcolare la funzione:
(
1 se Mn ↓ n
h0 (n) =
⊥ se Mn ↑ n
che è la funzione che accetta K, ovvero che ci dice se un certo intero n
appartiene a K (e se non vi appartiene, diverge). Dimostriamo che h0 (n) è
calcolabile fornendo l’algoritmo della MdT che la calcola:
2.11. Due teoremi fondamentali
1
2
3
4
49
accept_K ( x ) {
U(x , x) ;
return 1;
}
Listing 2.6: Codice della MdT che accetta K.
Il codice è incredibilmente semplice: lasciamo eseguire Mx su x. Se la computazione di Mx termina (quindi Mn ↓ n) restituiamo 1, altrimenti la funzione
divergerà poiché la computzione è infinita su Mx (quindi Mn ↑ n).
2.10.5
Ricapitolando
Una funzione può essere calcolabile oppure non calcolabile, mentre un linguaggio/insieme/problema può essere decidibile, semi-decidibile o non decidibile.
Quando un linguaggio/insieme/problema è semi-decidibile siamo in grado di
creare una funzione caratteristica che ci da risposte positive certe ma non dice
nulla sulle risposte negative.
Il fatto che un problema nella sua forma generale sia non decidibile non vuol
dire che non risolvibile in determinati casi specifici, ma solo che non è possibile
per tutti i possibili valori di input decidere il problema.
Ad esempio, pur sapendo che il problema dell’halt è indecidibile e quindi non
possiamo determinare se un programma qualsiasi termini, sappiamo che un
programma non contiene cicli termina.
2.11
Due teoremi fondamentali
In questa sezione andremo a studiare due teoremi di fondamentale importanza
per la calcolabilità.
2.11.1
Il teorema di Kleene
Il teorema di Kleene (anche detto teorema del punto fisso o secondo teorema
della ricorsione) è di difficile inutizione sebbene la dimostrazione sia molto
semplice.
Siamo ormai familiari con l’idea di numerare funzioni e MdT e sappiamo anche
che una funzione in senso platonico come coppia di elementi del dominio e del
codominio appare infinite volte con indici diversi in questa numerazione poiché
noi in realtà abbiamo codificato le MdT che calcolano le funzioni e non le
funzioni direttamente.
Proprio su questo fatto si basa il teorema di Kleene.
50
Capitolo 2. Calcolabilità
Teorema 2.5 (Teorema di Kleene). Per ogni funzione totale calcolabile t esiste
un certo e ∈ N tale che:
ϕe = ϕt(e)
Ciò che abbiamo appena letto è a dir poco controintuitivo. Stiamo infatti
dicendo che fissata una funzione totale e calcolabile t, una qualsiasi funzione
ϕ avrà, fra i suoi infiniti indici, un indice e tale per cui la funzione ϕt(e) è
uguale a ϕe .
Possiamo parafrasare la definizione dicendo che una funzione calcolabile ϕ (nel
senso di insieme di coppie di elementi del dominio e codominio) ha infiniti indici
nella numerazione che abbiamo costruito dato che noi numeriamo le MdT
e vi sono infiniti algoritmi che calcolano ϕ. Fra quegli infiniti indici, presa
una qualsiasi funzione totale calcolabile t siamo in grado di trovare due indici
correlati fra loro mediante t, ovvero se il primo è e, l’altro è t(e). Pertanto,
avremo ϕe e ϕt(e) che sono equivalenti fra loro, ovvero calcolano lo stesso
insieme di coppie di elementi del dominio e codominio, benché siano calcolate
da MdT diverse (hanno indice diverso).
Ricordando poi che l’indice di una MdT è la codifica del suo codice, è come se
stessimo dicendo che T trasforma un programma in un altro programma senza
cambiarne la semantica (e quindi possiamo costruire programmi per ricorsione).
Possiamo fare un piccolo esempio per chiarirci le idee. La funzione ϕ presa
in esame è una funzione unaria che restituisce l’input fornitogli: la funzione
identità. Tale funzione (ovviamente totale e calcolabile) dispone di infiniti indici
all’interno della numerazione delle MdT giacché esistono infiniti algoritmi che
la calcolano. Il più ovvio è chiaramente il seguente:
1
2
3
Mk 0 ( x ) {
return x ;
}
Listing 2.7: Esempio di codice che calcola la funzione identità (1).
Volendo, possiamo anche scrivere il seguente codice alternativo:
1
2
3
4
Mk 0 ( x ) {
y = 1;
return x ;
}
Listing 2.8: Esempio di codice che calcola la funzione identità (2).
Le due funzioni che abbiamo scritto sono fra loro equivalenti da un punto di
vista di coppie dominio-codominio, ma la codifica dei loro algoritmi fornisce
due codici diversi poiché i loro testi sono differenti.
2.11. Due teoremi fondamentali
51
Ciò che dice il teorema di Kleene è il fatto che fra tutti gli infiniti modi di
scrivere un algoritmo che calcoli la funzione identità, scelta una certa funzione
t siamo in grado di trovare un algoritmo che è trasformabile in un algoritmo
diverso sintatticamente che calcola la stessa cosa ,e la trasformazione è regolata da t.
Perciò, continuando col nostro esempio, se diciamo che t è la funzione che
aggiunge le righe x = x + 1; x = x − 1, siamo in grado di trovare una codifica
(e quindi un algoritmo) fra quelle delle MdT che calcolano l’identità che se
modificata tramite t, continua a calcolare l’identità.
Prendiamo dunque l’algoritmo:
1
2
3
4
Me ( x ) {
[...]
return x ;
}
Listing 2.9: Codice che calcola la funzione identità. Nella numerazione delle funzioni calcolabile è
identificata come ϕe .
la cui codifica è e e abbiamo garanzia che calcolando t(e) la funzione risultante
sarà ancora l’identità:
1
2
3
4
5
6
Mt(e) ( x ) {
[...]
x= x + 1 ;
x= x −1;
return x ;
}
Listing 2.10: Codice che calcola la funzione identità. Nella numerazione delle funzioni calcolabile è
identificata come ϕt(e) .
Si noti che l’e scelto (quindi l’algoritmo di partenza) non è uno qualsiasi, ma
è l’e tale per cui valga l’equivalenza fra ϕe ϕt(e) . Chiaramente, tale algoritmo
potrebbe non essere unico (vi sono diversi e possibili).
Dimostrazione del teorema di Kleene
Ora che ci siamo convinti di ciò che dice Kleene col suo teorema, mostriamone
anche la dimostrazione.
Teorema di Kleene. Iniziamo col definire una serie infinita di MdT, le MdT
che usano un parametro u.
Dunque, per ogni u ∈ N definiamo una MdT M con input x e che effettua la
seguenti computazioni:
1. Calcola U(u, u).
52
Capitolo 2. Calcolabilità
2. Se la computazione di U(u, u) termina si è calcolato ϕu (u), che è un
numero naturale, dunque, possiamo è possibile come indice di una MdT
e calcolare U(ϕu (u), x) (che calcola ϕϕu (u) (x)).
Non ci interessa il codice di queste M, certo è che però il loro codice dipende
da u e dunque possiamo affermare per il teorema Snm (Sezione 2.7.4) che M
ha un indice che è funzione di u, diciamo g(u) (dove g è il nome della funzione
di codifica).
Perciò effettuiamo un cambio di notazione:
M = Mg(u)
Scriviamo poi esplicitamente la funzione calcolata da Mg(u) , cioè ϕg(u) :
(
ϕϕu (u) (x) se ϕu (u) è definita
ϕg(u) =
⊥
altrimenti
Non abbiamo fatto altro che indicare come funzione il codice che abbiamo
illustrato poco fa.
A questo punto andiamo a considerare la funzone t indicata dal teorema,
ovvero una qualsiasi funzione totale calcolabile. Giacché sia t che g (la funzione
di codifica) sono entrambe totali e calcolabili, anche t ◦g è totale e calcolabile.
Perciò, la funzione t ◦ g esiste nella numerazione delle funzioni calcolabili e
quindi ha un indice, diciamo, v . Quindi:
t ◦ g = ϕv
Studiamo infine il comportamento di ϕg(u) se u = v :
ϕg(v ) (x) = ϕϕv (v ) (x) = ϕϕt (g(v )) (x)
La prima equivalenza è possibile dato che abbiamo escluso il caso di divergenza
di ϕg(v ) (x) poiché ϕv (v ) è certamente definita (dato che è t ◦ g, che è totale
e calcolabile). La seconda equivalenza è soltanto un cambio notazionale.
Dunque, se chiamiamo e il risultato di g(v ) possiamo ritrovare la definizione
del teorema di Kleene:
ϕe (x) = ϕϕt (e) (x)
Il risultato ottenuto vale quindi per ogni possibile t e per ogni funzione di
codifica g.
2.11.2
Il teorema di Rice
Il teorema di Rice è forse il più grande risultato ottenuto nel campo della
calcolabilità. Purtroppo, come stiamo per vedere, asserisce un risultato decisamente negativo: qualunque proprietà non banale delle funzioni calcolabili (e
quindi degli algoritmi) è indecidibile.
2.11. Due teoremi fondamentali
53
Definizione di famiglia di funzioni
Per poter comprendere il teorema di Rice è necessario conoscere la nozione di
famiglia di funzioni, che introduciamo in questo paragrafo.
Una famiglia di funzione F è definibile mediante la definizione dell’insieme SF :
SF = {x|ϕx ∈ F } ⊆ N
SF è l’insieme degli indici delle funzioni appartenenti ad F , in questo caso,
tutte le funzioni calcolabili. È importante non confondere l’insieme degli indici
SF e la famiglia di funzioni F .
SF deve anche rispettare una caratteristica fondamentale: F è una famiglia
di funzioni, ma in SF la stessa funzione può avere più indici diversi, pertanto
tutti gli infiniti indici di una determinata funzione devono appartenere a SF
(altrimenti SF sarebbe inconsitente). In altre parole, se i ∈ SF e ϕi = ϕj ,
allora j ∈ SF .
Per chiarificare il concetto facciamo alcuni esempi:
• K = {i |ϕi = i d} definisce una famiglia di funzioni poiché qualsiasi indice
dato alla funzione identità il calcolo di tale funzione è indipendente e
dunque tutti gli indici della funzione identità saranno presenti in K.
• T = {x|ϕx ↓ x} non definisce è una famiglia di funzioni poiché a seconda
dell’indice della funzione quella funzione potrà appartenere o meno a T
(cioè convergere oppure no).
• P = {x|ϕx è totale} definisce una famiglia di funzioni poiché se ϕx è
totale anche tutte le funzioni ad essa equivalenti saranno totali (dato
che due funzioni equivalenti hanno stesso dominio e codominio).
• Q = {x|ϕx ha y nel suo codominio} definisce una famiglia di funzioni poiché se ϕx ha y nel codominio anche tutte le funzioni ad essa
equivalenti lo avranno (due funzioni equivalenti hanno lo stesso dominio).
Talvolta può capitare che per brevità SF venga chiamato famiglia di funzioni
(anche se in realtà è un insieme di indici definito su F , la vera e propria famiglia
di funzioni).
Il teorema
Teorema 2.6. Sia F una famiglia di funzioni calcolabili. SF è decidibile se e
solo se o è vuoto o coincide con N.
dove SF è definito come visto poco fa:
SF = {x|ϕx ∈ F }
54
Capitolo 2. Calcolabilità
Cosa ci dice questo teorema? Semplicemente che se ci interroghiamo in merito
a proprietà semantiche di una funzione, avremo sempre un problema indecidibile. Le uniche proprietà sulle quali possiamo interrogarci sono quelle comuni
a tutte le funzioni, ovvero proprietà che o tutte o nessuna funzione ha (dato
che SF deve coincidere con N o col vuoto), il che significa che l’unica proprietà
interrogabile è il fatto stesso di essere funzione.
Infatti, se riuscissimo ad avere un insieme come, ad esempio, P = {x|ϕx è totale}
decidibile staremmo dicendo che siamo in grado di identificare un sottoinsieme
di funzioni e quindi P non avrebbe tutti gli elementi di Nviolando il teorema di
Rice.
Questo teorema dimostra una volta per tutte che con la sola sintassi di un algoritmo non siamo in grado di dire nulla di interessante in merito all’algoritmo,
a meno di utilizzare delle annotazioni semantiche, quali i tipi.
Dimostrazione
Come al solito, ci apprestiamo a dimostrare i due lati della bimplicazione
separatamente.
Dimostrazione che se SF è vuoto o coincide con N allora è decidibile (←). La
dimostrazione dell’implicazione da destra verso sinistra è ovvia. Abbiamo due
casi da considerare:
• Se SF è vuoto, la funzione lo decide è la funzione che diverge sempre
(che è ovviamente calcolabile).
• Se SF coincide con N, la funzione che lo decide è la funzione costantemente uno (che è ovviamente calcolabile).
Abbiamo individuato le due funzioni che decidono SF nei due casi indicati
dall’implicazione dunque possiamo affermare che tale implicazione è vera.
Dimostrazione che se SF è decidibile allora o è vuoto o coincide con N (→).
Questo lato dell’implicazione è decisamente più complesso da dimostrare. Dato che vogliamo dimostrare un risultato negativo cerchiamo di costruire un SF
che non sia né vuoto né coincidente con N e assumiamo per assurdo che sia
decidibile, dopodiché cerchiamo di ottenere un assurdo.
Iniziamo col considerare due indici:
• i , il minimo indice di una funzione che appartiene ad F (ϕi ∈ F ).
• j, il minimo indice di una funzione che non appartiene ad F (ϕj ∈
/ F ).
2.11. Due teoremi fondamentali
55
Le due funzioni ϕi e ϕj esisteranno certamente poiché SF non è né vuoto né
coincidente coi naturali, ergo esiste almeno una funzione in F e almeno una
funzione che non è in F .
Dopodiché, sapendo che SF è decidibile, possiamo identificare la sua funzione
caratteristica:
(
1 se x ∈ SF
f (x) =
0 se x ∈
/ SF
Creiamo ora una piccola variante di f :
(
j se x ∈ SF
g(x) =
i se x ∈
/ SF
È evidente che la funzione g(x) sia calcolabile, per scriverne l’algoritmo basta
infatti sfruttare la funzione f che sappiamo esistere per ipotesi. Si noti come
g ragioni in un certo senso al ‘’contrario‘’ rispetto ad f e a ciò che i e j
rappresentano; proprio questa contraddizione ci porterà all’assurdo.
Studiamo ora il comportamento di g (quando indichiamo g(x) ci riferiamo ora
al risultato della computazione di g(x) e non alla funzione in senso lato):
1. Se g(x) ∈ SF allora g(x) = i e dunque x ∈
/ SF
2. Se g(x) ∈
/ SF allora g(x) = j e dunque x ∈ SF
Per ora ci siamo soltanto limitati ad osservare come si comporta g e quali
implicazioni il suo risultato abbia.
D’altronde, per la definizione di SF possiamo anche affermare che (continuamo
ad intendere g(x) come numero naturale post-computazione di g):
3. Se g(x) ∈ SF allora ϕg(x) ∈ F .
4. Se g(x) ∈
/ SF allora ϕg(x) ∈
/ F.
5. Se x ∈ SF allora ϕx ∈ F .
6. Se x ∈
/ SF allora ϕx ∈
/ F.
Unendo ciò che sappiamo, possiamo dire:
• Se g(x) ∈ SF allora è sia vero che ϕg(x) ∈ F (da 3) che ϕx ∈
/ F (da 2
e 6).
• Se g(x) ∈
/ SF è sia vero che ϕg(x) ∈
/ F (da 4) che ϕx ∈ F (da 1 e 5).
Pertanto, le seguenti implicazioni sono entrambe vere:
56
Capitolo 2. Calcolabilità
• ϕg(x) ∈ F ←→ ϕx ∈
/ F.
• ϕg(x) ∈
/ F ←→ ϕx ∈ F .
Abbiamo semplicemente bimplicato fra loro proposizioni che erano a loro volta
bimiplicate da una proposizione comune.
Ora, possiamo sfruttare il teorema di Kleene ed affermare che esiste un certo
e per cui:
ϕg (e) = ϕe
in tal caso, una qualsiasi delle due implicazioni sopra sarebbero ovviamente
assurde. Abbiamo quindi trovato un valore e per il quale esiste un assurdo, ovvero che la stessa funzione appartenga e non appartenga contemporaneamente
ad F .
Applicare il teorema di Rice
Molti dei problemi che abbiamo risolto con inenarrabile sforzo in precedenza
sarebbero stati immediatamente risolvibili conoscendo il teorema di Rice.
Ad esempio, l’Esercizio 4.1.4, che chiedeva di dimostrare la non calcolabilità
della funzione:
(
1 se 5 appartiene al codominio di ϕx
f (x) =
0 altrimenti
la cui famiglia di funzioni definisce l’insieme di indici:
K = {x|5 appartiene al codominio di ϕx }
Come abbiamo già appurato in un esempio precedente, K definisce gli indici
di una famiglia di funzioni correttamente definita poiché funzioni equivalenti
hanno stesso codominio, ergo ogni funzione equivalente ad una funzione che ha
5 nel codominio avrà il suo indice in K poiché anch’essa avrà 5 nel codominio.
Ma allora, K è un insieme che si basa su una famiglia di funzioni propriamente
definita e quindi possiamo applicare il teorema di Rice: giacché non è vero né
che tutte le funzioni hanno 5 nel codominio né che nessuna funzione ha 5 nel
codominio, K non è decidibile.
Un altro esempio è dato dalla funzione:
(
1 se ϕx ↓ 0
f (x) =
0 altrimenti
la cui famiglia di funzioni definisce l’insieme di indici:
K = {x|ϕx ↓ 0}
2.12. Completezza funzionale e completezza algoritmica
57
Anche in questo caso, due funzioni uguali divergeranno entrambe su 0 indipendentemente dal loro indice, ergo K definisce li indici di una famiglia di funzioni
correttamente definita. Applichiamo anche qui il teorema di Rice: giacché non
è vero né che tutte le funzioni hanno convergono su 0 né che nessuna funzione
converge su 0, K non è decidibile.
2.12
Completezza funzionale e completezza algoritmica
Disponiamo ora di tutti gli strumenti per introdurre le due nozioni di completezza funzionale e di completezza algoritmica applicabili ad un certo modello
di calcolo.
Definizione 2.6 (Completezza funzionale). Un modello di calcolo gode della
proprietà di completezza funzionale se ogni funzione calcolabile è calcolabile
mediante quel modello. La nozione di completezza funzionale si può quindi
associare al concetto di potenza di un modello.
Definizione 2.7 (Completezza algoritmica). Un modello di calcolo gode della
proprietà di completezza algoritmica se ogni algoritmo è esprimibile utilizzando
quel modello. La nozione di completezza funzionale si può quindi associare al
concetto di espressività di un modello.
Il concetto di completezza algoritmica è evidentemente più sfuggevole rispetto
al concetto di completezza funzionale. Infatti, con ‘’esprimibile‘’ intendiamo
direttamente scrivibile usando le primitive del mondello, senza effettuare modifiche semantiche all’algoritmo.
Se consideriamo ad esempio le macchine di Turing, esse godono ovviamente
della proprietà di completezza funzionale grazie alla Tesi di Church-Turing, ma
non godono di completezza algoritmica poiché la ricorsione non è naturalmente implementabile usando le funzionalità offerte dalle MdT. Ciò implica che sia
possibile scrivere la funzione che ordina un array (completezza funzionale), ma
non che sia possibile scrivere il QuickSort (completezza algoritmica). Chiaramente, è possibile scrivere una versione del QuickSort iterativa, ma questo
punto l’algoritmo sarebbe totalmente snaturato e non potremmo più parlare
del QuickSort vero e proprio, che è quindi non implementabile mediante le
MdT.
L’idea è quindi che finché un algoritmo viene tradotto da un modello ad un
altro mediante una traduzione istruzione ad istruzione i due modelli si possono
dire equivalenti da punto di vista della completezza algoritmica, mentre se siamo costretti a cambiare la logica dell’algoritmo per implementare la funzione
nel secondo modello tale equivalenza non vale.
58
Capitolo 2. Calcolabilità
Riflettendoci un attimo risulta chiaro che nessun modello gode della completezza algoritmica per il semplice fatto che è sempre possibile costruire un nuovo modello con costrutti estremamente complessi che non sono direttamente
rappresentabili dagli altri modelli.
2.13
Calcolabilità secondo Kleene
Voltiamo pagina e come ultimo argomento relativo alla calcolabilità ci apprestiamo a studiare un nuovo modello di calcolo, proposto da Kleene10 . Tale
modello è fondamentalmente diverso dalle MdT che abbiamo studiato e si basa sulla nozione di funzione ricorsiva. È un modello assai più astratto di quello
di Turing, che utilizza direttamente le funzioni come ‘’unità‘’ di calcolo e che
quindi non effettua alcun tipo di codifica (ad esempio, i numeri naturali sono
primitivi).
Kleene fornisce un set di funzioni di base calcolabili per definizione. Tali funzioni vengono poi combinate mediante l’uso di particolari tecniche definite dal
modello al fine di ottenere funzioni ancora calcolabili. Secondo Kleene, dunque, una funzione è calcolabile se è scrivibile mediante la composizione di una
serie di funzioni calcolabili (torneremo in seguito su questa definizione).
Nelle sezioni a seguire andremo quindi a studiare le funzioni base e le operazioni di composizione, ricorsione e minimalizzazione. Dapprima studieremo la
composizione e la ricorsione su funzioni totali, poi, grazie allo strumento della
minimalizzazione che permette trasformare funzioni totali in funzioni parziali
estenderemo composizione e ricorsione anche alle funzioni parziali.
2.13.1
Le funzioni di base
Secondo Kleene le funzioni di base sono soltanto tre:
• Z(x) = 0, la funzione che restituisce sempre zero.
• S(x) = x + 1, la funzione successivo che restituisce il numero naturale
che segue x.
• Pik (x1 , . . . , xk ) = xi con i ≤ k, la funzione di proiezione che restituisce
l’i -esimo input sui k entranti.
2.13.2
La composizione
Nota: si ricordi che per ora trattiamo solo funzioni totali.
10
Il modello di calcolo è in realtà anche detto di Church poiché parte di ciò che utilizza
Kleene è stato ideato proprio da Church.
2.13. Calcolabilità secondo Kleene
59
Mediante l’operazione di composizione possiamo definire funzioni del tipo:
h(x1 , . . . , xn ) = f (g1 (x1 , . . . , xn ), . . . , gk (x1 , . . . , xn ))
Ovvero, definiamo h per composizione sfruttando le funzioni f e gi (con 1 ≤
i ≤ k) calcolabili. Riportiamo per chiarezza i domini e codomini delle funzioni
di cui stiamo parlando:
gi : Nn → N
f : Nk → N
h : Nn → N
Ciò che vorremmo dire che è se f , g1 , . . . , gk sono calcolabili allora anche h
lo è. Non resta che scrivere un algoritmo che implementi la composizione per
dimostrare tale affermazione:
1
2
3
4
5
H( x1 , . . . , xn ) {
∀ i con i c o m p r e s o f r a 1 e k
yi = Gi (x1 , . . . , xn )
F (y1 , . . . , yk )
}
Listing 2.11: Calcolabilità di una funzione composta. Gi è l’algoritmo che calcola gi mentre G è
l’algoritmo che calcola f .
L’algoritmo è banalissimo ma è formalmente necessario scriverlo per dimostrare
la calcolabilità di h. Si noti che l’algoritmo è così semplice grazie al fatto
che stiamo parlando di funzioni totali, dunque nessuna esecuzione di G né
l’esecuzione di F divergerà.
2.13.3
La ricorsione
Nota: si ricordi che per ora trattiamo solo funzioni totali.
Il principio di induzione
La ricorsione si basa sul principio di induzione: se una proprietà P vale per zero
e il fatto che P valga per n implica che valga anche per n + 1, allora abbiamo
che per ogni n la proprietà P vale. In formule:
(P (0) ∧ (P (n) → P (n + 1))) → ∀n P (n)
L’induzione è applicabile solamente quando si parla di numeri naturali poiché
tale insieme è ben formato, ovvero un insieme con un ordinamento e per il quale
è vero che ogni numero preso all’interno dell’insieme ha un numero finito di
60
Capitolo 2. Calcolabilità
numeri più piccoli di lui. La seconda caratteristica è fondalmentale per poter
avere un numero di passi finiti di implicazioni da 0 verso n e quindi, volendo,
possiamo ridimostrare tutta la catena induttiva per intero. Per esempio, se
vogliamo verificare che P (3) sia vero partiamo da P (0) che implica P (1),
il quale implica P (2), il quale implica P (3): con un numero finito di passi
abbiamo dimostrato che P (3) vale.
Funzioni definite per ricorsione
Come già fatto per la composizione vediamo come è possibile definire una nuova funzione f per ricorsione. Sfruttiamo a tal fine le funzioni g ed h, calcolabili
(e, per ora, totali).
Per comodità indichiamo con ~
x i k input fissi della funzione f , che rappresentano gli input sui quali la funzione lavora. Abbiamo poi un ulteriore input
(k + 1 totali quindi) che è necessario per implementare la ricorsione.
La funzione ricorsiva f si comporta diversamente su due casi:
f (~
x , 0) = g(x)
f (~
x , y + 1) = h(~
x , y , f (~
x , y ))
Il primo caso delinea evidentemente il passo di terminazione della ricorsione,
mentre il secondo caso implementa il passo ricorsivo. Il passo ricorsivo è
implementato grazie ad h, che prende come input tutti gli input statici, il
passo attuale di esecuzione y ed il risultato della computazione di f sul passo
attuale. La variabile y è evidentemente quella su cui si effettua la ricorsione.
Anche qui, riportiamo per chiarezza i domini e codomini delle funzioni di cui
stiamo parlando:
g:N→N
f : Nk+1 → N
h : Nk+2 → N
Giacché per implementare la ricorsione sfruttiamo la composizione di funzioni
la cui calcolabilità è già stata dimostrata sopra, possiamo asserire che anche
f è calcolabile.
2.13.4
Le funzioni primitive ricorsive
Tutte le funzioni costruite usando le operazioni di ricorsione e composizione a
partire dalle funzioni di base sono dette funzioni primitive ricorsive.
Si noti che non tutte le funzioni totali calcolabili sono incluse nelle funzioni
primitive ricorsive.
2.13. Calcolabilità secondo Kleene
61
Prendiamo ad esempio la funzione di Ackermann A, definita ricorsivamente
come segue:
A(0, x~2 ) = x2 + 1
A(x1 , 0) = A(x1 , 1)
A(x1 + 1, x2 + 1) = A(x1 , A(x1 + 1, x2 ))
La funzione di Ackermann è un esempio di funzione ricorsiva che non è primitiva
ricorsiva poiché cresce più velocemente di qualsiasi funzione ricorsiva primitiva.
Infatti la ricorsione è implementata sulle due variabili x1 e x2 e non solo su una
variabile, come Kleene definisce nel suo modello. Questo ‘’saltibeccare‘’ di
variabile in variabile durante la ricorsione impedisce di utilizzare la definizione
imposta da Kleene e dunque A non è una funzione primitiva ricorsiva.
Si noti che le funzioni primitive ricorsive hanno la stessa potenza dei programmi
for, ovvero quei programmi che usano loop che terminano sempre (ricordiamo
che composizione e ricorsione sono definite su funzioni totali).
2.13.5
Definire funzioni calcolabili totali secondo Kleene
Anche senza introdurre la minimalizzazione possiamo già vedere una definizione prototipale (che correggeremo dopo aver introdotto la minimalizzazione) di
cosa siano le funzioni calcolabili secondo Kleene: una certa funzione f è calcolabile se è scrivibile nei termini delle funzioni di base, ricorsione e composizione.
Per ora, chiaramente, possiamo scrivere solo funzioni totali.
La somma è calcolabile secondo Kleene
Dimostiramo ora che la somma è calcolabile secondo Kleene. Consideriamo
dunque la funzione +(x, y ) che restituisce come risultato la somma fra x ed
y.
Dato che non è sempre immediato ragionare con la ricorsione proviamo a
dare una definizione ricorsiva intuitiva della somma per poi scrivere la stessa
definizione secondo il formalismo usato da Kleene:
+ (x, 0) = x
+ (x, y + 1) = S(+(x, y ))
La condizione di terminazione è immediata: la somma fra x e 0 restituisce 0. Il
passo ricorsivo invece ci dice che per avere la somma fra x ed y + 1 dobbiamo
considerare il successore della somma fra x ed y .
Non resta che scrivere la nostra definizione rispettando il formalismo imposto
62
Capitolo 2. Calcolabilità
da Kleene. La condizione di terminazione è semplice da tradurre, possiamo
infatti usare la proiezione:
+(x, 0) = x
↓
+(x, 0) = P12 (x, 0)
Il passo ricorsivo è più delicato da tradurre: usiamo la funzione successore e
la funzione di proiezione per avere h e al suo interno mettiamo i tre parametri
richiesti:
+(x, y + 1) = S(+(x, y ))
↓
+(x, y + 1) = S(P33 (x, y , +(x, y )))
Per comprendere quanto abbiamo scritto riprendiamo in mano le definizioni di:
• Definizione di composizione:
h(x1 , . . . , xn ) = f (g1 (x1 , . . . , xn ), . . . , gk (x1 , . . . , xn ))
• Definizione del passo ricorsivo:
f (~
x , y + 1) = h(~
x , y , f (~
x , y ))
È ora evidente notare come S(P33 (x, y , +(x, y ))) sia una composizione che ci
dà:
h(x, y , +(x, y ))
che è proprio nella forma corretta per definire la ricorsione secondo Kleene. h
prima prenderà il terzo argomento che P prende in input e poi ne calcolerà il
successore, secondo la ovvia definizione di composizione di funzioni.
Siamo riusciti a scrivere la funzione + che calcola la somma utilizzando soltanto
le funzioni base di Kleene, la ricorsione e la composizione, ergo la somma è
calcolabile (e in questo caso, anche totale).
La prodotto è calcolabile secondo Kleene
Seguendo lo stesso principio adottato nella somma possiamo anche dimostrare la calcolabilità del prodotto. Ecco la funzione prodotto ·(x, y ) definita
seguendo il modello di Kleene:
· (x, 0) = Z(x)
· (x, 1) = P12 (x, 1)
· (x, y + 1) = +(P13 (x, y , ·(x, y )), P33 (x, y , ·(x, y ))
2.13. Calcolabilità secondo Kleene
63
La definizione del passo ricorsivo non è di immediata comprensione.
Per prima cosa controlliamo di aver rispettato la scrittura corretta per la ricorsione. Riprendiamo ancora una volta in mano la definizione di composizione
e vediamola applicata al nostro caso, ricordando che vogliamo ottenere una
funzione nella forma h(x, y , ·(x, y )):
h(x1 , . . . , xn ) = f (g1 (x1 , . . . , xn ), . . . , gk (x1 , . . . , xn ))
↓
h(x, y , ·(x, y )) = +(P13 (x, y , ·(x, y )), P33 (x, y , ·(x, y ))
Il mapping risulta quindi:
• n = 3 (ergo la quantità di input di h è pari a 3).
• Abbiamo k = 2 e più precisamente g1 = P13 e g2 = P33 .
• x1 = x, x2 = y e x3 = ·(x, y ).
• f = +;
Da un punto di vista formale, dunque, +(P13 (x, y , ·(x, y )), P33 (x, y , ·(x, y )) è
la scrittura corretta per definire la funzione di ricorsione sul prodotto.
Ma la funzione che abbiamo scritto calcola ciò che vogliamo noi? Il prodotto
fra x ed y + 1 è definito ricorsivamente in maniera intuitiva come la somma
fra x ed il prodotto fra x ed y .
La nostra formula sembra essere sensata, poiché P13 e P33 proiettano rispettivamente sul primo e sul terzo parametro, ovvero x e ·(x, y ). Non c’è modo
migliore di verificare che la funzione calcoli quanto desiderato che applicare
l’algoritmo che avevamo definito per la composizione (Listing 2.11), che in
questo caso calcola rispettivamente P13 (x, y , ·(x, y )) e P33 (x, y , ·(x, y )) cioè x
e ·(x, y ), dopodiché li combina con la funzione esterna, la somma, effettuando
in effetti +(x, ·(x, y )) proprio come desideravamo noi.
2.13.6
La minimalizzazione
Fino a questo punto abbiamo visto solo una descrizione parziale del modello di Kleene, siamo infatti in grado di costruire al massimo funzioni primitive
ricorsive. Ma, come sappiamo dal modello di Turing, dobbiamo anche essere in grado di trattare funzioni parziali: introduciamo dunque l’operazione di
minimalizzazione. Grazie alla minimalizzazione possiamo definire una funzione
parziale f partire dalla funzione totale g(~
x , y ):
f (~
x ) = min y (g(~
x , y ) = 0)
64
Capitolo 2. Calcolabilità
Abbiamo usato il solito formalismo per il quale ~
x indica una lista di k input.
Pertanto, i domini e i codomini delle funzioni usate sono: i domini e codomini
delle funzioni di cui stiamo parlando:
f : Nk → N
g : Nk+1 → N
Come funziona la minimalizzazione? L’algoritmo consiste nell’eseguire più volte g, con y man mano crescente (da 0 a infinito). Se la funzione g per un
certo y termina allora anche f termina, altrimenti divergerà per sempre. Ecco
quindi che cosa si intende con la scrittura min y : consideriamo il minimo y per
il quale g termina. Si noti che se tale y esiste, la funzione è totale, altrimenti
la funzione è parziale (esegue all’infinito).
Grazie alla minimalizzazione che abbiamo appena introdotto, siamo in grado
di estendere la composizione e la ricorsione per le funzioni parziali: in questo
modo, solamente la minimalizzazione richiederà funzioni totali, mentre composizione e ricorsione potranno accettare qualsiasi funzione calcolabile. Chiaramente, le funzioni risultanti dalla composizione o dalla ricorsione su funzioni
parziali saranno a loro volta parziali.
2.13.7
Definire funzioni calcolabili alla Kleene
Siamo ora in grado di dare la definizione precisa e definitiva di ciò che significa
essere funzioni calcolabili alla Kleene.
Definizione 2.8 (Funzioni calcolabili alla Kleene). Una funzione è calcolabile
alla Kleene (anche detta ricorsiva) se è una funzione parziale che si ottiene
a partire da funzioni di base, ricorsione su funzioni parziali, composizione su
funzioni parziali e minimalizzazione su funzioni totali.
In altre parole, esiste una sequenza finita di k funzioni calcolabili alla Kleene
che compongono la funzione di partenza (tali funzioni vengono detti passi di
composizione).
Capiamo bene che il modello che stiamo trattanto sia profondamente diverso da quello di Turing: Kleene impiega un approccio definitorio anziché
meccanico.
2.14. L’equivalenza fra le MdT ed il modello di Kleene
2.14
65
L’equivalenza fra le MdT ed il modello di Kleene
Conosciamo ora il modello di Kleene ed il modello di Turing e sappiamo anche
per la tesi di Church-Turing che i due modelli sono equivalenti11 . In questa
sezione ci apprestiamo a dimostrare tale equivalenza, ma non andremo nei
dettagli. Vedremo l’idea che sta dietro alla dimostrazione senza occuparci dei
vari dettagli implementativi che incontreremo.
Per dimostrare l’equivalenza è necessario dimostrare l’implicazione da due lati,
ovvero dimostrare che tutto ciò che è calcolabile mediante le MdT è anche
calcolabile con le funzioni ricorsive di Kleene e che tutto ciò che è calcolabile
mediante le funzioni ricorsive di Kleene è anche calcolabile con le MdT.
Trattiamo i due lati separatamente.
2.14.1
Il modello delle MdT è potente almeno quanto quello di
Kleene
La prima implicazione che dimostriamo è il fatto che ciò che possiamo scrivere
e calcolare con le funzioni di Kleene è scrivibile e calcolabile con le MdT.
Passiamo dunque da un qualcosa ad alto livello come le funzioni ricorsive di
Kleene ad un modello molto meno astratto, le MdT.
L’idea della dimostrazione è semplice: se riusciamo a scrivere delle MdT che
implementino tutte le primitive del modello di Kleene possiamo dire che le MdT
sono potenti almeno quanto il modello di Kleene.
Le funzioni di base come MdT
Le tre funzioni di base sono molto semplice ed è immediato scrivere tre MdT
che le calcolino. Non riportiamo il codice per semplicità (ma chi ha effettuato
la dimostrazione per la prima volta ha dovuto farlo!).
La composizione come MdT
La definizione di composizione per Kleene è la seguente:
h(x1 , . . . , xn ) = f (g1 (x1 , . . . , xn ), . . . , gk (x1 , . . . , xn ))
Disponiamo della MdT M 0 che calcola f , e delle k MdT M1 , ·, Mk che calcolano
g1 , ·, gk . Non resta che scrivere il codice della MdT che calcoli su ogni nastro
di lavoro una certa gi (si dovranno duplicare i vari input su tutti i nastri) e
poi combini i risultati mediante M 0 . Anche qui, non riportiamo il codice per
semplicità.
11
L’equivalenza fra ogni modello computazionale è sempre dimostrata, soltanto la tesi di
Church-Turing non ha dimostrazione.
66
Capitolo 2. Calcolabilità
La ricorsione e la minimalizzazione come MdT
Una volta visto come scrivere la composizione mediante MdT, ricorsione e
minimalizzazione sono soltanto casi particolari e dunque sono facilmente implementabili come MdT.
2.14.2
Il modello di Kleene è potente almeno quanto quello delle
MdT
La dimostrazione dell’altro lato dell’equivalenza è assai più complessa rispetto
a quella appena effettuata. Dobbiamo infatti passare da una rappresentazione
a basso livello ad una a più alto livello, dimostrando che ogni esecuzione della
MdT è simulabile attraverso le primitive del modello di Kleene. L’assenza di
struttura delle MdT ci impedisce un approccio diretto (trattare gli elementi
della MdT), dunque siamo costretti a trattare tutte le possibili computazioni,
l’unico elemento di stampo ricorsivo12 all’interno delle MdT.
Ricordiamo che una certa configurazione di una MdT è indicata come segue:
(α, q, s, β)
dove α è la stringa a sinistra della testina, q è lo stato attuale, s è il simbolo
sotto la testina e β è la stringa alla destra della testina.
Come sappiamo, la computazione non è altro che una sequenza di configurazioni, ovvero:
(α, q, s, β) `Mi (α0 , q 0 , s 0 , β 0 ) `Mi (α00 , q 00 , s 00 , β 00 ) `Mi . . .
Non resta che creare quattro funzioni usando il modello di Kleene che, usate
in maniera appropriata, riescano a riprodurre il susseguirsi delle configurazioni
di una MdT e quindi una computazione qualsiasi.
Le quattro funzioni che creeremo dovranno identificare un determinato elemento di una configurazione ad un certo passo dell’esecuzione. Noi ci limiteremo
a mostrarne la definizione intuitiva e non a dimostrare che tali funzioni sono
calcolabili alla Kleene (ovvero non ne diamo la definizione precisa usando il
modello di Kleene).
Identificazione dello stato alla Kleene
La prima funzione che definiamo è utile per identificare lo stato della MdT in
una certa configurazione:
qM (~
x , t)
12
L’esecuzione può essere visto come processo ricorsivo poiché dal passo k si va al passo
k + 1 modificando alcuni elementi dello stato k al fine di ottenere lo stato k + 1.
2.14. L’equivalenza fra le MdT ed il modello di Kleene
67
dove ~
x sono gli input che erano stati dati alla MdT M a inizio esecuzione e t
sono i passi13 di esecuzione eseguiti dalla MdT. Perciò qM (~
x , t) restituirà lo
stato della MdT al tempo t.
Dobbiamo risolvere un piccolo problema: se la MdT termina in 730 passi, quale
sarà il valore di qM (~
x , 731)? Il problema è che qM è una funzione parziale, che
non è definita da una certo valore di t in poi. Possiamo facilmente risolvere la
questione introducendo uno stato qf che viene restituito se l’input t è maggiore
o uguale ad un certo valore τ che è il numero di passi entro il quale la MdT
termina.
In altre parole, se la macchina M termina in τ passi allora qM (~
x , t) con t ≥ τ
restituisce sempre qf (cioè qM (~
x , t) = qf ).
Identificazione dello simbolo letto alla Kleene
In modo del tutto simile a quanto fatto per gli stati, andiamo a definire il
simbolo letto al passo t dell’esecuzione di una MdT M sfruttando la funzione:
iM (~
x , t)
Anche in questo caso dovremo modificare leggermente la funzione affinché
restituisca un simbolo di default se la computazione è terminata prima del
numero di passi richiesto (t > τ ).
Identificazione della stringa a destra e a sinistra della testina alla Kleene
Ormai è chiaro cosa vogliamo fare, definiamo quindi le funzioni:
LM (~
x , t)
RM (~
x , t)
che restituiscono rispettivamente la stringa a sinistra e a destra della MdT
M al passo di esecuzione t. Anche qui si dovrà tenere conto dell’uso di un t
maggiore del numero di passi entro il quale M termina restituendo una stringa
di default se t > τ .
Riprodurre un’esecuzione
In linea teorica dovremmo ora dimostrare che le quattro funzioni di cui abbiamo parlato sono calcolabili secondo Kleene, ma per semplicità assumiamo che
lo siano (è dimostrato che lo sono).
Vediamo invece come utilizzare tali funzioni per riprodurre un’esecuzione e
13
Il passo d’esecuzione di una MdT è il numero di transizioni effettuati per arrivare allo
stato in cui la MdT si trova.
68
Capitolo 2. Calcolabilità
dimostrare definitamente che il modello di Kleene è potente almeno quanto
quello delle MdT dato che siamo in grado di riprodurre tutte le possibili esecuzioni delle MdT usando funzioni calcolabili alla Kleene.
Siamo in grado di, dato un certo input ~
x , simulare il calcolo della funzione
f (~
x ) effettuato dalla MdT M 14 usando il seguente algoritmo:
• Verifichiamo prima di tutto se f (~
x ) è definita, ovvero se esiste un certo
numero di passi m entro i quali l’esecuzione termina ovvero si verifica:
qM (~
x , n) = qM (~
x , m) = qf con m ≥ n
• Se la computazione termina in n passi, possiamo leggere il risultato
sfruttando RM (~
x , n) (la testina è riportata a inizio stringa).
Risulta dunque evidente che le funzioni qM (~
x , t), iM (~
x , t), LM (~
x , t), RM (~
x , t)
debbano essere funzioni definite ‘’per casi‘’, che ad ogni passo restituiscono
la corretta configurazione. Così facendo possiamo riprodurre configurazione
dopo configurazione e comprendere se la computazione termina o meno, e se
termina con che output termina. L’approccio adottato è quindi vicino a quello
della minimalizzazione, si esegue con un numero sempre crescente di passi t
la funzione di stato al fine di trovare lo stato finale qf .
Termina qui la dimostrazione di equivalenza fra il modello di Turing e quello
di Kleene; pur non essendo entrati nei dettagli specifici della definizione delle
MdT e delle funzioni ricorsive dovrebbe essere chiaro qual è l’idea che viene
sfruttata dalla dimostrazione per avere l’equivalenza. Giacché il modello di
Kleene è potente almeno quanto quello delle MdT ed il modello delle MdT è
potente almeno quanto quello di Kleene possiamo asserire che i due modelli
sono equivalenti in potenza.
2.15
Il linguaggio WHILE (cenni)
Concludiamo il capitolo relativo alla calcolabilità parlando del linguaggio WHILE, un approccio alla teoria della calcolabilità più recente delle MdT e per
molti versi più vicino al mondo informatico odierno. Infatti, il linguaggio WHILE è molto più ad alto livello rispetto alle MdT senza però arrivare al livello di
complessità dei moderni linguaggi che sono pieni di orpelli, poco schematici e
tutto fuorché minimali. Anche se noi faremo solo alcuni cenni in merito al linguaggio WHILE, l’intera teoria della calcolabilità che abbiamo studiato fin’ora
potrebbe essere rifatta adottando questo modello.
14
Supponiamo che M rispetti tutti i protocolli standard delle MdT, ad esempio il fatto
che la testina sia piazzata sul primo simbolo a sinistra a inizio esecuzione e che al termine
dell’esecuzione tale testina venga riportata a inizio stringa.
2.15. Il linguaggio WHILE (cenni)
69
Mediante il linguaggio WHILE si fornisce una definizione di calcolabilità che per
molti versi assomiglia a quella data da Turing, ossia una definizione verbosa:
proviamo a mettere a confronto la definizione di problema calcolabile secondo
Turing e Kleene:
1. Una funzione f è calcolabile se esiste una macchina di Turing M che
per ogni elemento ~
x nel dominio di f (usiamo un vettore per rappresentare funzioni di ogni arietà) M converge su quell’~
x e al termine della
computazione di M troviamo sul suo nastro f (~
x ).
2. Una funzione f è calcolabile se è scrivibile nei termini delle funzioni di
base, ricorsione, composizione e minimalizzazione di Kleene.
È evidente che la definizione data da Turing sia decisamente più ‘’tecnica‘’ e
meno teorica e ciò è ascrivibile al fatto che mentre Turing ragiona con nastro e
simboli per rappresentare una funzione, Kleene ragiona direttamente usando le
funzioni. Come stiamo per apprendere il linguaggio WHILE ha una definizione
di calcolabilità vicina a quella delle MdT, semplicemente più ad alto livello.
2.15.1
La sintassi
Qual è la sintassi adottata dal linguaggio WHILE? Nel rispetto dell’idea di
fornire un linguaggio minimale (ma abbastanza ad alto livello), non disponiamo
di molto. Più precisamente, possiamo usare:
• Assegnazioni a zero: X:= 0
• Funzione successore: X:= s(Y)
• Esecuzione di istruzioni ci in sequenza: begin c1 , . . . , cn end
• Cicli while con condizione di diversità: while(X != Y) do begin ...
end
Un appunto: spesso la funzione predecessore X:= pd(Y) viene considerata fra
le funzioni di base, anche se è ridondante poiché è facilmente computabile
mediante il successore (lo dimostreremo in seguito).
Osserviamo che il linguaggio non offre numeri naturali, che sono implementati mediante il successore. Ad esempio, il numero tre è rappresentato come
s(s(s(0))).
Possiamo definire più formalmente la sintassi del WHILE fornendone la grammatica context-free in Figura 2.4.
Si noti che le variabili sono formate da stringhe finite arbitrarie di lettere maiuscole A, B, . . . , Z e di cifre decimali 0, 1, . . . , 9 che hanno l’unico obbligo di
iniziare con una lettera maiuscola come accade, per esempio, a X e A ma
70
Capitolo 2. Calcolabilità
Figura 2.4: Grammatica context-free per la sintassi del linguaggio WHILE.
anche PIPPO1 e CASA3. Per comodità di rappresentazione chiameremo le
nostre variabili X1, . . . , XN.
Possiamo ora definire formalmente un programma WHILE.
Definizione 2.9. Un programma WHILE è una sequenza ordinata finita (eventualmente vuota) di comandi, separati da punti e virgole, preceduta da begin
e seguita da end. I comandi sono definiti usando la grammatica context-free
sopra illustrata.
2.15.2
I/O, variabili e vettore di stato
Proprio come accadeva con le MdT un programma WHILE non dispone di
alcuna nozione esplicita di input/output. Pertanto un programma WHILE non
ha uno stato di per sé ma sfrutta delle variabili (l’equivalente delle celle del
nastro) per immagazzinare i suoi dati.
Il formalismo adottato per quanto riguarda la memorizzazione dei dati nelle
variabili sfrutta un vettore di stato, l’equivalente del nastro per la MdT, il quale
mantiene il contenuto di ogni variabile usata dal programma in questione:
a = (a1 , . . . , an ) con ai ∈ N
Naturalmente ogni ai è memorizzato nella variabile Xi .
Il vettore di stato di un programma non è infinito come lo era il nastro delle
MdT, però lo possiamo pensare tale e poi affermare che utilizziamo al massimo
k celle, se k sono le variabili dichiarate nel programma.
2.15. Il linguaggio WHILE (cenni)
71
Per convenzione possiamo pensare che un programma che computi la funzione
f : Nk → N utilizzi i primi k elementi del suo vettore di stato per memorizzare
l’input (immagazzinato quindi nelle variabili X1, . . . , XK) e che usi la cella
relativa a X1 per memorizzare l’output. In questo senso, le istruzioni begin
ed end rispettivamente all’inizio ed alla fine di un programma WHILE implementano una sorta di proiezione sulla prima variabile. Usando la notazione di
Kleene diremmo che begin ed end implementano la funzione P1k se la funzione
calcolata ha arità k.
2.15.3
Una semantica per la sintassi: SOS
Disponiamo ora di una sintassi precisa e ben definita, ma a questa è necessario
attribuire una semantica al fine di comprendere come un programma WHILE
possa computare una funzione. Una semantica intesa come un insieme di
convenzioni che ci consentiranno, appunto, di associare una funzione parziale
ad ogni programma del linguaggio, di formare conseguentemente la classe
delle funzioni che sono calcolabili con programmi WHILE ed eventualmente
di confrontare questa classe con le altre che già conosciamo e derivano da
approcci alternativi alla computabilità.
Introduciamo dunque la nozione di semantica operazionale strutturale (in breve
S.O.S.), che sfrutta il formalismo del calcolo proposizionale per definire delle
regole semantiche sulla sintassi. Una regola della S.O.S. appare nella forma:
A
B
A∧B
Una regola di questo genere ci dice che, una volta dedotti in qualche modo A
e B (detti assiomi), possiamo inferire A ∧ B. Si noti che con una sola scrittura
abbiamo in realtà rappresentato infinite regole, poiché A e B sono variabili e
quindi rappresentano potenzialmente infinite istanze di valori.
La Figura 2.5 mostra la semantica operazionale dei programmi WHILE.
Le regole che abbiamo riportato si leggono nel seguente modo: sopra alla
linea orizzontale abbiamo il vettore di stato a prima e dopo la computazione
del costrutto in esame; più precisamente indichiamo con a0 il vettore di stato
ottenuto al termine della computazione.
Per quanto concerne a ciò che c’è sotto alla riga orizzontale, osserviamo una
struttura del tipo hP, ai → hP 0 , a0 i. La prima coppia consiste di un programma
P che contiene il costrutto preso in esame e del vettore di stato prima della
computazione a, mentre la seconda consta del programma P 0 ottenuto in
seguito all’esecuzione di P e del vettore di stato a0 ottenuto al termine della
computazione.
Il tutto ci sarà più chiaro non appena prenderemo in considerazione un esempio.
Calcolabilità e Linguaggi di Programmazione 109
72
Capitolo 2. Calcolabilità
SCzero
SCs
a= (ai, • • • , ak) e a = (a1, . • . ,
(Xu : = O, a)
a=
(end, a')
• • • , ak) e a' = (ai, • . . , au_i, s (a, ) , a.+1, . • • , ak)
(Xu : = s (Xv) , a)
SCpd
SCwhilei
O, au+ i, • • . , ak)
a = (a1, • • • , ak ) e a' =
(end, a')
••.,
, pd ( av),au+i, • • • ak)
(Xu : = pd (Xv) , a) ---+ (end, a')
a = (ai, . • • , ak) e av,
av
(whi le Xu Xv do C, a) --> (C; whi le Xu Xv do C, a)
a = (a i ,
SCwhile
2
, ak) e au = av
(whi le Xu Xv do C, a) ---> (end, a)
SCprogram i
SCprogram 2
SCseq i
(begin end, a) ---> (end, a)
(begin C end, a) ---> (C, a)
(Ci, a) --->
S C s eq2
al ) e
(Ci ; C2 a) —>
end
; C2, a')
(Ci, a) ---> (end, a')
(Ci ; C2, a) > (C2, a')
Figura
operazionale
dei programmi
WHILE. Ogni
costrutto del linguaggio ha una o
Figura2.5:
6.1Semantica
Semantica
Operazionale
dei Programmi
WHILE
più regole semantiche.
Le regole SCzero, SCs e SC pd descrivono come vengono modificati i valori del
vettore di quindi
stato quando
un comando
di assegnamento
vieneregole
eseguito.
regole
Vediamo
nel dettaglio
il significato
di alcune delle
cheLe
abbiamo
whi
1
e.
In
parSCwhile2
gestiscono
invece
i
comandi
di
iterazione
riportato
(per
semplicità
non
le
trattiamo
tutte).
Un
paio
di
appunti
sulla
SCwhile i e
ticolare,
la
regola
SC
whilei
mostra
come
permettere
l'esecuzione
del
corpo
del
notazione:
whi le nel caso in cui il test è soddisfatto; la regola SC while2 ammette, invece, la
• Una lettera
minuscola
di fianco ad
il nome
di una
rappresenta
un
terminazione
del comando
di iterazione
quando
il test
nonvariabile
è soddisfatto.
Le regole
comandi
composti
(sequenze
comandi
indicei intero,
e.g., Xugestiscono
è la u-esima
variabile
(e quindi
contienedil’u-esimo
e SCprogram,
SCprogram
tra un elemento
begin e un
di end).
a). In questo caso, naturalmente, si procede alla valutazione
del loro corpo. Infine, le regole SCseq, e SCseq trattano sequenze di comandi e,
in particolare,
sequenzializzazio
neregole
dei comandi
specificati.
• I codici implementano
riportati nella la
maggior
parte delle
non mostrano
il begin
e l’endSiano
iniziale.
esempio ilWHILE
codice ache
possiamo
vedere
nella prima
Definizione.
P unAdprogramma
k variabili
e a(0)
=- (a1,
• • • ak)
regola
(i.e.,
Xu
:=
0)
dovrebbe
in
realtà
essere
begin
Xu
:= infinita,
0 end
un vettore di stato. Una computazione di P è una sequenza, possibilmente
per essere corretto. Allo stesso modo, il codice prodotto consta di un
solo end, che vuole indicare il programma vuoto, il quale però dovrebbe
essere scritto come begin end. La ragione per cui la sintassi appare
‘’semplificata‘’ nelle regole è una mera ragione di comodità di scrittura,
2.15. Il linguaggio WHILE (cenni)
73
che viene però resa formalmente valida grazie alle regole SCprogram1 ed
SCprogram2 . Capiremo dopo in che senso accade tutto ciò.
Regola SCzero . La regola SCzero riguarda il costrutto di assegnamento a
zero. Il codice di partenza è chiaramente l’assegnazione a zero, ossia Xu :=
0, in associazione con il vettore di stato a: l’esecuzione di tale codice produce
un codice vuoto ed un nuovo vettore di stato a0 . Sopra la linea orizzontale, gli
assiomi ci spiegano come sono costituiti a ed a0 . In particolare notiamo che
tutti gli elementi di a sono stati ricopiati in a0 senza subire alcuna modifica
tranne l’u-esimo, au , che è ora 0.
Regola SCs . La regola SCs riguarda l’uso della funzione successore s. II
codice di partenza è Xu := s(Xv) in associazione con il vettore di stato a:
l’esecuzione di tale codice produce il solito codice vuoto ed un nuovo vettore
di stato a0 . Dagli assiomi possiamo osservare che tutti gli elementi di a sono
stati ricopiati in a0 tranne l’u-esimo, au , che ora vale il successore di Xv , ossia
s(av ).
Regola SCpd . La regola SCpd è analoga alla regola del successore ma impiega
il predecessore.
Regola SCseq2 . Iniziamo a trattare regole un po’ più complesse. La regola
SCseq2 riguarda le sequenze di codice. II codice di partenza è C1 ;C2 in associazione con il vettore di stato a: l’esecuzione di tale codice produce il codice
C2 , ossia il codice di partenza dal quale è stata rimossa l’istruzione eseguita
C1 . Tale codice è poi associato ad un nuovo vettore di stato a0 . Gli assiomi ci
illustrano come l’istruzione C1 venga eseguita.
Regole SCprogram1 e SCprogram2 . Si tratta di due regole di utilità che trasformano rispettivamente un programma vuoto nell’end (andando così a rispettare
il formalismo usato anche nelle altre regole) e un programma nella sua serie di
istruzioni.
Regole SCwhile1 e SCwhile2 . Le regole semantiche per il while sono le più
sofisticate che dobbiamo trattare. La seconda regola si attiva quando la condizione di verifica è falsa, infatti, negli assiomi abbiamo au = av . Se ciò accade,
il while viene direttamente saltato e si va in un codice vuoto (end); chiaramente il vettore di stato non ne risente in alcun modo ed infatti riassociamo
al codice vuoto il vettore di stato iniziale a.
La prima regola viene invece attivata quando la condizione del loop è vera
(notiamo dagli assiomi che au 6= av ) e il codice prodotto non è altro che lo
74
Capitolo 2. Calcolabilità
stesso ciclo while di prima con anteposto C, il codice interno al while. È una
definizione intuitiva che implementa naturalmente il loop.
Da questa regola possiamo trarre una grande considerazione: una qualsiasi
informazione semantica di un programma non è deducibile a partire dalla sua
intassi e ciò è comprovato dalla regola SCwhile1 . Infatti, da un punto di vista
prettamente sintattico, il codice prodotto non mostra alcuna differenza rispetto al codice di partenza (a parte la ripetizione di C): se vogliamo dire qualcosa
di semantico su un programma (ed in particolare su un while) dobbiamo capire
se Xu e Xv sono diversi oppure no, e ciò è chiaramente comprensibile solo
in fase di esecuzione e non staticamente. Abbiamo quindi trovato un’ulteriore
comprova del fatto che Rice avesse ragione.
A questo punto la domanda sorge spontanea: cosa ce ne facciamo di tutta
questa architettura semantica? Possiamo utilizzarla al fine di comprendere se
un programma WHILE termina (i.e., ha derivazione finita) e se termina con
che a0 termina. È dunque possibile usare le regole semantiche passando da una
all’altra ed eseguendo di fatto il programma. Se ne deduce che, proprio come
con le MdT che accettano un linguaggio, il linguaggio WHILE ci dice qualcosa
soltanto in merito ai casi positivi (che terminano) e nulla sui casi negativi.
2.15.4
Definizione di calcolabilità secondo il linguaggio WHILE
Possiamo finalmente dare la definizione di calcolabilità secondo il modello del
linguaggio WHILE, ossia quando una funzione si dice WHILE-calcolabile.
Definizione 2.10. Una funzione parziale f : Nk → N si dice WHILE-calcolabile
se f è la funzione calcolata da qualche programma WHILE P .
Ma cosa intendiamo per calcolata?
Sia P un programma a k variabili X1, . . . , XK per qualche k > 1. Si dice
funzione calcolata da P la funzione parziale fP : Nk → N tale che, per ogni
a ∈ Nk :
• fP (a) è il valore di X1 nell’output della computazione di P su a se questa
computazione ha fine (lo troviamo nel vettore di stato);
• fP (a) non è definita altrimenti.
A questo punto, volendo, potremmo codificare ogni programma in linguaggio
WHILE (ad esempio con una codifica ASCII piuttosto che con la numerazione
di Göedel) e numerare così tutte le funzioni calcolabili. Da lì, potremmo ridimostrare tutti i teoremi che già abbiamo dimostrato ed approfondito usando
il modello delle macchine di Turing.
Capitolo 3
Complessità
3.1
Introduzione
Nello scorso capitolo abbiamo sviscerato il tema della calcolabilità, ovvero ciò
che si può calcolare. Scopo di questo capitolo è invece quello di rispondere
alla domanda quanto costa valutare?
La teoria della complessità è un terreno ben più scivoloso della teoria della
calcolabilità poiché la tesi sulla quale l’intera teoria si fonda è più ‘’debole‘’
rispetto alla tesi di Church-Turing.
Cosa significa domandarsi quanto costa un algoritmo? Il costo è definito in
termini di quante e quali risorse devono essere utilizzate per calcolare un determinato algoritmo. Se ne deduce che la nozione di complessità sia naturalmente
associabile agli algoritmi, anche se come impareremo a breve è anche possibile
determinare la complessità di un certo problema.
Le risorse di cui parliamo sono tendenzialmente spazio e tempo, anche se attualmente si stanno effettuando ricerche anche sulla complessità energetica.
Noi tratteremo la complessità classica e quindi ci concentreremo solamente
sulle prime due tipologie di complessità.
Dopo le prime formalizzazioni, la teoria della complessità non è mai stata
considerata un granché: lo sviluppo tecnologico ha portato le capacità computazionali e spaziali a livelli mai immaginati prima, dunque, è sembrato poco
interessante studiare quanto costa calcolare a fronte di una tale mole di potenza di calcolo. Oggi però, con lo sviluppo dei dispositivi mobile, risparmiare
risorse è diventato nuovamente un tema importante e dunque la teoria della
complessità è tornato ad essere un argomento di interesse.
76
3.1.1
Capitolo 3. Complessità
Il modello di calcolo
Per misurare la complessità è ovvio che non si possano utilizzare delle misure
assolute (minuti, secondi, ecc); si rende necessario adottare un modello di
calcolo entro il quale definire una unità base di tempo e di spazio. Ancora una
volta le macchine di Turing sembrano fare al caso nostro! Infatti, l’unità base
temporale potrebbe essere il passo (l’esecuzione di una funzione di transizione)
mentre l’unità base spaziale potrebbe essere la cella sul nastro entro il quale
viene riposto un simbolo.
Chiaramente in fase di utilizzo del modello non guarderemo al codice della MdT
che implementa l’algoritmo che stiamo studiando, ma andremo a lavorare su un
algoritmo assai più leggibile scritto per una macchina moderna con la garanzia,
grazie alla tesi di Church-Turing, che la classe di complessità fra i due algoritmi
sia in rapporto polinomiale.
Si noti che la tesi di Church-Turing è valida solamente se non consideriamo le
macchine quantistiche.
3.1.2
Complessità sui problemi
Definire la complessità su un certo algoritmo è naturale: è sufficiente considerare la somma dei costi di ogni istruzione che il codice dell’algoritmo contiene.
D’altronde, noi siamo interessati a comprendere quanto sia complesso un problema e quindi estendiamo la nozione di complessità anche ai problemi dicendo
che la complessità di un problema è la minima complessità di un algoritmo che
lo risove.
Già da questa definizione iniziamo a comprendere come il campo della complessità sia meno solido rispetto alla calcolabilità: un problema non decidibile
rimarrà per sempre non decidibile, mentre la classe di complessità di un problema può variare nel corso del tempo con l’invenzione di nuovi algoritmi più
intelligenti in grado di risolvere quel problema.
Si noti che in certi casi è possibile dimostrare la complessità ottima di un
problema. Ad esempio, i problemi di ordinamento hanno complessità ottima
O(n · log(n)) e dunque non potrà mai esistere un algoritmo che risolva un
problema di ordinamento in una classe di complessità più piccola di n · log(n).
In generale, comunque, non è possibile avere una dimostrazione che identifichi
la complessità ottima di un problema.
3.1.3
La tesi di Cook
La tesi di Cook è un po’ l’equivalente della tesi di Church-Turing per la complessità, si tratta quindi della tesi fondante l’intera teoria della complessità
3.2. Misurare la complessità
77
(temporale). Enunciamo ora il teorema, anche se lo comprenderemo appieno
solo dopo aver compreso come misuriamo la complessità.
Definizione 3.1 (Tesi di Cook). Le funzioni calcolabili sono divisibili in due
insiemi:
• Le funzioni trattabili, che hanno complessità temporale al massimo polinomiale.
• Le funzioni intrattabili, che hanno una complessità temporale più che
polinomiale.
Le funzioni trattabili sono quelle funzioni calcolabili in teoria ma anche in
pratica, mentre le funzioni intrattabili sono calcolabili in teoria ma hanno una
complessità temporale così grande da essere praticamente incalcolabili.
Leggendo la tesi di Cook possiamo ancora una volta evincere come la teoria
della complessità sia poco rigida: è pur vero che un problema con complessità
polinomiale è trattabile, ma se l’esponente di n è estremamente grande, il
problema non è comunque di difficile risoluzione?
3.2
Misurare la complessità
Entriamo nel vivo del discorso cercando di capire come sia possibile misurare
la complessità.
3.2.1
La funzione di complessità
L’idea è quella di utilizzare una funzione f (n) che esprima la variazione di
complessità al variare dei dati (n, appunto).
Consideriamo dunque la funzione: fA (n) che è una misura del tempo/spazio
necessario per eseguire l’algoritmo A su una MdT avendo come input dati di
dimensione minore o uguale ad n.
Definire la dimensione dell’input mediante un numero naturale non è banale
e a seconda di come si compie tale operazione la complessità temporale può
ovviamente cambiare. Tendenzialmente, a seconda della struttura dati in input
esiste un modo standard di definirne l’unità base e dunque n sarà la quantità
di unità base date in pasto all’algoritmo. La riflessione che abbiamo appena
fatto ci fa capire come la complessità temporale e spaziale non siano fra loro
scollegate: a seconda di come memorizziamo un dato la complessità temporale
nel processarlo può variare. In ogni caso, daremo sempre per scontato di saper
compiere questa operazione.
La funzione fA (d’ora in poi f per comodità) per essere adatta agli scopi per
cui la vogliamo utilizzare, deve rispettare le tre seguenti condizioni:
78
Capitolo 3. Complessità
1. f deve essere definita per ogni n, ergo, deve essere una funzione totale.
In realtà ci basta che sia definita per ogni n sopra un certo N dato: certe
volte potremmo non voler definire la complessità di un algoritmo se i dati
sono nulli o troppo pochi (n piccolo).
2. f deve essere crescente, ovvero f (n + 1) > f (n).
3. f non deve essere limitata, ovvero lim f (n) = ∞. Questo punto è
n→∞
naturale conseguenza del punto 2.
Il punto 2 ci fa capire che non stiamo trattando le funzioni che non utilizzano
input, cioè le funzioni con complessità costante. D’altronde, si tratta di funzioni poco interessanti dal punto di vista della complessità (che è, appunto,
costante).
3.2.2
Esempi di funzioni di complessità
Elenchiamo ora le f che vengono normalmente utilizzate come funzioni di
complessità.
• Polinomi:
k
p(n) = ak nk + ak−1 nk−1
+ . . . + a1 n + a0
con ai ≥ 0e. Il grado del polinomio e quali ai sono uguali a zero dipende
dal problema trattato.
• Esponenziali:
e(n) = e
22
..
.n
La quantità di elementi ad esponente dipende dal problema trattato.
• Logaritmi:
log(n)
con n > 0. La base del logaritmo dipende dal problema trattato.
Tutte e tre le funzioni che abbiamo preso in considerazione rispettano naturalmente le tre condizioni che abbiamo imposto sulle funzioni di complessità,
tranne i polinomi per i quali è necessario aggiungere che deve esistere almeno
un ai 6= 0 con i 6= 0 al fine di garantire il fatto che p(n) sia crescente.
3.2.3
La notazione o-grande
Stando a quanto detto fin’ora, ogni algoritmo avrebbe una sua funzione di complessità più o meno arzigogolata (ad esempio un polinomio di ottavo grado) e
risulterebbe parecchio scomodo confrontare fra loro i le classi di complessità.
Proseguiamo allora la la nostra trattazione introducendo i simboli di Landau,
3.2. Misurare la complessità
79
particolari notazioni che ci permettono di confrontare localmente due funzioni,
esistono vari simboli, o-piccolo, o-grande, theta, ciascuno dei quali ha una sua
precisa definizione (noi ci concentriamo sull’o-grande). Come vedremo, grazie
alla notazione o-grande possiamo definire poche ed utili classi di complessità
entro le quali categorizzare gli algoritmi che studieremo.
Definizione 3.2 (O-grande). Una funzione f è o-grande di un’altra funzione
g se e solo se esiste un numero naturale N0 ed un numero reale positivo c tali
per cui per ogni n maggiore di N0 il valore di f su n è minore di c volte il valore
di g su n.
In simboli:
f = O(g) ⇐⇒ ∃N0 ∈ N ∧ ∃c > 0 ∈ R tali che ∀n ≥ N0 si ha f (n) ≤ c · g(n)
Da un punto di vista intuitivo, se f è o-grande di g possiamo pensare che f
non cresca più di g dopo un certo naturale N0 .
Ordinamento parziale sulle classi di complessità
L’o-grande ci permette di definire un’ordinamento parziale? Ovvero, siamo
in grado di ordinare le classi di complessità fra loro sfruttando la nozione di
o-grande?
Una relazione d’ordine parziale deve essere per definizione riflessiva, transitiva
e antisimmetrica. Chiediamoci dunque se queste tre proprietà sono rispettate
dalla relazione o-grande:
• O-grande è riflessiva? In formule:
?
f = O(f )
Sì, o-grande è riflessiva. Infatti, applicando la definizione di o-grande, è
sufficiente scegliere c = 1 e N0 = 0 per verificare l’uguaglianza appena
scritta, cioè: f (n) ≤ 1 · f (n) per ogni n ≥ 0.
• O-grande è transitiva? In formule:
?
f = O(g) ∧ g = O(h) ⇒ f = O(h)
Sì, o-grande è transitiva. Sappiamo infatti che:
– Dato che f = O(g), allora è vero che f (n) ≤ c · g(n) per un certo
n ≥ N0 .
– Dato che g = O(h), allora è vero che g(n) ≤ c 0 · h(n) per un certo
n ≥ N00 .
80
Capitolo 3. Complessità
Dunque, se noi impostiamo c 00 = c · c 0 e N000 = max(N0 , N00 ) possiamo
facilmente asserire che:
f (n) ≤ c 00 · h(n) per un certo n ≥ N000
e quindi che f è o-grande di h.
• O-grande è antisimmetrica? In formule:
?
f = O(g) ⇒ g 6= O(f )
No, o-grande non è antisimmetrica. Per dimostrarlo possiamo rispondere alla domanda inversa, cioè è possibile che f sia o-grande di g e
contemporaneamente g sia o-grande di f ? Se prendiamo le due definizioni otteniamo rispettivamente che f (n) ≤ c ·g(n) e che g(n) ≤ c 0 ·f (n).
Se andiamo a sostituire, otteniamo che:
f (n) ≤ c · c 0 · f (n)
ovvero che:
1 ≤ c · c0
dato che c e c 0 sono numeri reali è semplice trovare una assegnazione per
c e una per c 0 che rendano vera la disequazione di cui sopra (banalmente,
ad entrambi assegniamo 1). Pertanto è possibile che f sia o-grande di
g e contemporaneamente g sia o-grande di f , ergo non è vero che la
relazione di o-grande è antisimmetrica.
L’assenza della antisimettricità non ci permette di ottenere una relazione d’ordine basata su o-grande. In tal senso ci corre però in aiuto un teorema molto semplice che ci permette di distinguere quando ci troviamo in un caso
antisimmetrico e quando no.
Teorema 3.1. Se è vero che:
f (n)
=`>0
n→∞ g(n)
lim
allora siamo nel caso non antisimmetrico, quindi f = O(g) e g = O(f ).
Se invece è vero che:
f (n)
=0
n→∞ g(n)
lim
allora siamo nel caso antisimmetrico, quindi f = O(g) e g 6= O(f ).
Dimostriamo separatamente i due casi.
3.2. Misurare la complessità
81
Dimostrazione del caso non antisimmetrico. Usiamo la definizione di limite per
dire che:
f (n)
<`+ε
∀ε ` − ε <
g(n)
Diciamo però di scegliere un ε < ` senza perdere di generalità per ragioni che
ci saranno chiare in seguito. Ricordiamo inoltre che ` è positivo per ipotesi.
Esaminiamo il lato destro della disequazione:
f (n)
<`+ε
g(n)
moltiplichiamo a destra e a sinistra per g(n) (che è positivo1 ) ed otteniamo:
f (n) < (` + ε)g(n)
Dunque, se definiamo c = (` + ε) (sappiamo che ε è positivo quindi la sua
somma con ` è positiva) abbiamo ritrovato la nostra definizione di o-grande,
cioè f = O(g).
Consideriamo ora l’altro lato della disequazione:
`−ε<
f (n)
g(n)
Anche in questo caso moltiplichiamo a destra e a sinistra per g(n) (sempre
positivo) e otteniamo:
g(n)(` − ε) < f (n)
Grazie al fatto che ε è positivo, abbiamo garanzia che (` − ε) sia diverso da
zero e dunque possiamo usarlo come denominatore:
g(n) <
Ecco quindi che g = O(f ) con c =
f (n)
`−ε
1
.
`−ε
Dimostrazione del caso antisimmetrico. Anche in questo caso ricorriamo alla
definizione di limite, ma sapendo che ` è uguale a zero scriviamo:
∀ε − ε <
f (n)
< +ε
g(n)
Siamo interessati solo al lato destro della disequazione. Infatti proprio considerando quel lato possiamo, in maniera del tutto analoga a quanto fatto prima,
1
Il fatto che g(n) sia positivo si evince dal fatto che sia una funzione non limitata, come
impone la terza condizione imposta sulle funzioni di complessità.
82
Capitolo 3. Complessità
dimostrare che f = O(g) con c = ε.
Per dimostrare invece che g 6= O(f ) usiamo una dimostrazione per assurdo.
Diciamo perciò che g = O(f ), e cerchiamo di arrivare ad una inconsistenza
logica.
Dalla definizione di o-grande sappiamo che:
g(n)
≤ f (n)
c
e dunque che:
1
f (n)
≤
c
g(n)
Ma giacché c è sempre positivo per definizione di o-grande, possiamo dire:
f (n)
≥0
g(n)
Se calcoliamo il limite di
f (n)
otteniamo quindi un certo ` certamente magg(n)
giore di zero. In formule:
lim
f (n)
n→∞ g(n)
>0
Ma questo risultato contraddice la condizione di partenza del caso antisimmetrico per cui quel limite deve valere 0. Abbiamo trovato l’assurdo, ergo non è
vero che g = O(f ) e quindi è vero che g 6= O(f ).
3.2.4
Le classi di complessità standard
Non resta che sfruttare la nostra bella notazione o-grande per definire alcune classi standard entro le quali categorizzare le complessità degli algoritmi,
così da evitare di scrivere l’intera e precisa funzione di complessità per ogni
algoritmo che studiamo.
I polinomi.
Consideriamo il generico polinomio:
p(n) = ak nk + ak−1 nk−1 + . . . + a1 n + a0
con ai ≥ 0 e almeno un ai 6= 0 con i 6= 0.
Dimostrare che p è o-grande di p 0 , dove p 0 (n) = nk (per brevità si scrive p =
O(nk )) è davvero semplice: è sufficiente creare una catena di maggiorazioni.
Iniziamo col dire che il nostro polinomio è minore o uguale allo stesso polinomio
con tutti i valori assoluti per ogni ai ni :
p(n) = ak nk + ak−1 nk−1 + . . . + a1 n + a0 ≤
| ak nk | + | ak−1 nk−1 | + . . . + | a1 n | + | a0 |
3.2. Misurare la complessità
83
Poi, eseguiamo una seconda maggiorazione andando a porre ogni ni pari a nk :
| ak nk | + | ak−1 nk−1 | + . . . + | a1 n | + | a0 |≤
| ak nk | + | ak−1 nk | + . . . + | a1 nk | + | a0 nk |
Non resta che raccogliere k per ottenere che:
p(n) ≤ nk · (| ak | + | ak−1 | + . . . + | a1 | + | a0 |)
Ora, giacché il contenuto della parantesi è necessariamente positivo grazie ai
valori assoluti e almeno un ai 6= 0 con i 6= 0, possiamo chiamarlo chiamarlo c,
ottenendo quindi:
p(n) ≤ c · nk
Abbiamo trovato la definizione di O-grande, per cui p = O(nk ).
Le funzioni esponenziali.
Sappiamo che, per ogni intero positivo k:
nk
=0
n→∞ 2n
lim
Applichiamo semplicemente il Teorema 3.1 per dire che nk = O(2n ), ma non
viceversa. Allo stesso modo, giacché:
2n
n = 0
n→∞ 22
lim
n
possiamo dire che 2n = O(22 ), ma non viceversa, e così via.
Le funzioni logaritmiche.
mo che:
Per L(n) = parte intera del logaritmo din abbiaL(n)
=0
n→∞ n k
lim
per ogni k intero positivo. Dunque L = O(nk ) e non viceversa.
Possiamo così individuare un sottoinsieme di funzioni totalmente ordinato da
O:
n
L, . . . , n, n2 , . . . , nk , . . . , 2n , . . . , 22 , . . .
Consideriamo tutte queste funzioni, quelle che ad esse si identificano tramite
O, o quelle che sono in relazione O con una di loro. Immaginiamole come
possibili indici di complessità di un algoritmo. Ci possiamo domandare fino a
quale livello in O possiamo ritenere che il corrispondente algoritmo sia efficiente
e quando non più. Una risposta a questa domanda richiede che decidiamo con
precisione quale criterio di misura intendiamo adottare per la complessità di
algoritmi e problemi, se il tempo, o la memoria, o altri parametri ancora.
Successivamente affronteremo la questione quando il parametro privilegiato è
il tempo.
84
Capitolo 3. Complessità
3.2.5
Un esempio introduttivo: MCD
È tempo di mettere in pratica quanto abbiamo studiato. In questa sezione
considereremo un esempio un po’ prematuro ma certamente istruttivo che ci
farà capire come calcolare la complessità su algoritmi numerici sia piuttosto
difficile da calcolare.
Vogliamo risolvere il classico problema del calcolo del massimo comune divisore
fra due numeri a e b. L’algoritmo di Euclide è certamente il più famoso
algoritmo mediante il quale calcolare l’MCD:
1
2
3
// P e r i p o t e s i , a > b e b > 0
MCD( a , b ) {
r = a % b;
4
i f ( r == 0 ) r e t u r n b ; // b MCD
e l s e MCD( r , b ) ;
5
6
7
}
Listing 3.1: Algoritmo di Euclide per il calcolo dell’MCD.
Come vediamo si tratta di un algoritmo ricorsivo. Ogni chiamata della procedura MCD effettua una divisione, operazione che ha costo lineare nel numero
di cifre del valore più grande coinvolto dalla divisione e quindi costo logaritmico
nel valore stesso2 .
Calcolare la complessità di questo algoritmo non è così banale. Conosciamo
il costo dell’operazione base di divisione (log1 0(a)) ma non sappiamo quante
chiamate ricorsive compia l’algoritmo.
Volendo, possiamo scrivere il nostro algoritmo in una forma più comoda secondo la definizione ricorsiva:
def
MCD(a, b) = if(mod(a,b)) == 0) then b else MCD(b, mod(a,b))
Calcolo della complessità
Al fine di calcolare la complessità dell’algoritmo MCD dobbiamo contare qualcosa che sia paragonabile ai passi di una MdT (ricordiamo che il modello di
calcolo per la teoria della complessità è quello delle macchine di Turing).
Una volta ottenuto il numero di passi, sapendo il costo di una singola divisione
(sempre in termini di passi di una MdT) non dovremo far altro che moltiplicare
tale costo per il numero di passi ed avremo ottenuto la complessità dell’algoritmo.
2
Il numero di cifre per scrivere il valore x in base y è pari a logy (x).
3.2. Misurare la complessità
85
I lettori più attenti si saranno resi conto di un inghippo: se è vero che la
teoria della complessità vuole trattare gli algoritmi (e in generale i problemi)
astraendosi dalla dimensione dei dati, come possiamo calcolare la complessità
dell’MCD senza considerare come sono rappresentati i dati, ad esempio, per
valutare la divisione?
Come sappiamo la complessità della divisione è dipendente dal numero di cifre
costituito dai due operandi, e se i valori che tiamo trattando sono rappresentati in basi diverse, il logaritmo che ci dice il numero delle cifre deve avere basi
diverse (ad esempio, log2 per il binario e log1 0 per il decimale).
Fortunatamente, la nostra astrazione regge. Infatti, noi lavoriamo su classi di
complessità definite mediante l’o-grande e non mediante valori precisi: logaritmi con basi diverse hanno tutte lo stesso o-grande (differiscono fra loro di una
costante), dunque, possiamo ignorare la base di un logaritmo (tendenzialmente si usa base 2) e procedere tranquillamente col calcolo della complessità.
Torniamo al nostro calcolo: come sappiamo le MdT non sono in grado di
rappresentare la ricorsione, pertanto la prima cosa che dobbiamo fare è trasformare l’algoritmo MCD ricorsivo in una versione iterativa (Listing 3.2)).
1
2
3
4
5
6
7
8
9
// P e r i p o t e s i , a > b e b > 0
MCD( a , b ) {
w h i l e ( mod ( a , b ) != 0 ) {
temp = b ;
b = mod ( a , b ) ;
a = temp ;
}
return b ;
}
Listing 3.2: Algoritmo di Euclide in versione iterativa per il calcolo dell’MCD.
Andiamo ora ad analizzare la sequenza di iterazioni al fine di comprendere
come si comporta l’algoritmo:
a = b · q0 + r0 dove r0 < b
b = r0 · q1 + r1 dove r1 < r0
r0 = r1 · q2 + r2 dove r2 < r1
..
.
rs−1 = rs · qs+1 + 0
L’ultimo passo, s, è il primo per cui un resto vale zero. Possiamo dunque
definire la catena di disequazioni:
b > r0 > r1 > r2 > · · · > 0
86
Capitolo 3. Complessità
Osserviamo che giacché b è un naturale e l’insieme dei naturali è un insieme
ben fondato3 , abbiamo appena dimostrato il fatto che l’algoritmo di Euclide
termina. Dedichiamoci ora al calcolo di s, ovvero il numero di passi entro i
quali l’algoritmo termina.
Date le equivalenze di cui sopra possiamo facilmente dire:
a = b · q0 + r0 = (r0 · q1 + r1 ) · q0 + r0
e giacché tutti i valori coinvolti sono positivo possiamo dire:
a = (r0 · q1 + r1 ) · q0 + r0 > r1 + r0
e poi, dato che r0 > r1 possiamo anche dire:
r1 + r0 > 2r1
Ricongiungendo il tutto otteniamo che:
a > 2r1
Abbiamo ottenuto una correlazione fra a ed r1 .
Andando a generalizzare, possiamo ottenere che alla i -esima iterazione si ha
che:
a > 2i r2i−1
Per verificare la formula è sufficiente considerare qualche altra iterazione dell’algoritmo. Sfruttiamo ora la correlazione appena trovata per calcolare il
valore di s in funzione di a (che è poi il nostro input).
Supponiamo per comodità che il numero di passi s sia un numero pari4 , ossia
che:
s = 2t
Ovviamente al passo t-esimo avremo che:
a > 2t r2t−1
Se a partire da questa formula riusciamo a calcolare t dovremo semplicemente
raddoppiare il suo valore per ottenere immediatamente s.
3
Un insieme ben fondato è un insieme sul quale è definibile una relazione d’ordine ben
fondata. Una relazione d’ordine su un insieme S si dice ben fondata o buon ordinamento
se ogni sottoinsieme Y ⊆ S non vuoto è dotato di minimo. In altre parole un insieme ben
fondato è un insieme nel quale ogni elemento ha un numero finito di elementi minori di lui.
4
Considerare un numero di passi pari significa eventualmente aumentare di uno il numero
di passi effettivo, una modifica che non intacca per nulla il nostro calcolo dato che noi stiamo
cercando una classe di complessità e non il conteggio preciso dei passi di esecuzione.
3.3. La complessità di problemi
87
Mediante qualche passaggio algebrico otteniamo:
!
a
a
t
> 2 ⇒ log2
> log2 (2t )
r2t−1
r2t−1
ossia:
log2
a
r2t−1
!
>t
Come sappiamo a noi non interessano le costanti ma vogliamo esprimere i
passi soltanto in funzione dell’input, dunque, possiamo dire:
t < log2 (a)
e dato che s = 2t abbiamo che:
s < log2 (a)
Abbiamo semplicemente ignorato il 2 poiché costante.
Ora che sappiamo che il numero di passi è minore di log2 (a) non resta che
calcolare la complessità del singolo passo e moltiplicarla per il numero di passi.
Ogni iterazione dell’algoritmo di euclide compie una divisione, la cui complessità è lineare nel numero di cifre dell’input a. Se supponiamo che a sia
rapppresentato in binario, allora a avrà log2 (a) cifre e dunque ogni iterazione
dell’algoritmo di Euclide costerà log2 (a). Ricordiamo che, come già osservato,
come viene rappresentato a (i.e., la base del logaritmo) è ininfluente rispetto
al calcolo della complessità.
In definitiva l’algoritmo di Euclide calcolato su due interi a e b con a > b termina in log2 (a) passi ognuno dei quali computa una divisione che costa log2 (a).
Pertanto, abbiamo che:
MCD(a, b) = O(log 2 (a)) = O(log(a))
ossia che l’algoritmo per il calcolo dell’MCD di Euclide è logaritmico nella
dimensione dei dati in input.
3.3
La complessità di problemi
Sino ad ora abbiamo parlato di complessità di algoritmi ma la teoria della
complessità, proprio come la teoria della calcolabilità, tratta problemi e non
singoli algoritmi. Andiamo dunque a definire la complessità di un problema.
Definizione 3.3. Un problema S appartiene ad una classe di complessità C(n)
se esiste una Macchina di Turing deterministica che lo decide in un numero
di passi di computazione minore o uguale ad C(n) dove n è la dimensione
dell’input.
88
Capitolo 3. Complessità
Per poter comprendere appieno la definizione appena riportata bisogna ricordare che ogni problema è caratterizzabile da un insieme per il quale la MdT
che lo decide è in grado di dire se un certo input x vi appartiene o meno.
Chiaramente la teoria della complessità si concentra sui problemi calcolabili
(che senso avrebbe calcolare la complessità di un problema non calcolabile?)
e dunque ci aspettiamo sempre di trovare una MdT che decida il problema in
esame. L’elemento di interesse sul quale ci concentriamo non è più la calcolabilità di un problema (che diamo per scontata) ma bensì in quanti passi un
certo problema viene calcolato.
Ora che disponiamo della nozione di calcolabilità di un problema possiamo pensare di classificare tutti i problemi calcolabili grazie alle classi di complessità
che abbiamo già accennato in precedenza.
Dalla prossima sezione iniziamo un percorso che ci farà scoprire le peculiarità
ed i problemi tipici di alcune di queste classi.
Si osservi che stando alla definizione appartenenza ad una classe di complessità uno stesso problema può appartenere a diverse classi: se abbiamo un
algoritmo A polinomiale per risolvere S è sempre possibile trovarne un’altro
più complesso (magari esponenziale) che risolve S (è sufficiente far compiere
ad A delle operazioni inutili). Per questa ragione l’appartenenza ad una classe
di complessità è definita per minimo.
3.4
La classe di complessità polinomiale
Riprendiamo in mano la Definizione 3.3 e istanziamola sulla classe di problemi
polinomiali:
Definizione 3.4 (P). Sia S un problema definito su un alfabeto A (S ⊆ A? ).
S appartiene alla classe di complessità P se esiste una Macchina di Turing
deterministica che lo decide in tempo polinomiale nella dimensione dell’input.
Ricordiamo che Cook definisce questi problemi trattabili, ossia effettivamente
computabili in tempi ragionevoli.
Introduzione ai problemi caratteristici
Ogni classe di complessità dispone di alcuni problemi tipici che in qualche modo
possono essere presi come ‘’rappresentanti‘’ di tutti gli altri (formalizzeremo
questo fatto parlando della riduzione fra problemi).
I due problemi caratteristici della classe P sono:
3.4. La classe di complessità polinomiale
89
• 2COL: il problema di colorare ogni nodo di un grafo utilizzando due colori,
rispettando però la condizione per cui presi due nodi a caso collegati da
un arco questi abbiano sempre colori diversi.
• 2SAT: il problema di determinare la soddisfacibilità di una congiunzione
di digiunzioni costituite da esattamente due letterali.
A breve sapremo tutto di questi problemi. Per i più impazienti possiamo anticipare che 2COL e 2SAT sono riducibili fra loro (ma non sapete ancora cosa
vuol dire!) e che i problemi 3SAT e 3COL, apparentemente non tanto diversi
da 2SAT e 2COL non sono inclusi in P.
3.4.1
Il problema 2COL
Il problema 2COL pone l’obiettivo di colorare ogni nodo di un grafo utilizzando
due colori: una soluzione è corretta se nessuna coppia di nodi direttamente
collegati fra loro è colorata con lo stesso colore. Iniziamo con l’osservare che
esistono grafi non 2-colorabili (ad esempio un triangolo), dunque 2COL non è
un problema di banale risoluzione (i.e., non tutti i grafi sono 2-colorabili).
La dimensione del dato
Parliamo prima di tutto della dimensione del problema. Un grafo generico G
è costituito da un insieme di vertici V = {v1 , . . . , vn } ed un insieme di archi
(rappresentati come coppie di nodi) E = {(vi , vj ), . . . , (vk , vq )}. Pertanto la
dimensione del grafo è calcolabile come la somma delle cardinalità dei due
insiemi; in simboli:
n = |V | + |E|
Diciamo poi che esiste un cammino fra i vertici vi1 , vi2 , vi3 , . . . vim se e solo se
∀i, j (vij , vi(j+1) ) ∈ E con j che varia da 1 ad m.
Una bozza di algoritmo
Andiamo a dimostrare che 2COL appartiene a P abbozzando un algoritmo
di risoluzione del problema. Si osservi che questo approccio è largamente
adottato nella teoria della complessità: non si scrive praticamente mai un
algoritmo in tutti i suoi dettagli implementativi, d’altronde, noi siamo soltanto
interessati al comportamento asintotico dell’algoritmo in questione.
Supponendo di chiamare i due colori c1 e c2 , possiamo scegliere un nodo a
caso e colorarlo come c1 . Dopodiché percorriamo tutti i suoi archi arrivando ai
suoi nodi direttamente vicini: coloriamo quei nodi come c2 . Ripetiamo questa
operazione fino a coprire l’intero grafo (se il grafo è una foresta ripetiamo il
procedimento per ogni componente connessa). Ogni volta che coloriamo un
nodo sono soltanto due gli esiti possibili:
90
Capitolo 3. Complessità
1. Il grafo è tutto colorato e l’algoritmo termina dichiarando il grafo 2colorabile.
2. Non è possibile applicare il colore ci al nodo poiché uno dei suoi nodi
adiacenti è colorato come ci . L’algoritmo termina dichiarando il grafo
non 2-colorabile.
Il nocciolo della questione risiede nel fatto che che quando si cade nel caso
2 l’algoritmo termini. Da un punto di vista intuitivo potremmo pensare di
dover fare backtracking, ma data la costituzione del problema (i.e., il fatto
che i colori siano soltanto due) una volta definito il colore di partenza (ed
è irrilevante quale questo sia) tutte le altre scelte sono obbligate. Trovare
un’inconsistenza è dunque sufficiente per dichiarare il grafo non 2-colorabile.
Calcolo della complessità. Calcolare la complessità del problema 2COL risulta immediato: per computare la colorazione non dobbiamo far altro che
percorrere tutti i nodi e parte degli archi del grafo, dunque, la complessità del
problema è O(n) dove n ricordiamo essere |V | + |E|.
Infine, giacché la complessità lineare è una complessità polinomiale, possiamo
dire che 2COL ∈ P.
3.4.2
Il problema 2SAT
2SAT è il problema di determinare la soddisfacibilità di una congiunzione di
digiunzioni costituite da esattamente due letterali, i.e., trovare un’assegnazione di valori di verità che renda vera una congiunzione di disgiunzioni costituiti
da esattamente due letterali. Arriveremo alla definizione logica del problema 2SAT mediante l’ausilio di un altro problema del tutto identico ma più
facilmente descrivibile.
Notazione
Introduciamo prima di tutto un po’ di notazione.
Ammettiamo di avere un alfabeto potenzialmente infinito:
p0 , p1 , p2 , . . .
e di poterne scrivere le lettere anche in maiuscolo:
P0 , P1 , P2 , . . .
Se vogliamo rendere il nostro alfabeto finito non dobbiamo far altro che includere le cifre da 0 a 9 nell’alfabeto stesso.
Decidiamo ora di poter formare soltanto parole di due lettere del tipo:
3.4. La classe di complessità polinomiale
91
1. Pi pj con i 6= j;
2. Pi Pj con i 6= j;
3. pi pj con i 6= j.
chiamiamo queste parole costituite da due letterali clausole. Il problema che
vogliamo risolvere consiste nel prendere un insieme di clausole c = {c1 , . . . , cn }
con n ≥ 1 e trovare una parola w che lo ricopra (nel senso che condivida
con ciascuna di esse almeno una lettera minuscola oppure maiuscola). La
soluzione w dovrà contenere almeno due letterali e dovrà rispettare i vincoli
imposti sulle clausole (i.e., non può apparire lo stesso letterale sia maiuscolo
che minuscolo).
Il problema appena descritto è identico al problema di soddisfacibilità di un
insieme di disgiunzioni di coppie di letterali congiunte fra loro.
Possiamo osservare questa sovrapposizione se:
• Interpretiamo con lettere minuscole variabili logiche positive e con lettere
maiuscole variabili logiche negate:
p0 ≡ A
P0 ≡ ¬A
• Interpretiamo ogni clausola come una disgiunzione:
p0 p1 ≡ A ∨ B
• Interpretiamo l’insieme delle clausole come una congiunzione di clausole:
C = {p0 p1 , p2 P1 } ≡ (A ∨ B) ∧ (C ∨ ¬B)
• Interpretiamo la parola w intersecante C come un’assegnazione di valori
di verità sui letterali dell’insieme delle clausole logiche:
w = p0 p1 p3 ≡ {A, B, C}
Dovrebbero ora risultare chiare le limitazioni imposte sulla costruzione delle
clausole e su w : se i = j in una clausola stiamo asserendo una semplice tautologia, mentre se i = j in w allora stiamo assegnando due valori di verità opposti
alla stessa variabile logica. Anche la nomenclatura che abbiamo adottato rispetta la metafora: le clausole intese come coppie di lettere sono clausole
logiche. Si osservi che con clausole del tipo pi Pj implementiamo l’implicazione
logica.
Per ragioni di comodità tratteremo 2SAT usando l’interpretazione ‘’linguistica‘’ anziché logica.
92
Capitolo 3. Complessità
Una bozza di algoritmo
Come già fatto per 2COL andiamo ad abbozzare un algoritmo che risolva il
problema 2SAT al fine di calcolarne la complessità.
Iniziamo col definire la dimensione dell’input come numero di letterali coinvolti:
la dimensione del problema è pari a 2n se n è il numero di clausole in C, infatti,
ogni clausola contiene esattamente due letterali.
L’algoritmo è il seguente:
1. Scegliamo un letterale a caso pi e lo includiamo in w (rispettando i vincoli
sulle clausole).
2. Rimuoviamo da C (l’insieme di clausole) tutte le clausole che contengono
pi : quelle clausole sono infatti coperte poiché pi ∈ w .
3. Per via dei vincoli imposti sulle clausole ed in particolare su w , la scelta
di pi fa sì che sia impossibile includere Pi in w . Pertanto, per far sì che
l’insieme di clausole sia coperto, ogni clausola che contiene Pi deve necessariamente avere l’altro letterale incluso in w . Si considerano dunque
tutte le clausole del tipo pi λk (dove λ può essere p o P ) e per ogni λk
trovato si ripetono il punto 2 ed il punto 3 dell’algoritmo.
Al termine delle chiamate ricorsive generate dal punto 3 vi sono due possibili
situazioni:
1. L’insieme delle clausole C è vuoto: w è la parola che ricopre C e dunque
l’equivalente logico dell’insieme di clausole è soddisfacibile.
2. L’insieme delle clausole C non è vuoto:
(a) Non è stato possibile aggiungere una certa clausola λk a w per via
dei vincoli imposti sulla parola ricoprente: l’insieme delle clausole
non è soddisfacibile poiché non esiste una parola ricoprente.
(b) È stato possibile aggiungere tutte le clausole trovate al punto 3 ma
semplicemente vi sono ancora clausole da considerare indipendenti
da quelle già considerate: ripartiamo dal punto 1 scegliendo un
nuovo pi .
Le osservazioni che possiamo fare su questo algoritmo sono molto simili a
quelle già fatte per 2COL: non abbiamo bisogno di backtracking perché le
variabili hanno soltanto due assegnazioni possibili (pi o Pi ) e la scelta di un
certo letterale obbliga le scelte sugli altri (se dipendenti). Inoltre abbiamo
che la scelta del valore del letterale di partenza è irrilevante; avremmo potuto
scegliere Pi come letterale di partenza ed avremmo avuto un algoritmo analogo.
3.4. La classe di complessità polinomiale
93
Un esempio di esecuzione. Per convincerci del funzionamento dell’algoritmo proviamo a fare un piccolo esempio.
Consideriamo l’insieme di clausole:
C = {p0 P1 , p0 p2 , P1 p3 , p4 P0 , P4 P5 }
che è equivalente alla formula logica:
(A ∨ ¬B) ∧ (A ∨ C) ∧ (¬B ∨ D) ∧ (E ∨ ¬A) ∧ (¬E ∨ ¬F )
Applichiamo ora l’algoritmo:
• Dal passo 1 scegliamo a caso un letterale, diciamo p0 .
w = p0
• Dal passo 2 rimuoviamo tutte le clausole ove p0 appare.
C = {P1 p3 , p4 P0 , P4 P5 }
• Dal passo 3 individuiamo le clausole del tipo P0 λk : ogni λk trovato viene
incluso in w e su λk vengono ripetuti i passi 2/3:
– Individuiamo la clausola p4 P0 e dobbiamo trattare p4 : lo includiamo
in w e rimuoviamo le clausole ove appare (passo 2):
w = p0 p4
C = {P1 p3 , P4 P5 }
– Dal passo 3 individuiamo le clausole del tipo p4 λk : ogni λk trovato
viene incluso in w e su λk vengono ripetuti i passi 2/3:
∗ Individuiamo la clausola P4 P5 e dobbiamo trattare P5 : lo includiamo in w e rimuoviamo le clausole ove appare (passo
2):
w = p0 p4 P5
C = {P1 p3 }
Chiusura delle chiamate ricorsive: non vi sono più dipendenze
da trattare.
• Ricominciamo dal passo 1 individuando una nuova clausola a caso, diciamo p3 .
w = p0 p4 P5 p3
94
Capitolo 3. Complessità
• Dal passo 2 rimuoviamo tutte le clausole ove p3 appare.
C = {}
Dato che C è vuoto, l’algoritmo termina e w è la nostra soluzione.
La parola w trovata (i.e., p0 p4 P5 p3 ) in effetti ricopre tutte le clausole contenute in C.
Volendo seguire la metafora logica, osserviamo che la parola w non descrive
un’assegnazione di valori di verità completa: i letterali che non appaiono in
w (in questo caso p1 e p2 ) possono assumere qualsiasi valore, dunque, in un
certo senso w definisce una famiglia di assegnazioni di verità possibili, tutte
soddisfacenti la formula logica equivalente a C.
Abbiamo dunque ottenuto che l’assegnazione di valori di verità:
A = True
D = True
E = True
F = False
B, C = True/False (qualsiasi valore)
soddisfa la formula equivalente a C:
(A ∨ ¬B) ∧ (A ∨ C) ∧ (¬B ∨ D) ∧ (E ∨ ¬A) ∧ (¬E ∨ ¬F )
Infatti, l’assegnazione di A a vero soddisfa la prima e la seconda clausola, l’assegnazione di D a vero soddisfa la terza, l’assegnazione di E a vero soddisfa
la quarta ed infine l’assegnazione di F a falso soddisfa la quinta. Le assegnazioni di B e C sono invece irrilevanti dal punto di vista della soddisfacibilità
(posto ovviamente di adottare le assegnazioni precedentemente definite sugli
altri letterali).
Calcolo della complessità. Calcolare la complessità del nostro algoritmo per
la risoluzione di 2SAT è semplice. Andiamo a contare quante volte leggiamo
l’insieme di letterali, considerando che ve ne sono 2n letterali, dove n è il
numero di clausole in C.
Ogni volta che includiamo un letterale in w (passo 1) dobbiamo leggere tutti
gli altri, ma ad ogni lettura l’insieme dei letterali diminuisce di due unità (la
clausola in il letterale letto appare). Pertanto il numero di letture risulta essere:
2n + (2n − 2) + (2n − 4) + (2n − 6) . . . + 2
Raccogliendo il due otteniamo:
2(n + (n − 1) + (n − 2) + . . . + 1)
3.4. La classe di complessità polinomiale
95
che sfruttando la somma di Gauss sappiamo essere:
n(n − 1)
2(n + (n − 1) + (n − 2) + . . . + 1) = 2
2
!
= n(n − 1)
Pertanto il numero di letture compiute dall’algoritmo è O(n2 ). Giacché ogni
lettura costa un passo, possiamo dire che la complessità dell’algoritmo di risoluzione di 2SAT è O(n2 ) e quindi che 2SAT ∈ P.
In realtà esiste anche un algoritmo lineare per risolvere 2SAT, tale algoritmo
però richiede un formato diverso dei dati in input.
3.4.3
Riduzione polinomiale fra problemi
Abbiamo già detto che 2SAT e 2COL sono problemi molto simili. In questa
sezione introdurremo il concetto di riduzione fra problemi, uno strumento che
ci renderà possibile definire con più precisione questa similarità fra 2SAT e
2COL, e in generale ci permetterà di stabilire quando un problema è riducibile
ad un altro.
Definizione 3.5. Un problema S si riduce in un problema S 0 se e solo se per
ogni dato D di S ∃ una funzione f tale che f (D) è un dato di S 0 e S ha
soluzione su D se e solo se S 0 ha soluzione su f (D).
La riduzione è detta polinomiale se f è polinomiale nel suo input.
Indichiamo con S ≤p S 0 il fatto che S si riduca polinomialmente a S 0 .
Saper ridurre un problema in un altro è di fondamentale importanza quando si
parla di complessità: supponiamo di conoscere un algoritmo polinomiale AS 0
per risolvere S 0 (ergo S 0 ∈ P): se sappiamo che S è riducibile polinomialmente
a S 0 (S ≤p S 0 ), allora disponiamo di una f che preso un qualsiasi dato D di S
produce un f (D) che è input di AS 0 .
A questo punto, costruire un algoritmo per risolvere S è immediato: prendiamo
il dato D che è input per il problema S e lo trasformiamo in f (D), dopodiché
diamo f (D) come input ad AS 0 e, qualsiasi sia la risposta5 di AS 0 , grazie al
fatto che S ≤p S 0 avremo garanzia che sarà anche la risposta corretta per S.
Chiamiamo AS l’algoritmo che abbiamo appena descritto: cosa sappiamo della
sua complessità? AS non è l’altro che la composizione di f e di AS 0 ; ma essendo
sia f che AS 0 polinomiali per definizione, possiamo asserire che S ∈ P.
In conclusione, osserviamo che:
5
Non dimentichiamo che i nostri problemi sono sempre problemi di decisione, ergo, le
risposte sono sempre del tipo sì/no.
96
Capitolo 3. Complessità
• Se un problema qualsiasi è riducibile polinomialmente in un altro problema di classe polinomiale, allora il problema di partenza è a sua volta
polinomiale.
• Se sappiamo che S ≤p S 0 ciò non è sufficiente per asserire che S 0 ≤p S:
le due dimostrazioni devono essere condotte indipendentemente.
3.4.4
Riduzione di 2COL a 2SAT
Mettiamo in pratica quanto appena appreso e andiamo a dimostrare che 2COL ≤p
2SAT. Per fare ciò dobbiamo prima di tutto trovare una funzione che trasformi
in tempo polinomiale un grafo in un’insieme di clausole, dopodiché dovremo
dimostrare che il grafo di partenza G è 2-colorabile se e solo se l’insieme di
clausole ottenuto mediante f a partire da G è soddisfacibile.
La funzione di trasformazione
Sappiamo che un grafo G è costituito da un insieme di vertici V ed un insieme
di archi E:
V = {v1 , . . . , vm }
E = {(vi , vj ), . . . , (vk , vq )}
Se ne deduce che la dimensione del dato di partenza è pari a |V | + |E|.
Chiamiamo I la funzione generatrice delle clausole a partire dal grafo.
Per ogni vertice vi ∈ V generiamo una clausola del tipo:
pi1 pi2
mentre per ogni arco (vi , vj ) ∈ E generiamo una coppia di clausole del tipo:
Pi1 Pj1 , Pi2 Pj2
dunque, I(G) è definita come6 :
I(G) = {pi1 pi2 |∀i tale che 1 ≤ i ≤ m} ∪ {Pi1 Pj1 , Pi2 Pj2 |(vi , vj ) ∈ E}
La funzione di trasformazione genera pertanto un numero di clausole pari a
|V | + 2|E| e conseguentemente un numero di letterali pari a 2(|V | + 2|E|).
Per compiere la trasformazione la funzione I deve percorrere l’intero grafo una
sola volta e dunque I è lineare (i.e., polinomiale) nel suo input G.
Il primo passo della dimostrazione è fatto: abbiamo costruito una funzione
polinomiale che trasforma un grafo in un insieme di clausole.
6
Non chiediamoci perché questa funzione di trasformazione sia definita così ma
consideriamola come caduta dal cielo.
3.4. La classe di complessità polinomiale
97
La correttezza della funzione di trasformazione
Dobbiamo ora verificare la trasformazione compiuta da I sia corretta, ossia
che:
G è 2-colorabile ⇔ I(G) è 2-soddisfacibile
Chiaramente intendiamo per I(G) il risultato della funzione I su G e non la
funzione di per sé (useremo anche in seguito questa notazione laddove necessario). Come sempre per dimostrare una bi-implicazione dobbiamo dimostrare
i due lati.
Implicazione verso destra (⇒). Vogliamo dimostrare che se G è 2-colorabile
allora anche I(G) è 2-soddisfacibile. Giacché supponiamo per ipotesi che G
sia 2-colorabile, deve esistere una funzione c : V → {c1 , c2 } che colora G correttamente: allora dimostriamo che se esiste c, deve necessariamente esistere
anche w , la parola che ricopre l’insieme di clausole I(G).
Sfruttiamo dunque la colorazione fornita da c per costruire w seguendo queste
due semplici condizioni7 :
1. Se c(vi ) = c1 allora aggiungiamo i due letterali pi1 Pi2 a w .
2. Se c(vi ) = c2 allora aggiungiamo i due letterali Pi1 pi2 a w .
Dobbiamo ora verificare che la così costruita w ricopra l’insieme di clausole
generato da I(G). Iniziamo col considerare un grafo semplice costituito da tre
vertici e due archi (ossia un cammino), dopodiché l’estensione ad un cammino
generico sarà immediata.
Consideriamo dunque il seguente grafo G:
G=
vj
vi vh
Questo grafo è chiaramente colorabile, e avremo che c(vi ) = c(vh ) 6= c(vj ).
Applicando I(G) a tale grafo otteniamo l’insieme di clausole:
CG = {pi1 pi2 , pj1 pj2 , ph1 ph2 , Pi1 Pj1 , Pi2 Pj2 , Pj1 Ph1 , Pj2 Ph2 }
dove le prime tre clausole sono rispettivamente per i vertici vi , vj e vh mentre
le altre due coppie di clausole sono rispettivamente per gli archi (vi , vj ) e
(vj , vh ). Osserviamo che quanto detto in precedenza è confermato: CG conta
esattamente sette clausole, poiché |V | + 2|E| = 3 + 2 · 2 = 7.
7
Ancora una volta supponiamo che tale costruzione ci sia stata donata da un essere divino
(o più di uno, per i lettori politeisti).
98
Capitolo 3. Complessità
Generiamo ora la w usando il metodo definito poc’anzi e supponendo che
c(vi ) = c(vh ) = c1 e c(vj ) = c2 (avremmo potuto considerare analogamente
la colorazione inversa):
w = pi1 Pi2 ph1 Ph2 Pj1 pj2
Non resta che verificare che w ricopra CG per dimostrare che un qualsiasi
cammino di due archi e tre nodi 2-colorabile dato in input ad I produce un
insieme di clausole 2-soddisfacibile:
• pi1 pi2 ∈ CG è coperta da pi1 ∈ w ;
• pj1 pj2 ∈ CG è coperta da pj2 ∈ w ;
• ph1 ph2 ∈ CG è coperta da ph1 ∈ w ;
• Pi1 Pj1 ∈ CG è coperta da Pj1 ∈ w ;
• Pi2 Pj2 ∈ CG è coperta da Pi2 ∈ w ;
• Pj1 Ph1 ∈ CG è coperta da Pj1 ∈ w ;
• Pj2 Ph2 ∈ CG è coperta da Ph2 ∈ w .
Perfetto! w ricopre CG . Ora, giacché abbiamo dimostrato che un qualsiasi
cammino di due archi e tre nodi 2-colorabile dato in input ad I produce un
insieme di clausole 2-soddisfacibile, è sufficiente dire che qualsiasi cammino è
scrivibile in una serie di cammini di due archi e tre nodi e quindi che se un
qualsiasi G è 2-colorabile allora anche I(G) è 2-soddisfacibile.
Implicazione verso sinistra (⇐). Dimostriamo ora l’altro lato dell’implicazione, ossia che se I(G) è 2-soddisfacibile allora G è 2-colorabile. La dimostrazione risulta essere un po’ più sofisticata rispetto a quella appena effettuata
dato che vi sono infinite clausole di partenza possibili. Per questa ragione effettuiamo una dimostrazione per assurdo.
Per ipotesi I(G) è 2-soddisfacibile, pertanto deve esistere una w che lo ricopre.
Ciò implica per definizione sul funzionamento di I che w contenga:
1. Per ogni vertice vi ∈ V il letterale pi1 oppure il letterale pi2 ;
2. Per ogni arco (vi , vj ) ∈ E:
(a) Il letterale Pi1 oppure il letterale Pj1 .
(b) Il letterale Pi2 oppure il letterale Pj2 .
3.4. La classe di complessità polinomiale
99
Teniamo a mente questi fatti perché ci torneranno utili in seguito per ottenere
l’assurdo.
Analogamente a quanto fatto prima, andiamo ora a definire la funzione di
colorazione c sfruttando w 8 :
1. Se pi1 ∈ w allora c(vi ) = c1 .
2. Se pi1 ∈
/ w allora c(vi ) = c2 .
Assumiamo poi che esista un arco fra i nodi vi e vj nel grafo G (ossia (vi , vj ) ∈
E) e che, per assurdo, questi nodi vengano colorati con lo stesso colore (i.e.,
violando le regole di colorazione), diciamo c1 . Se ciò accade, per come è
definita la funzione di colorazione c, in w devono essere presenti i due letterali
pi1 e pj1 . Tale fatto implica automaticamente (per i vincoli di consistenza di w )
che Pi1 e Pj1 non possano essere in w . Ma tale limitazione è in contraddizione
con la regola 2(a) che abbiamo enunciato poco fa: w deve necessariamente
contenere letterale Pi1 oppure il letterale Pj1 . Abbiamo ottenuto l’assurdo!
Ripercorriamo il ragionamento: abbiamo assunto per assurdo che c fornisca
una colorazione erronea per il grafo G da cui è stato generato un insieme
di clausole 2-soddisfacibili mediante I: se ne deduce che ogni qualvolta I(G)
sia 2-soddisfacibile il grafo di partenza G sia 2-colorabile, altrimenti avremmo
un’assurdo sulla costruzione di w .
Abbiamo dimostrato entrambi i lati della bi-implicazione e dunque abbiamo
dimostrato:
G è 2-colorabile ⇔ I(G) è 2-soddisfacibile
Questa implicazione unita al fatto che I(G) sia polinomiale ci permette di dire
che 2COL sia riducibile a 2SAT (in simboli 2COL ≤p 2SAT).
Ricordiamo che 2COL ≤p 2SAT non significa in alcun modo che 2SAT sia
riducibile a 2COL: come stiamo per imparare, dimostrare questa riduzione è
tutt’altro che immediato.
3.4.5
Riduzione di 2SAT a 2COL
Ridurre 2SAT a 2COL è un problema complesso. Alcuni potrebbero pensare
che basti semplicemente trovare una funzione I 0 che mimi in senso inverso ciò
che faceva la funzione di trasformazione I appena studiata, e non sarebbero in
fallo. Il problema consiste nel fatto che I genera un particolare set di clausole
(basti pensare che non genera mai clausole del tipo pi Pj ) e non un set qualsiasi: capiamo dunque che partire da un set di clausole qualsiasi e ricostruirvi
8
Come sempre, la costruzione ci arriva dal cielo.
100
Capitolo 3. Complessità
un grafo risulta tutt’altro che banale.
Per queste ragioni la riduzione di 2SAT a 2COL si effettua adottando un metodo alternativo: partiamo da un algoritmo noto che sfrutta un grafo (chiamiamolo G 0 ) per risolvere il problema 2SAT, poi, modifichiamo quel grafo
ottenendone un nuovo, diciamo G. Le modifiche apportate a G 0 al fine di
ottenere G dovranno far sì che se esiste una ‘’soluzione‘’ sul grafo G 0 (i.e.,
si è partiti da un insieme di clausole soddisfacibili) allora G è 2-colorabile e
viceversa.
Procediamo dunque in questo modo: prima studiamo l’algoritmo che risolve
2SAT usando il grafo G 0 e poi trattiamo la trasformazione da G 0 a G: se
le due operazioni sono polinomiali (e dunque la loro composizione è polinomiale) e se dimostriamo l’affermazione ‘’se esiste una ’soluzione’ sul grafo
G 0 allora G è 2-colorabile e viceversa‘’ avremo correttamente dimostrato che
2SAT ≤p 2COL.
Algoritmo di risoluzione di 2SAT mediante grafo
Il primo passo da fare è dunque quello di studiare l’algoritmo che sfrutta un
grafo per risolvere 2SAT, dimenticandoci totalmente di 2COL. Quando abbiamo introdotto 2SAT abbiamo già studiato un algoritmo di risoluzione per
questo problema, ma abbiamo anche accennato al fatto che ne esistesse uno
più performante (lineare invece che quadratico) che richiede però la trasformazione dei dati in input: è proprio di quell’algoritmo che stiamo parlando. Per
comodità, lo chiameremo A2SAT .
Trasformazione dell’insieme di clausole. Riprendiamo in mano la nostra
metafora logica/linguistica: una clausola è una disgiunzione di due variabili
logiche. La disgiunzione è difficilmente rappresentabile in un grafo dove un arco
o esiste o non esiste: una disgiunzione non ci permette mai di individuare con
precisione quale dei due letterali è vericato o se lo sono tutti e due. Sarebbe più
comodo poter lavorare soltanto con implicazioni e congiunzioni e questo, grazie
alle regole base della logica, è possibile. Infatti, vale la seguente equivalenza:
a ∨ b ≡ (¬a → b) ∧ (¬b → a)
Perciò, possiamo prendere il nostro insieme di clausole da ricoprire C e tradurlo in un insieme logicamente equivalente costituito però da clausole che
usano solo implicazioni (la congiunzione era già presente a livello dell’insieme
di clausole).
Facciamo un esempio per chiarirci le idee. Riprendiamo in mano l’insieme di
clausole che abbiamo già usato negli esempi precedenti:
C = {p0 P1 , p0 p2 , P1 p3 , p4 P0 , P4 P5 }
3.4. La classe di complessità polinomiale
101
Applicando l’equivalenza logica appena descritta possiamo trasformare C ottenendo un insieme di clausole logiche (ad ogni riga corrisponde la trasformazione
di una clausola):
C={
P0 → P1 , p1 → p0 ,
P0 → p2 , P2 → p0 ,
p1 → p3 , P3 → P1 ,
P4 → P0 , p0 → p4 ,
p4 → P5 , p5 → P4
}
Possiamo notare che la metafora logica/linguistica è un po’ forzata qui, dato che abbiamo introdotto le implicazioni logiche mantenendo però i letterali
positivi e negativi formalizzati mediante p maiuscole e minuscole, inoltre, la
congiunzione è rappresentata dalla virgola. A questo punto della trattazione
dovrebbe comunque essere chiara la totale equivalenza fra le due rappresentazioni.
Si osservi che l’eventuale presenza di doppioni in C non è un problema, dato
che ogni clausola logica è in congiunzione con tutte le altre.
Creazione del grafo orientato. Dopo aver trasformato C, andiamo a creare
un grafo orientato in cui ogni letterale è un nodo (quindi si distingue fra letterali
maiuscoli e minuscoli) e fra due letterali esiste un arco orientato se uno implica
l’altro.
A partire da C otteniamo dunque il grafo G 0 :
P4
P0
P1
p1
p5
p2
P3
p3
p0
p4
P2
P5
A questo punto, per sapere se C è soddisfacibile sfruttiamo il seguente teorema:
Teorema 3.2. L’insieme di clausole C è soddisfacibile se e solo se il grafo
orientato ottenuto a partire da C (in cui ogni letterale è un nodo e fra due
102
Capitolo 3. Complessità
letterali esiste un arco orientato se uno implica l’altro) non ha un ciclo che
collega un nodo pi con un nodo Pi per un qualche i .
Non dimostriamo questo teorema ma è semplice convincerci del fatto che sia
corretto: avere un ciclo fra pi e Pi nel grafo G 0 significherebbe, giacché l’implicazione è transitiva, dire che l’insieme di clausole che vogliamo soddisfare
contiene l’implicazione pi → Pi , ossia A → ¬A.
Tanto per convincerci della correttezza del teorema prendiamo un contro
esempio. Consideriamo l’insieme di clausole C 0 :
C 0 = {p0 p1 , p0 P1 , P0 p1, P0 P1 }
C 0 è evidentemente non soddisfacibile: abbiamo semplicemente preso tutte le
combinazioni di valori di verità della disgiunzione, che, in congiunzione fra loro
portano sempre ad un falso.
Trasformando C 0 otteniamo:
C0 = {
P0 → p1 , P1 → p0 ,
P0 → P1 , p1 → p0 ,
p0 → p1 , P1 → P0 ,
p0 → P1 , p1 → P0 ,
}
Il grafo che si ottiene a partire da C 0 è:
P0
P1
p0
p1
Evidentemente sono presenti molti cicli, ma noi siamo interessati solo a quelli
che colleghino un Pi con il suo pi . Possiamo dunque osservare che P1 è
collegato a p0 il quale è collegato a p1 che a sua volta è collegato a p0 il
quale è collegato a P1 . Ecco individuato il ciclo che collega P1 a p1 : C 0 non è
soddisfacibile.
3.4. La classe di complessità polinomiale
In merito alla complessità.
ritmo.
103
Parliamo ora della complessità di questo algo-
Se il numero di letterali (diversi fra loro) in C è pari ad n e il numero di clausole
è m, (chiaramente con m ≥ n) allora il problema 2SAT ha dimensione n + m.
La trasformazione che effettuiamo (ossia la generazione di G 0 ) crea tanti nodi
quanti sono i letterali (i.e., n nodi) e due archi per ogni clausola in C (i.e.,
2m) per una dimensione totale di n + 2m. Pertanto la creazione del grafo ha
costo lineare.
La teoria dei grafi ci dice poi che verificare la presenza di determinati cicli
all’interno di G 0 ha costo lineare, perciò, l’intero algoritmo A2SAT ha costo
lineare, ergo, A2SAT ∈ P.
Trasformazione del grafo: da G 0 a G
Il primo dei due passi che dovevamo fare è stato compiuto: abbiamo appreso
un algoritmo che trasforma il problema 2SAT in un problema di individuazione
di cammini su un grafo, il quale ha un algoritmo di risoluzione che abbiamo
chiamato A2SAT e che ha costo polinomiale.
Di fatto abbiamo dimostrato la riduzione 2SAT ≤p A2SAT . Si osservi che questa riduzione è interessante, dato che ci permette di diminuire la complessità
dell’algoritmo di risoluzione di 2SAT da quadratica a lineare.
Non resta che ridurre il problema A2SAT a 2COL, così avremo ottenuto una
catena di riduzioni che ci permetterà finalmente di dire che 2SAT ≤p 2COL.
Prima di iniziare, un appunto sulla notazione: chiamiamo G 0 positivo se non
ha cicli che collegano pi e Pi per un qualche i e G 0 non positivo altrimenti.
Riflettiamo su ciò che vogliamo fare: se G 0 è positivo vogliamo che G sia
colorabile e viceversa se G non è colorabile vogliamo che G 0 sia non positivo.
Per aiutarci nello scopo possiamo tenere a mente che un qualsiasi grafo privo
di cicli (si intende cicli qualsiasi) è 2-colorabile, mentre un qualsiasi grafo che
contenga almeno una clique9 K3 non è 2-colorabile.
Usando quanto appena detto, per risolvere il problema è sufficiente apportare
tre semplici modifiche a G:
1. Consideriamo ogni arco orientato come un arco non orientato (li sdoppiamo in pratica).
9
Nella teoria dei grafi, una clique è un insieme di vertici V in un grafo non orientato, tale
che, per ogni coppia di vertici in V , esiste un arco che li collega. Nel caso specifico di clique
K3 possiamo pensare ad un ‘’triangolo‘’.
104
Capitolo 3. Complessità
2. Se G 0 era positivo (prima della modifica al passo 1) vogliamo garantire
che G sia colorabile, pertanto rimuoviamo tutti i cicli di G 0 . Per fare ciò
è sufficiente creare dei nodi ‘’dummy‘’ che sdoppino i nodi coinvolti in
cicli.
3. Se G 0 era non positivo (prima della modifica al passo 1) vogliamo garantire che G non sia colorabile, pertanto aggiungiamo una clique a
G0.
Ecco fatto! Abbiamo ottenuto il nostro G che, per costruzione, è 2-colorabile
se G 0 era positivo (e quindi C era soddisfacibile) e viceversa.
In merito alla complessità. Evidentemente la complessità della trasformazione da G 0 a G è lineare: se G 0 era positivo dobbiamo percorrere l’intero grafo
e rimuovere i cicli, se invece G 0 non era positivo dobbiamo solo aggiungere una
componente costante al grafo.
Abbiamo appena dimostrato che A2SAT ≤p 2COL, mentre nella sezione precedente abbiamo dimostrato che 2SAT ≤p A2SAT , perciò possiamo usare le
riduzioni in cascata per dire finalmente che 2SAT ≤p 2COL.
3.4.6
Un problema interessante: il test di primalità
Dedichiamo ora qualche parola su uno dei problemi più rilevanti per la computer science, ossia il test di primalità (detto PRIMES). Ci interessiamo a questo
problema non solo per la sua importanza storica, ma anche per un fatto peculiare: nel 2002 alcuni scienziati ne hanno cambiato la classe di complessità.
Il test di primalità è largamente utilizzato in ambito crittografico e consiste
semplicemente nel dire se un certo numero è primo o meno.
Fino al 2002 il test di primalità era ritenuto appartenere alla classe RP 10 ma
nel 2002 Agrawal, Kayal e Saxena fornirono un algoritmo deterministico polinomiale per PRIMES (noto come algoritmo AKS) di complessità O(log(N)12 ),
dimostrando quindi che PRIMES ∈ P. Purtroppo, dato che l’esponente del
logaritmo è piuttosto elevato, la scoperta dei tre scienziati non ha comportato
significativi vantaggi rispetto ai ben noti metodi probabilistici.
Il fatto che nella storia recente si sia spostato PRIMES da una classe di complessità ad un altra è emblematico del fatto che la tesi di Cook è molto meno
‘’assoluta‘’ rispetto alla tesi di Church-Turing.
10
La classe RP (Randomized Polynomial time) è la classe di complessità dei problemi
decisionali eseguiti su una macchina di Turing probabilistica. La sua relazione rispetto alle
classiche classi P ed N P è la seguente: P ⊆ RP ⊆ N P. Non diciamo altro onde evitare
crisi isteriche di pianto.
3.4. La classe di complessità polinomiale
105
Un approccio naive a PRIMES
Non è nostro interesse addentrarci nelle complesse dimostrazioni relative all’appartenza di PRIMES a P, limitiamoci soltanto a calcolare la complessità
di un algoritmo banalissimo per la risoluzione di PRIMES.
L’algoritmo più ignobile che ci venga in mente per verificare se un certo numero N è primo consiste nel dividere N per tutti i suoi predecessori a partire
da 2 e verificare che per ognuno di essi il resto della divisione sia diverso da 0.
La complessità di questo algoritmo è presto detta: compiamo N − 2 (diciamo
N per comodità) divisioni, ognuna delle quali costa log2 (N) (ossia il numero di
cifre dell’input, supponendo di usare una rappresentazione binaria). Pertanto
il numero di operazioni da compiere è N · log2 (N).
Come sappiamo la complessità deve però essere definita sulla dimensione dell’input (i.e., il suo numero di cifre) e non sul suo valore.
Pertanto, se chiamiamo n il numero di cifre di N (ossia n = log2 (N)) avremo
che:
N · log2 (N) = N · n
Dopodiché, usiamo l’equivalenza algebrica:
N = 2log2 (N)
per scrivere:
N · n = 2log2 (N) · n
e quindi poter definire correttamente la complessità usando soltanto la dimensione dell’input n:
2log2 (N) · n = 2n · n = O(2n )
Se ne deduce che l’algoritmo che abbiamo trovato per la risoluzione di PRIMES è esponenziale.
Volendo è possibile migliorare leggermente il nostro algoritmo evitando
di di√
videre N per tutti
√ i suoi predecessori, ma fermandoci invece a N (facendo
dunque soltanto N divisioni). In altre parole stiamo dicendo che se per ogni
√
N
valore k con 2 ≤ k ≤ N la divisione
da resto diverso da 0 allora N è
k
necessariamente primo.
Possiamo dimostrare questo fatto
per assurdo: diciamo di non limitare l’al√
goritmo e di trovare un j > N che divide con resto zero N (dimostrando
quindi che N non è primo). Si osservi che per essere giunti a questo punto
dell’algoritmo tutti i numeri precedenti a j sono già stati verificati. Ora, se j
divide N con resto zero, deve necessariamente√esistere un quoziente q tale per
cui N = q · j e quel q deve essere minore di N altrimenti la moltiplicazione
106
Capitolo 3. Complessità
√
fra j (che è maggiore di N) e q darebbe un numero maggiore di N (mentre
noi sappiamo che N = q · j).
√
Ecco l’assurdo: dal canto suo q non può essere minore di N poiché altrimenti l’algoritmo lo avrebbe già trovato in precedenza e si sarebbe fermato per
questo, infatti, l’algoritmo prosegue finché trova un k < N per cui la divisione
N
ha resto diverso da 0.
k
√
Abbiamo quindi dimostrato che che esiste un j > √N che divide con resto
zero N deve necessariamente esserci un q minore di N (e quindi già trovato
dall’algoritmo) che lo divide con resto zero (e quindi avremmo già asserito che
N non è primo in precedenza).
Se ne deduce che sia inutile verificare tutti i
√
numeri successivi a N.
La miglioria che abbiamo appena apportato
all’algoritmo non fa altro che
√
ridurre il numero delle divisioni da N a N, ottenendo quindi una complessità:
√
N · log2 (N)
Rifacendo gli stessi passi di trasformazione prima, otteniamo che:
√
√
√
N · n = 2log2 ( N) · n = O(2log2 ( N) )
Purtroppo non siamo riusciti a ridurre la classe di complessità: siamo sempre
nell’esponenziale.
3.4.7
La classe dei problemi co-P
Per come abbiamo definito i problemi (di fatto, insiemi) e la loro decidibilità
(dire sì o no a seconda che un certo elemento sia nell’insieme o meno), dato
un problema S è di immediata semplicità definire il suo problema complementare.
Più formalmente, definiamo il problema complementare di S ⊆ A? come l’insieme A? − S, ossia come l’insieme dei ‘’non risultati‘’ di S. Ma, se S è
un problema decidibile allora disponiamo di una MdT MS che lo decide. Se
chiamiamo S 0 = A? − S il problema complementare di S, allora possiamo immediatamente ottenere una MdT MS 0 che lo decida: basta far eseguire MS e
se il suo risultato è sì restituiamo no, mentre se il suo risultato è no restituiamo sì. In pratica MS 0 non fa altro che invertire i risultati di MS e così facendo
calcola S 0 , il problema complementare di S.
Dal punto di vista della complessità chiamiamo l’insieme dei problemi complementari a problemi polinomiali co-P, e per quanto appena detto risulta ovvio
asserire che:
co-P = P
3.4. La classe di complessità polinomiale
107
Infatti, tornando all’esempio di poco fa, se S ∈ P allora per definizione S 0 ∈
co-P. Poi, sempre per il fatto che S ∈ P, possiamo dire che MS esegue
in tempo polinomiale e dunque lo farà anche MS 0 (dato che sostanzialmente
esegue MS più una operazione) dimostrando che S 0 ∈ P.
Se l’equivalenza co-P = P risulta ovvia per la classe polinomiale impareremo a
breve che non lo è altrettanto quando applicata su altre classe di complessità.
3.4.8
Note finali in merito alla classe polinomiale
Ci apprestiamo a conludere la trattazione in merito alla classe P. Abbiamo
studiato 2COL e 2SAT, due problemi rappresentativi della classe polinomiale
e abbiamo anche dimostrato che sono riducibili fra loro. Prima di introdurre
nuove classi di complessità, aggiungiamo qualche nota finale in merito a P.
Definizione alternativa della classe di problemi polinomiali
Sappiamo bene che se A è un alfabeto finito, allora un certo insieme S ⊆ A?
rappresenta un problema di decisione. La definizione che abbiamo dato di un
problema S ∈ P è la seguente:
‘’Un problema S appartiene alla classe di complessità P se esiste una Macchina
di Turing deterministica che lo decide in tempo polinomiale nella dimensione
dell’input.‘’
Alcuni testi forniscono invece la definizione:
‘’Un problema S appartiene alla classe di complessità P se esiste una Macchina
di Turing deterministica che lo accetta in tempo polinomiale nella dimensione
dell’input.‘’
Questa definizione può sembrare strana, ma in realtà risulta legittima se pensiamo che una MdT che accetta un linguaggio può essere facilmente trasformata
in una MdT che lo decide se conosciamo la complessità del problema trattato. Supponiamo ad esempio di avere una MdT che accetti un linguaggio S in
tempo O(nk ): possiamo introdurre nella MdT un contatore di passi che una
volta raggiunto il passo nk restituisca no. Così facendo, se l’input appartiene a
S la risposta sarà sì dato che la MdT di partenza accettava S (e ciò accadrà
certamente prima del passo nk ) e se invece l’input non appartiene ad S prima
o poi si arriverà al passo nk e si avrà no come risposta.
Suddivisioni della classe di complessità polinomiale
Abbiamo più volte parlato di complessità lineari, quadratiche, ecc. Tale gerarchia di classi che noi abbiamo sempre dato per scontato è in realtà formalizzata
108
Capitolo 3. Complessità
e viene indicata mediante la dicitura DTIME. In pratica ogni problema S appartenente a P è O(nk ) per un certo k, ma a seconda del k ad esponente
potremmo in realtà dire che S appartiene alla classe di complessità DTIME-K
O(nk ).
Basandoci su questa classificazione più sofisticata possiamo definire P come:
P=
[
DTIME-K O(nk )
k
3.5
La classe di complessità non deterministica polinomiale
In questa sezione parleremo della celeberrima classe di complessità N P e del
suo rapporto controverso con la classe P. Invece di dare la definizione nuda e
cruda di questa classe di complessità faremo alcune riflessioni che ci porteranno
a comprenderne il significato e l’importanza, potendo così capire appieno la
sua definizione.
3.5.1
Non c’è il due senza il tre: da 2SAT a 3SAT
Parlando di 2SAT abbiamo asserito che tale problema è in P solo in virtù del
fatto che le clausole trattate sono di dimensione 2. Infatti, dovendo lavorare
con clausole costituite da tre letterali (e quindi trattando il problema 3SAT)
l’algoritmo di risoluzione che abbiamo adottato fin’ora non ci sarebbe più di
aiuto: il nocciolo di quell’algoritmo era il fatto che la scelta di un letterale pi
imponesse per ogni clausola del tipo Pi λk la scelta di λk . Capiamo però che
avendo una clausola con tre letterali bindarne uno soltanto non ha un effetto
costrittivo totale sulle clausole in cui appare il letterale inverso.
Chiaramente un discorso del tutto analogo si potrebbe fare per 2COL e 3COL:
la scelta di un colore per il nodo vi non impone più univocamente il colore su
tutti i nodi collegati a vi .
Comprendiamo dunque che 3COL e 3SAT sono problemi che non fanno parte
di P, ma anzi richiedono algoritmi facenti uso di backtrack e che potenzialmente potrebbero dover esplorare tutte le possibili combinazioni di colori/valori di
verità. Sia chiaro che questa non è una dimostrazione definitiva del fatto che
3COL e 3SAT non appartengano a P, ma disponendo soltanto di algoritmi
esponenziali per la risoluzione di questi problemi non possiamo far altro che
piazzarli in una classe di complessità più grande di P; infatti, come stiamo per
imparare, questi problemi si trovano nella classe N P.
3.5. La classe di complessità non deterministica polinomiale
3.5.2
109
Considerare il problema di verifica
Continuiamo a parlare di 3SAT e concentriamoci sul problema di verificare
una soluzione per 3SAT. Disponiamo dunque di un certo insieme di clausole
costituite da tre letterali C e anche di una parola w che potrebbe ricoprire C
(e quindi essere una soluzione di 3SAT su C).
Il problema di verifica è di immediata scrittura: è sufficiente scorrere tutti i
letterali di w e rimuovere da C ogni clausola ricoperta. Tale algoritmo ha
dunque costo `(w ) · n dove n è il numero di clausole in C e ` è una funzione
che restituisce la lunghezza di una parola. Se ne deduce che la complessità
dell’algoritmo di verifica per 3SAT sia lineare nel numero di clausole contenute
in C, dunque, il problema di verifica di 3SAT è incluso in P.
Bene, cosa ce ne facciamo di tutto ciò? Parecchio, dato la classe N P è quella
classe che contiene i problemi i cui problemi di verifica sono polinomiali.
Colpo di scena, eh?
3.5.3
Prima definizione di N P
Facciamo chiarezza sull’affermazione di poco fa.
La classe N P vuole raccogliere quei problemi che sono più complessi di P ma
che godono di una particolarità: hanno problemi di verifica della loro soluzione
che sono polinomiali. Questo tipo di definizione che sembra essere un po’
nebulosa è in effetti un poco fuorviante: stiamo definendo una proprietà di
un problema (essere in N P) guardando la proprietà (essere in P) di un altro
problema ad esso correlato (il problema di verifica di una soluzione). Ecco
dunque la definizione della classe di complessità N P:
Definizione 3.6 (N P). Sia S un problema definito su un alfabeto A (S ⊆ A? ).
S ∈ N P se e solo se esiste un S 0 che è estensione polinomiale di S e tale che
S 0 ∈ P.
Per comprendere la definizione appena riportata dobbiamo conoscere la nozione di estensione polinomiale:
Definizione 3.7 (Estensione polinomiale). Sia S un problema definito su un
alfabeto A (S ⊆ A? ). S 0 è un’estensione di S se e solo se per ogni parola
w ∈ S ∃y ∈ A? tale che concat(w y ) ∈ S 0 . In particolare S 0 è un’estensione
polinomiale di S se e solo se `(y ) ≤ ps (`(w )) dove ps è un polinomio.
Abbiamo formalizzato quanto detto a parole fin’ora: se prendiamo ad esempio
3SAT (l’S della definizione), possiamo dire che questo appartiene a N P poiché
il suo problema di verifica (S 0 ) è una sua estensione polinomiale ed appartiene
a P. Nel prossimo paragafo lo andremo a dimostrare formalmente.
110
Capitolo 3. Complessità
Per quanto riguarda la definizione di estensione possiamo parafrasarla dicendo
che ogni parola w ∈ S ha una sua corrispondente parola in S 0 che più lunga
di un certo y e che la contiene (concat è una funzione di concatenazione).
L’estensione è poi detta polinomiale se la lunghezza di y è non maggiore di un
polinomio nella lunghezza di w .
Si osservi dunque che imponiamo due condizioni:
• Col fatto di imporre S 0 estensione polinomiale di S stiamo dicendo
che il passaggio dal problema originale a quello di verifica deve essere
polinomiale.
• Nella definizione imponiamo anche che l’algoritmo di verifica vero e
proprio sia polinomiale (S 0 ∈ P).
È tempo di fare un esempio per impratichirci con questi nuovi concetti.
3.5.4
3SAT è N P secondo la prima definizione
Al fine di rendere la definizione appena fornita di N P un po’ più chiara proviamo ad applicarla al nostro problema 3SAT.
Iniziamo col dire che le soluzioni note per risolvere 3SAT sono tutte esponenziali, dunque 3SAT non appartiene a P. Stando alla nomenclatura della
Definizione 3.6, w è un input per il problema 3SAT (una scelta un po’ sfortunata dato che fin’ora w era la parola ricoprente l’insieme di clausole C: facciamo
attenzione a non confonderci). Pertanto w è una codifica di C, l’insieme delle
clausole di cui vogliamo decidere la soddisfacibilità. Non resta che dire che se
n è il numero di clausole in C allora w sarà lunga 3n (ricordiamo che stiamo
trattando clausole di tre letterali).
Consideriamo ora y , una possibile soluzione per l’input w . Tale y sarà null’altro che una una sequenza di letterali maiuscoli o minuscoli che potrebbero o
meno ricoprire C (i.e., essere soluzione per l’input w ). Ciò che possiamo dire
di y è che sarà al massimo lunga 3n, proprio come w , ovvero conterrà al massimo tutti i possibili letterali espressi in C (anche se sarà tendenzialmente più
piccola). Ecco quindi che abbiamo verificato il fatto che S 0 sia un’estensione
polinomiale di S, giacché dalla definizione avevamo:
`(y ) ≤ ps (`(w ))
e qui abbiamo:
`(y ) ≤ 3n
Ovviamente 3n è un polinomio nella lunghezza di w poiché è proprio la lunghezza di w .
3.5. La classe di complessità non deterministica polinomiale
111
Insomma, abbiamo definito un problema S 0 che prende in input w (codifica
delle clausole da soddisfare) e y (una possibile soluzione) e risponde sì se y
ricopre w e no altrimenti. S 0 è dunque il problema di verifica di S e giacché
sono vere le due condizioni:
• S 0 è estensione polinomiale di S (lo abbiamo appena dimostrato),
• S ∈ P (lo abbiamo dimostrato illustrando l’algoritmo di verifica in
Sezione 3.5.2)
possiamo dire che S ∈ N P.
In seguito alla dimostrazione appena effettuata possiamo comprendere con
ancora più precisione la definizione di estensione polinomiale: dire ∀w ∈ S
significa dire ‘’ogni input per il quale l’algoritmo che decide S restituisce sì‘’,
e noi stiamo dicendo che per ognuno di quegli input ‘’positivi‘’ esiste almeno
una soluzione y tale da creare parola w y la quale si troverà nell’insieme S 0 ,
facendo sì che l’algoritmo di verifica che decide S 0 terminerà su sì avendo la
coppia w , y in input.
Riportandoci al caso di 3SAT possiamo dire se l’insieme w è soddisfacibile
allora deve apparire in S e deve esistere anche una sua soluzione y ; mediante
quella soluzione creiamo la parola w y che appartiene a S 0 e sulla quale quindi
il problema di verifica (S 0 stesso) restituirà sì (confermando che y è soluzione
di w ).
Infine, il fatto che y cresca in modo non più che polinomiale rispetto a w
ci permette di dire che S 0 non solo è un’estensione qualsiasi, ma è un’estensione polinomiale. Se poi è anche vero che l’algoritmo di decisione per S 0 è
polinomiale possiamo finalmente dire che S ∈ N P.
3.5.5
Seconda definizione di N P
La definizione che abbiamo visto non è l’unica definizione della classe N P. Ne
esiste infatti un’altra un po’ meno macchinosa che però richiede un cambio del
modello computazionale per poter essere compresa. Introduciamo le macchine
di Turing non determistiche.
La macchina di Turing non deterministica
Abbiamo già affermato in precedenza che la MdT non deterministica ha la
stessa potenza computazionale di una MdT deterministica, ossia, le due macchine sono in grado di calcolare esattamente le stesse cose (tutto ciò che è
calcolabile). Dando ora una definizione più precisa della MdT non deterministica potremo anche fornire una spiegazione più precisa del perché le due
macchine siano equivalentemente potenti (addirittura scopriremo di averlo indirettamente già dimostrato!).
112
Capitolo 3. Complessità
Dal punto di vista della complessità le due macchine sono però due modelli
molto diversi: noi siamo interessati a contare i passi di esecuzione e una MdT
deterministica ne esegue potenzialmente molti di più rispetto ad una deterministica.
Una MdT non deterministica dispone di:
• Un’alfabeto A ∪ {∗}, dove con ∗ intendiamo lo spazio vuoto.
• Un’insieme di stati Q ∪ {q0 }, dove q0 è lo stato iniziale.
• Le operazioni della macchina sono definite mediante una serie di relazioni
di transizione δ.
Osserivamo che non si parla più di funzioni di transizione ma bensì di relazioni.
Questo accade poiché da un certo stato è potenzialmente possibile applicare
più di una δ. In altre parole possiamo dire che uno stato q ∈ Q ed un simbolo
s ∈ A letto dalla testina non bastano più per definire deterministicamente il
comportamento della MdT.
Insomma, δ è definita nelle MdT non deterministiche così come lo era nelle
MdT deterministiche, ossia:
δ : A ∪ {∗} × Q ∪ {q0 } → A ∪ {∗} × Q ∪ {q0 } × {+1, −1}
ma la differenza consta nel fatto che possano esistere due transizioni del tipo:
δ(q0 , 1, q 0 , s 0 , Sp0 )
δ(q0 , 1, q 00 , s 00 , Sp00 )
Come vediamo, entrambe le transizioni hanno stato di partenza q0 e simbolo
letto 1.
Anche la nozione di terminazione e di output della MdT devono essere rivedute.
Per le MdT deterministiche tale definizione era ovvia: una MdT deterministica
termina con un certo output se la sua esecuzione si interrompe e sul nastro
possiamo leggere il risultato del suo lavoro. Dal canto loro le MdT non deterministiche sono in grado di svolgere per uno stesso input molte computazioni
diverse; perciò diciamo che una MdT non deterministica termina se esiste una
computazione per cui termina e l’output sarà ovviamente ciò che leggiamo sul
nastro alla conclusione della computazione in cui la MdT termina.
Sulla complessità. Per quanto concerne la complessità della MdT non deterministica possiamo riadattare le varie definizioni già date per la MdT deterministica, ad esempio possiamo dire che una MdT non deterministica M
decide polinomialmente un problema S se esiste almeno una computazione di
M che decide S in tempo polinomiale.
3.5. La classe di complessità non deterministica polinomiale
113
Un albero di computazioni. Ma cosa significa davvero lavorare con una
MdT non deterministica? Giacché una MdT non deterministica raccoglie in se
stessa tutte le sue possibili computazioni, è come se noi potessimo contemporaneamente osservarle e trattarle tutte.
L’immagine che segue dovrebbe aiutarci a comprendere cosa sia una MdT non
deterministica:
M
a1 b1 c1 d1
a2 b2 c2 d2
a3 b3 c3 d3
..
.
..
.
c4
..
.
L’albero appena mostrato rappresenta tutte le computazioni di M, una macchina non deterministica che è tale sulla scelta della prima relazione di transizione. a, b, c, e d sono tutte computazioni diverse della stessa macchina M ed
ogni nodo dell’albero indica l’applicazione di una certa regola delta. Quando
abbiamo un branch abbiamo un punto di non determinismo, ossia un punto
in cui possiamo scegliere una δ piuttosto che un’altra (in questo caso il non
determinismo è sull’applicazione della prima regola). Osserviamo poi che la
computazione che abbiamo chiamato c al passo c4 termina. Grazie a questa
precisa computazione possiamo dire che M termina, e se quella computazione
è polinomiale nell’input possiamo anche dire che M decide polinomialmente il
problema trattato. Tutte le altre computazioni non sono di nostro interesse.
Due macchine diverse, stessa potenza. Grazie allo schema che abbiamo
appena visto possiamo fare chiarezza sulle motivazioni per le quali le MdT
deterministiche e quelle non deterministiche computano esattamente le stesse
cose (ma in tempi diversi). Una MdT deterministica è infatti sempre in grado
di simularne una non deterministica: è sufficiente prendere tutte le computazioni possibili della MdT non deterministica, codificarle e poi eseguirle in
ampiezza (i.e., prima il passo 1 della prima computazione, poi il passo 1 della
seconda computazione, poi il passo 1 della terza, ecc.). Come accennato poco
fa, si osservi che abbiamo già dimostrato che ciò è possibile usando la macchina INTERinf , ossia la macchina parallela con grado di parallelismo infinito
(Sezione 2.9.2).
114
Capitolo 3. Complessità
N P: non deterministic P
Giacché padroneggiamo la nozione di MdT non deterministica possiamo finalmente introdurre la seconda definizione di N P:
Definizione 3.8 (N P). Sia S un problema definito su un alfabeto A (S ⊆ A? ).
S ∈ N P se e solo se esiste una MdT MS non deterministica che decide S in
tempo polinomiale.
Ricordiamo che dire che MS decide S in tempo polinomiale equivale a dire che
∀w ∈ A? (per ogni possibile input) esiste almeno una computazione di MS su
w che risponde sì o no in tempo polinomiale a seconda che w appartenga ad
S o meno.
Alcune osservazioni:
• La seconda definizione di N P è del tutto simile a quella di P, solo che
usiamo una MdT non deterministica anziché una deterministica. Questa
distinzione risulta naturale se ripensiamo al fatto che una MdT non deterministica ‘’comprende‘’ molte computazioni e se assimiliamo al concetto
di ‘’scelta‘’ in un tipico problema N P il concetto di non determinismo.
Consideriamo ad esempio 3COL: avere una MdT non deterministica che
decide 3COL (chiamiamola M) significa che esiste una computazione di
M (chiamiamola c) per cui, in tempo polinomiale, M ci dice se il grafo
in input è colorabile o meno.
Concentrandoci sul caso di avere in input un grafo colorabile: se assimiliamo al non determinismo della MdT la scelta di un colore possiamo
pensare a c come alla computazione che ‘’sceglie sempre i colori giusti‘’,
come guidata da un oracolo: d’altronde M è in grado di produrre ogni
possibile computazione (i.e., assegnazione di colori) e quindi fra quelle
ci sarà anche quella che in tempo polinomiale riesce a colorare il grafo.
Per quanto riguarda il caso (meno interessante) di in un input non colorabile, una qualsiasi computazione fornirà la risposta corretta (il grafo
non è colorabile).
• Quando diciamo ‘’esiste una computazione che dice sì o no a seconda
che l’input w sia o meno in S‘’ non intendiamo necessariamente la stessa
computazione (si veda il punto precedente come dimostrazione di questo
fatto) .
• Le due definizioni di N P sono ovviamente equivalenti fra loro (altrimenti
N P sarebbe due cose diverse contemporaneamente) e lo andremo a
dimostrare in seguito.
• Per rappresentare una complessità maggiore la prima definizione di N P
modifica il problema mentre la seconda modifica il modello calcolo.
3.5. La classe di complessità non deterministica polinomiale
115
• L’acronimo N P dovrebbe ora risultare chiaro: N P non vuol dire ‘’non
polinomiale‘’ ma ‘’non deterministic P‘’.
3.5.6
3SAT è N P secondo la seconda definizione
Abbiamo già dimostrato che 3SAT appartiene ai problemi non deterministici
polinomiali secondo la prima definizione di N P. Per impratichirci con l’uso
delle MdT non deterministiche andiamo a dimostrare che 3SAT ∈ N P anche
secondo la seconda definizione di N P.
La dimostrazione del fatto che 3SAT ∈ N P consiste nel costruire una MdT
non deterministica la quale abbia almeno una computazione che in tempo
polinomiale individua la risposta sì o no a seconda che l’input appartenga o
meno ad S. Chiamiamo tale macchina M 3SAT .
Costruire il codice di M 3SAT è davvero semplice! Una MdT non deterministica è
sostanzialmente una MdT a parallelismo infinito: ogni computazione di M 3SAT
non deve far altro che testare una possibile assegnazione dei valori di verità
dei letterali coinvolti in C e se almeno una termina con sì allora sì è la
risposta (C è soddisfacibile), altrimenti no sarà la risposta, ottenibile da una
qualsiasi computazione. Si osservi che avevamo già fatto un ragionamento
simile parlando delle assegnazioni di colori di 3COL.
Facciamo qualche conto: se l’insieme di clausole C contiene n clausole, allora
m = 3n sarà il numero massimo di letterali contenuti in C (nell’improbabile
caso in cui non vi siano mai ripetizioni, ma d’altronde stiamo calcolando un
o-grande). Allora M 3SAT dovrà calcolare un totale di 2m possibili assegnazioni
di verità potenzialmente soddisfacenti C.
In definitiva M 3SAT eseguirà in parallelo 2m algoritmi di verifica (ecco che
appare il collegamento fra le due definizioni!), ognuno dei quali prenderà una
delle 2m assegnazioni e verificherà se questa soddisfa o meno C. Per generare
tutte le possibili soluzioni non bisogna far altro che leggere tutti i letterali e
non deterministicamente scegliere un assegnamento per ognuno di essi: costo
3n. Dopodiché, ogni algoritmo di verifica costerà un tempo polinomiale (lo
abbiamo dimostrato in Sezione 3.5.2) e dunque la somma fra 3n ed un tempo
polinomiale è ancora una volta un tempo polinomiale.
Se ne deduce che ogni computazione di M 3SAT è polinomiale e che M 3SAT
decide correttamente 3SAT, quindi, 3SAT ∈ N P.
3.5.7
L’equivalenza fra le due definizioni
Lo abbiamo detto innumerevoli volte: dato un problema S definito sull’alfabeto
A? possiamo usare due definizioni diverse per tentare di dimostrare che S ∈
116
Capitolo 3. Complessità
N P. Se ne deduce che le due definizioni debbano essere equivalenti fra loro,
e lo andremo a dimostrare in questa sezione.
Dopo tutto quello che abbiamo detto in merito alle MdT non deterministiche
l’equivalenza fra le due definizioni dovrebbe risultare piuttosto intuitiva: mentre
con la prima definizione scegliamo una particolare soluzione y e la verifichiamo
in tempo polinomiale, con la definizione 2 andiamo a provarle tutte in maniera
non deterministica. Come sempre, per dimostrare la bi-implicazione fra due
elementi andiamo a dimostrare le singole implicazioni.
La definizione 1 implica la definizione 2. Grazie alla definizione 1 sappiamo che
la costruzione di y è polinomiale nella lunghezza dell’input al problema (`(y ) ≤
ps (`(w ))), inoltre sappiamo che ∃S 0 estensione polinomiale di S, il che significa che esiste una MdT MS 0 che decide S 0 in tempo polinomiale.
Per poter dimostrare la veridicità della definizione 2 dobbiamo invece creare
una MS non deterministica che decida S in tempo polinomiale. Beh, abbiamo già fatto qualcosa di simile quando abbiamo dimostrato che 3SAT è N P
secondo la seconda definizione; MS dovrà semplicemente:
1. Calcolare non deterministicamente tutte le possibili estensioni polinomiali di S, i.e., tutti i possibili input per MS 0 , ossia tutte le stringhe
concat(w y ). Giacché `(y ) ≤ ps (`(w )) questa operazione è fattibile in
tempo polinomiale.
2. Lanciare MS 0 su ogni stringa concat(w y ) generata al passo precedente.
Giacché MS 0 esegue in tempo polinomiale e il passo di generazione delle estensioni polinomiali è polinomiale, la somma delle due operazioni è ancora polinomiale, ergo MS decide S in tempo polinomiale e questo è proprio quanto
asserito dalla definizione 2.
La definizione 2 implica la definizione 1 . Grazie alla definizione 2 disponiamo
di MS , una MdT non deterministica che decide S in tempo polinomiale. Abbiamo più volte detto che tale macchina in sostanza esegue parallelamente il
codice di una macchina deterministica che verifica una determinata soluzione di
S (fornitagli in maniera non deterministica). Possiamo dunque rappresentare
MS come segue:
3.5. La classe di complessità non deterministica polinomiale
117
MS
a1 b1 c1 d1 · · ·
a2 b2 c2 d2
a3 b3 c3 d3
..
.
..
.
..
.
..
.
Stando a questa nomenclatura possiamo dire che ogni computazione, ad esempio a, testerà una specifica soluzione k 0 , b una soluzione k 00 , ecc. Ora, giacché
una computazione è una serie deterministica di passi, questa può essere codificata mediante un numero univoco (in modo del tutto simile a quanto fatto
per codificare le macchine di Turing). Dopodiché, possiamo rimappare le cifre
usate per la codifica (e.g., da 0 a 9) su una quantità corrispondente di caratteri
dell’alfabeto A così da ottenere che ogni computazione è sostanzialmente una
parola costruita su A, e quindi inclusa in A? . Se chiamiamo y la codifica di una
computazione, allora potremo anche dire che `(y ) è polinomiale dato che la
singola computazione deterministica di MS è polinomiale (proprio grazie alla
definizione 2).
A questo punto possiamo finalmente definire il problema S 0 : S 0 è il problema
di decisione che preso in input una parola concat(w y ) controlla (i.e., restituisce sì o no) se y è la codifica di una MdT che si ferma su w in tempo
polinomiale. In altre parole la y che abbiamo costruito non è soltanto più una
soluzione da verificare ma è direttamente la computazione che al suo interno
verifica una soluzione (quale questa sia è cablato nella codifica y ). S 0 è quindi
una problema di verifica del fatto che una computazione verifichi o meno una
certa soluzione per w .
Ora, giacché S 0 è decidibile deterministicamente in tempo polinomiale (basta
eseguire la computazione y ), e giacché S 0 è estensione polinomiale di S per
costruzione (prende come input concat(w y ) e `(y ) ≤ ps (`(w ))) possiamo dire
che S ∈ N P in virtù del fatto che S 0 esiste (abbiamo ottenuto la definizione
1).
3.5.8
P è incluso in N P
È semplice dimostrare che P ⊆ N P, ossia che se un problema S è in P
è automaticamente anche in N P (per quanto riguarda il viceversa avremo
occasione di parlarne profusamente).
118
Capitolo 3. Complessità
Tale dimostrazione si può effettuare ottenendo indistintamente la prima o la
seconda definizione:
• Definizione 1: se un certo problema S appartiene a P, allora sappiamo
che esiste una MdT MS deterministica polinomiale che decide S e viceversa. Ma S è per definizione estensione polinomiale di sé stesso, basta
avere y nullo. Pertanto S 0 = S e quindi abbiamo che MS decide S 0 .
Abbiamo ottenuto la definizione 1.
• Definizione 2: come prima, se un certo problema S appartiene a P,
allora sappiamo che esiste una MdT MS deterministica polinomiale che
decide S e viceversa. Sappiamo bene che una MdT deterministica è
fondamentalmente un caso particolare di una MdT non deterministica,
quindi possiamo pensare che MS sia non solo la MdT deterministica che
decide S, ma anche la MdT non deterministica che decide S. Ecco
quindi che abbiamo ottenuto la definizione 2.
3.5.9
Alcuni esempi di problemi N P
Riportiamo per completezza una lista di problemi N P (che non sappiamo se
si trovino in P o meno):
• SAT (con dimensioni di clausole qualsiasi).
• COL (con un numero qualsiasi di colori).
• KNAPSACK (il problema dello zaino).
• TSP (problema del commesso viaggiatore).
• Il gioco del quindici.
3.6
L’annoso problema: P = N P?
Sappiamo che P è incluso in N P, ma tale inclusione è propria? In altre parole
è vero che ogni problema N P è anche P? Il quesito che ci stiamo ponendo è
riassumibile nell’equivalenza:
?
P = NP
La risposta a questa domanda tormenta i teorici della complessità fin dalla
nascita delle classi P e N P, tanto da mettere in palio un premio di un milione
di dollari per chiunque sia in grado di dimostrarne o smentirne l’equivalenza.
La teoria della complessità è costellata di inclusioni fra insiemi, ma molto
raramente si sa dire se quelle inclusioni siano o meno proprie. Il caso più
eclatante è proprio quello di P = N P, di cui iniziamo a parlare ora e del quale
discuteremo ancora in seguito considerando di volta in volta l’effetto di tale
equivalenza sulle nuove classi e definizioni che studieremo.
3.7. Classi N P-hard ed N P-complete
3.6.1
119
Conseguenze dirette dell’equivalenza fra P ed N P
Che impatto avrebbe sulla computer science dimostrare che P = N P?
1. Se fosse vero che P = N P allora staremmo dicendo che ogni problema che ha un algoritmo di verifica polinomiale deve avere un algoritmo
di risoluzione polinomiale, il che equivarrebbe a dire che il problema di
verificare una soluzione ed il problema di trovarla avrebbero la stessa
complessità.
2. La classe P sfrutta come modello computazionale le MdT deterministiche polinomiali mentre la classe N P impiega le MdT non deterministiche
polinomiali. Avere che P = N P sarebbe come dire che non solo le MdT
deterministiche e le MdT non deterministiche hanno la stessa potenza
(e questo lo sappiamo già e lo abbiamo pure dimostrato), ma che addirittura eseguono nello stesso numero di passi (in termini di o-grandi
ovviamente).
Basta leggere queste due conseguenze per convincersi del fatto che probabilmente P ed N P non siano uguali, ma per ora non esiste alcuna dimostrazione
in merito e dunque P = N P rimane un allettante problema aperto.
3.6.2
Come si dimostra P = N P?
Al fine di capire se P = N P potremmo attivarci in due sensi:
1. Dimostrare direttamente che P = N P provando che per ogni problema
S appartenente ad N P esiste un algoritmo di risoluzione polinomiale (e
che quindi S ∈ P).
2. Dimostrare che P =
6 N P trovando un contro esempio, ossia dimostrando
che esiste un problema in N P per cui si riesce a dimostrare che non può
esistere un algoritmo di risoluzione polinomiale. Attenzione: dimostrare
la non esistenza di un algoritmo polinomiale è tutto fuorché semplice,
sono pochissimi i casi in cui si riesce a dimostrare la complessità minima
di un problema (ne abbiamo parlato in Sezione 3.1.2).
Sembra evidente che la seconda opzione sia la più semplice (dobbiamo provare
un ‘’esiste‘’ anziché un ‘’per ogni‘’), ma le classi N P-complete e N P-hard
che stiamo per studiare ci faranno capire che in realtà le due strade sono del
tutto equivalenti.
3.7
Classi N P-hard ed N P-complete
In questa sezione parleremo di due importanti classi di problemi che giocano
un ruolo fondamentale nella teoria della complessità, le classi N P-hard ed
120
Capitolo 3. Complessità
N P-complete. Trattiamo insieme queste due classi poiché sono strettamente
correlate fra loro.
Partiamo direttamente dalle due definizioni; come sempre parliamo di S, un
problema definito su un alfabeto A (S ⊆ A? ).
Definizione 3.9 (N P-hard). S ∈ N P-hard se e solo ∀S 0 ∈ N P S 0 ≤p S
Definizione 3.10 (N P-complete). S ∈ N P-complete se e solo S ∈ N P-hard
ed S ∈ N P-complete
Parafrasando le due definizioni, possiamo dire che:
• Un problema è N P-hard se qualsiasi altro problema in N P è riducibile
a lui. Attenzione: la definizione non ci dice nulla di esplicito sulla classe
complessità del problema N P-hard, che può trovarsi di per se in una
qualsiasi classe di complessità.
• Un problema è N P-complete se è un problema N P e se qualsiasi altro
problema in N P è riducibile a lui (dalla definizione di N P-hard).
È semplice capire che la classe N P-complete sia di estremo interesse: trovato
un problema in questa classe (ed un algoritmo A che lo risolva), ogni altro problema di N P può essere risolto mediante l’impiego di A sfruttando la nozione
di riduzione.
I problemi N P-complete sono dunque una sorta di grimaldello dell’intera classe N P e questo, come stiamo per vedere, ha delle importanti conseguenze in
merito all’equivalenza P = N P.
3.7.1
La classe N P-complete e le conseguenze sul problema P =
NP
Consideriamo e dimostriamo il seguente teorema:
Teorema 3.3. Sia S un problema definito su un alfabeto A (S ⊆ A? ) e sia
S ∈ N P-complete. Allora, S ∈ P se e solo se P = N P.
Mettendo insieme le informazioni che abbiamo in merito alle classi di complessità P, N P ed N P-complete il teorema che abbiamo riportato non dovrebbe
stupirci. Infatti, se S ∈ N P-complete allora qualsiasi problema N P si riduce
a lui. Se poi quel problema è anche in P (i.e., ha un algoritmo di risoluzione
polinomiale), allora è come dire che ogni problema in N P è in P (mediante
la riduzione ad S) e quindi che P = N P. Questo teorema comprova quanto
avevamo accennato nella Sezione 3.6.2 poiché rende del tutto equivalente dimostrare che P 6= N P piuttosto che P = N P: in entrambi i casi dobbiamo
3.7. Classi N P-hard ed N P-complete
121
dimostrare un’esistenziale. Insomma, la classe N P-complete si fa sempre più
allettante, almeno per chi non ha già un milione di dollari sul proprio conto in
banca.
Diamo ora una dimostrazione più formale del teorema, che si basa comunque
sul ragionamento appena riportato (come il solito dimostriamo separatamente
i due lati della bi-implicazione).
Implicazione verso destra (⇒). Il fatto che un certo problema S appartenente sia alla classe N P-complete che alla classe P implichi l’equivalenza P =
N P si può dimostrare facendo vedere che l’appartenenza di S alla classe
N P-complete automaticamente permette di ottenere un algoritmo di risoluzione polinomiale per un qualsiasi altro problema S 0 ∈ N P e quindi che le
classi P e N P sono coincidenti.
Consideriamo dunque un qualsiasi problema S 0 ∈ N P.
Dato che S ∈ N P-complete, per definizione sappiamo che S 0 ≤p S: chiamiamo f la funzione che trasforma i dati del problema S 0 a quelli del problema S.
L’algoritmo polinomiale per risolvere S 0 è semplicissimo: prende l’input w e lo
trasforma usando f (che ha costo polinomiale per definizione), dopodiché dà
f (w ) come input per l’algoritmo polinomiale che risolve S (ricordiamo che tale
algoritmo esiste in virtuà del fatto che S ∈ P per ipotesi). In questo modo
abbiamo risolto un qualsiasi S 0 ∈ N P usando una MdT polinomiale (la somma di due computazioni polinomiale è ancora polinomiale) e dunque qualsiasi
problema N P è anche P, ergo, P = N P.
Implicazione verso sinistra (⇐). Questo lato dell’implicazione di dimostra da
sé usando nozioni insiemistiche: il fatto che P = N P (vero per ipotesi) fa sì
che ogni problema N P sia anche P. Giacché S ∈ N P-complete, per definizione S appartiene anche ad N P e quindi, per quanto detto sopra, appartiene
anche a P.
3.7.2
SAT è N P-complete: il teorema di Cook-Levin
Disponiamo ora di una bella definizione per la classe N P-complete, ma per
rendere tale definizione in qualche modo utile dobbiamo dimostrare che esiste
almeno un problema N P-complete. In questo senso, Cook e Levin dimostrano
uno dei più importanti risultati della calcolabilità:
Teorema 3.4 (Teorema di Cook-Levin). SAT è N P-complete
Una nota storica: Cook e Levin dimostrano l’appartenenza di SAT alla classe
N P-complete durante la guerra fredda senza comunicare fra loro ma ottenendo lo stesso risultato.
122
Capitolo 3. Complessità
Non ripercorreremo per intero la dimostrazione del teorema di Cook-Levin
poiché estremamente complessa e non così interessante, ma ci limiteremo a
studiare alcune delle intuizioni avute dagli scienziati così da padroneggiare alcune tecniche dimostrative degne di nota.
Per dimostrare che SAT è N P-complete dobbiamo dimostrare separatamente che SAT è N P-hard e che SAT è N P. La prima dimostrazione è il vero
ostacolo, mentre la seconda dimostrazione è di immediata risoluzione considerando che sappiamo già che 3SAT ∈ N P. Più precisamente sappiamo che
il problema di verifica di 3SAT è polinomiale (e quindi 3SAT è N P) e dunque,
giacché l’algoritmo che verifica 3SAT è identico all’algoritmo che verifica SAT
solo che considera clausole più lunghe, anche SAT è N P.
Non resta che dimostrare che SAT è N P-hard.
SAT è N P-hard (cenni). Dimostrare che un problema è N P-hard è un’impresa non da poco. Si deve infatti dimostrare un ‘’per ogni‘’: ogni problema
S 0 ∈ N P deve essere riducibile ad S se vogliamo dimostrare che S ∈ N P-hard.
Per poter trattare in maniera omogenea tutti i problemi N P dobbiamo necessariamente manipolare l’unica cosa che li accomuna, ossia le varie MdT non
deterministiche che li decidono. È proprio questa l’intuizione geniale avuta da
Cook e Levin: dato un problema S 0 ∈ N P prendiamo la MdT non deterministica che lo decide e la trasformiamo (o meglio, ne trasformiamo tutte le
computazioni) in un insieme di clausole C tale che se C è soddisfacibile allora
l’input w della MdT apparteneva ad S 0 , mentre se C non è soddisfacibile w
non appartiene ad S 0 .
Capiamo dunque che quanto fatto da Cook e di Levin è un lavoro amanuense
che porta a tradurre tutte le componenti di un’esecuzione di una MdT non
deterministica in un insieme di clausole. È proprio per questo che non studiamo la dimostrazione per intero ma ci limitiamo ad osservare alcune intuizioni
interessante adottate nella procedura di trasformazione da una MdT ad un
insieme di clausole.
In definitiva, la strategia che seguiremo sarà quella di scrivere, per ogni w , un
insieme di clausole f (w ) che cerca proprio di seguire il lavoro di M 0 (la MdT
non deterministica che decide S 0 in tempo polinomiale) su w e, in particolare,
è soddisfatto da qualche valutazione v se e solo se w ∈ S 0 , cioè se e solo se
qual-che computazione di M 0 su w ha buon esito.
Rimuovere il non determinismo della disgiunzione.
Il primo problema che andiamo a trattare è il naturale non determinismo della
3.7. Classi N P-hard ed N P-complete
123
disgiunzione11 . Trattare il non determinismo insito in una clausola ci servirà
in seguito per poter dire che una clausola è verificata se e solo se uno solo dei
suoi letterali è vero.
Ecco quindi che per p0 , p1 , . . . , ps lettere minuscole, Is (p0 , . . . , ps ) rappresenta
l’insieme delle clausole costituito da:
• Una clausola p0 p1 . . . ps
• Tante clausole Pi Pj con i < j ≤ s
È facile provare che questo insieme di clausole è verificato se e solo se uno e
solo uno fra i letterali p0 , p1 , . . . , ps è vero. Facciamo un esempio.
Abbiamo la clausola p0 p1 p2 che ricordiamo voler dire dal punto di vista logico
p0 ∨ p1 ∨ p2 . Se questa clausola è soddisfatta non sappiamo quale dei tre (o
se tutti e tre) i letterali al suo interno sono stati soddisfatti e ciò non ci piace.
Sostituiamo allora a questa clausola la costruzione I3 :
I3 = {p0 p1 p2 , P0 P 1, P0 P2 , P1 P2 }
Come vediamo, se esattamente uno fra i letterali pi è vero allora I3 è soddisfatto, altrimenti I3 risulta essere non soddisfatto. Ciò è reso possibile dalla
presenza delle varie coppie Pi Pj che combinano tutti i letterali possibili: supponendo che pi e pj siano veri, avremo che Pi e Pj saranno falsi, così come la
loro disgiunzione, la quale falsificherà tutto l’insieme I3 .
Notazione e limitazione della dimensione del nastro.
Dobbiamo convenire un po’ di notazione per poter proseguire con la dimostrazione. Chiamiamo innanzi tutto M 0 la MdT non deterministica che decide S 0
in tempo polinomiale. Poi, diciamo che A0 (l’alfabeto sul quale lavora M 0 ) si
compone dei simboli a1 , . . . , am e che a0 rappresenti il simbolo ?. Possiamo
poi supporre che la MdT M 0 ammetta gli stati q0 , . . . qh e, in particolare, si
arresti se e solo se entra nello stato qh .
Consideriamo adesso una parola w su A0 : sia n = `(w ). Sappiamo che almeno
una computazione di M 0 termina al più in p 0 (n) passi, dove p 0 è un polinomio
ed n (lo abbiamo appena detto) è la lunghezza dell’input. A questo punto possiamo notare che una computazione di p 0 (n) passi riguarda al più 2p 0 (n) + 1
quadri del nastro, più precisamente i p 0 (n) quadri a destra e i p 0 (n) quadri a
sinistra del quadro di partenza. Infatti, le istruzioni di M 0 potranno al massimo
spostare la testina costantemente a sinistra, oppure costantemente a destra,
nei p 0 (n) passi interessati. Attenzione: l’esecuzione toccherà comunque al più
p 0 (n) quadri diversi, ma a monte non possiamo sapere se saranno i p 0 (n) quadri
11
Come avevamo già osservato in precedenza, avere A ∨ B vero non ci dice nulla su quale
dei due o se tutti e due i letterali sono veri.
124
Capitolo 3. Complessità
a destra o a sinistra, quindi dobbiamo limitare il nastro a 2p 0 (n) + 1 quadri.
Numeriamo allora per semplicità i 2p 0 (n)+1 quadri coinvolti, da 0 fino a 2p 0 (n)
procedendo da sinistra verso destra; così il quadro centrale, quello di partenza,
riceve il numero p 0 (n).
Per costruire f (w ) e le sue clausole faremo uso delle seguenti lettere minuscole
dell’alfabeto di SAT. Per convenienza le indichiamo:
ci,j,t ,
sr,t ,
di,t
dove:
• i ≤ 2p 0 (n) si riferisce ai quadri della computazione,
• j ≤ m si riferisce ai simboli di A0 ,
• r ≤ h si riferisce agli stati di M 0 ,
• t ≤ p 0 (n) si riferisce ai passi della computazione.
Chiaramente potremo fare anche uso delle loro corrispondenti maiuscole:
Ci,j,t ,
Dr,t ,
Di,t
Come utilizziamo queste lettere? Possiamo intuirlo parlando di una tra le tante
possibili valutazioni di queste lettere, V , quella che meglio anticipa i nostri
propositi e, in riferimento alla computazione di M 0 su w precedentemente
scelta, assume:
• V (ci,j,t ) = 1 se e solo se al passo t sul quadro i c’è scritto aj ;
• V (sr,t ) = 1 se e solo se al passo t M 0 è nello stato qr ;
• V (di,t ) = 1 se e solo se al passo t M 0 esamina il quadro i .
Chiamiamo V la valutazione standard.
Dovrebbe a questo punto risultare chiaro (almeno da un punto di vista intuitivo) che f (w ) dovrà contenere una serie di clausole che esprimono, mediante i
letterali c, s e d corredati di tutti i loro indici, tutti i passi di una computazione
di M 0 su w . Come sempre non dobbiamo dimenticare che la riduzione deve
avvenire in tempo polinomiale, dunque, chiediamoci quanti di questi letterali
dobbiamo produrre per tradurre una computazione.
Il numero totale di possibili ci,j,t è 2p 0 (n) · m · p 0 (n) (banalmente, la moltiplicazione fra i limiti di tutti gli indici), dunque se g è il grado di p 0 (n) abbiamo
2ng · m · ng possibili c, che richiedono dunque un O(n2g ) per essere prodotti.
Allo stesso modo possiamo dire che per scrivere tutti gli sr,t ci costa h · p 0 (n)
3.7. Classi N P-hard ed N P-complete
125
passi, che è O(ng ). E infine scrivere tutti i di,t costa 2p 0 (n) · p 0 (n) passi, che
è O(n2g ). Pertanto, abbiamo:
O(n2g ) + O(ng ) + O(n2g ) = O(n2g )
ossia che per riprodurre tutte le singole lettere che ci serviranno per riprodurre
la computazione di interesse impieghiamo un tempo polinomiale, e più precisamente O(n2g ) (ricordiamo che g è il grado di p 0 (n)).
Passiamo adesso a scrivere le clausole di f (w ). Le suddividiamo in sette gruppi, per ciascuno dei quali indichiamo anche in quali casi la valutazione standard
V soddisfa le clausole elencate, e quanti passi sono richiesti per scrivere queste
clausole. Come già sottolineato, f (w ) cerca di accompagnare e descrivere la
computazione di M 0 su w scelta in precedenza. Dei sette gruppi di clausole
che dovremmo generare per descrivere una intera computazione ne vediamo
soltanto una, tanto per dare un assaggio del funzionamento della dimostrazione.
Per ogni t ≤ p 0 (n) poniamo:
At = I2p0 (n) (d0,t , . . . , d2p0 (n),t )
Così V soddisfa At se e solo se, al passo t della computazione di M 0 su w precedentemente scelta, M 0 esamina uno e uno solo dei quadri 0, . . . , 2p 0 (n). Dal
punto di vista della computazione, siccome ciascuna lettera richiede O(n2g )
passi per essere scritta, il tempo richiesto per scrivere At (e quindi tutte le
coppie di lettere maiuscole per i < j ≤ 2p 0 (n)) è O(n4g ). Ancora polinomiale.
La dimostrazione proseguirebbe a questo punto con la definizione di altri gruppi, ma è ormai chiaro che ognuno di questi imponga delle limitazioni su M 0
affinché la descrizione in clausole delle sue computazioni risulti consistente e
corretta nei termini della risposta sull’appartenenza di w ∈ S 0 . Più precisamente i primi quattro gruppi di clausole (il cui primo è At ) modellano condizioni
statiche su M 0 affinché la macchina sia consistente (At ad esempio dice che
si legge un solo simbolo ogni passo), mentre le tre restanti modellano aspetti
più dinamici della computazione di M 0 .
Al termine della costruzione dei sette gruppi possiamo finalmente asserire
che SAT è N P-hard poiché abbiamo ridotto qualsiasi problema S 0 ∈ N P
a SAT modellando ogni computazione possibile di una MdT M 0 come insieme
di clausole che è soddisfacibile se l’input w di M 0 apparteneva a S 0 e non lo è
altrimenti.
Ciò che è importante comprendere della dimostrazione del teorema di CookLevin è quindi:
126
Capitolo 3. Complessità
• Come è stato risolto il problema del non determinismo relativo alla
disgiunzione fra letterali di una clausola (la funzione Is ).
• Quanto sia importante conoscere il limite temporale di esecuzione di M 0
al fine di poter creare un insieme di clausole finito che descriva ogni
computazione, la quale è finita proprio grazie al limite di complessità di
M 0.
Conseguenze del teorema di Cook-Levin
Supponiamo ora di voler dimostrare che un certo problema S ∈ N P è anche
N P-complete; dobbiamo creare una (estenuante) costruzione che riduca ogni
problema in N P ad S? Fortunatamente, no.
Possiamo sfruttare il teorema di Cook-Levin a nostro favore: se dimostriamo
che SAT si riduce polinomialmente ad S, allora avremo automaticamente dimostrato mediante una catena di riduzioni che giacché qualsiasi problema si
riduce polinomialmente a SAT (da Cook-Levin) e SAT si riduce polinomialmente a S, qualsiasi problema N P si riduce polinomialmente ad S, quindi, S ∈ N P-complete. Proprio per confermare quanto appena asserito nella
prossime sezioni andremo a dimostrare che 3SAT e 3COL sono N P-complete.
3.7.3
3SAT è N P-complete
Prima di dimostrare che 3SAT è N P-complete (spoiler: lo è) riflettiamo cosa
significhi tale affermazione: dire che 3SAT è N P-complete significa dire che
SAT ≤p 3SAT. Capiamo dunque che la differenza che c’è fra 2SAT ed 3SAT
è enorme rispetto alla differenza fra 3SAT e qualsiasi altro problema SAT.
Infatti, da 3SAT in poi ogni problema SAT è appartenente a N P (sono N P
i due estremi, 3SAT e SAT), mentre per passare da 2SAT a 3SAT dobbiamo
cambiare classe di complessità.
SAT si riduce polinomialmente a 3SAT. Per dimostrare che 3SAT si riduce
polinomialmente a SAT dobbiamo trovare una funzione di trasformazione polinomiale che prenda un insieme di lunghezza qualsiasi di clausole C e lo trasformi un insieme di clausole lunghe tre C3 , il tutto chiaramente preservandone
la soddisfacibilità (se C era soddisfacibile C3 deve essere soddisfacibile e se
C non era soddisfacibile C3 non deve essere soddisfacibile). Chiaramente non
imponiamo limitazioni sul numero di letterali e sul numero di clausole di C3 ,
purché queste non crescano più che polinomialmente rispetto a quelle di C (a
quel punto la trasformazione non sarebbe più polinomiale).
Procediamo dunque mostrando la trasformazione per clausole lunghe 1, 2, 3
e 4 o più letterali. Forniamo quindi quattro regole di trasformazione diverse e
la quarta sarà generale, ossia in grado di trasformare una qualsiasi clausola di
3.7. Classi N P-hard ed N P-complete
127
lunghezza n > 3 in un insieme di clausole lunghe 3 (ovviamente sempre preservando la soddisfacibilità e la non soddisfacibilità). Una precisazione in merito
alla notazione: indichiamo con q i letterali aggiunti mentre con p indichiamo
i letterali della clausola di partenza. Inoltre nei nostri esempi consideriamo
sempre letterali minuscoli ma il discorso sarebbe analogo considerando letterali maiuscoli. Osserviamo che le prime tre (di fatto due) trasformazioni che
introduciamo devono in un certo senso ‘’estendere‘’ la clausola di partenza
senza intaccarne la soddisfacibilità, i.e., la soddisfacibilità deve dipendere in
C3 dalla clausola di partenza in C. La quarta trasformazione deve ragionare
invece in modo diverso: lo vedremo direttamente quando ne parleremo.
1. Trasformazione di clausole lunghe 1:
C = {p0 }
⇓
C3 = {p0 q0 q1 , p0 Q0 q1 , p0 q0 Q1 , p0 Q0 Q1 }
La trasformazione è ovvia: abbiamo affiancato a p0 l’intera tabella di
verità della disgiunzione (che messa in congiunzione dà sempre falso),
quindi è necessario che p0 sia vero per poter soddisfare tutte le clausole
del nuovo insieme.
2. Trasformazione di clausole lunghe 2:
C = {p0 p1 }
⇓
C3 = {p0 p1 q0 , p0 p1 Q0 }
Il ragionamento è sempre lo stesso: l’aggiunta alla clausola di partenza
p0 p1 deve essere, se congiunta, sempre falsa affinché la soddisfacibilità
di C 0 dipenda solo da p0 p1 . In questo caso abbiamo aggiunto un solo
letterale q ed i suoi due valori di verità q0 e Q0 che in congiunzione sono
falsi, ergo, solo se p0 p1 è vero (i.e. p0 o p1 ) C3 è soddisfatto.
3. Trasformazione di clausole lunghe 3:
C = {p0 p1 p2 }
⇓
C3 = {p0 p1 p2 }
Una clausola lunga 3 è già una clausola legittima per il problema 3SAT,
quindi non facciamo nulla.
4. Trasformazione di clausole lunghe h (con h > 3):
C = {p0 p1 . . . ph−1 }
⇓
C3 = {p0 p1 q0 , p2 Q0 q1 , p3 Q1 q2 , . . . , ph−3 Qh−5 qh−4 , ph−2 ph−1 Qh−4 }
128
Capitolo 3. Complessità
Questa volta la trasformazione è decisamente meno intuitiva. Iniziamo
con l’osservare che sono stati aggiunti h − 4 letterali q0 , . . . qh−4 ed i
corrispettivi maiuscoli. Per convincerci della correttezza della trasformazione studiamo i due casi dell’implicazione ‘’Se C è soddisfatto allora C3
è soddisfatto e viceversa‘’:
(a) C’è una valutazione v che soddisfa C3 e mostriamo che questa soddisfa anche C. Ragioniamo per assurdo, e supponiamo che esista
una valutazione v che soddisfa C3 ma che questa non soddisfi C.
Dato che v non soddisfa C, è necessariamente vero che:
v (p0 ) = v (p1 ) = · · · = v (ph−1 ) = 0
Dalla prima equazione possiamo dedurre che v (q0 ) = 1, perché altrimenti la prima clausola di C3 (p0 p1 q0 ) non sarebbe soddisfatta
(ergo C3 non sarebbe soddisfatto, mentre per ipotesi lo è). Dato
che v (q0 ) = 1, abbiamo che v (Q0 ) = 0 e questo fa sì che per
soddisfare la seconda clausola (p2 Q0 q1 ), si abbia v (q1 ) = 1. Continuando a ragionare in questo modo e costringendo man mano i
valori delle q per soddisfare ogni clausola di C3 , arriviamo al fondo
di C3 avendo che v (qh−4 ) = 1 e quindi che v (Qh−4 ) = 0. Questo
comporta però un assurdo, poiché giacché C non è soddisfatta (per
ipotesi assurda) abbiamo v (ph−2 ) = v (ph−1 ) = 0 e avendo anche
v (Qh−4 ) = 0 per quanto detto sopra abbiamo che l’ultima clausola
di C3 (ph−2 ph−1 Qh−4 ) non è soddisfatta e ciò è in assurdo con
l’ipotesi di partenza.
Se ne deduce che per avere C3 soddisfatta, almeno un pi deve
essere vero, e quindi C deve essere soddisfatto.
(b) Consideriamo ora una una valutazione v che soddisfa C e mostriamo che esiste una valutazione v3 che estende v e soddisfa C3 .
Dato che per ipotesi C è soddisfatta, sappiamo che esiste almeno
un j < h tale che v (pj ) = 1. Sia j l’indice minimo con questa
proprietà (cioè il primo p soddisfatto). Ora, distinguiamo tre casi
sul valore di j:
• Se j = 0 oppure j = 1, è sufficiente avere:
v3 (qi ) = 0 per ogni i ≤ h − 4
In questo modo v3 soddisferà C3 poiché la prima clausola sarà
vera grazie al fatto che p0 o p1 è vero, e tutte le altre saranno
vere perché l’assegnazione appena effettuata renderà ogni Qi
vero e quindi ogni altra clausola di C3 oltre alla prima vera (in
ogni clausola di C3 a parte la prima c’è un Qi ).
3.7. Classi N P-hard ed N P-complete
129
• Se j = h − 2 oppure j = h − 1 ci comportiamo specularmente
a quanto appena fatto. Infatti, è sufficiente avere:
v3 (qi ) = 1 per ogni i ≤ h − 4
In questo modo v3 soddisferà C3 poiché l’ultima clausola sarà
vera grazie al fatto che ph−2 o ph−1 è vero, e tutte le altre
saranno vere perché l’assegnazione appena effettuata renderà
ogni qi e quindi ogni altra clausola di C3 oltre all’ultima vera
(in ogni clausola di C3 a parte l’ultima c’è un qi ).
• Infine, se 2 ≤ j < h − 3 impostiamo:
v3 (qi ) = 1 se i ≤ j − 2
v3 (qi ) = 0 altrimenti
In questo modo v3 soddisferà C3 perché le prime clausole fino
a quella in cui appare pj sono soddisfatte dai vari qi , mentre
quelle a seguire sono soddisfatte dai vari Qi . Volendo fare
un piccolo esempio, possiamo supporre di avere j = 5, ossia
v (pj = 5). Osserviamo il nostro C3 :
C3 = {
p0 p1 q0 , . . . ,
p3 Q1 q2 , p4 Q2 q3 , p5 Q3 q4 , p6 Q4 q5 , p7 Q5 q6 ,
. . . , ph−2 ph−1 Qh−4
}
È ora immediato vedere che se q0 , . . . qj−2 (i.e., q0 , . . . q3 ) sono veri e qj−1 , . . . qh−4 (i.e., q4 , . . . qh−4 ) sono falsi (e quindi
Qj−1 , . . . , Qh−4 sono veri) C3 è soddisfatto.
Abbiamo pertanto dimostrato che data una qualsiasi valutazione v che soddisfi C è sempre possibile trovare una sua
estensione v3 che soddisfi C3 .
Con questo si concludono le regole di trasformazione della nostra funzione.
Cosa abbiamo fatto? Abbiamo mostrato quattro regole di trasformazione e
per ognuna di esse abbiamo dimostrato che la soddisfacibilità di C implica
la soddisfacibilità di C3 e viceversa. Ora, se consideriamo un C di partenza
che non è più soltanto un insieme costituito da una clausola ma è un insieme
variegato di clausole di qualsiasi lunghezza, possiamo applicare clausola per
clausola la trasformazione appropriata e giacché ogni trasformazione preserva
130
Capitolo 3. Complessità
la soddisfacibilità e la non soddisfacibilità, l’unione delle trasformazioni preserverà queste proprietà in maniera altrettanto corretta.
Per quanto concerne la complessità, la nostra funzione aggiunge a C3 h − 4
clausole per ogni clausola trovata in C (nel caso peggiore di avere tutte clausole di partenza più grandi di 3 letterali), e giacché h è la dimensione dell’input
di partenza, la complessità ottenuta è polinomiale.
Ecco quindi che abbiamo costruito una funzione polinomiale in grado di trasformare un insieme qualsiasi di clausole in un insieme di clausole costituite da
tre letterali cadauna: abbiamo dimostrato che SAT ≤p 3SAT.
Ora, come avevamo detto in precedenza, possiamo considerare un qualsiasi
problema S 0 ∈ N P e dimostrare che si riduce a 3SAT mediante la catena di
riduzioni:
S 0 ≤p SAT ≤p 3SAT
Tale catena è corretta in virtù del fatto che SAT è N P-complete. Se ne
deduce che giacché ogni problema è riducibile a 3SAT, 3SAT è N P-hard.
Questo fatto, unito all’appartenenza di 3SAT a N P, ci permette di dire che
3SAT è N P-complete.
3.7.4
3COL è N P-complete
Non paghi della sontuosa dimostrazione del fatto che 3SAT ∈ N P-complete,
ci apprestiamo a dimostrare che anche 12 3COL è N P-complete.
Quando abbiamo dimostrato che 2SAT si riduce a 2COL abbiamo accennato
al fatto che fosse un problema particolarmente infimo a causa della difficoltà
nel trattare le disgiunzioni. Il passaggio da 2COL a 3COL ci è in questo senso
di aiuto: l’aggiunta di un colore ci permette di tradurre più semplicemente le
clausole in pezzi di grafo.
Come in ogni riduzione, andiamo a descrivere la funzione che traduce un insieme di clausole ognuna delle quali costituta da tre letterali in porzioni di
grafo.
Dalle clausole al grafo
Supponiamo di disporre del solito insieme di clausole C = {c1 , . . . , cn } in cui
i letterali usati sono p1 , . . . , pm . Come al solito, ogni ci usa esattamente tre
letterali disgiunti.
La costruzione del grafo a partire da C consta di due fasi di preprocessing
12
Scopriremo presto che i problemi N P-complete sono ‘’molti‘’.
3.7. Classi N P-hard ed N P-complete
131
e di una fase di costruzione vera e propria. Le fasi di preprocessing sono le
seguenti:
1. Si genera una clique centrale di dimensione 3 in cui ogni nodo è colorato
in modo diverso:
1
2
3
Con 1, 2 e 3 ci riferiamo dunque ai tre colori possibili mediante i quali il
grafo è colorabile.
2. Per ogni letterale pi fra i vari p1 , . . . , pm presenti in C si generano due
nodi (pi e Pi ) e li si collega fra loro oltre che al nodo con colore 3:
1
2
3
pi
Pi
Grazie a questa accortezza si rende il fatto che non sia possibile assegnare
contemporaneamente due valori di verità diversi alla stessa variabile.
Tanto per fare un esempio, se avessimo un insieme di clausole in cui letterali
coinvolti sono pi , pk e pj , al termine della fase di preprocessing avremmo il
grafo:
pj
Pj
1
Pk
2
3
pi
Pi
pk
Per quanto concerne l’ultima fase (quella di costruzione vera e propria) per ogni
clausola kj contenuta in C andiamo ad aggiungere cinque vertici e nove archi
al grafo che stiamo costruendo. Supponendo che la clausola kj sia λ0 λ1 λ2
(dove λ è una lettera maiuscola o minuscola) aggiungiamo al grafo che stiamo
costruendo il seguente grafo:
132
Capitolo 3. Complessità
λ0
aj
cj
λ1
dj
bj
1
λ2
ej
Cosa abbiamo aggiunto? aj , bj , cj , dj ed ej sono i cinque nuovi vertici che
aggiungiamo al grafo relativamente alla clausola j-esima. Il nodo che vediamo
indicato con 1 deve essere collegato all’1 della clique creata durante il primo
passo di preprocessing, mentre i vari λ0 , λ1 e λ2 (che rappresentano i letterali
contenuti in kj ) devono essere collegati con i corrispondenti nodi creati nella
seconda fase di preprocessing. Per fare chiarezza in merito a questo processo
di costruzione facciamo un piccolo esempio.
Un esempio di traduzione da clausole a grafo.
insieme di clausole:
C = {p0 p1 P2 , P0 p1 p2 }
Consideriamo il seguente
In Figura 3.1 vediamo il risultato della trasformazione di C in un grafo usando
la procedura appena illustrata. Osserviamo che il grafo contenuto conta 19
Figura 3.1: Grafo risultante dalla trasformazione dell’insieme di clausole {p0 p1 P2 , P0 p1 p2 }.
3.7. Classi N P-hard ed N P-complete
133
vertici e 30 archi. l risultato può sembrare scoraggiante, visto che le clausole
di partenza sono solo 2 e le lettere coinvolte solo 6; anzi, più precisamente
m = 2 (numero di letterali) e n = 3 (numero di clausole in C). Se però
andiamo a calcolare la formula generale che determina il numero di archi e
nodi in funzione di n ed m otteniamo che si hanno:
• 3 + 2n + 5m vertici,
• 3 + 3n + 9m archi.
Le due quantità sono quindi polinomiali rispetto ad n (m è esprimibile in funzione di n; nel caso peggiore m = 3n ed in generale m ≤ 3n) il che è un’ottima
notizia: la nostra funzione di trasformazione è polinomiale, così come richiesto
dalla nozione di riduzione. Non resta che dimostrare che la trasformazione sia
corretta per poter asserire che 3SAT si riduce a 3COL.
Verifica della correttezza del grafo
La nostra interessante costruzione deve essere verificata, ossia, dobbiamo dimostrare che il grafo ottenuto G è 3-colorabile se e solo se l’insieme di clausole
C è 3-soddisfacibile. Ci serve un’analisi attenta e minuziosa.
Se G è 3-colorabile, C è 3-soddisfacibile. Giacché G è colorabile deve esistere
una colorazione c che lo colora correttamente. Giacchè c è definita a meno di
una permutazione sui tre colori 1, 2, 3 possiamo supporre che c colori i vertici
che abbiamo chiamato 1, 2 e 3 proprio coi colori 1, 2 e 3. Se ne deduce che
tutti i vertici pi e Pi (chiaramente con i ≤ n) che sono collegati al nodo 3
debbano essere colorati uno col colore 1 e l’altro col colore 2 (sono uniti da
un arco e quindi non possono essere entrambi colorati di 1 o di 2).
Definiamo ora una valutazione v che sfrutti la conosciuta c; più precisamente
per ogni i ≤ n abbiamo che:
v (pi ) = 1 se e solo se c(pi ) = 1
e v (pi ) = 0 altrimenti. Definiamo la valutazione per Pi in maniera analoga.
Se ora riusciamo a mostrare che v soddisfa C avremo dimostrato la nostra
implicazione. Dobbiamo dunque mostrare che per ogni clausola kj in C c’è
almeno un letterale a cui c da colore 1 (e che quindi ha valutazione 1, così da
soddisfare kj ). Fissiamo j e supponiamo, come fatto in precedenza, che kj sia
costituita da tre letterali λ1 λ2 λ3 .
Andiamo ora a guardare il grafo: giacché dj e ej sono collegati fra loro e
con 1, c(dj ) e c(ej ) non possono che assumere valori 2 e 3 (o il contrario).
Consideriamo i due casi:
134
Capitolo 3. Complessità
1. Se c(ej ) = 2, allora λ2 (che è collegato a 3) deve necessariamente avere
valore 1. In tal caso siamo a posto, perché abbiamo che per una qualsiasi
kj se c(ej ) = 2 λ2 deve essere colorata 1 e per come abbiamo definito
la valutazione ciò significa che λ2 sarà vera e kj sarà soddisfatta.
2. Se invece c(ej ) = 3 significa che avevamo scelto c(dj ) = 2. Tale scelta
a sua volta fa sì che c(cj ) debba essere colorato come 1 o come 3.
A seconda di questa scelta aj e bj , dato che sono collegati fra loro, si
dovranno distribuire i colori 2 e 3, oppure 2 e 1. In ogni caso uno dei
nodi fra aj e bj dovrà essere colorato con 2, e quindi uno dei due nodi
fra λ0 e λ1 dovrà essere colorato di 1 (poiché ognuno di essi è collegato
a 3).
Abbiamo quindi ottenuto che quale che sia l’assegnazione di colori di G, se
questa è legittima allora almeno una lettera di ogni clausola kj ha colore 1,
e quindi per come abbiamo definito la valutazione dei letterali ogni clausola
è soddisfatta con la conseguenza che C sia soddisfatto. Abbiamo dimostrato
che Se G è 3-colorabile, C è 3-soddisfacibile
Se C è 3-soddisfacibile, G è 3-colorabile. La dimostrazione del lato inverso dell’implicazione si svolge in modo simile a quanto appena effettuato. Se C è
soddisfacibile, esiste una valutazione v che lo soddisfa. Possiamo definire una
funzione di colorazione che si basa sulla valutazione v che conosciamo:
c(pi ) = 1 se e solo se v (pi ) = 1
mentre c(pi ) = 2 altrimenti. Osserviamo che v potrà impostare come vero da
uno a tre letterali in ogni clausola kj ∈ C. Non resta che considerare tutte le
possibili assegnazioni di tre letterali di una clausola kj escludendo chiaramente
il caso in cui nessuno dei tre sia vero (perciò dobbiamo trattare 23 −1 = 7 casi)
e verificare così che G sia colorabile per ognuna di queste assegnazioni. Stiamo
ragionando in maniera analoga a quanto fatto in precedenza: se la porzione di
grafo associata ad una singola clausola kj risulta colorabile per una qualsiasi
configurazione che soddisfa kj , allora il grafo sarà totalmente colorabile per un
qualsiasi configurazione che soddisfa G. Come sempre, consideriamo kj nella
forma λ0 λ1 λ2 .
1. v (λ0 ) = 1, v (λ1 ) = 0, v (λ2 ) = 0.
Tale valutazione implica la colorazione:
c(λ0 ) = 1, c(λ1 ) = 2, c(λ2 ) = 2
Dimostriamo la colorabilità del sottografo relativo a kj con questa configurazione di partenza fornendone direttamente una colorazione legittima:
3.7. Classi N P-hard ed N P-complete
λ0 = 1
aj = 2
cj = 3
λ1 = 2
135
dj = 2
bj = 1
1
ej = 3
λ2 = 2
Per semplicità abbiamo scritto λ0 = 1 intendendo che la colorazione per
il nodo λ0 è 1, cioè che c(λ0 ) = 1.
2. v (λ0 ) = 0, v (λ1 ) = 1, v (λ2 ) = 0.
Tale valutazione implica la colorazione:
c(λ0 ) = 2, c(λ1 ) = 1, c(λ2 ) = 2
Possiamo dimostrare la colorabilità del sottografo relativo a kj con questa configurazione di partenza fornendo direttamente un esempio di colorazione legittima. Tale colorazione è simile a quella illustrata al punto
precedente, basta invertire i colori di aj e di bj :
λ0 = 2
aj = 1
cj = 3
λ1 = 1
dj = 2
bj = 2
1
λ2 = 2
3. v (λ0 ) = 0, v (λ1 ) = 0, v (λ2 ) = 1.
ej = 3
136
Capitolo 3. Complessità
Tale valutazione implica la colorazione:
c(λ0 ) = 2, c(λ1 ) = 2, c(λ2 ) = 1
Dimostriamo la colorabilità del sottografo relativo a kj con questa configurazione di partenza fornendone direttamente una colorazione legittima:
λ0 = 2
aj = 1
cj = 2
λ1 = 2
dj = 3
bj = 3
1
ej = 2
λ2 = 1
4. v (λ0 ) = 1, v (λ1 ) = 1, v (λ2 ) = 0.
Tale valutazione implica la colorazione:
c(λ0 ) = 1, c(λ1 ) = 1, c(λ2 ) = 2
Dimostriamo la colorabilità del sottografo relativo a kj con questa configurazione di partenza fornendone direttamente una colorazione legittima:
λ0 = 1
aj = 2
cj = 1
λ1 = 1
dj = 2
bj = 3
1
λ2 = 2
ej = 3
3.7. Classi N P-hard ed N P-complete
137
5. v (λ0 ) = 1, v (λ1 ) = 0, v (λ2 ) = 1.
Tale valutazione implica la colorazione:
c(λ0 ) = 1, c(λ1 ) = 2, c(λ2 ) = 1
Possiamo dimostrare la colorabilità del sottografo relativo a kj con questa
configurazione di partenza fornendo direttamente un esempio di colorazione legittima. Tale colorazione è identica a quella illustrata al punto
precedente:
λ0 = 1
aj = 2
cj = 1
λ1 = 2
dj = 2
bj = 3
1
ej = 3
λ2 = 1
6. v (λ0 ) = 0, v (λ1 ) = 1, v (λ2 ) = 1.
Tale valutazione implica la colorazione:
c(λ0 ) = 2, c(λ1 ) = 1, c(λ2 ) = 1
Possiamo dimostrare la colorabilità del sottografo relativo a kj con questa configurazione di partenza fornendo direttamente un esempio di colorazione legittima. Tale colorazione è simile a quella illustrata al punto
precedente, basta invertire i colori di aj e di bj :
138
Capitolo 3. Complessità
λ0 = 2
aj = 3
cj = 1
λ1 = 1
dj = 2
bj = 2
1
ej = 3
λ2 = 1
7. v (λ0 ) = 1, v (λ1 ) = 1, v (λ2 ) = 1.
Tale valutazione implica la colorazione:
c(λ0 ) = 1, c(λ1 ) = 1, c(λ2 ) = 1
Possiamo dimostrare la colorabilità del sottografo relativo a kj con questa
configurazione di partenza fornendo direttamente un esempio di colorazione legittima. Tale colorazione è identica a quella illustrata al punto
precedente:
λ0 = 1
aj = 3
cj = 1
λ1 = 1
dj = 2
bj = 2
1
λ2 = 1
ej = 3
A questo punto abbiamo dimostrato che per un qualsiasi insieme di clausole C
che sia soddisfacibile, ogni sottografo relativo ad ogni clausola è 3-colorabile
(proprio in funzione del fatto che ogni clausola è soddisfacibile). Dato che
ogni parte del sottografo è 3-colorabile, l’intero grafo è 3-colorabile.
3.8. Classi N P-intermediate e co-N P
139
Abbiamo finalmente dimostrato che la nostra funzione di trasformazione è
corretta e nella sezione precedente che è polinomiale: possiamo ufficialmente
asserire che 3SAT ≤p 3COL.
Tanto per curiosità potremmo chiederci cosa accade al nostro grafo se forniamo un C non soddisfacibile. Grazie alla dimostrazione che abbiamo appena
effettuato sappiamo già che il grafo ottenuto deve risultare non colorabile:
verifichiamolo.
Supponiamo che la clausola kj all’interno di C non sia soddisfacibile. Stando
ai formalismi usati fin ora, possiamo dire che la sua valutazione sia v (λ0 ) = 0,
v (λ1 ) = 0, v (λ2 ) = 0 e quindi la colorazione del suo grafo c(λ0 ) = c(λ1 ) =
c(λ2 ) = 2. Ora, è facile verificare che per qualsiasi configurazione di colori il
grafo relativo a kj sia non colorabile:
λ0 = 2
aj
cj
λ1 = 2
dj
bj
1
λ2 = 2
ej
La dimostrazione della non colorabilità di questo grafo è semplicissima: data
la colorazione di λ2 , c(ej ) deve essere necessariamente 3 e quindi c(dj ) deve
essere 2. A questo punto non esiste alcun modo di colorare la clique aj -bj -cj
senza violare i vincoli sulla colorazione dato che aj e bj possono distribuirsi
soltanto i valori 1 o 3 e quindi cj , collegato ad entrambi, deve essere colorato
come 2. Ciò però non può essere perché anche dj è colorato come 2.
3.8
Classi N P-intermediate e co-N P
Il grosso di ciò che c’è da dire in merito alle classi N P ed N P-complete
l’abbiamo affrontato. In questa sezione andiamo a trattare alcuni classi ‘’collaterali‘’ fornendone le definizioni e poco più, inoltre, studieremo come queste
classi siano correlate al problema P = N P.
140
Capitolo 3. Complessità
3.8.1
Esistono problemi non N P-complete?
Nelle scorse sezioni abbiamo dimostrato che 3SAT e 3COL sono N P-complete.
Potrebbe a questo punto venirci un dubbio legittimo: esistono problemi in N P
che non N P-complete? Esiste una classe apposita che definisce questo tipo
di problemi, la classe N P-intermediate.
Definizione 3.11 (N P-intermediate). Sia S un problema definito su un alfabeto A (S ⊆ A? ). S ∈ N P-intermediate se e solo se S ∈ (N P − P) e
S∈
/ N P-complete.
Osserviamo la presenza dell’ipotesi S ∈ (N P − P): stiamo chiedendo che S
sia un problema ‘’davvero‘’ N P e che quindi non sia in P. Questo fatto ci
lascia intuire che la definizione della classe N P-intermediate abbia molto a
che fare col problema P = N P e come stiamo per scoprire tale intuizione è
fondata.
Ad oggi i teorici della complessità non hanno ancora individuato alcun problema
N P-intermediate, ma nel 1975 Richard Ladner dimostra il seguente teorema:
Teorema 3.5 (Teorema di Ladner). Se P =
6 N P allora esistono problemi
N P-intermediate.
La dimostrazione di questo teorema sfrutta l’ipotesi che P =
6 N P per costruire
un problema artificiale che è N P ma non è N P-complete. Tale problema13
è però costruito ad hoc e perciò non interessante. Rimane dunque un quesito
aperto se esistano problemi ‘’naturali‘’ che siano N P-intermediate. L’altra
direzione del teorema è triviale: possiamo dire che P = N P se e solo se
N P-intermediate è una classe vuoto.
Alcuni dei problemi che sono considerati buoni candidati per l’appartenenza alla
classe N P-intermediate sono il problema dell’isomorfismo fra grafi, il problema
di fattorizzare interi ed il problema di calcolare logaritmi discreti.
3.8.2
La classe co-N P: il solito problema aperto
Quando abbiamo parlato della classe P abbiamo anche introdotto la classe
co-P, osservando quanto fosse banale la sua definizione e quanto fosse ovvia
la sua uguaglianza con P. Quando si parla però della classe N P e quindi della
classe co-P tale uguaglianza diventa tutto fuorché banale.
La definizione di co-N P è analoga a quella di co-P: dato un problema S ⊆ A?
in N P, definiamo il problema S c opposto come S c ⊆ A? − S. Ma se pensiamo
alla definizione 1 della classe N P (Definizione 3.6) possiamo toccare con
mano la complessità della descrizione della classe co-N P. Infatti, un problema
13
Per i più curiosi: il problema usato è una modifica di SAT.
3.9. Classi EX P e N EX P
141
co-N P deve ‘’invalidare‘’ la definizione di N P, pertanto dobbiamo avere che
S c deve essere un problema tale per cui ogni estensione polinomiale S 0c di S c
non deve appartenere a P. In sostanza, giacché la definizione di N P parla
dell’esistenza di un’estensione S 0 , la sua negazione diventa un ‘’per ogni‘’ che,
come al solito, è molto arduo da trattare.
Di fatto ad oggi non si sa se co-N P = N P, anche perché dimostrare tale
equivalenza avrebbe enormi ripercussioni sul problema P = N P (tanto per
cambiare). Se supponiamo infatti che P = N P, avremmo che co-P = co-N P
e giacché come sappiamo P = co-P, sarebbe immediato dire che:
N P = P = co-P = co-N P
In definitiva, P = N P implicherebbe N P = co-N P.
3.9
Classi EX P e N EX P
Concludiamo la nostra trattazione sulle classi di complessità temporale introducendo un paio di definizioni in merito alle classi esponenziali.
Definizione 3.12 (EX P). Sia S un problema definito su un alfabeto A (S ⊆
A? ). Un problema S appartiene alla classe di complessità EX P se esiste una
Macchina di Turing deterministica che lo decide in tempo esponenziale nella
dimensione dell’input.
Un tipico esempio di problema esponenziale è il rompicapo della la torre di
Hanoi.
Volendo, potremmo ripercorrere tutte le definizioni che abbiamo dato in merito
alla classe P e riapplicarle sulla classe EX P. Ad esempio, potremmo definrie
la classe N EX P:
Definizione 3.13 (N EX P). Sia S un problema definito su un alfabeto A
(S ⊆ A? ).Un problema S appartiene alla classe di complessità N EX P se esiste
una Macchina di Turing non deterministica che lo decide in tempo esponenziale
nella dimensione dell’input.
Poi, potremmo parlare della gerarchia delle classi esponenziali; abbiamo le sotto
n
2n
classi: O(2n ), O(22 ), O(22 ), ecc.
Non facciamo niente di tutto questo poiché fondamentalmente i problemi e le
riflessioni che riscontreremmo sulle classi EX P ed N EX P sarebbero analoghi
a quelli riscontrati sulle classi P ed N P.
142
3.9.1
Capitolo 3. Complessità
Rapporti fra le classi di complessità temporale
Come cappello conclusivo sull’argomento, riportiamo una serie di inclusioni fra
classi di complessità temporale:
P ⊆ N P ⊆ EX P ⊆ N EX P
Come già accennato in precedenza, solo di poche di queste classi si conosce
l’inclusione propria. Più precisamente, sappiamo che:
• P 6⊆ EX P, ossia sappiamo che esiste almeno un problema EX P che non
è P (ma non sappiamo se l’equivalenza sia fra P ed N P oppure fra N P
e EX P).
• N P 6⊆ N EX P, ossia sappiamo che esiste almeno un problema N EX P
che non è N P (ma non sappiamo se l’equivalenza sia fra N P ed EX P
oppure fra EX P e N EX P).
Un appunto sulla notazione: con 6⊆ intendiamo che non c’è l’equivalenza fra
gli insiemi, ma ovviamente l’inclusione ⊂ vale.
3.10
Complessità in spazio: una breve overview
Per completezza forniamo una breve trattazione della complessità in spazio,
delineandone le classi principali e le fondamentali differenze rispetto alla complessità in tempo.
La prima osservazione che dobbiamo fare in merito alla complessità in spazio
è che si fonda su un’ipotesi di base molto diversa da quella della complessità in
tempo: mentre quest’ultima sfrutta una risorsa non riutilizzabile (i.e., il tempo), la complessità in spazio conta celle di memoria che sono per loro natura
riutilizzabili.
La riutilizzabilità dello spazio ne rende il calcolo in termini di complessità decisamente più intricato e anche per questa ragione non ci addentriamo più di
tanto nell’argomento.
3.10.1
Il modello di calcolo: MdT a tre nastri
Come sempre quando si parla di una teoria della complessità è necessario
definire il modello sul quale ragioniamo. Per quanto concerne la complessità in
spazio impieghiamo una MdT con tre nasti: ricordando che una MdT con più
nastri ha la stessa potenza di una MdT con un nastro solo, usiamo tre nastri
per rendere più agevole il calcolo della complessità. Infatti abbiamo:
• Un nastro di input sul quale vengono piazzati i dati che utilizzerà la MdT
per la sua computazione. È un nastro che viene utilizzato in sola lettura.
3.10. Complessità in spazio: una breve overview
143
• Un nastro di lavoro sul quale avviene la computazione vera e propria. È
un nastro usato sia in lettura che in scrittura.
• Un nastro di output sul quale viene scritto il risultato della computazione.
È un nastro che viene utilizzato in sola scrittura.
La complessità in spazio viene calcolata considerando soltanto lo spazio occupato sul nastro di lavoro.
Possiamo quindi dire che la complessità in spazio è una funzione sM (n) che
ci dice il massimo numero di celle utilizzate sul nastro di lavoro quando la
dimensione dell’input è minore o uguale ad n. Ovviamente, come già per la
complessità in tempo, definiamo una serie di classi di complessità sfruttando
lo strumento dell’o-grande.
3.10.2
Tempo e spazio: una relazione esponenziale
Prima di descrivere le varie classi di complessità spaziale studiamo la relazione
che esiste fra la complessità spaziale e quella temporale. Al fine di trovare
questa relazione dobbiamo definire il numero di passi di una MdT in funzione
delle celle che tale MdT utilizza (S M (n)). Vediamo come fare.
Iniziamo col dire che una computazione di MdT è chiaramente rappresentabile
come una serie di configurazioni che si susseguono fra loro. Poi, giacché l’unità
base della complessità temporale è il passo, definito come il passaggio fra una
configurazione ed un altra, possiamo dire che il numero di configurazioni di una
computazione sarà il massimo numero di passi che la computazione effettua.
Ciò detto, possiamo facilmente stabilire un limite superiore per il numero di
configurazioni possibli (i.e., di passi) di una MdT calcolandone tutte permutazioni di configurazioni legittime. Una volta determinato tale limite sapremo
potremo quindi dire che una qualsiasi esecuzione non potrà mai avere più configurazioni di quel limite, e quindi non potrà mai avere una complessità in tempo
maggiore di quel limite (poiché numero di configurazioni = numero di passi =
complessità in tempo). Osserviamo che tale ragionamento è legittimo in virtù
del fatto che che in una computazione non possono esistere due configurazioni
uguali, altrimenti si avrebbe un loop infinito. Apprestiamoci dunque al calcolo
di questo limite.
Sia M una MdT di complessità spaziale sM . Quando M opera su un input w
di lunghezza ≤ n, possiamo assumere che la sua computazione coinvolga:
• Al più n quadri sul nastro di input.
• Al più sM (n) quadri sul nastro di lavoro.
144
Capitolo 3. Complessità
Ricordiamo poi che ogni configurazione di M in questo contesto è una terna
che esprime:
(a) Il quadro in esame sul nastro di scrittura.
(b) La sequenza che compare sugli sM (n) quadri di lavoro.
(c) Lo stato di M.
A partire da quanto appena affermato è immediato calcolare il numero delle
possibili configurazioni di M su input di lunghezza n:
• Ci sono n possibili scelte per (a).
• Il numero delle possibili scelte per (b) è funzione sia di n che della cardinalità di A, l’alfabeto di M. Più precisamente abbiamo che ognuno
degli sM (n) quadri coinvolti può ospitare un elemento di A o eventualmente il simbolo ∗, perciò, il numero di possibili scelte per (b) è pari a
(|A| + 1)sM (n) .
• Le possibilità relative a (c) corrispondono invece al numero degli stati di
M, che è un k prefissato e pertanto indipendente sia da w che dalla sua
dimensione n.
In definitiva il numero di configurazioni che M può assumere nelle sue computazioni su input di lunghezza n è pari a:
k · n · (|A| + 1)sM (n)
Riscriviamo questa formula cercando di rimuovere le costanti (|A| + 1) e k
sfruttando un o-grande:
s
k · n · (|A| + 1)sM (n) = 2log2 (k) · n · 2log2 ((|A|+1) M
(n) )
= 2log2 (k) · n · 2sM (n)·log2 (|A|+1)
= n · 2k+sM (n)·log2 (|A|+1)
= n · 2O(sM (n))
Pertanto il nostro numero di configurazioni (i.e., la massima complessità temporale) è:
n · 2O(sM (n))
Pertanto, se supponiamo una complessità in spazio lineare (SM (n) = O(n))
avremo che la complessità temporale è:
n · 2O(n) = 2log2 (n) · 2O(n) = 2log2 (n)+O(n) = 2O(n)
Abbiamo quindi scoperto che la relazione fra la complessità in tempo e quella
in spazio è esponenziale e quindi che la complessità temporale sarà sempre
3.10. Complessità in spazio: una breve overview
145
maggiore di quella in spazio. Questo fatto non dovrebbe stupirci dato che,
come già detto, lo spazio è riutilizzabile mentre il tempo non lo è.
Si tenga presente che la relazione trovata è basata su una massimizzazione (tutte le possibili configurazioni), quindi chiaramente non è possibile data la complessità in spazio o in tempo calcolare direttamente la controparte
sfruttando la formula n · 2O(n) .
3.10.3
Le classi di complessità temporali
Il fatto che lo spazio sia riutilizzabile in un certo senso ‘’riscala‘’ tutte le classi
di complessità che abbiamo studiato per il tempo. La classe di complessità
spaziale logaritmica L assume in questo frangente una notevole importanza,
poiché vale la relazione:
L⊆P
dove P è la classe di problemi polinomiali in tempo. Tale inclusione è dimostrabile a partire dalla relazione che abbiamo trattato nello scorso paragrafo.
Detto questo, la complessità in spazio mima la complessità in tempo e come
prevedibile definisce le classi:
• L, che contiene i problemi per i quali esiste una MdT deterministica
che li calcola usando una quantità di spazio logaritmica nella dimensione
dell’input.
• N L, che contiene i problemi per i quali esiste una MdT non deterministica che li calcola usando una quantità di spazio logaritmica nella
dimensione dell’input.
• PSPACE, che contiene i problemi per i quali esiste una MdT deterministica che li calcola usando una quantità di spazio polinomiale nella
dimensione dell’input.
• N PSPACE, che contiene i problemi per i quali esiste una MdT non
deterministica che li calcola usando una quantità di spazio polinomiale
nella dimensione dell’input.
• ecc.
Si precisa che la classe N L ha una sola definizione (quella che usa le MdT non
deterministiche) e non ne ha una alternativa basata sul problema di verifica
come invece era per la classe N P.
146
3.10.4
Capitolo 3. Complessità
PATH, il tipico problema N L
Così come per la classe N P abbiamo parlato profusamente del problema caratteristico 3SAT, anche la classe N L dispone di un problema caratteristico:
PATH.
PATH è un problema di decisione sui grafi: dato un grafo orientato G = (V, E)
e due nodi s, t ∈ V si vuole sapere se esista o meno un cammino che collega s e t. È facile dimostrare l’appartenenza di PATH alla classe N L: basta
trovare una MdT non deterministica M PATH che risolva il problema in spazio
logaritmico.
Bozza dell’algoritmo di risoluzione per PATH
La macchina non deterministica M PATH non dovrà far altro che partire dal
nodo s del grafo ed esaminarne non deterministicamente i nodi adiacenti. Per
ognuno di questi nodi s 0 trovati si verifica se s 0 = t cioè il nodo di nostro
interesse. Se tale condizione è verificata l’algoritmo termina restituendo sì,
altrimenti esamina (sempre non deterministicamente) tutti i nodi adiacenti ad
esempio s 0 : a questo punto si ripeterà il test e così via. Possiamo rappresentare
l’esecuzione di M PATH usando il solito albero delle computazioni:
s
s0
s 00
s 000
.. .. .. .. .. .. .. .. ..
. . . . . . . . .
A puro titolo rappresentativo abbiamo supposto che ogni arco avesse tre nodi
adiacenti.
Parliamo della complessità di M PATH : trattandosi di una MdT non deterministica ognuna delle sue computazioni viene eseguita in parallelo, inoltre, stando
alla definizione di MdT non deterministica ci basta che esista un cammino che
termini con la corretta decisione e in spazio logaritmico per poter dire che
M PATH decida logaritmicamente PATH (e quindi che PATH ∈ N L). Ragioniamo dunque su una singola computazione: quando spazio occupa?
Iniziamo col dire che una computazione può durare al massimo n = |V | passi
poiché, se presente, il cammino fra s e t può al massimo coinvolgere tutti i nodi
del grafo. Questo ci porta ad avere una naturale condizione di terminazione:
se il conteggio dei passi è maggiore di n l’algoritmo termina con risposta no.
Ciò che dobbiamo mantenere durante l’esecuzione dell’algoritmo è dunque:
3.10. Complessità in spazio: una breve overview
147
• Il nodo attuale, che è rappresentabile mediante un indice che va da 0 a
n. Supponendo di rappresentare tale indice in binario (ma come al solito
non è rilevante la rappresentazione usata) ci servirà uno spazio pari a
log2 (n) per memorizzarlo.
• Il numero di passi (per la terminazione), anch’esso rappresentabile con
un indice che va da 0 ad n: altri log2 (n) di spazio occupato per la sua
memorizzazione.
Ci si potrebbe chiedere se il nodo attuale sia un’informazione sufficiente per
la riuscita dall’algoritmo ed in particolare per evitare loop. Tale obiezione è
facilmente smontabile giacché la MdT non deterministica prova tutte le combinazioni, anche quelle con loop, ma termina necessariamente grazie al limite
sul numero di passi possibili. In conclusione, proprio in virtù del fatto che tutti
i path del grafo vengono percorsi, se la soluzione c’è viene trovata.
Tornando al calcolo della complessità spaziale, l’intero algoritmo ha bisogno di
soltanto due spazi grandi log2 (n) per eseguire (lo spazio è riutilizzabile!) e quindi la complessità totale risulta essere 2 log2 (n) che è ovviamente O(log2 (n)).
Giacché abbiamo trovato una MdT non deterministica che risolve in spazio
logaritmico il problema PATH, possiamo dire ufficialmente che PATH ∈ N L.
3.10.5
La classe dei giochi: N PSPACE
Non studiamo alcun problema caratteristico di N PSPACE ma ci limitiamo a
dire chetendenzialmente in questa classe di complessità si trovano i problemi
relativi a giochi, ad esempio, il problema delle N Regine.
3.10.6
Riflessioni finali in merito alle classi di complessità spaziale
Grande colpo di scena: nell’ambito della complessità in spazio il problema
PSPACE = N PSPACE è stato risolto. Le due classi sono effettivamente
equivalenti e questo è dovuto al fatto che lo spazio è una risorsa riutilizzabile.
Rimane invece aperto il problema L = N L, la cui soluzione, data la relazione
esponenziale fra complessità spaziale e temporale, ci darebbe automaticamente la risposta al problema P = N P.
Riportiamo infine una gerarchia di inclusione fra alcune delle classi di complessità temporale ed alcune di quelle di complessità spaziale:
L ⊆ N L ⊆ P ⊆ N P ⊆ PSPACE = N PSPACE ⊆ EX P
Sappiamo inoltre che L 6⊆ PSPACE, ossia che esiste almeno un problema
PSPACE che non è L. Come già in precedenza, con 6⊆ intendiamo che non
c’è l’equivalenza fra gli insiemi, ma ovviamente l’inclusione ⊂ vale.
148
Capitolo 3. Complessità
Capitolo 4
Esercizi
In questo capitolo raccoglieremo diversi esercizi che mostrano i risvolti pratici
dei risutalti studiati in teoria.
4.1
Esercizi sulla calcolabilità
4.1.1
Guida rapida alle metodologie di dimostrazione
Riportiamo un piccolo arsenale di tecniche basilari dal quale attingere quando
si cerca di dimostrare la non calcolabilità di una funzione:
• Composizione di funzioni: quando un algoritmo per calcolare una funzione f usa soltanto g, oppure usa g ed altre funzioni che sappiamo
essere calcolabili possiamo dedurre che se g è calcolabile, f è calcolabile.
Qualora però g non sia calcolabile non possiamo dire nulla in merito ad
f : potrebbe esistere un’altro modo di scrivere f che non coinvolge g e
che quindi potrebbe essere calcolabile.
• Riduzione: vogliamo dimostrare che una funzione H non è calcolabile
sapendo che h non è calcolabile. Dal punto di vista delle implicazioni
stiamo quindi cercando di dire che:
¬calcolabilità h ⇒ ¬calcolabilità H
è vero. Rimuovendo i negati otteniamo la formula logica (equivalente):
calcolabilità H ⇒ calcolabilità h
Al fine di dimostrare tale implicazione assumiamo per assurdo che H sia
calcolabile e dimostriamo che se lo fosse allora anche h sarebbe calcolabile (i.e., la calcolabilità di H implica la calcolabilità di h). L’idea di base
è quindi quella di scrivere h in funzione di H.
150
Capitolo 4. Esercizi
• Diagonalizzazione: talvolta per dimostrare la non calcolabilità di una
funzione ϕ(x) può essere utile assumerla calcolabile e vedere come si
comporta avendo come input la sua codifica (e quindi osservare il comportamento di ϕx su v ar phix (x)).
4.1.2
Dimostrazioni di numerabilità e non numerabilità
Non numerabilità di un insieme
Dimostrare che l’insieme dei sottoinsiemi dei numeri naturali 2N non è numerabile.
Un tipico esempio di insieme di sottoinsiemi è l’insieme delle parti e viene indicato come P(n). La cardinalità dell’insieme delle parti di un insieme con n
elementi è 2n .
Per questo esercizio ci concentriamo invece su insieme infinito di sottoinsiemi
infiniti, ovvero l’insieme dei sottoinsiemi dei numeri naturali (non necessariamente quello delle parti), che chiamiamo 2N .
Dimostrazione per assurdo della non numerabilità di 2N . Assumiamo per assurdo che 2N sia numerabile. Rappresentiamo l’i -esimo sottoinsieme all’interno
di 2N come:
Ai = a0i a1i a2i a3i . . .
dove ogni aji è ottenuto grazie alla funzione caratteristica di quel sottoinsieme:
i
f (j) =
(
1
se j ∈ Ai
0
se j ∈
/ Ai
In pratica ogni sottoinsieme Ai viene identificato mediante i naturali che contiene, a loro volta identificati grazie alla funzione caratteristica di Ai .
Giacché 2N è numerabile (per assurdo), siamo in grado di scrivere la tabella
che li elenca tutti:
0
1
2
3
4
..
.
0
a00
a01
a02
a03
a04
..
.
1
a10
a11
a12
a13
a14
..
.
2
a20
a21
a22
a23
a24
..
.
3
a30
a31
a32
a33
a34
..
.
4
a40
a41
a42
a43
a44
..
.
···
···
···
···
···
···
..
.
4.1. Esercizi sulla calcolabilità
151
L’emento sulla riga i e colonna j ci dice quindi se l’insieme Ai contiene il
naturale j (aji è la risposta della funzione caratteristica f i (j)).
Applichiamo ora la diagonalizzazione classica. Andiamo a costruire un nuovo
insieme c = c0 c1 c2 . . . all’interno del quale il generico ck varrà 1 se akk valeva
0 e viceversa varrà 0 se akk valeva 1.
In questo modo c sarà necessariamente diverso da tutti gli insiemi già presenti
nella tabella, dimostrando così che 2N non è un insieme numerabile.
Restrizione a 2N finito. Se imponiamo che ogni sottoinsieme abbia al massimo k elementi (un k infinito limitato quindi) al suo interno, avremo sempre
una tabella con infinite righe ed infinite colonne (controlliamo la funzione catteristica per ogni naturale!), ma ogni riga avrà al più k elementi ad 1.
L’insieme 2N
f inito risulta essere un insieme numerabile, infatti, se consideriamo
uno dei suoi sottoinsiemi:
{a0 , a1 , . . . , ak }
possiamo usare ogni ai per scrivere le cifre un numero binario che rappresenterà univocamente quell’insieme. Abbiamo appena descritto una funzione biettiva che traduce numeri naturali in rappresentazione binaria nei nostri insiemi,
dunque per definizione 2N
f inito è numerabile.
4.1.3
Creazione di macchine di Turing
MdT che fa la somma in base 1
Si disegni la macchina di Turing che faccia la somma di numeri unari.
Se rappresentiamo gli operandi della somma in base 1, la MdT che calcola
n + m avrà sul nastro n + 1 uni per rappresentare n (ricordiamo che un 1 è
per lo zero) ed m + 1 uni per rappresentare m. Le due sequenze di 1 saranno
separate da un asterisco.
Dopo la computazione della somma ci aspettiamo che sul nastro della MdT
appaiano esettamente (n + 1 + m + 1 - 1) uni.
Ecco dunque il codice della macchina di Turing che compie il calcolo:
[1] δ(q0 , 1, q0 , 1, +1)
[2] δ(q0 , ∗, q1 , 1, +1)
[3] δ(q0 , ∗, q1 , 1, +1)
[4] δ(q0 , ∗, q2 , ∗, −1)
[5] δ(q1 , ∗, q0 , ∗, +1)
La regola [1] permette di scorrere il primo numero (si va a destra senza modificare nulla), la regola [2] inserisce un 1 al posto dell’asterisco che suddivide i
152
Capitolo 4. Esercizi
due numeri: a questo punto abbiamo creato una sequenza di (n + 1 + m + 1
+1) uni: dobbiamo rimuoverne due dal fondo. La regola [3] permette di scorrere il secondo numero e la regola [4] e [5] implementano la cancellazione dei
due 1 di troppo.
Si lascia al lettore il compito di creare la sequenza di transizioni che ne dimostri
la correttezza.
MdT che fa la somma in base 2
Si disegni la macchina di Turing che fa la somma di numeri binari.
Soluzione a cura di Elena Monfroglio.
Immaginiamo di avere un nastro di partenza configurato come illustrato in
Figura 4.1.
…
=
*
c
…
c
+
c
…
c
*
0
*
…
q0
Figura 4.1: Vogliamo sommare due numeri che hanno la stessa quantità di cifre (se non la hanno
immaginiamo di inserire una quantità di zeri all’inizio del più corto tale per cui i numeri risultano avere
le stesse cifre). Il simbolo c può valere 1 o 0 (che sono nell’alfabeto della MdT). A fondo nastro,
tra i due asterischi, rappresentiamo il resto (inizializzato ovviamente a 0). Il risultato della somma si
troverà a sinistra dell’uguale iniziale.
Forniamo direttamente la lista delle transizioni di della MdT che fa la somma di
numeri binari. In seguito descriveremo il suo funzionamento ed i vari formalismi
che abbiamo adottato.
4.1. Esercizi sulla calcolabilità
153
[01] δ(q0 , =, q0 , =, +1)
[27] δ(q11R , 0, q0W , 1, −1)
[02] δ(q0 , 0, q0 , 0, +1)
[28] δ(q11R , 1, q1W , 1, −1)
[03] δ(q0 , 1, q0 , 1, +1)
[29] δ(q11R , ∗, q11R , ∗, +1)
[04] δ(q0 , +, q1 , +, −1)
[30] δ(q1W , 0, q1W , 0, −1)
[05] δ(q0 , =, q0 , =, +1)
[31] δ(q1W , 1, q1W , 1, −1)
[06] δ(q1 , 0, q0R , +, +1)
[32] δ(q1W , +, q1W , +, −1)
[07] δ(q1 , 1, q1R , +, +1)
[33] δ(q1W , =, q1RES , =, −1)
[08] δ(q1 , +, q1 , +, −1)
[34] δ(q1W , ∗, q1W , ∗, −1)
[09] δ(q1R , +, q1R , +, +1)
[35] δ(q0W , 0, q0W , 0, −1)
[10] δ(q1R , 1, q1R , 1, +1)
[36] δ(q0W , 1, q0W , 1, −1)
[11] δ(q1R , 0, q1R , 0, +1)
[37] δ(q0W , +, q0W , +, −1)
[12] δ(q1R , ∗, q1SIN , ∗, −1)
[38] δ(q0W , =, q0RES , =, −1)
[13] δ(q0R , +, q0R , +, +1)
[39] δ(q0W , ∗, q0W , ∗, −1)
[14] δ(q0R , 1, q0R , 1, +1)
[40] δ(q0RES , ∗, q0 , 0, +1)
[15] δ(q0R , 0, q0R , 0, +1)
[41] δ(q1RES , ∗, q0 , 1, +1)
[16] δ(q0R , ∗, q0SIN , ∗, −1)
[42] δ(q1 , =, q2 , =, +1)
[17] δ(q0SIN , 1, q01R , ∗, +1)
[43] δ(q2 , +, q2 , +, +1)
[18] δ(q0SIN , 0, q00R , ∗, +1)
[44] δ(q2 , ∗, q2 , ∗, +1)
[19] δ(q1SIN , 1, q11R , ∗, +1)
[45] δ(q2 , 0, qF IN0 , ∗, −1)
[20] δ(q1SIN , 0, q01R , ∗, +1)
[46] δ(q2 , 1, qF IN1 , ∗, −1)
[21] δ(q00R , 0, q0W , 0, −1)
[47] δ(qF IN1 , ∗, qF IN , ∗, −1)
[22] δ(q00R , 1, q1W , 0, −1)
[48] δ(qF IN , +, qF IN , +, −1)
[23] δ(q00R , ∗, q00R , ∗, +1)
[49] δ(qF IN , =, qF IN , =, −1)
[24] δ(q01R , 0, q1W , 0, −1)
[50] δ(qF IN , 0, qF IN , 0, −1)
[25] δ(q01R , 1, q0W , 1, −1)
[51] δ(qF IN , 1, qF IN , 1, −1)
[26] δ(q01R , ∗, q01R , ∗, +1)
[52] δ(qF IN , ∗, q3 , 1, −1)
Evidentemente il codice non è molto leggibile ed invitiamo il lettore a mettere
alla prova le transizioni. Come idea generale, possiamo dire che mediante
gli stati q0 e q1 andiamo da sinistra verso destra e leggiamo le ultime cifre
significative dei due addendi. A seconda della configurazione di queste cifre
(00, 01, 10 o 11) cambierà lo stato qxxR (il caso 01 e 10 sono trattati insieme).
A questo punto, a seconda dello stato ‘’R‘’ transiremo in uno stato QxW che
ci dirà se dobbiamo scrivere 0 o 1. Prima di fare ciò, proseguiamo fino alla cella
che tiene il resto e qui computiamo la somma fra ciò che dobbiamo scrivere ed
il resto. A questo punto disponiamo di uno stato qxRES che, tornando all’inizio
154
Capitolo 4. Esercizi
del nastro, piazzerà il risultato a sinistra dell’uguale. Si parte così con q0 che
computerà la somma fra le nuove due cifre meno significative, e così via. Al
termine della somma gli stati qF IN e varianti rifiniscono il lavoro (qF IN0 e q3
sono stati finali).
4.1.4
Dimostrazioni di calcolabilità
Calcolabilità di una funzione (1)
Si dimostri che la seguente funzione è calcolabile:
(
1 se Mx (ϕx (x)) è definita
0
h (x) =
⊥ altrimenti
Per dimostare che qualcosa è calcolabile è sufficiente esporre un algoritmo (in
un linguaggio qualsiasi grazie alla tesi di Church-Turing) che calcoli la funzione
richiesta.
In questo caso un semplice algoritmo potrebbe essere:
1
2
3
4
5
h_primo ( x ) {
Mx = d e c o d i f i c a ( x ) ;
Mx ( x ) ;
return 1;
}
Listing 4.1: Algoritmo per il calcolo di h0 .
h0 è quindi calcolabile. Il risultato potrebbe sembrare sorprendente per via del
fatto che h (la funzione dell’halt) e h0 si assomigliano molto, ma la differenza
fra le due funzioni è cruciale: in h dobbiamo essere in grado di riconoscere la
divergenza e quindi restituire ‘’no‘’, mentre per h0 la divergenza è direttamente
un risultato e quindi non la dobbiamo riconoscere (infatti lasciamo che sia Mx
ad eseguire ed eventualmente convergere o divergere).
Calcolabilità di una funzione (2)
Si dimostri che la seguente funzione è calcolabile:
(
1 se P = N P
p(x) =
0 altrimenti
Purtroppo non sempre è possibile fornire un algoritmo per dimostrare la calcolabilità di una funzione. La funzione p ne è la prova, poiché risolverebbe
il problema delle classi P ed N P, problema tuttora aperto nella teoria della
complessità computazionale.
A livello informale, esso richiede di determinare se ogni problema per il quale
4.1. Esercizi sulla calcolabilità
155
un computer è in grado di verificare la correttezza di una soluzione positiva,
in un tempo trattabile, è anche un problema che può essere risolto dal computer in un tempo trattabile (ovvero se il computer è in grado di trovare da
solo una soluzione positiva in un tempo trattabile). Se la risposta è no, allora
esistono problemi per i quali è più complesso calcolare una certa soluzione che
verificarla.
Conosciamo anche la definizione più formale del problema, che fa uso delle
classi di complessità P e N P. La prima consiste di tutti quei problemi di
decisione che possono essere risolti con una macchina di Turing deterministica
in un tempo che è polinomiale rispetto alla dimensione dei dati di ingresso;
la seconda consiste di tutti quei problemi di decisione le cui soluzioni positive
possono essere verificate in tempo polinomiale avendo le giuste informazioni,
o, equivalentemente, la cui soluzione può essere trovata in tempo polinomiale
con una macchina di Turing non deterministica. Il problema delle classi P e
N P si risolve quindi nella seguente domanda:
P è uguale a N P?
Se fossimo in grado di risolvere il problema sarebbe immediato scrivere la
funzione p (sarà la funzione costantemente 1 o costantemente 0), ma non
siamo in grado di scrivere un algoritmo che risponda alla domanda cruciale P
è uguale a N P?
Se ne deduce che p è calcolabile anche se non siamo in grado di scrivere un
algoritmo che la calcoli.
Calcolabilità di una funzione (3)
Si dimostri che la seguente funzione è calcolabile:
(
1 se nell’espansione decimale di π esistono almeno x 5 consecutivi
g(x) =
0 altrimenti
Anche in questo caso non possiamo scrivere un algoritmo che calcoli g e dobbiamo quindi dare una dimostrazione alternativa.
I casi possibili sono soltanto due:
• Le sequenze di 5 nell’espansione decimale di π sono infinite limitate,
ovvero esiste un certo n per cui è vero che non esistono sequenze di 5
lunghe n o più nell’espansione decimale di π.
• Le sequenze di 5 non sono limitate.
Se ci troviamo nel primo caso è sufficiente verificare se l’x passata in input
è maggiore di n: se lo è, non esistono sequenze di 5 con almeno x elementi
poiché n è il limite, se invece non lo è ve ne sono dunque restituiamo 1. Se ci
troviamo nel secondo caso possiamo restituire direttamente 1.
156
Capitolo 4. Esercizi
Calcolabilità di una funzione (4)
Si dimostri che la seguente funzione non è calcolabile:
(
1 se ϕx (0) ↓
f (x) =
0 se ϕx (0) ↑
Approcciamo il problema iniziando a costruire una MdT diversa da quella che
calcolerebbe f : costruiamo una MdT parametrica in x (quindi in realtà creiamo
infinite MdT) che identifichiamo con M x (attenzione, x, non essendo a pedice,
non rappresenta la codifica della macchina ma soltanto un identificativo utile
ai nostri scopi). Tale MdT converge su 0 se e solo se Mx converge su x. Il
codice di M x è di immediata scrittura:
1
2
3
4
5
6
7
Mx ( p ) {
i f ( p == 0 ) {
r e t u r n U(x, x) //M_x( x )
} else {
return 1;
}
}
Listing 4.2: Codice di M x .
Di primo acchito il codice può sembrare strano; proviamo a rileggere con attenzione la definizione di M x : dobbiamo avere che MdT converge su 0 se e
solo se Mx converge su x. Parafrasando, potremmo dire che se 0 è l’input
della macchina M x , questa deve convergere se e solo se Mx converge su x.
Qualora l’input di M x sia diverso da 0, non ci interessa l’esito della computazione (return 1). Il codice riportato in Listing 4.2 ripercorre esattamente
quanto appena detto.
n
Sfruttiamo ora il teorema Sm
(Sezione 2.7.4) per affermare che giacché all’inx
terno di M x appare come parametro, la codifica di tale MdT dorà necessariamente essere funzione di x. In altre parole, possiamo compiere il cambio di
notazione:
M x = Mg(x)
dove g è la funzione di codifica delle MdT adottata (l’avevamo chiamata #
nell’ambito della numerazione di Göedel in Sezione 2.4.1).
Dimostrazione per assurdo della non calcolabilità di f . Assumiamo per assurdo che la funzione f (quella del problema) sia calcolabile e vediamo come
questa si comporta tale funzione sul valore g(x):
(
1 se ϕg(x) (0) ↓
f (g(x)) =
0 se ϕg(x) (0) ↑
4.1. Esercizi sulla calcolabilità
157
Osserviamo che ϕg(x) (0) è la funzione calcolata dalla nostra Mg(x) (el fu M x )
dato che la codifica è la stessa, e giacché la definizione di Mg(x) ci diceva che
tale MdT converge su 0 se e solo se Mx converge su x, possiamo riscrivere f
come segue:
(
1 se Mx (x) ↓
f (g(x)) =
0 se Mx (x) ↑
Abbiamo semplicemente sostituito la definizione di Mg(x) .
I lettori più attenti avranno notato che con la sua nuova definizione f è diventata molto simile alla funzione dell’halt, con una sola differenza: la funzione
dell’halt prende in input x mentre f prende in input g(x). In ogni caso, possiamo dire che f (g(x)) non è calcolabile poiché calcola esattamente ciò che
calcola la funzione dell’halt (che come sappiamo non è calcolabile). In altre
parole ciò che abbiamo dimostrato fin ora è che f ◦ g non è calcolabile poiché
identica a h(x), la funzione dell’halt.
Ragioniamo ora su questi due fatti:
• g è la funzione di codifica delle MdT e pertanto essa è per costruzione
totale e calcolabile.
• La composizione di funzioni è calcolabile (Sezione 2.9.1).
Considerando dunque queste due asserzioni, unite al fatto che f ◦ g non è
calcolabile, possiamo asserire che l’unica componente che può rendere f ◦ g
non calcolabile è f (poiché sia g che la composizione di funzioni lo sono).
Se ne deduce quindi che f non è calcolabile, altrimenti saremmo in grado di
calcolare il problema dell’halt.
Calcolabilità di una funzione (5)
Si dimostri che la seguente funzione non è calcolabile:
(
1 se 5 appartiene al codominio di ϕx
f (x) =
0 altrimenti
È immediato dimostrare la non calcolabilità di questa funzione adoperando il
teorema di Rice.
Consideriamo l’insieme di indici:
SF = {x|ϕx ha 5 nel suo codominio}
il quale definisce correttamente una famiglia di funzioni poiché se ϕx ha 5 nel
codominio anche tutte le funzioni ad essa equivalenti lo avranno (due funzioni
158
Capitolo 4. Esercizi
equivalenti hanno lo stesso dominio). Ora, ricordando che il teorema di Rice ci
dice che data F una famiglia di funzioni calcolabili, SF è decidibile se e solo se
o è vuoto o coincide con N, è evidente che SF non sia né vuota né coincidente
con N poiché esiste almeno una funzione che abbia 5 nel codominio (e.g.,
la funzione costantemente 5) e almeno una che non lo ha (e.g., la funzione
costantemente 1). Se ne deduce che SF non sia decidibile.
Ora, giacché la funzione f (x) verifica se x appartiene o meno a SF e giacché
SF non è decidibile, f (x) non è calcolabile.
Calcolabilità di una funzione (6)
Si dimostri che la seguente funzione è calcolabile:
(
ϕx (n) + 1 se ϕx (n) ↓
f (n) =
⊥
altrimenti
Per dimostrare che f è calcolabile è sufficiente mostrare un algoritmo che
la calcoli. Tale algoritmo sarà consterà semplicemente nel richiamare Mx e,
se questa termina restituire il risultato della computazione +1 (altrimenti la
computazione divergerà già in Mx ).
Estensione ad una funzione totale. Consideriamo ora f 0 , un estensione
totale di f e dimostriamo che tale funzione non è calcolabile.
(
ϕx (n) + 1 se ϕx (n) ↓
f 0 (n) =
↓
altrimenti
Abbiamo usato un po’ impropriamente la notazione di convergenza a significare che non ci interessa a cosa vogliamo che converga f 0 qualora ϕx (n) non
converga, ma ci interessa soltanto dire che vogliamo che converga (e dunque
che f 0 sia totale).
Dimostrazione per assurdo della non calcolabilità di f 0 . Assumiamo per assurdo che f 0 sia calcolabile.
Tale funzione avrà dunque un certo indice e all’interno della numerazione delle
funzioni e quindi possiamo effettuare il cambio notazionale:
f 0 = ϕe
Interroghiamoci ora sul comportamento di ϕe proprio su e:
(
ϕe (e) + 1 se ϕe (e) ↓
ϕe (e) =
↓
altrimenti
4.1. Esercizi sulla calcolabilità
159
Noi sappiamo però abbiamo assunto ϕe totale e dunque ci troveremo sempre
nel primo caso poiché ϕe (e) può soltanto convergere. Possiamo pertanto
scrivere:
ϕe (e) = ϕe (e) + 1
che è una evidente contraddizione.
Osserviamo dalla dimostrazione che abbiamo appena eseguito due fatti
importanti:
1. Se f ha indice k nella numerazione delle funzioni (e dunque f = ϕk )
possiamo notare che la definizione:
ϕk (k) = ϕk (k) + 1
ha senso poiché f non è totale, dunque può divergere, e data l’assurdità dell’equazione appena scritta certamente divergerà su k (e quindi
abbiamo scritto ⊥ = ⊥ + 1, che è legittimo).
2. Conseguenza del punto che abbiamo appena trattato, sappiamo che f
diverge su almeno un valore, cioè il suo indice k.
Calcolabilità di una funzione (7)
Dimostrare che ogni funzione parziale a dominio finito è calcolabile.
Se una funzione f parziale ha dominio finito allora possiamo rappresentare il
suo dominio/codominio come un insieme finito di coppie:
S = {< a1 , b1 >, < a2 , b2 >, . . . , < an , bn >}
dove con la lettera a rappresentiamo elmenti del dominio e con la lettera b
elementi del codominio. A questo punto scrivere una MdT che calcoli f è
immediato; basta scrivere un codice per casi:
1
2
3
4
5
6
7
f (x) {
i f ( x = a1 ) r e t u r n b1 ;
e l s e i f ( x = a2 ) r e t u r n b2 ;
...
e l s e i f ( x = an ) r e t u r n bn ;
else return undefined .
}
Listing 4.3: Algoritmo per il calcolo di una funzione parziale a dominio finito f .
Giacché abbiamo fornito il codice della MdT che calcola f , f è calcolabile.
160
4.1.5
Capitolo 4. Esercizi
Dimostrazioni relative alla decidibilità e alla semidecidibilità
Chiusura su unione ed intersezione
Dimostrare che la decidibilità è chiusa per unione ed intersezione, ovvero se
L1 è decidibile ed L2 è decidibile allora L1 ∪ L2 e L1 ∩ L2 sono decidibili.
Per dimostrare la chiusura sull’unione definiamo la funzione:
(
1 se x ∈ L1 ∨ x ∈ L2
k(x)
0 altrimenti
Essendo k la funzione che decide L1 ∪ L2 , se esiste una MdT che la calcola
abbiamo dimostrato la chiusura per unione della nozione di decidibilità. Tale
MdT ovviamente esiste poiché esistono le due MdT che decidono L1 ed L2 .
Per dimostrare la chiusura sull’intersezione definiamo la funzione:
(
1 se x ∈ L1 ∧ x ∈ L2
k 0 (x)
0 altrimenti
Essendo k la funzione che decide L1 ∩ L2 , se esiste una MdT che la calcola
abbiamo dimostrato la chiusura per intersezione della nozione di decidibilità.
Anche in questo caso, tale MdT ovviamente esiste poiché esistono le due MdT
che decidono L1 ed L2 .
Semidecidibilità di un insieme (1)
Dimostrare che l’insieme T non è semidecidibile.
T = {x|ϕx è totale }
L’insieme T è dunque l’insieme dei codici delle MdT che calcolano funzioni
totali.
Dimostrazione per assurdo della non semidecidibilità di K. Assumiamo per assurdo che T sia semidecidibile. Usando l’equivalenza fra le proposizioni esposte in Sezione 2.10.1 possiamo dire che T è l’immagine di una funzione totale
calcolabile, cioè esiste una certa f siffatta:
f :N→T
Dato che T è l’insieme dei codici delle MdT che calcolano funzioni totali, la
nostra f è sostanzialmente in grado di enumerare tutte le funzioni totali. In
Sezione 2.7.1 abbiamo dimostrato che ciò non è possibile, dunque f non può
esistere: abbiamo ottenuto l’assurdo, pertanto T non è semidecidibile.
4.1. Esercizi sulla calcolabilità
4.1.6
161
Modello di calcolo di Kleene
Dimostrare la calcolabilità (1)
Dimostrare che la funzione min(x, y ) è calcolabile secondo Kleene usando soltanto composizione e ricorsione. Chiedersi inoltre se è possibile implementare
un algoritmo che calcoli tale funzione in un numero di passi pari a min(x, y )
usando soltanto composizione e ricorsione (non minimalizzazione).
Possiamo rispondere immediatamente alla seconda domanda: da un punto di
vista intuitivo, un semplice algoritmo che in min(x, y ) passi determini il minimo
consiste nel sottrarre da x e da y una unità ad ogni passo ricorsivo e appena
x o y sono zero, abbiamo il minimo. Per effettuare un’implementazione di
questo tipo dovremmo però ricorrere su due variabili, problema che abbiamo
già trattato parlando della funzione di Ackerman, che non è funzione primitiva
ricorsiva.
Per quanto concerne la prima domanda la questione è più complessa e richiede
una serie di dimostrazioni in cascata.
Iniziamo col ridefinire la funzione minimo aggiungendo una variabile che sfrutteremo per la ricorsione:
min(x, y ) = g(x, y , sottr(x, y ))
L’idea dietro a questa definizione è piuttosto astuta: giacché la sottrazione fra
numeri naturali non può mai dare esito negativo (ma eventualmente da esito
0), possiamo usare la funzione sottr per calcolare il minimo implementando un
semplice if-then-else. In altre parole vogliamo che g computi l’equivalente del
seguente pseudocodice:
g(x, y , sottr(x, y )) = if (sottr(x,y) == 0) then x else y
È ovvio comprendere che tale codice computi il minimo: se x < y (ergo x
è il minimo) la sottrazione x − y restituirà un valore uguale a zero e dunque
restituiamo x. Nel caso in cui invece x > y (il minimo è y ) la sottrazione x − y
sarà maggiore di zero e quindi restituiremo y .
Disponiamo di una splendida definizione del minimo in pseudocodice ma questo ovviamente non basta per dire che il minimo è calcolabile alla Kleene.
Dobbiamo infatti:
1. Trasformare g da pseudocodice ad una funzione ricorsiva primitiva di
Kleene.
2. Dimostrare che la funzione di supporto che abbiamo usato, sottr, sia
ricorsiva primitiva.
Trattiamo i due punti separatamente.
162
Capitolo 4. Esercizi
Definizione di g come funzione ricorsiva primitiva. Dobbiamo tradurre
un costrutto if-then-else sfruttando le strutture base offerte da Kleene. Ricordiamo che la ricorsione è definibile secondo Kleene mediante la seguente
struttura:
f (~
x , 0) = g(x)
f (~
x , y + 1) = h(~
x , y , f (~
x , y ))
Il punto sul quale dobbiamo focalizzarci è il fatto che un if-then-else non ha
nulla di ricorsivo: si tratta di due casi statici. Ciò nonostante Kleene ci costringe a definire ricorsivamente g, ma noi possiamo facilmente bypassare la
cosa implementando una ricorsione ‘’farlocca‘’. Definiamo g come segue:
g(x, y , 0) = P12 (x, y )
g(x, y , z + 1) = P24 (x, y , z, g(x, y , z))
Il caso base è di immediata comprensione: giacché la terza variabile di g contiene la sottrazione fra x ed y , se tale sottrazione vale 0, proiettiamo x come
risultato. In sostanza abbiamo implementato il ramo then della nostra condizione. Il caso ricorsivo implementa invece il ramo else, che rappresenta
quindi il branch da attivarsi laddove la sottrazione fra x ed y non restituisca
valore nullo. Ricordiamo che ciò è implementato gratuitamente dal sistema di
Kleene: se la sottrazione risultasse pari a zero si attiverebbe automaticamente
il caso base. Osserviamo che la ricorsione non è utilizzata ai fini del calcolo del
minimo ma viene comunque impiegata poiché parte integrante della definizione
di funzione ricorsiva primitiva imposta da Kleene. In pratica g ricorre z + 1
volte dove z + 1 è il delta ottenuto da x − y . Al termine di queste z + 1 chiamate ricorsive il risultato che viene man mano riportato è quello imposto dalla
proiezione che è, ovviamente, y . Dovrebbe dunque risultare chiaro perché la
ricorsione sia totalmente inutile: y sarebbe già stato restituibile sin dal primo
passo ricorsivo.
Anche se ciò che abbiamo fatto può sembrare per certi versi sconcertante o
stupido, non esiste altro modo mediante il quale implementare if-then-else nel
linguaggio di Kleene. Per convincerci di ciò possiamo trasporre il problema
usando un linguaggio (poco) più ad alto livello.
Consideriamo il problema di scrivere il costrutto if-then-else in linguaggio
WHILE, ossia utilizzando soltanto cicli while, la condizione di diversità e lo
zero.
1
2
// V o g l i a m o i m p l e m e n t a r e
// i f ( x ! = y ) t h e n S1 e l s e S2
3
4
5
function (x , y) {
t = 0 ; // i n i z i a l i z z o
4.1. Esercizi sulla calcolabilità
163
6
// ramo t h e n
w h i l e ( ( x ! = y ) + ( t ! = 0 ) −1) { // ( x ! = y ) && ( t ! = 0 )
S1 ;
t = 0;
}
// ramo e l s e
w h i l e ( ( t != 0) ) {
S2 ;
t = 0;
}
7
8
9
10
11
12
13
14
15
16
17
}
Listing 4.4: Implementazione di un if-then-else in linguaggio WHILE.
Per il momento ignoriamo la scrittura relativa alla condizione del primo while,
la tratteremo nel dettaglio a breve. Per quanto concerne il resto del codice,
possiamo osservare l’impiego di una variabile di supporto t che ci permette
di regolare l’entrata nei loop affinché questi vengano eseguiti una ed una sola
volta (ed un solo dei due). Ciò è utile poiché il primo while implementa il ramo then mentre il secondo implementa il ramo else. Data questa semantica,
capiamo che in nessun caso vogliamo che i cicli while si ripetano (e t implementa proprio questo). Questo tipo di ragionamento ci riporta al problema del
minimo che stavamo trattando: così come qui abbiamo scritto dei cicli while
farlocchi, nella definizione di g abbiamo usato una ricorsione farlocca.
Per completezza dedichiamo qualche parola alla prima condizione del primo
while. Dando per scontato che i valori 1 e 0 indichino i valori booleani true
e false avremo che la condizione di diversità che sfruttiamo restituirà 0 o 1
rispettivamente a seconda del fatto che i due operandi coinvolti siano uguali o
diversi. A questo punto è semplice verificare che l’operazione (x != y) + (t
!= 0) -1 restituisce 1 se e solo se sia (x != y) che (t != 0) restituiscono
1, ossia, sono veri. Perciò l’operazione che abbiamo scritto implementa l’AND
logico. Si osservi infine che abbiamo (per semplicità) usato tranquillamente somma e sottrazione ma in realtà dovremmo dimostrarne la calcolabilità
usando solo i costrutti di base del linguaggio dato.
La funzione sottr è ricorsiva primitiva. Torniamo al nostro problema: siamo riusciti a definire g come una funzione ricorsiva primitiva secondo Kleene,
ma la nostra definizione fa uso della procedura sottr, la quale dobbiamo dimostrare essere ricorsiva primitiva.
Nella Sezione 2.13.5 abbiamo dimostrato che la somma è primitiva ricorsiva
usando la funzione di successore. Compiamo ora una ragionamento analogo
164
Capitolo 4. Esercizi
definendo la sottrazione mediante l’uso della funzione predecessore:
sottr(x, 0) = P11 (x)
sottr(x, y + 1) = pred(P33 (x, y , sottr(x, y )))
Ora sottr è correttamente definita come funzione ricorsiva primitiva, ma fa uso
della procedura pred che calcola il predecessore. In maniera del tutto analoga
a quanto appena fatto, dobbiamo andare a dimostrare che pred è una funzione
ricorsiva primitiva1 .
La funzione pred è ricorsiva primitiva. La definizione ricorsiva di pred è
molto semplice ed anch’essa fa uso di una ricorsione farlocca:
pred(0) = Z(0)
pred(x + 1) = P12 (x, pred(x)))
Si osservi che abbiamo adottato in questo caso una versione ridotta della
definizione ricorsiva di Kleene dato che l’unica variabile che abbiamo è anche
quella sulla quale ricorriamo.
Per quanto concerne la definizione in sé possiamo osservare che la chiamata
ricorsiva a pred(x) è di fatto inutile dato che comunque, al termine delle x
chiamate ricorsive, noi restituiremo comunque x.
Perfetto! Anche l’ultimo tassello che avevamo utilizzato per definire g è ora
dimostrato essere ricorsivo primitivo.
Come cappello conclusivo ricapitoliamo ciò che abbiamo fatto: per prima cosa
abbiamo definito il minimo come una chiamata ricorsiva ad una funzione g:
min(x, y ) = g(x, y , sottr(x, y ))
tale funzione g è a sua volta definita come:
g(x, y , 0) = P12 (x, y )
g(x, y , z + 1) = P24 (x, y , z, g(x, y , z))
ed è una funzione che sostanzialmente calcola il costrutto if (sottr(x,y)
== 0) then x else y. Al fine di dimostrare che g è ricorsiva primitiva abbiamo dovuto dimostrare che le sotto-funzioni da lui impiegate fossero ricorsive
primitive. A tal fine abbiamo dimostrato che sia sottr che pred sono funzioni
primitive ricorsive secondo Kleene.
1
Si osservi che questo tipo di ‘’dimostrazioni in cascata‘’ è tipico quando si lavora con
le funzioni di Kleene. Tale fatto non dovrebbe sorprenderci dato la definizione stessa del
linguaggio di Kleene si basa sul concetto di costruire nuove funzioni ricorsive a partire da
funzioni già definite ricorsive.
4.1. Esercizi sulla calcolabilità
165
If-then-else in cascata. Qualora si dovessero implementare degli degli ifthen-else in cascata la procedura da utilizzarsi è identica a quella appena illustrata. Si partirà dalla nostra funzione g che espleterà col suo caso base il
ramo then del primo if, mentre con il suo ramo else chiamerà una seconda
funzione g 0 che rappresenterà il primo if innestato: il caso base di g 0 implementerà il ramo then del primo if innestato, mentre il suo caso ricorsivo chiamerà
una terza funzione g 00 che rappresenterà il secondo if innestato e così via.
4.1.7
Altro
Portare un esempio di funzione non calcolabile
Si porti un esempio di funzione non calcolabile.
Il problema della piastrellatura di Wang è un tipico esempio di funzione non
calcolabile. Si hanno in input n tipi di piastrelle diverse (tutte della stessa
dimensione ma con colorazioni differenti) e si desidera piastrellare una stanza
facendo sì che due piastrelle adiacenti abbiano sempre lo stesso colore in contatto.
La Figura 4.2 mostra un esempio di insieme di piastrelle con una possibile relativa piastrellatura corretta.
Figura 4.2: In alto, un insieme di piastrelle. In basso una piastrellatura corretta che usa le piastrelle
indicate in alto.
Definita una superficie s da piastrellar, un insieme di n tipi di piastrelle qualsiasi e potendone usare infinite di ogni tipo non è possibile dire se tale insieme
permetta o meno di piastrellare s per intero.
166
Capitolo 4. Esercizi
4.2
Esercizi sulla complessità
4.2.1
Dimostrazioni di appartenenza a P
4.2.2
Dimostrare l’appartenenza a P (1)
Sia S un problema di decisione. Dimostrare che se S è un insieme finito,
S ∈ P.
Il quesito riportato ricorda molto l’Esercizio 4.1.4, nel quale dovevamo dimostrare che una funzione parziale a dominio finito è calcolabile. Di fatto la prima
parte della dimostrazione è identica, l’unica nota che dobbiamo aggiungere è
il calcolo della complessità della soluzione.
Se S è un insieme finito allora possiamo banalmente scrivere una MdT M S che
lo decide. Tale MdT, proprio come quella dell’Esercizio 4.1.4 (il cui codice
è nel Listing 4.3) ragionerà per casi e verificherà, per ogni w ∈ S, se l’input
della MdT è uguale o meno ad w . Il numero di confronti che dovremo fare è
dunque pari alla cardinalità di S (chiamiamola k): tale valore è indipendente
dalla dimensione del dato in input, infatti, se x è il dato in input n = `(x)
è la sua dimensione. Ogni confronto (effettuato lettera per lettera) costerà
dunque n dato che ogni parola w deve essere confrontata con x. Tirando le
somme, dobbiamo effettuare k confronti ognuno dei quali costa n, pertanto
la complessità O(k · n), che, poiché k è costante rispetto ad n, è a sua volta
O(n).
Possiamo dunque asserire che giacché il tempo impiegato da M S per decidere
S è lineare nel suo input, S appartiene alla classe di complessità polinomiale
(S ∈ P).
Bibliografia
[1] C. Toffalori. Teoria della computabilità e della complessità. Collana di
istruzione scientifica. Serie di informatica. McGraw-Hill, 2005.
167
Download