dispensa PDF

annuncio pubblicitario
Corso di Informatica Generale II u.d.
Traccia delle lezioni sulla calcolabilità
Mauro Brunato
17 maggio 2006
Indice
1 Introduzione
1.1 Esempi . . . . . . . . . .
1.1.1 Calcolo numerico .
1.1.2 Calcolo simbolico
1.1.3 Teorie matematiche
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
2
2
2
3
2 Risultati di base
2.1 Alfabeti, stringhe e programmi . . .
2.1.1 L’insieme dei programmi C
2.2 Funzioni . . . . . . . . . . . . . . .
2.3 Funzioni calcolabili . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
4
4
4
5
6
3 Funzioni non calcolabili
3.1 La terminazione di un programma C . . . . . . . . . . . . . . . . . .
3.2 Irrisolvibilità dell’halting problem . . . . . . . . . . . . . . . . . . .
3.3 Una semplificazione . . . . . . . . . . . . . . . . . . . . . . . . . . .
7
7
8
9
4 Ricorsività
4.1 Esempi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
4.2 Il problema della fermata è ricorsivamente enumerabile . . . . . . . .
4.2.1 Un dubbio... . . . . . . . . . . . . . . . . . . . . . . . . . .
10
10
10
11
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
2
Sommario
Questa dispensa prsenta una traccia delle lezioni relative alla calcolabilità. Non è
necessariamente comprensibile da chi non ha seguito le lezioni.
Il documento è soggetto a variazioni e aggiustamenti durante il corso del semestre
di insegnamento; l’ultima versione piò sempre essere recuperata dalla sezione “risorse”
della pagina del corso di Informatica Generale II u.d. per Matematica sul sito
http://dit.unitn.it/˜brunato/info2/
Per distinguere le versioni è sufficiente confrontare la data riportata sotto il titolo
con quella scritta nell’elenco delle risorse (non è dunque necessario scaricare il file
tutte le volte).
Capitolo 1
Introduzione
Nell’attività di un matematico esistono alcuni metodi di lavoro che portano a risultati
comunicabili: la dimostrazione di un teorema, l’esecuzione di un calcolo, l’introduzione di nuove definizioni. . . Per contro, un articolo scientifico che utilizza procedimenti “arbitrari” come la lettura dei fondi di caffè o ispirazioni varie non può essere
considerato accettabile.
Nel seguito, considereremo “rigoroso” un procedimento che può essere espresso in
un linguaggio di programmazione, come la Macchina di Turing o un linguaggio di programmazione moderno, che hanno esattamente la stessa potenza: ogni cosa esprimibile
in uno dei due formalismi è esprimibile nell’altro.
Il punto fondamentale della teoria della calcolabilità è proprio l’Ipotesi di ChurchTuring: ogni procedimento matematico che sia in qualche modo “accettabile” può essere espresso in termini di un programma per Macchina di Turing (o di programma
C).
1.1 Esempi
1.1.1 Calcolo numerico
C’è poco da dire: è un campo nel quale i calcolatori da sempre battono gli uomini, e
un’operazione sui numeri è un caso evidente di procedimento esprimibile in linguaggio
C.
1.1.2 Calcolo simbolico
La risoluzione simbolica di equazioni, integrali e altro è un campo in cui regna la
creatività umana: i risultati migliori si ottengono tuttora applicando con cognizione
di causa le regole classiche (integrazione per parti, procedimenti di riduzione a forme
note. . . ). Ciononostante, esistono programmi estremamente versatili: Mathematica,
Maple ed altri. Resta aperto il problema estetico di portare un particolare integrale
in una forma “elegante”: l’eleganza è un criterio non esprimibile (almeno per ora) in
termini di un linguaggio di programmazione, ma è percepita in modo diverso dai vari
esseri umani, quindi non è detto che possa essere formalizzata.
In altri termini: condizione necessaria all’esprimibilità di un procedimento in termini di linguaggi di programmazione è che questo procedimento sia inteso in senso
univoco da tutti gli esseri umani.
2
1.1.3 Teorie matematiche
Un teorema è una proposizione che viene dimostrata a partire da proposizioni precedentemente assunte come vere. Una teoria è data da un insieme di proposizioni vere a
priori (assiomi) e da tutti i teoremi originati da queste. La dimostrazione di un teorema
non è altro che una sequenza finita di proposizioni, l’ultima essendo l’enunciato del
teorema che si vuole dimostrare, ciascuna delle quali
• è un assioma, oppure
• è un teorema dimostrato in precedenza, oppure
• segue dall’applicazione del modus ponens a due proposizioni precedenti.
Non è difficile convincersi che ogni dimostrazione può essere vista in questo modo.
Se è cosı̀, è possibile scrivere un programma C, per quanto complesso, in grado di
generare tutti i teoremi veri in una particolare teoria. Questo programma può ricevere
come input gli assiomi della teoria (ad esempio l’aritmetica) e proverà a combinarli tra
loro in tutti i modi possibili, ottenendo tutti i teoremi.
È convinzione comune che nessun programma potrà mai sostituire il lavoro di un
matematico, perché la discussione precedente non considera l’utilità o l’interesse di
alcuni risultati rispetto ad altri, ma in sé la dimstrazione di teoremi è esprimibile in C.
3
Capitolo 2
Risultati di base
2.1 Alfabeti, stringhe e programmi
Sia A un insieme finito che noi chiameremo alfabeto. Alcuni esempi: lettere dell’alfabeto latino, Kanji giapponesi, simboli compresi nel codice ASCII.
Una stringa in A è una sequenza finita di elementi di A. La lunghezza di una stringa
è il numero di elementi che la compongono. L’insieme delle stringhe di lunghezza
n > 0 è quindi isomorfo all’insieme potenza An . Consideriamo anche la stringa nulla,
unico elemento di A0 . L’insieme di tutte le stringhe si denota con
A∗ =
∞
[
An .
n=0
Osserviamo che, dato che A è un insieme finito, l’insieme delle stringhe di lunghezza n è anch’esso finito, ed ha cardinalità |A|n . L’insieme A∗ è invece infinito, ma
è numerabile, in quanto unione numerabile di insiemi finiti.
2.1.1 L’insieme dei programmi C
Un programma C è scritto come una sequenza di caratteri. In altre parole, i programmi C sono stringhe di caratteri ASCII. Ovviamente, non tutte le stringhe sono programmi C (è necessario che sia rispettata una data sintassi). Se chiamiamo A l’insieme dei caratteri ASCII e C l’inieme dei programmi C, otteniamo il seguente risultato
fondamentale:
C ⊂ A∗ .
È evidente che l’insieme dei programmi C non può essere finito1 , quindi la seguente
proposizione è vera:
Proposizione 1 L’insieme dei programmi C è numerabile.
1 Si
pensi ad esempio alla possibilità di scrivere programmi C di lunghezza arbitraria
4
2.2 Funzioni
Sia X un qualunque insieme numerabile (ad esempio, i numeri naturali, oppure le
stringhe su un alfabeto). Sia
X X = {f : X → X}
l’insieme di tutte le possibili funzioni aventi dominio e codominio in X. Con un procedimento diagonale analogo a quello di Cantor è possibile dimostrare per assurdo
che X X non è numerabile. Supponiamo di possedere un’enumerazione (f1 , f2 , . . . )
di tutte le funzioni f : X → X. Sia (x1 , x2 , . . . ) un’enumerazione degli elementi di
X (che è un insieme numerabile per ipotesi). Allora è possibile definire la funzione
g : X → X:
(
x1 se fi (xi ) 6= x1
g(xi ) =
x2 altrimenti.
È facile convincersi che g differisce da ogni funzione nella presunta enumerazione
(f1 , f2 , . . . ) di X X , che quindi è incompleta. Abbiamo quindi il seguente risultato:
Proposizione 2 Sia X un insieme numerabile, e sia X X l’insieme di tutte le funzioni
da X in X. Allora X X è infinito e non numerabile.
Abbiamo le due seguenti “specializzazioni” importanti.
Corollario 1 L’insieme delle funzioni che mappano stringhe ASCII in stringhe ASCII
è infinito e non numerabile.
Corollario 2 L’insieme delle funzioni che mappano interi in interi è infinito e non
numerabile.
Notiamo inoltre che la prova per diagonalizzazione funziona anche se il codominio
è finito, a patto che sia composto di più di un elemento. Consideriamo il caso delle
funzioni di decisione, che mappano un dominio X su un insieme binario, ad esempio
{0, 1}.
Ad esempio, la funzione f : N → {0, 1} che mappa n in 1 se e soltanto se n è
primo è una funzione di decisione. In generale, una funzione di decisione può essere
vista come un procedimento che risponde “sı̀” o “no” per un elemento del dominio. In
altre parole, una funzione di decisione classifica gli elementi di X in due categorie.
Ogni funzione di decisione f : X → {0, 1} corrisponde a un sottoinsieme di X:
χf = {x ∈ X : f (x) = 1}.
Viceversa, a ogni sottoinsieme Y ⊆ X corrisponde una funzione di decisione, detta la
funzione caratteristica di Y :
(
1 se x ∈ Y
χY (x) =
0 altrimenti.
Conseguenze immediate della Proposizione 2 sono le seguenti.
Corollario 3 L’insieme delle funzioni di decisione sugli interi è infinito e non numerabile.
Corollario 4 L’insieme delle funzioni di decisione sulle stringhe è infinito e non numerabile.
5
2.3 Funzioni calcolabili
Abbiamo visto che, da un punto di vista sintattico, un programma C può essere visto
come una stringa.
Dal punto di vista operativo (“semantico”), un programma C è una cosa che riceve
dati in ingresso e produce dati in uscita. I dati in ingresso e in uscita sono anch’essi
stringhe ASCII, quindi un programma C è una funzione che mappa stringhe ASCII in
sringhe ASCII. Nella notazione delle sezioni precedenti, abbiamo2
∗
C ⊂ (A∗ )A .
Si tratta di un’inclusione propria?
Ricordiamo dalla Proposizione 1 che l’insieme dei programmi C è numerabile,
∗
mentre dal corollario 1 (A∗ )A non lo è. Se definiamo calcolabile una funzione per la
quale esiste un programma C che la calcola, abbiamo il seguente risultato:
Teorema 1 Esistono funzioni su stringhe di un alfabeto che non sono calcolabili.
Possiamo anche limitarci a programmi C che operano su numeri interi (anch’essi
ovviamente numerabili) e alle funzioni di decisione sui numeri interi (non numerabili):
Teorema 2 Esistono funzioni di decisione sugli interi che non sono calcolabili.
2 In realtà, non stiamo considerando che più programmi (infiniti) calcolano la stessa mappa; il discorso,
comunque, non cambia
6
Capitolo 3
Funzioni non calcolabili
Il teorema 1 assicura che esistono funzioni non calcolabili. Nell’ambito delle funzioni di decisione sulle stringhe, la più famosa (nonché il primo esempio) è correlata
all’halting problem.
3.1 La terminazione di un programma C
Esistono programmi C la cui esecuzione termina sempre. Ad esempio, il seguente
programma che decide se il numero in ingresso è primo oppure no termina per ogni
valore in ingresso:
scanf ("%d", &n);
for ( i = 2; i < n && n % i; i++ );
printf ("%d", i == n);
Viceversa, il programma
scanf ("%d", &n);
for ( i = n-1; i < n; i-- );
printf ("%d", i == n);
non termina mai, dato che la condizione di continuazione del ciclo for è sempre
soddisfatta.
Altri programmi terminano, oppure no, a seconda del dato in ingresso. Il programma
scanf ("%d", &n);
for ( i = 0; i != n; i-- );
printf ("%d", i == n);
termina se il valore in ingresso è negativo, altrimenti continua a decrementare i indefinitamente.
Dato un programma p ∈ C ⊂ A∗ e una stringa s ∈ A∗ , definiamo p(s) la stringa
fornita in uscita dal programma p quando riceve in input la stringa s. Dato che il programma non termina necessariamente, diremo che p(s) ∈ A∗ ∪ {∞}, dove la scrittura
convenzionale p(s) = ∞ significa che il programma p non termina quando riceve in
ingresso la stringa s.
Il problema di determinare se un programma termina o prosegue indefinitamente
può essere espresso nei seguenti termini.
7
Problema 1 (Halting problem) Dato un programma p ∈ C ⊂ A∗ e una stringa s ∈
A∗ , stabilire se p(s) ∈ A∗ .
Si noti che, per via della tesi di Church-Turing, il problema può dirsi risolto soltanto
se si individua una procedura formale e finita (in altre parole, un programma C) in grado
di dare una risposta per ogni possibile programma e per ogni input.
3.2 Irrisolvibilità dell’halting problem
Possiamo dimostrare per assurdo che una procedura per determinare la fermata di ogni
programma e ogni input non esiste. Supponiamo infatti che esista. Potremo esprimerla
come una funzione C:
int HALT (char *p, char *s)
{
...
}
La funzione HALT riceve in ingresso due stringhe. La prima viene interpretata come
programma C, la seconda come l’input da fornire al programma. La funzione HALT
termina sempre e restituisce 1 se p(s) ∈ A∗ , 0 se p(s) = ∞. Supponiamo anche che se
p non contiene un programma C ben formato la funzione restituisca 1 (in altri termini,
se p non è un programma C, allora assumiamo che p termina).
Nell’ipotesi che la funzione HALT possa essere realizzata, chiamiamo H il seguente
programma:
gets (s);
if ( HALT(s,s) )
while ( 1 );
printf ("1");
Il programma H riceve una stringa s in ingresso, verifica se s, intesa come programma,
termina scrivendo quando riceve s stessa come ingresso, ovvero se s(s) ∈ A∗ . In tal
caso, entra in un ciclo infinito. Altrimenti, se la procedura HALT (che supponiamo
esistente) dice che s(s) = ∞, H termina scrivendo “1”.
Data una stringa s in ingresso, H(s) può valere 1 oppure ∞. In particolare,
• se HALT dice che s(s) ∈ A∗ , allora H(s) = ∞;
• se HALT dice che s(s) = ∞, allora H(s) = 1 ∈ A∗ .
L’assurdo si ottiene sostituendo nella descrizione sopra s con H, ovvero fornendo
a H come input la stringa che definisce H stesso. Infatti, otteniamo che
• se HALT dice che H(H) ∈ A∗ , allora H(H) = ∞;
• se HALT dice che H(H) = ∞, allora H(H) = 1 ∈ A∗ .
Insomma, la funzione HALT sbaglia sistematicamente quando viene applicata al
programma H con input H.
8
3.3 Una semplificazione
È possibile dimostrare molte “semplificazioni” del risultato precedente. Ad esempio:
• non è possibile realizzare un programma che decide se un programma si ferma
per ogni possibile stringa in ingresso;
• non è possibile realizzare un programma che decide se un programma si ferma
per qualche stringa in ingresso.
• non è possibile realizzare un programma che decide se un programma si ferma
per una stringa di ingresso prefissata.
• non è possibile realizzare un programma che decide se un programma che non
richiede dati in ingresso si ferma.
9
Capitolo 4
Ricorsività
Sia X un insieme numerabile, ad esempio N, oppure A∗ . Un sottoinsieme Y ⊂ X si
dice ricorsivo (in X) se la sua funzione caratteristica χY : X → {0, 1} è calcolabile.
In altri termini, un insieme si dice ricorsivo se esiste un programma C che ermina per
ogni ingresso decidendo se un elemento x ∈ X appartiene a Y .
Nelle stesse ipotesi, il sottoinsieme Y si dice ricorsivamente enumerabile in X se
esiste un programma C in grado di elencarne gli elementi.
Un insieme ricorsivo è anche ricorsivamente enumerabile. Infatti, dato un programma che calcola la sua funzione caratteristica χY , la seguente procedura stampa tutti e
soli gli elementi di Y :
Per ogni elemento x ∈ X
Se χY (x) = 1
Stampa x
4.1 Esempi
L’insieme dei numeri primi è ricorsivo in N, in quanto esiste un programma C che,
ricevendo in ingresso un numero naturale, scrive “1” se è primo, “0” altrimenti.
L’insieme C dei programmi C ben formati è ricorsivo in A∗ ; infatti, esiste un programma C che permette di distinguere tra le stringhe che rappresentano un programma
C e quelle che non li rappresentano. Questo programma è il compilatore, il cui compito
è, fra gli altri, quello di verificare se una stringa (contenuta in un file) è un programma
C.
Ecco una procedura che elenca tutti i programmi C:
Per ogni stringa s ∈ A∗
Se il compilatore compila s senza notificare errori
Stampa s
4.2 Il problema della fermata è ricorsivamente enumerabile
Consideriamo i programmi che non richiedono dati in ingresso. Per coerenza con la notazione già introdotta, se p ∈ C chiamiamo p() la stringa prodotta in uscita se non viene fornito input, con la convenzione che se p non termina allora p() = ∞. Definiamo
10
l’insieme dei programmi senza input che terminano:
HLT = {p ∈ A∗ : p() ∈ A∗ }
Una formulazione alternativa delle conclusioni del capitolo 3 è che
HLT non è ricorsivo in A∗ .
Pur non essendo ricorsivo, HLT è ricorsivamente enumerabile. Infatti, è sufficiente
generare uno dopo l’altro tutti i programmi C validi ed avviarli in parallelo; non appena
un programma si ferma, viene stampato. In questo modo, ogni programma che si
ferma (cioè che appartiene a HLT ) viene prima o poi stampato; i programmi che non
appartengono a HLT non si fermeranno mai, quindi non verranno mai stampati.
Ecco una possibile realizzazione pratica della procedura descritta, che fa uso di un
insieme S di stringhe e di un contatore n:
S = ∅;
n = 0;
per ogni stringa s ∈ A∗ {
Incrementa n;
Aggiungi s all’insieme S;
Per ogni stringa p ∈ S {
Esegui p per n secondi;
Se p è terminato prima di n secondi {
Stampa p;
Rimuovi p dall’insieme S;
} } }
Il nucleo del programma è l’istruzione “Esgui p per n secondi” che esegue il programma descritto dalla stringa p per un tempo limitato, interrompendolo nel caso in
cui la sua esecuzioni duri più a lungo del tempo indicato. Supponiamo che un dato
programma q ∈ A∗ termini. Allora, prima o poi arriverà il suo turno di essere inserito nell’insieme S, e prima o poi n diventerà abbastanza grande da permettere il suo
completamento, e verrà stampato.
Se q, viceversa, non termina, la condizione per la stampa non si verificherà mai, per
quanto a lungo si esegua il programma, quindi q non verrà mai stampato.
Si noti che la procedura è estremamente lenta: l’insieme S si “gonfia” ben presto di programmi, molti dei quali non terminanti, e per ciascuno di questi è prevista
l’esecuzione ad ogni iterazione del ciclo esterno.
4.2.1 Un dubbio...
Potremmo chiederci: dato che HLT è ricorsivamente enumerabile, per decidere se un
programma p appartiene a HLT basta verificare se p viene stampato dalla procedura
appena descritta. . .
Dov’è il problema?
11
Scarica