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