Professional Documents
Culture Documents
4 novembre 2015
1
Ogni parte di questo Work per la quale non sia altrimenti specicato, sia essa fruibile in formato cartaceo o per mezzo
di un ausilio elettronico, e sviluppata dall'Original author , e solo quelle, sono distribuite in accordo con la licenza Creative
Commons Attribution-Noncommercial-No Derivative Works 3.0 Unported.
Indice
I
1
IN AULA
Pensiero computazionale e algoritmi
1.1
1.2
Algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
1.2.2
Programmazione-come-trasformazione-di-congurazioni-attraverso-azioni . . . . . . . . . . . .
1.3
2.2
2.3
1.3.1
1.3.2
. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .
9
9
10
13
13
2.1.1
13
Assegnazione e congurazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
15
16
2.3.1
20
2.3.2
Iterazioni di iterazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
21
2.3.3
Riferimenti bibliograci
22
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
23
3.1
24
3.2
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
26
3.2.2
32
3.2.3
33
3.2.4
34
Il principio di induzione
3.4
3.6
25
. . . . . . . . . . . . . . . . . . . . . . .
3.2.1
3.3
3.5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
35
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.4.1
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
37
3.4.2
43
3.4.3
43
3.4.4
. . . . . . . . . . . . . . . . . . .
45
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
46
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
47
3.6.1
Iterazioni annidate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
49
3.6.2
. . . . . . . . . . . . . . . . . . . .
52
3.6.3
Approfondimenti facoltativi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
52
4.2
4.3
main
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
53
53
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
55
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
59
4.4
Campi statici e
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
63
4.5
67
4.6
Jeliot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
71
final
INDICE
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
73
5.2
74
5.3
76
5.4
76
6.2
6.3
6.4
73
5.1
79
79
6.1.1
Fattoriale
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
79
6.1.2
Quadrato
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
6.2.1
Funzione di McCarty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
6.2.2
Funzione di Ackermann
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
83
6.3.1
Funzione identit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
83
6.3.2
Funzione successore
84
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
85
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
86
6.4.1
Torre di Hanoi
6.4.2
6.4.3
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
88
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
89
Ricorsione di coda
6.6
. . . . . . . . . . . . . . . . . . . . . . .
7.2
7.4
7.4.1
7.5
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
93
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
94
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
Array
91
93
7.1
7.3.1
87
. . . . . . . . . . . . . . . . . . . . . . . . . . . . .
6.5
7.3
81
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
96
97
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
98
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
100
101
7.5.1
Ricerca lineare
101
7.5.2
Inserimenti e cancellazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
101
7.5.3
Filtri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
102
. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
7.6
102
7.7
103
7.8
Matrici bidimensionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .
104
Parte I
IN AULA
Capitolo 1
Saper programmare un computer solo una delle componenti il sapere di un informatico. In particolare, programmare uno dei mattoni che dovrebbero costituire quel che sempre pi insistentemente viene chiamato pensiero
computazionale (computational thinking ).
A prescindere da mode e terminologie che, nel tempo, possano ranare e meglio identicare soggetti e ambiti di cui
intendano trattare, con pensiero computazionale si parla di un insieme di capacit e strumenti che, se di patrimonio
comune, possano incrementare comprensione e gestione della complessa societ in cui viviamo.
In Computational Thinking Janette Wing sviscera, usando diverse prospettive, il concetto di computational
thinking, riferendosi esplicitamente al posto che la programmazione debba occupare nel bagaglio culturale informatico:
Thinking like a computer scientist means more than being able to program a computer. It requires thinking
at multiple levels of abstraction.
La programmazione, quindi, deve essere un mattone, il quale, tuttavia non costituisce l'intera casa (culturale).
La spiegazione di cosa sia il pensiero computazionale, fornita da Wing, sfrutta parole che dicilmente appartengono
al gergo comune. Il primo paragrafo emblematico. Una traduzione letterale ragionevolmente fedele, che tiene conto
dello scopo di questa introduzione, la seguente:
Il pensiero computazionale si basa sulle capacit e le limitazioni dei processi computazionali, indipendentemente dal fatto che essi siano eseguiti da un umano o da un computer. I metodi ed i modelli computazionali
ci danno il coraggio di risolvere problemi e progettare sistemi che nessuno di noi sarebbe in grado di affrontare da solo. . . . A livello pi fondamentale [denire cosa signichi pensiero computazionale] equivale
alla questione: Cosa computabile?. Oggi conosciamo solo parti delle risposte a tale domanda.
Il paragrafo cita disinvoltamente processo, metodi e modelli computazionali e ricorda che alla domanda Cosa
computabile non sappiamo rispondere ecacemente.
Questo corso di programmazione impostato per cominciare a capire cosa si possa intendere quando si parli di
processi computazionali. Tratteremo di processi computazionali attraverso semplici algoritmi, tradotti in un opportuno
linguaggio di programmazione che possa essere interpretato da un calcolatore, il quale usi opportunamente lo spazio
di memoria disponibile e, al termine, produca il risultato voluto.
l'esplicitazione ed il controllo di innumerevoli concetti e tecniche, a diversi livelli di astrazione, che la programmazione
rende evidenti al loro livello di base.
Lo sviluppo e la comprensione di altri aspetti del computational thinking come, pescando a caso da Wing: la comprensione del comportamento umano, l'uso di astrazione e decomposizione per attaccare problemi dicili, l'adottare
euristiche per risolvere un problema, etc. saranno soggetto di ulteriori corsi.
Il nostro focus comprendere cosa siano, come siano esprimibili e quale scopo abbiano gli algoritmi.
1.2
Algoritmo
1.2.1
Sono dati due boccioni (jugs ) utilizzati per i distributori d'acqua, utilizzati negli uci, ad esempio.
primo, che identichiamo con
Il
(piccolo), contiene al
pi 3 litri. Abbiamo a disposizione un rubinetto, da usarsi senza alcuna limitazione, per mettere acqua in
CAPITOLO 1.
g
p
p.
p,
contenente 4 litri, e
o viceversa.
da questo momento, ci accordiamo sul fatto che rappresentiamo ogni istante rilevante nella soluzione a DHP con una
(m, n),
congurazione. Per denizione, ogni congurazione di DHP una coppia di numeri interi
(5, 1),
oppure
(3, 2),
oppure
(1, 3),
altrimenti non sappiamo cosa ciascuna di esse rappresenti. Ci accordiamo nel leggere
La congurazione
(m, n)
come ad esempio
indica
contenente
m5
litri d'acqua, e
(m, n)
contenente
n3
Con la lettura data, la congurazione iniziale, in accordo con la descrizione informale del problema, deve essere
(0, 0),
mentre quella cui vogliamo arrivare, ovvero, la congurazione nale deve essere
(4, 0).
Il testo del problema aerma che possiamo sia immettere acqua nei
boccioni, usando il rubinetto o travasando, sia vuotare i boccioni. Volendo cogliere l'essenza, ovvero, volendo astrarre
da dettagli irrilevanti su cosa signichino nella realt le parole riempire e travasare, indichiamo le operazioni
ammesse per modicare la congurazione disponibile per mezzo della seguente notazione:
col rubinetto,
col rubinetto,
g n p
p m g
n5
litri di
m3
litri di
in
p,
in
g,
g,
p,
Nota 1
fondamentale che ogni azione sia descritta senza ambiguit e termini in un ammontare nito di tempo.
A questo punto, denite congurazioni e azioni su di esse, la speranza che esista un'opportuna sequenza nita di
azioni in grado di trasformare
(0, 0)
in
(4, 0).
(0, 0)
(4, 0):
gp
gp
(0, 0)
(0, 0)
(0, 3)
(5, 0)
p 3 g
g 3 p
(3, 0)
(2, 3)
(3, 3)
(2, 0)
p 2 g
g 2 p
(5, 1)
(0, 2)
(0, 1)
(5, 2)
p 1 g
g 1 p
(1, 0)
(4, 3)
(1, 3)
(4, 0)
p 3 g
(4, 0)
1.3.
Commento 1
Il problema DHP ha catturato l'attenzione di ricercatori, come testimonia l'articolo scientico
Jug measuring:
due boccioni (jugs ), esiste in determinati casi e la sua soluzione richiede una tecnica avanzata di programmazione,
quella dinamica, ed collegata alla soluzione del famoso
1.2.2
Programmazione-come-trasformazione-di-congurazioni-attraverso-azioni
In generale, per programmare basta ssare il numero delle componenti le congurazioni con le quali rappresentare
lo stato di avanzamento di soluzione di un problema, ed avere a disposizione un insieme di azioni che trasformino una
congurazione in un'altra.
Le azioni non sono qualsiasi. Partendo da una delle possibili congurazioni iniziali, iterando la loro applicazione,
esse debbono essere in grado di produrre una delle congurazioni nali in cui compaia il risultato cercato.
Per capirci, chiamiamo programmazione-come-trasformazione-di-congurazioni-attraverso-azioni il metodo di programmazione che adotteremo inizialmente. Quindi, programmare non coincide necessariamente col saper usare qualcuno dei linguaggi di programmazione tipici, attualmente di moda, come, ad esempio,
VisualBasic,
etc. .
con lo scopo di condurre da una congurazione iniziale, la quale contiene i dati di ingresso del problema, ad una
congurazione nale, cui appartiene il risultato.
Solo ad algoritmo individuato, vale la pena di tradurlo in un linguaggio di programmazione anch un calcolatore
possa instancabilmente fornire soluzioni a tutti i casi in cui lo stesso algoritmo possa essere applicato.
Il metodo di individuare la struttura delle congurazioni e di elencare le operazioni per passare da una
congurazione all'altra un approccio generale alla programmazione.
1.3
1.3.1
m, n N,
Supponiamo
m, n
siano
2, 3,
rispettivamente.
ed
n.
ma dovr anche avere la componente utile a contenere il risultato, ovvero un valore scelto tra 2 e 3. In tal caso, la
congurazione iniziale potr, ad esempio, essere (2,3,r ) nella quale possiamo immaginare
valore.
Le azioni ragionevoli diventan due. Una copier la prima componente nella terza, se la prima contiene un valore
non inferiore alla seconda. Altrimenti, verr applicata l'altra azione che copia la seconda componente nella terza. Le
due azioni che possono individuare il massimo in (2,3,r ) ed in realt in ogni
Esercizio 1
(m, n, r)
sono:
(m, n, r) (m, n, m)
se
m n,
per qualsiasi
(m, n, r) (m, n, n)
se
m < n,
per qualsiasi
r .
r)
2 < 3.
m, n, o N. Ipotizzando che le
(m, n, o, r), in cui r rappresenti il risultato voluto, scrivere le azioni che
congurazione (2, 3, 4, 2), supponendo che la congurazione iniziale sia (2, 3, 4, r).
(1.1)
(m, n, o, p) (n, m, o, p)
y
(m, n, o, p) (m, o, n, p)
z
(m, n, o, p) (m, n, p, o)
se
mn
mo
se
nm
no
se
om
on .
10
CAPITOLO 1.
valori alla volta tra quelli disponibili, (ii) il risultato deve essere prodotto senza perdere alcuno dei valori inizialmente
disponibili.
Una possibile soluzione:
(m, n, o, p) (n, m, o, p)
y
(m, n, o, p) (m, o, n, p)
z
(m, n, o, p) (m, n, p, o)
Come esempio, segue una
porzione iniziale
se
m<n
se
n<o
se
o<p .
dello
(1, 3, 2, 5)
x
z
(1, 3, 5, 2)
x
(3, 1, 2, 5)
y
z
Commento 2
MAX2P, MIN3P, MIN3P' mettono in evidenza la necessit di discriminare tra valori in una congurazione. Lo scopo
applicare l'azione appropriata.
Questo un primo puntatore alla utilit di codicare precise strutture linguistiche nei linguaggi di programmazione.
In particolare, ci riferiamo alla selezione, o costrutto
if-then,
1.3.2
N.
n N,
supponiamo di saper
che 3+1 valga 4 e che 3-1 valga 2. Tuttavia, la nostra assunzione ci impedisce di saper dare signicato
diretto a 3+2 o a 3-2, ad esempio. Per calcolare 3+2, occorre spezzare il problema, riconducendolo a
problemi pi semplici che sappiamo risolvere. In questo caso, 3+2 sar ottenuto calcolando l'espressione
(3+1)+1. Analogamente, 3-2 sar ottenuto calcolando (3-1)-1.
n 1, i valori m + n e m n potranno
(. . . (m + 1) + . . . 1) e (. . . (m 1) . . . 1).
| {z }
| {z }
Il problema individuare operazioni su opportune congurazioni le quali, in analogia col DHP, ssati
m, n N qualsiasi,
(m, n)
ed
m + n,
n.
m ed n, possiamo assumere
possa costituire la congurazione iniziale. Questo implica che le azioni in grado di modicare una
(m, n):
(m, n) (m + 1, n 1) .
Ad esempio, applicando iterativamente (1.2) alla congurazione iniziale
(1.2)
(3, 2)
otteniamo:
(1.3)
(m + n, 0):
(m, n),
(m, n) (m + 1, n 1) ((m + 1) + 1, (n 1) 1) . . . (m + n, 0) .
(1.4)
1.3.
Esercizio 2 (
11
per dare signicato diretto a 3-2, ad esempio, occorre spezzare il problema, riconducendolo a (3-1)-1. Rispetto a
SIDP, esiste un ulteriore vincolo. La dierenza tra
ed
m n.
2. SIDP' ha lo stesso enunciato di SIDP. Occorre individuare l'operazione che, partendo da una opportuna congurazione,
contenente
m, n N,
m + n,
incrementi e decrementi di una unit sugli elementi delle congurazioni. L'ulteriore vincolo di SIDP', rispetto a SIDP,
che le congurazioni debbano avere tre componenti, tutte rilevanti per l'ottenimento del risultato. Fornire un paio
di
istanze
3. SIDP ha lo stesso enunciato di SIDP'. Il vincolo nuovamente sulla forma delle congurazioni. Esse devono avere
cinque componenti, due delle quali servano a non distruggere i valori iniziali
un paio di
istanze
Fornire
n m,
somme generiche
n2
incrementi o decrementi di una singola unit ai valori coinvolti nelle congurazioni. Fornire un paio di
istanze
del
iterata
di azioni?
Consideriamo nuovamente la sequenza di congurazioni (1.4). Il motivo per cui abbiamo interrotto l'applicazione delle
azioni che
(m + n, 0)
contiene il risultato.
(m + n, 0)
in (1.4):
. . . (m + n, 0) (m + n + 1, 1) (((m + n) + 1) + 1, 1 1) . . . .
L'osservazione suggerisce che l'azione
(1.5)
vada meglio denita, anch non sia pi applicabile, quando non abbia pi
senso continuare a modicare (riscrivere) la congurazione raggiunta. In particolare, possiamo ridenire (1.2) come
segue:
(m, n) (m + 1, n 1)
se
n 6= 0 .
(1.6)
In generale, quando, programmando per congurazioni e azioni, risulti necessario iterare qualche azione, diventa
indispensabile aggiungere condizioni che, ad un certo punto, impediscano l'iterazione, concludendo il processo di
calcolo.
Esercizio 3 Quando necessario, completare le azioni usate per la soluzione ai problemi dell'Esercizio 2 con le condizioni
di terminazione.
(3, 2),
12
CAPITOLO 1.
Se
(m, n)
m + n.
vale per ogni congurazione di SIDP, le diamo il nome di propriet invariante. Il fatto che si senta la necessit di
assegnare un nome specico ad un concetto deve essere preso come testimonianza della rilevanza del concetto stesso.
Torneremo pesantemente sull'argomento.
Per ora aggiungiamo che, se le soluzioni a SIDP', MSDP, MSDP' sono corrette, allora anche per essi esiste una
propriet invariante.
Tuttavia, pi in generale , vedremo che l'esistenza di propriet invarianti una caratteristica di qualsiasi algoritmo,
e programma associato.
Osservazioni nali
Abbiamo fornito un insieme di strumenti formali per descrivere almeno una sequenza di operazioni in grado di
trasformare la congurazione iniziale in quella nale. Nell'ambito in cui ci muoviamo, essere formale signica
essenzialmente che gli strumenti non sono ambigui:
La Sezione 1.3.2 Algoritmi del testo di riferimento [SM14, Capitolo 1] parla di algoritmi. Vale la pena chiedersi
se, alla ne, entrambe le presentazioni parlino dello stesso concetto.
attraverso l'illustrazione del signicato di parole chiave come: action, eect, object, change of state, language,
process or computation, sequential, processor, variabile, assegnamento a variabile, choice of notation pattern of
behaviour. Il metodo seguito richiama il nostro, ma scende ad un livello di dettaglio che noi raggiungeremo solo
pi in l e che esplicita diversi degli aspetti che Wing, in Computational Thinking sottintende riferendosi a
metodi e strumenti del computational thinking.
Come diversi altri articoli che, periodicamente, vengono pubblicati What are the best programming languages
to learn today? raorza quanto sostenuto di Wing, circoscrivendo il discorso alla scelta di linguaggi che valga la
pena di conoscere. Fin dall'inizio, si noter come l'accento sia posto su concetti che i linguaggi di programmazione
devono permettere di gestire ed esprimere, non sulla tecnologia in s del linguaggio di programmazione.
Capitolo 2
2.1
2.1.1
Assegnazione e congurazioni
(m0 , m1 , . . . , mn ) .
(2.1)
Per rappresentare (2.1) in un programma imperativo suciente immaginar d'assegnare un nome ad ogni posizione
della tupla generica (2.1):
x0
m0 ,
x1
m1 ,
xn
mn
...
...,
La struttura linguistica che, nei linguaggi di programmazione imperativi, ssi la corrispondenza nome/posizione
in una congurazione l'assegnazione. In particolare, la sequenza di assegnazioni :
xZero = m0 ;
xUno = m1 ;
(2.2)
...
xEnne = mn ;
costruisce (2.1). La sequenza (2.2) contiene un'assegnazione per riga. Gli aspetti essenziali di ogni assegnazione sono:
xZero, xUno,
Ciascuno tra
Il simbolo = detto di assegnazione ed indica proprio che il valore alla sua destra associato alla variabile alla
sua sinistra.
A destra del simbolo di assegnazione pu comparire una qualsiasi espressione che, valutata, possa fornire un
valore, mentre a sinistra pu comparire solo il nome di una variabile.
xZero = 25;
(2.3)
xUno = xUno + 1;
(2.4)
14
CAPITOLO 2.
xZero = m0 ;
xUno = m1 ;
...
(2.5)
xEnne = mn ;
xZero = 25;
xUno = xUno + 1; .
Al termine di
(2.5)
c' il valore 25 che pu, o meno, dierire dal numero
m0 ,
in
xZero
in
xUno c' il valore che otteniamo prendendo il valore inizialmente in xUno, ovvero m1 , e sommandogli il valore
xUno contiene il numero m1 + 1.
1; quindi, al termine,
Riassumendo, il blocco di assegnazioni (2.2) e le due assegnazioni (2.3), (2.4) hanno il seguente eetto globale sulle
congurazioni che stiamo manipolando:
(2.6)
...
xEnne = mn ;
(m0 , m1 , . . . , mn )
xZero = 25;
(25, m1 , . . . , mn )
xUno = xUno + 1;
(25, m1 + 1, . . . , mn ) ,
assumendo che
(?, ?, . . . , ?)
indichi la congurazione in cui ciascun valore nelle varie posizioni sia ancora indenito.
(m, n) di valori
(n, m).
(m, n) (n, m) .
(2.7)
L'interpretazione di una congurazione per mezzo di assegnazioni non permette di denire un'azione che scambi le
componenti della congurazione di partenza in maniera altrettanto diretta quanto in (2.7).
Chiamiamo
in
occorrer,
b = a; .
Tuttavia, non appena interpretassimo tale assegnazione otterremmo la congurazione
la possibilit di spostare
in
a.
In generale, manipolare congurazioni con assegnazioni implica una maggiore sequenzialit nella descrizione di quel che c' da fare per passare da una congurazione all'altra.
2.2.
15
Nel caso specico, pur essendo necessarie solo due componenti, esse non sono sucienti, usando le assegnazioni
per risolvere EXC2P. Serve una terza componente
tmp,
di assegnazioni:
(2.8)
b = tmp;
// (n, m, m) in cui la posizione pi a destra ormai inutile
La presenza di
m in posizione tmp, ovvero nella variabile di uso temporaneo tmp, non incia la qualit della soluzione
perch quest'ultima costituita dalle prime due componenti. Quel che segue:
a =
b =
tmp
a =
b =
0;
1;
= a;
b;
tmp;
un algoritmo che realizza lo scambio di valori tra due variabili, usandone una terza intermedia.
2.2
Riprendiamo MAX2P della Sottosezione 1.3.1. Per riformulare la sua soluzione in termini di costrutti linguistici che
appartengano a tipici linguaggi di programmazione, occorre introdurre il costrutto selezione, indicato anche come
if-then-else (se-allora-altrimenti).
Nello specico, assumendo che la variabile a
contenga il valore
e la variabile
contenga
n,
possiamo riscrivere
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
// (m, n, ?)
(a > b) then
// (m, n, ?)
max = a;
// (m, n, m)
if
else
// (m, n, ?)
max = b;
// (m, n, n)
end if
il comando di selezione, che comincia con la parola riservata if , confronta i valori associati ad
dell'espressione
a e b,
per mezzo
a > b:
se il risultato di
l'assegnazione
Altrimenti, se il risultato di
interpreta l'assegnazione
in
, allora si
All'ingresso di ciascun ramo la congurazione non ancora cambiata perch non sono state interpretate assegnazioni.
max
se abbiam seguito il
else.
Massimo.java
tra i contenuti di
un programma Java che rana l'Algoritmo 1 distinguendo i tre casi possibili del confronto
16
CAPITOLO 2.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
// (m, n, ?)
(a > b) then
// (m, n, ?)
max = a;
// (m, n, m)
if
end if
if (a
<= b) then
// (m, n, ?)
max = b;
// (m, n, n)
end if
L'Algoritmo 2 un ulteriore algoritmo per MAX2P. Esso sfrutta il fatto che sia ammesso denire selezioni
(se-allora) in cui si esprima il solo ramo
parola chiave
if
then,
if-then
vera.
m, n, o, p.
a, b,
L'esercizio consiste nello scrivere un algoritmo che risolva MAX4P sia in termini di congurazioni/azioni, sia in termini
di assegnazioni/selezioni.
Esistono vari approcci. Di seguito ne suggeriamo sommariamente un paio:
La prima strategia di soluzione cos descritta: (i) la prima azione confronta il contenuto di
b.
contenuto di
con quello di
c.
e di
e di
b;
con quello di
d.
con quello di
e di
c;
(iii)
d.
La seconda strategia di soluzione assume l'uso di congurazioni con 5 elementi dei quali l'ultimo
cos descritta: (i) la prima azione assegna il contenuto di
di
con quello di
max.
max.
con quello di
max.
copiato in
Essa
copiato in
max.
(iii) La terza
max.
a a max.
con quello di
max.
Se il primo supera il
max.
Siano dati 4 numeri naturali qualsiasi, memorizzati un una congurazione di quattro posizioni
a, b, c, d.
BS4P
il problema che, presa una quadrupla data, restituisce una quadrupla in cui i valori sono stati ordinati in ordine non
decrescente leggendo
a, b, c, d
Riprendere la soluzione al problema SIDP, fornita attraverso la riscrittura di congurazioni, e tradurla in un algoritmo
che sfrutti i costrutti sequenza e selezione della programmazione strutturata.
(SIDPSequenzaSelezione.java una classe Java che propone una soluzione approssimata del problema SIDP,
sfruttando solo sequenza e selezione.)
2.3
Riprendiamo il problema SIDP nella Sottosezione 1.3.2. Lo abbiamo risolto in due passi:
2.3.
17
Conf. prima di 3
Conf. dopo 4
(2, 3)
(3, 2)
(4, 1)
(3, 3)
(4, 2)
(5, 1)
(3, 2)
(4, 1)
(5, 0)
1
2
e 3 in
b.
Abbiamo anche osservato che, ad un certo punto, ovvero quando la congurazione nale contenga la soluzione cercata,
sia necessario terminare l'iterazione di (1.2).
persa.
L'azione (1.2), completata con una condizione di applicabilit che ne eviti una possibile iterazione innita,
diventa:
(m, n) (m + 1, n 1)
se
n>0 ,
(2.9)
oppure
(m, n) (m + 1, n 1)
>0 .
(2, 3)
(2.10)
genera la sequenza:
(2.11)
L'ultima congurazione non pu pi essere riscritta proprio perch la condizione di applicabilit della regola falsa.
Riformulare la soluzione (2.9) di SIDP attraverso costrutti linguistici, tipici di linguaggi di programmazione, richiede
l'introduzione di almeno un costrutto iterativo.
Optiamo per il costrutto
while-do (mentre-fai).
1:
2:
3:
4:
5:
6:
7:
a = 2;
b = 3;
// (2, 3)
while (b > 0) do
a = a + 1;
b = b - 1;
end while
Nello specico, assumiamo che la variabile
contenga il valore
e la variabile
il valore
3.
controllata di una tra (2.9) e (2.10) viene espresso come nell'Algoritmo 3, che va letto come segue:
Se il risultato di
b > 0
b,
true
col valore
0,
il valore in
b > 0:
strettamente maggiore di
, allora:
a = a + 1;,
Altrimenti, se il risultato di
quindi
b = b - 1;.
while e . . .
b > 0
false
b > 0,
il valore in
while.
end while. Si dice che si esce dal corpo del while.
Osserviamo che, la lettura appena proposta, coincide con la traduzione di (2.10), piuttosto che di (2.9), siccome
usiamo l'espressione
b > 0
e non
n > 0,
Nel caso in questione, l'evolversi delle congurazioni rappresentato dalla Tabella 2.1.
Il valore 0 nella colonna N.ro di cicli gi percorsi indica che non tutte le istruzioni che costituiscono il corpo
while sono gi state interpretate. Il valore 1 nella colonna N.ro di cicli gi percorsi indica che la sequenza
18
CAPITOLO 2.
La Tabella 2.1 non contiene ulteriori righe perch, una volta raggiunta la congurazione
(5,0)
vale 0. Quindi,
vale
b > 0
false
SommaSuccessoreIterato.java
Esercizio 5
signica che
a,
(5, 0)
b
in
elevato ad
b,
supponendo per di
saper solo calcolare il prodotto tra due numeri ed il predecessore di un numero. La soluzione deve prima essere espressa
in termini di congurazioni ed azioni su di esse.
selezioni o iterazioni.
d,
n=7
ed il resto
d = 2,
7/2
d.
q =3
e resto
r = 1.
Un
algoritmo utilizzabile quello che, alle scuole elementari, viene utilizzato per illustrare il meccanismo della divisione.
7 steccoline
2 destinatari,
senza creare dierenze, ovvero, non assegnando steccoline rimanenti se non ne esistono abbastanza per
Steccoline
Steccoline
Steccoline
|1 |2 |3 |4 |5 |6 |7
|3 |4 |5 |6 |7
|5 |6 |7
|7
Dest. 1
Dest. 2
|1
|2
|1 |3
|2 |4
|1 |3 |5
|2 |4 |6
Dest. 1
Dest. 2
Dest. 1
Dest. 2
Dest. 1
Dest. 2
Da sinistra, inizialmente, nulla stato distribuito. Quindi, si distribuiscono due steccoline, una per destinazione.
Si ripete l'operazione nch non rimane una singola steccolina che non pi equamente distribuibile.
Il numero
Esse
devono contenere sia numeratore e denominatore iniziali, sia le posizioni per tener conto del numero di distribuzioni
intere eettuate e dell'eventuale resto. Inizialmente, non abbiamo eettuato alcuna distribuzione, e tutte le steccoline
disponibili rappresentano il resto, ovvero quanto rimane da distribuire. La congurazione iniziale pu quindi essere:
(7, 2, 0, 7)
(2.12)
in cui la prima posizione il numeratore, la seconda il denominatore, la terza i quoziente e la quarta il resto.
L'azione deve premettere di contare il numero di distribuzioni eettuate, nel contempo distribuendo quanto
distribuibile, ovvero:
(n, d, q, n) (n, d, q + 1, n d) .
(2.13)
Tuttavia, (2.13) non completa, perch pu essere applicata anche quando il resto non ha abbastanza steccoline
da distribuire. Va completata con la condizione che arresta l'iterazione di (2.13):
q/r
(n, d, q, n) (n, d, q + 1, n d)
(2.14)
2.3.
19
Conf. prima di 5
Conf. dopo 6
(7, 2, 0, 7 )
(7, 2, 1, 5 )
(7, 2, 2, 3 )
(7, 2, 1, 7 )
(7, 2, 2, 5 )
(7, 2, 3, 3 )
(7, 2, 1, 5 )
(7, 2, 2, 3 )
(7, 2, 3, 1 )
1
2
e 2 in
b.
q/r
q/r
q/r
(2.15)
Il prossimo passo consiste nel descrivere sia le congurazioni di QRSP, sia l'azione da iterare per mezzo di assegnazioni. Le assegnazioni che realizzano l'azione dovranno comparire nel corpo di una opportuna iterazione, che ne
interrompa l'applicazione al momento giusto.
1: q = 0;
2: r = n;
3: // (n, d, 0, n)
4: while (r >= d) do
5:
q = q + 1;
6:
r = r - d;
7: end while
Nello specico, supponiamo
col valore
del numeratore e
del denominatore. L'Algoritmo (4) costruisce le congurazioni di QRSP trasformandole attraverso una
n,
pi le assegnazioni
q = 0;
creano la congurazione
r = n;
L'espressione di terminazione
r >= d
r non
Se il risultato di
r >= d
true
d.
Quindi:
d;
, allora:
q = q + 1;,
quindi
r = r -
while e . . .
Altrimenti, se il risultato di
corpo del while.
r >= d
false
il valore in
r >= d,
argomento di
minore di quello in
while.
while sono gi state interpretate. Il valore 1 nella colonna N.ro di cicli gi percorsi indica che la sequenza
di tutte le istruzioni nel corpo del while stata interpretata esattamente 1 volta, non essendo mai ripartiti
dalla parola chiave while. Il valore 2 nella colonna N.ro di cicli gi percorsi indica che la sequenza di tutte le
istruzioni nel corpo del while stata interpretata esattamente 2 volte, dopo essere ripartiti dalla parola chiave
while al termine del primo percorrimento. Il valore 3 nella colonna N.ro di cicli gi percorsi indica che la
sequenza di tutte le istruzioni nel corpo del while stata interpretata esattamente 3 volte, essendo ripartiti
dalla parola chiave while al termine sia del primo, sia del secondo percorrimento. E cos via.
(7, 2, 3, 1 ) signica
r >= d vale false e si deve proseguire dalla prima istruzione
La Tabella 2.2 non contiene ulteriori righe perch, una volta raggiunta la congurazione
che il valore in
che segue
r inferiore a quello in d.
Quindi,
end while. Siccome nessuna istruzione esiste al di sotto di end while la congurazione consegnataci
dall'Algoritmo 4
quarta, ovvero
r,
(7, 2, 3, 1 )
q,
contiene il quoziente e la
per il valore in
n.
20
CAPITOLO 2.
Esercizio 6 Risolvere ciascuno dei seguenti esercizi in due passi: (i) impostando la loro progettazione come azioni da
applicare a congurazioni opportune, (ii) traducendo la soluzione al punto (i) in algoritmi che impieghino assegnazioni,
selezioni, o iterazioni:
1. PDSP che dato un numero intero
m,
2. PDMP che determina se il valore assegnato ad una variabile sia pari o dispari, sfruttando l'operatore modulo `%' che
si comporta come segue:
n%2
vale 0 se
n,
calcoli la somma dei primi numeri naturali, senza sfruttare la nota formula
n(n+1)
.
2
operazioni di base: decremento di una unit, risposta positiva o negativa alla domande n
0?.
2.3.1
n = 0,
n2
semplicemente usando
n n.
Il vincolo espresso sulle operazioni utilizzabili forza all'uso di una strada alternativa. Una possibile la seguente:
(n + 1)2 = (n + 1) (n + 1) = n2 + 2 n + 1 .
Possiamo convincerci non dimostrare!
Supponiamo
n + 1 = 3.
(2.16)
Allora:
32 = (2 + 1)2
= 22 + 2 2 + 1 .
Ma
coincide con
(1 + 1)
(2.17)
(1 + 1)2 = 12 + 2 1 + 1
=4 .
(2.18)
Quindi (2.17) produce eettivamente il risultato atteso, col processo di calcolo seguente:
32 = (2 + 1)2
= 22 + 2 2 + 1
(usando
(2.18))
=4+4+1
=9 .
Se generalizziamo il calcolo sviluppato dall'esempio, otteniamo quanto segue:
(n + 1)2 = n2 + 2 n + 1
= ((n 1) + 1)2 + 2 n + 1
= ((n 1)2 + 2 (n 1) + 1) + 2 n + 1
= (((n 2) + 1)2 + 2 (n 1) + 1) + 2 n + 1
= (((n 2)2 + 2 (n 2) + 1) + 2 (n 1) + 1) + 2 n + 1
= ...
= (n n)2 + (2 (n n) + 1) + . . . + (2 (n 1) + 1) + (2 n + 1)
n
X
=0+
(2 (n i) + 1)
i=0
n
X
(2 (n i)) +
i=0
= (n + 1) + 2
n
X
i=0
n
X
(n i)
i=0
= (n + 1) + 2
n1
X
i=0
(n i) .
(2.19)
2.3.
21
Conf. prima di 5
Conf. dopo 6
(3, 1, 0)
(3, 2, 2)
(3, 1, 2)
(3, 2, 3)
(3, 2, 2)
(3, 3, 3)
La sommatoria
Pn1
i=0
(n i)
n,
ovvero con
n = 3.
n, n 1, n 2, . . . , 1
in una
opportuna variabile. Nell'ultimo punto dell'Esercizio 6 abbiamo gi visto come si accumuli un valore in una variabile,
che possiamo chiamare
s.
n + (n 1) + (n 2) + . . . + 1
n, ottenendo n2 in s.
si trovi in
s,
1:
2:
3:
4:
5:
6:
7:
8:
s = 0;
i = 0;
// (n, 1, 0)
while (i < n ) do
s = s + (n - i);
i = i + 1;
end while
//
9: //
10: s
11: //
il quadrato, la seconda ad
e la terza ad
n. La linea
n, col valore n di cui calcolare
s.
Il valore 0 nella colonna N.ro di cicli gi percorsi indica che non tutte le istruzioni che costituiscono il corpo
while sono gi state interpretate. Il valore 1 nella colonna N.ro di cicli gi percorsi indica che la sequenza di tutte le
istruzioni nel corpo del while stata interpretata esattamente 1 volta, non essendo mai ripartiti dalla parola chiave
while.
Nel caso in esame, la seconda componente nella congurazione contiene valore 3 che rende falsa la condizione
< 3.
(3, 3, 9),
Nota 2
La potenziale rilevanza della strategia di soluzione per QPNP sta nel fatto che utilizzi operazioni le quali, viste al
livello della
per 2 equivale ad uno spostamento verso sinistra di tutti i bit, operazione molto veloce. Sottrazioni di valori crescenti
di una singola unit da uno stesso valore, sono ottimizzabili semplicemente. Ma queste sono proprio le operazioni
che abbiamo individuato come necessarie per il calcolo di
2.3.2
n2 .
Iterazioni di iterazioni
Vale la pena osservare che esistono problemi i quali possano richiedere l'annidamento di iterazioni, esattamente come
altri possano richiedere l'annidamento di selezioni, come nelle soluzioni da dare ai problemi dell'Esercizio 4.
Un problema da risolvere annidando un paio di iterazioni MSDP', assegnato nell'Esercizio 2.
Una possibile soluzione a MSDP' in termini di congurazioni e azioni la seguente:
Le congurazioni sono quadruple in cui la prima e la seconda componente, che possiamo chiamare
rispettivamente, contengano i valori
ed
ed
n,
n.
La quarta componente serve ad accumulare il risultato che, via via, approssima il valore nale da calcolare,
ovvero
m n.
Il signicato della terza componente sar pi chiaro fra poco, una volta introdotte le azioni.
(m, n, i, r) (m, n 1, m, r)
b
(m, n, i, r) (m, n, i 1, r + 1)
se
se
n>0
n0
i=0
(2.20)
i>0 .
(2.21)
22
CAPITOLO 2.
alla quarta componente, avendo lo scopo nale di sommarle una volta in pi il valore
Possiamo sperimentare non dimostrare la veridicit della precedente aermazione, con un esempio.
(3, 2, 0, 0).
n=0
i = 0,
(2.22)
(3, 0, 0, 6).
volte, la terza componente, servita ad incrementare la quarta tante volte quanto indica la prima componente.
Una possibile traduzione, in termini di assegnazioni ed iterazioni, delle congurazioni e delle azioni proposte
come soluzione a MSDP' il seguente Algoritmo 6 nel quale l'aspetto rilevante l'annidamento di due iterazioni:
L'Algoritmo 6 commentato con le congurazioni iniziale e nale che vediamo nella sequenza (2.22).
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
r = 0;
i = 0;
// (3, 2, 0, 0)
while (n > 0 && i = 0) do
n = n - 1;
i = m;
while (n >= 0 && i > 0) do
i = i - 1;
r = r + 1;
end while
end while
// (3, 0, 0, 6)
La Tabella 2.4 raccoglie le congurazioni via via costruite dall'Algoritmo 6 sotto l'ipotesi che la variabile
il valore 3 e la variabile
Conf. prima di 5
Conf. dopo 6
(3, 2, 0, 0)
(3, 1, 3, 0)
0
1
2
m contenga
contenga il valore 2.
(3, 1, 0, 3)
(3, 0, 3, 3)
0
1
2
Conf. prima di 8
(3,
(3,
(3,
(3,
(3,
(3,
1,
1,
1,
0,
0,
0,
3,
2,
1,
3,
2,
1,
0)
1)
2)
3)
4)
5)
e 2 in
n,
Conf. dopo 9
(3,
(3,
(3,
(3,
(3,
(3,
1,
1,
1,
0,
0,
0,
2,
1,
0,
2,
1,
0,
1)
2)
3)
4)
5)
6)
ovvero assumendo
m = 3, n = 2.
possibile rileggere il modo in cui la Tabella 2.4 sia costruita come segue:
Il corpo del ciclo pi esterno contiene una sequenza di due assegnazioni ed una iterazione. Eseguire il corpo del
ciclo pi esterno signica interpretare le due assegnazioni ed interpretare tante volte quanto necessario il corpo
del ciclo interno.
i,
2.3.3
Riferimenti bibliograci
Vari capitoli de [SM14] coprono gli argomenti trattati da questa parte del programma didattico, non necessariamente
nello stesso ordine sviluppato a lezione: [SM14, Capitolo 1], Sezioni 1.3.2, 1.3.3, 1.3.4, [SM14, Capitolo 2], Sezione 2.1,
[SM14, Capitolo 3], Sezione 3.1, [SM14, Capitolo 4], Sezioni 4.1, 4.2.
Capitolo 3
naturali, usando iterativamente incrementi e decrementi di opportune variabili. L'Algoritmo 7 quello gi visto nella
Sottosezione 1.3.2
1:
2:
3:
4:
5:
6:
// a contiene m N
// b contiene n N
while (b > 0) do
a = a + 1;
b = b - 1;
end while
Cosa succede se, al posto dell'Algoritmo 7, per un qualche motivo, ad esempio un banale errore di battitura,
1:
2:
3:
4:
5:
6:
// a contiene m N
// b contiene n N
while (b >= 0) do
a = a + 1;
b = b - 1;
end while
La dierenza tra i due algoritmi sta nell'espressione usata come argomento della parola chiave
while.
Una possibile ovvia strategia per rispondere alla domanda appena posta consiste nell'eseguire il testing dell'Algoritmo 8, ovvero consiste nel simularne il comportamento, usando valori signicativi di
Esempio 3 (
Testing
valga
valga
1.
Ovvero
e
b.
contenga
ed
per
contenga
b.
2.
Possiamo
a,
b?
Siccome abbiamo eseguito il testing anche per l'Algoritmo 3, che coincide con l'Algoritmo 7, possiamo porre su di esso
la domanda complementare:
L'Algoritmo 7 fornisce il valore corretto in
Per il semplice fatto che l'insieme
a,
b?
esaustivo n per gli Algoritmi 7 e 8, n per altri che abbiamo scritto sin qui, n per un'innit d'altri algoritmi ben
pi interessanti che scriveremo.
A,
per
24
CAPITOLO 3.
Il capitolo sviluppa gli strumenti concettuali e formali per rispondere positivamente, ovvero per parlare di dimo-
3.1
Vale la pena di cominciare citando un esempio del febbraio 2015. Proving that Android's, Java's and Python's sorting
algorithm is broken (and showing how to x it) un articolo il cui succo si evince estrapolando alcune righe dei
paragra iniziali:
Tim Peters developed the Timsort hybrid sorting algorithm in 2002. It is a clever combination of ideas from
merge sort and insertion sort, and designed to perform well on real world data. TimSort was rst developed
for Python, but later ported to Java (where it appears as java.util.Collections.sort and java.util.Arrays.sort)
[. . . ]. TimSort is today used as the default sorting algorithm for Android SDK, Sun's JDK and OpenJDK.
[. . . ]
[. . . ] Unfortunately,
we weren't able to prove its correctness. A closer analysis showed that this was,
TimSort was broken and our theoretical considerations nally led us to a path
towards nding the bug (interestingly, that bug appears already in the Python implementation). [. . . ]
Con metodo formali basati sull'individuazione di propriet invarianti che le congurazioni manipolate da un algoritmo devono soddisfare, si scoperto che l'algoritmo standard di ordinamento di sistemi diusissimi conteneva un errore.
In particolare, l'analisi formale delle propriet stata condotta usando un l'ambiente di sviluppo Key, descritto come
Integrated Deductive Software Design. In Key, ad esempio, sono stati sviluppati e vericati semi-automaticamente
sistemi per transazioni di pagamento elettronico, basati su carte con chip, sulle quali risiede software Java. La seguente
gura:
illustra una piccola porzione di codice, in cui sono evidenti descrizioni formali, basate sul linguaggio
Language,
Java Modeling
di propriet che i campi delle classi usate devono soddisfare e che possono essere vericate formalmente
3.1.
25
di cui
Dafny
{
var a: int := m;
var b: int := n;
while (b > 0)
invariant m + n == a + b;
invariant 0 <= b && b <= n;
{
a := a + 1;
b := b - 1;
}
assert m + n == a && b == 0;
3
5
7
9
11
Dafny
Osservando la loro struttura, e cancellando idealmente quel che non conosciamo, nell'Algoritmo 9 possiamo
riconoscere l'Algoritmo 7 e nell'Algoritmo 10 ritroviamo l'Algoritmo 8.
Eseguiamo due esperimenti.
Il primo consiste nel copiare il codice dell'Algoritmo 9 nella nestra apposita della pagina
Microsoft.
Dafny@rise4fun from
Un click sul tasto a sfondo viola produrr, ad un certo punto, un messaggio che comunica l'avvenuta
verica senza errori. Lo stesso procedimento, ma con l'Algoritmo 10 segnaler, in maniera un po' criptica, che il loop
invariant non pu descrivere il comportamento del loop, ovvero del ciclo descritto dal costrutto iterativo.
Intuitivamente, la sorgente d'errore sta nella dierente espressione argomento del
while.
dell'errore scovata automaticamente. Il motivo la presenza dei seguenti elementi sintattici, che chiamiamo direttive :
invariant m + n == a + b;
(3.1)
(3.2)
assert m + n == a && b == 0;
(3.3)
m, n, a
l'assegnazione iniziale nel corpo dell'iterazione venga interpretata, sia appena l'ultima assegnazione nale del corpo
dell'iterazione venga eseguita. In particolare:
ed
ed
n,
estremi inclusi.
Attraverso un'interpretazione passo passo del codice, sperimentiamo quanto appena aermato, riguardo a (3.1) e
(3.2).
3.1.1
26
CAPITOLO 3.
b > 0
sia vera ed usando ancora una volta le tuple per descrivere a colpo d'occhio come i valori evolvano. Riguardo
alle tuple, ci accordiamo sul fatto che la prima posizione contenga il valore di
ed, inne, la quarta di
b.
m,
la seconda quello di
n,
la terza di
m, n, a
m+n==a+b.
Analogamente,
sempre contenuta
nell'intervallo indicato.
ed
m, n, a
vale indipen-
n.
Il nostro obiettivo imparare a descrivere le relazioni tra i valori delle variabili, o delle congurazioni, che
rimangano vere man mano che l'interpretazione del corpo delle iterazioni procede. Tali relazioni sono dette
Esercizio 7
Copiare
Usando opportunamente le congurazioni, vericare che all'inizio ed alla ne di ogni ciclo la relazione tra
3.2
n, d, q
ed
invariant n == (d * q + r);.
ed
n;
2. descrivere come il metodo adottato al precedente punto 1 sia ragionevolmente generale per essere
applicato a tipici algoritmi iterativi.
3.2.1
Abbiamo lo scopo di dimostrare che il predicato invariante in 3.1.1 sia indipendente dai valori inizialmente assegnati a
m
m
ed
ed
n. Quindi, vogliamo che lo stesso predicato m+n==a+b sia vero prima e dopo
n sono volutamente non specicati, perch m+n==a+b deve essere vero prima
3.2.
27
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
valori generici di
ed
n.
essere vero sia alla linea 6, sia alla linea 9, anche se pu sembrare singolare che i valori in
passando, dalla linea 6 alla linea 9, pur avendo che il predicato
Intuitivamente, la verit del predicato
m+n == a+b
possano cambiare,
m+n == a+b rimane immutata perch a e b, alla ne, sono una incrementata
e l'altra decrementata della stessa quantit. Formalmente, quel che avviene spiegato dettagliatamente nei commenti
del seguente algoritmo:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
24:
25:
26:
27:
28:
29:
30:
31:
32:
33:
34:
35:
predicato a linea 32, dovrebbe essere naturale dedurre quale sia la conseguenza di sapere che
al termine di una qualsiasi iterazione.
Deduciamo assieme le conseguenze.
28
CAPITOLO 3.
Siccome
m+n == a+b
vero al termine di ogni iterazione vero anche quando il corpo dell'iterazione non pu
in
m+n == a+b,
ottenendo
termine dell'iterazione,
Il procedimento seguito generale perch indipendente da un qualsiasi valore specico ssato per
m ed n.
per questo motivo che la dimostrazione di correttezza (parziale) migliore ed indiscutibilmente pi solida
di un semplice testing per vericare che un algoritmo funzioni a dovere.
ziale di SIDP.
(a) Scambiare le istruzioni 7 e 8 nell'Algoritmo 11 e dimostrarne la correttezza parziale.
(b) Dimostrare la correttezza parziale de:
1:
2:
3:
4:
5:
6:
7:
8:
end while
1:
2:
3:
4:
5:
6:
7:
8:
end while
non corretto.
2. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema MSIP che, dati due numeri naturali
n,
per
n,
m*n
come somma di
iterata per
volte.
m*n == |m+...+m
{z } .
n volte
ris == |m+...+m
{z } ,
n-k volte
assumendo che
vari tra
m.
0.
ris accumula
ris:
1: // m contiene un valore di N
2: // n contiene un valore di N
3: ris = 0;
4: k = n;
5: // facilmente verificabile che m*n==ris+m*k vero.
6: while (k > 0) do
7:
/* Supponiamo che m*n==ris+m*k sia vero.
8:
Allora vero anche m*n==(ris+m)+m*(k-1) che otteniamo dal
9:
precedente, attraverso i seguenti passaggi:
10:
m*n==ris+0+m*k==ris+(m-m)+m*k==(ris+m)-m+m*k==(ris+m)+m*(k-1).
via via
3.2.
29
11:
*/
12:
ris = ris + m;
13:
/* Lassegnazione ris = ris + m d nome ris al valore,
14:
inizialmente presente in ris prima dellassegnazione,
15:
ma incrementato di x.
16:
Siccome in m*n==(ris+m)+m*(k-1) compare
17:
lespressione ris+m, e ris un nuovo nome per
18:
essa, possiamo rimpiazzare ris+m con ris in
19:
m*n==(ris+m)+m*(k-1). Otteniamo cos un nuovo
20:
predicato vero m*n==ris+m*(k-1) in questo punto del programma.
21:
/
*
22:
k = k - 1;
23:
Lassegnazione k=k-1, d il nome k al valore,
24:
presente in k prima dellassegnazione,
25:
ma decrementato di 1.
26:
Siccome in m*n==ris+m*(k-1) compare
27:
lespressione k-1, e k un nuovo nome per
28:
essa, possiamo rimpiazzare k-1 con k in
29:
m*n==ris+m*(k-1). Otteniamo cos un nuovo
30:
predicato vero m*n==ris+m*k. Il nuovo predicato
31:
coincide con quello iniziale m*n==ris+m*k.
32:
Proprio per il fatto che il predicato appena
33:
ottenuto sia identico a quello di partenza.
34:
alla linea 7, possiamo affermare che
35:
m*n==ris+m*k sia invariante, ovvero che la sua
36:
forma sintattica non cambi da inizio a fine ciclo.
37:
*/
38: end while
39: /* Per arrivare in questo punto del programma, occorre interrompere
40:
literazione. Questo succede se k==0. Inoltre, nessuna istruzione
41:
che modifichi il valore in ris viene interpretata dopo lultima
42:
assegnazione k=k-1 nel corpo delliterazione. Quindi il predicato
43:
invariante m*n==ris+m*k vero. Sostituendo 0 a k in m*n==ris+m*k
44:
otteniamo m*n==ris+m*0==ris. Ovvero, al termine delliterazione,
45:
il valore di ris pari al prodotto di m ed n, ottenuto per somme
46:
successive, ed indipendentemente dai valori iniziali di m ed n.
47: */
Prendendo spunto dalla dimostrazione di correttezza parziale per riscrittura di MSIP, risolvere i seguenti esercizi:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: k = 0;
5: while (k < n) do
6:
ris = ris + m;
7:
k = k + 1;
8: end while
di
di
N
N
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: k = 0;
5: while (k <= n) do
6:
ris = ris + m;
7:
k = k + 1;
8: end while
di
di
N
N
ris == m*n
30
CAPITOLO 3.
3. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema PMIP che, dati due numeri naturali
n,
mn
come moltiplicazione di
iterata per
volte.
n,
mn == |m*...
{z *m} .
n volte
mn == ris |*m*...
{z *m}
k volte
ris == |m*...
{z *m} ,
n-k volte
assumendo che
vari tra
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
//
e 0.
m. Il
di N
di N
ris
ris:
m contiene un valore
n contiene un valore
ris = 1;
k = n;
/* mn == ris * mk vero, per sostituzione di valori.
*/
while (k > 0) do
/* Suppongo mn == ris * mk vero, riscrivibile come
mn == (ris * m) * m(k - 1)
*/
ris = ris * m;
/* mn == ris * m(k - 1) vero.
*/
k = k - 1;
/* mn == ris * mk vero ed identico al predicato iniziale.
*/
//
end while
/* mn == ris * mk && k = 0
implica
mn == ris * m0 == ris * 1 == ris, ovvero
ris contiene il risultato, indipendentemente dai
valori iniziali di m ed n.
*/
Prendendo spunto dalla precedente dimostrazione di correttezza parziale di PMIP, risolvere i seguenti esercizi:
(a) Scambiare le istruzioni 11 e 14 e dimostrare la correttezza parziale.
(b) Dimostrare la correttezza parziale del seguente algoritmo alternativo che risolve PMIP:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 1;
4: k = 0;
5: while (k < n) do
6:
ris = ris * x;
7:
k = k + 1;
8: end while
di
di
N
N
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: k = 0;
5: while (k < n) do
6:
ris = ris * x;
7:
k = k + 1;
di
di
N
N
3.2.
8:
31
end while
ris == mn
4. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema DPIP che, dati due numeri naturali
n,
m-n
volte ad
m.
m-n == m |-1-...-1
{z
} .
n volte
ris == m |-1-...-1
{z } ,
n-k volte
assumendo che
vari tra
n
1.
0.
ris accumula
ris:
via via
1: // m contiene un valore di N
2: // n contiene un valore di N
3: ris = 0;
4: if (m > n) then
5:
n = 0;
6:
ris = m;
7:
k = n;
8:
// m - n == ris - k vero, per sostituzione dei valori.
9:
while (k > 0) do
10:
/* Suppongo m - n == ris - k vero, riscrivibile come
11:
m - n == ris - k + 0 == ris - k + 1 - 1 == (ris - 1) - (k - 1).
12:
*/
13:
ris = ris - 1;
14:
/* m - n == ris - (k - 1).
15:
*/
16:
k = k - 1;
17:
/* m - n == ris - k .
18:
*/
19:
end while
20: end if
21: /* m - n == ris - k && k == 0 implica
22:
m - n == ris - k == ris - 0 == ris, ovvero ris
23:
contiene il risultato cercato indifferentemente dai
24:
valori iniziali di m ed n.
25: */
Prendendo spunto dalla dimostrazione precedente, dimostrare la correttezza parziale de:
(a)
1: // m contiene un valore di N
2: // n contiene un valore di N
3: ris = 0;
4: k = 0;
5: if (m > n) then
6:
ris = m;
7:
while (k < n) do
8:
ris = ris - 1;
9:
k = k + 1;
10:
end while
11: end if
5. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema QRDIP che, dati due numeri naturali
d,
il divisore, ed
s,
e resto
per mezzo di
s.
32
CAPITOLO 3.
r,
Dato il dividendo
d ed il divisore s il predicato
d, avr la forma seguente:
da
d== |s+...+s
{z } +r
con
r < s .
q volte
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
ed
r:
/* // d contiene un valore di N
/* // s contiene un valore di N
r = d;
q = 0;
/* // d == q*s + r vero per sostituzione di valori
while (r >= s) do
/* Suppongo d == q*s + r vero, riscrivibile come
d == q*s + 0 + r == q*s + s - s + r == (q + 1)*s + (r - s).
*/
r = r - s;
/* d == (q + 1)*s + r
*/
q = q + 1;
/* d == q*s + r
*/
end while
Usando la dimostrazione precedente come spunto e scambiando le istruzioni 10 e 13, dimostrare la correttezza parziale
dell'algoritmo ottenuto.
3.2.2
Per denizione, chiamiamo QPNP il problema che richiede di calcolare il quadrato di un numero naturale
la seguente specica propriet dei prodotti notevoli: il valore
tra
(x+1)2 e x2.
(x+1)*(x+1)
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
//
x contiene un valore di N
n = 0;
ris = 0;
while (n < x) do
/* n*n == ris
implica
n*n+2*n+1 == ris+2*n+1 == ris+(n+1)+(n+1)-1
implica
(n+1)*(n+1) == ris+2*(n+1)-1
*/
n = n + 1;
/* n*n == ris+2*n-1
*/
ris = ris + 2 * n - 1;
/* n*n == ris && n <= x
implica
n*n == ris
*/
end while
x, sfruttando
(x+1)2
x2:
3.2.
33
1: // x contiene un valore di N
2: n = 0;
3: ris = 0;
4: while (n < x) do
5:
ris = ris + 2 * n + 1;
6:
n = n + 1;
7: end while
8: */
Procedendo in maniera analoga alla dimostrazione della correttezza parziale di QPNP, usare il prodotto notevole
appropriato per calcolare il cubo di un valore numerico sotto il vincolo di saper calcolare solo somme, elevamenti al
quadrato e triplicazioni.
3.2.3
Nella sezione precedente abbiamo insistito sui predicati invarianti di ciclo e su come la loro struttura sintattica rimanga
invariata nel descrivere sia la congurazione che precede la prima istruzione del corpo di una iterazione sia quella che
segue l'ultima istruzione dello stesso corpo.
interessante spargere predicati che descrivano propriet utili alla dimostrazione di correttezza parziale anche
in altri punti di un algoritmo, oltre che all'inizio ed alla ne del corpo di una iterazione.
Il seguente algoritmo illustra come predicati possano descrivere quel che succede alle congurazioni in presenza di
selezioni:
1: // a contiene m N
2: // b contiene n N
3: // Per ipotesi, la congurazione si descrive col predicato a == m && b == n
4: if (a > b) then
5:
/* Percorrere questo ramo, permette di meglio specializzare
6:
la proprieta della configurazione, aggiungendo ad essa il fatto
7:
che la condizione della selezione sia vera:
8:
a == m && b == n && a > b . (1)
9:
Qualsiasi istruzione seguente agira su configurazioni in
10:
cui vale (1) e qualsiasi predicato che (1) possa implicare.
11:
Ad esempio, (1) implica:
12:
a-b == m-n , (2)
13:
ottenuta applicando ad a==m una sottrazione ad entrambi
14:
i membri di quantita identiche fra loro, cioe b ed n.
15:
/
*
16:
a = a - b;
17:
/* Lassegnazione chiama col nome a il valore
18:
della sottrazione del valore in b da quello di a.
19:
Come in altre situazioni analoghe, sostituiamo a ad
20:
a-b in (2), ottenendo almeno:
21:
a == m-n . (3)
22:
Siccome, prima dellassegnazione vero che a > b, avremo:
23:
a > 0 . (4)
24:
Al termine di questo ramo della selezione le
25:
proprieta rilevanti dello stato sono descritte
26:
dal predicato:
27:
a == m-n && a > 0 . (5)
28:
/
*
29: else
30:
/* Percorrere questo ramo, permette di meglio specializzare
31:
la proprieta della configurazione, aggiungendo ad essa il fatto
32:
che la condizione del costrutto condizionale sia vera:
33:
a == m && b == n && a <= b . (6)
34:
Qualsiasi istruzione seguente agira su configurazioni in
35:
cui vale (6) e qualsiasi predicato che (6) possa
36:
implicare.
37:
Ad esempio, (6) implica:
38:
b-a == n-m, (7)
34
CAPITOLO 3.
39:
ottenuta applicando a b==n una sottrazione ad entrambi
40:
i membri di quantita identiche fra loro, cioe a ed m.
41:
*/
42:
a = b - a;
43:
/* Lassegnazione chiama col nome a il valore
44:
della sottrazione del valore di a da quello di b. E
45:
quindi possibile sostituire a al posto di
46:
b-a in (7), ottenendo almeno:
47:
a == n-m . (8)
48:
Siccome , prima dellassegnazione a<=b, avremo:
49:
a >= 0 . (9)
50:
Al termine di questo ramo della selezione le
51:
proprieta rilevanti dello stato sono descritte dal
52:
predicato:
53:
a == n-m && a >= 0 . (10)
54:
/
*
55: end if
56: /* In questo punto del programma abbiamo percorso uno dei due rami
57:
quindi, grazie a (5) e (10) lunica cosa che possiamo sapere :
58:
a >= 0 .
59: */
3.2.4
MCD il classico problema di calcolare il massimo comun divisore di due numeri naturali
ed
y.
C' chi (Euclide . . . ) ha studiato per noi le propriet del MCD. Esse sono riassumibili attraverso il seguente insieme
di equazioni:
MCD(x y, y)
MCD(x, y) = MCD(x, y x)
se
se
se
x>y
x<y .
x=y
(3.4)
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
21:
22:
23:
a contiene x N
b contiene y N
// MCD(a,b) == MCD(x,y)
while (a != b) do
/* a!=b && MCD(a,b)==MCD(x,y) implica
MCD(a,b)==MCD(x,y) && (a > b || a < b)
*/
if (a > b) then
/* a > b implica MCD(a-b,b)==MCD(a,b)==MCD(x,y)
*/
a = a - b;
// MCD(a,b) == MCD(x,y)
//
//
end if
if (b >
a) then
/* b > a implica MCD(a,b-a)==MCD(a,b)==MCD(x,y)
*/
b = b - a;
// MCD(a,b) == MCD(x,y)
end if
end while
3.3.
IL PRINCIPIO DI INDUZIONE
35
Esercizio 10 (Correttezza parziale per MCD alternativo) Dimostrare la correttezza parziale del seguente algoritmo per l'MCD di due numeri naturali
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
ed
y:
xN
yN
while (a != b) do
while (a > b) do
a = a - b;
//
//
a
b
contiene
contiene
end while
while (a < b) do
b = b - a;
end while
end while
La soluzione in
3.3
MCDTriploCiclo.java.
Il principio di induzione
Prima di procedere oltre, utile richiamare principio di induzione che vedremo strettamente collegato sia alla dimostrazione di correttezza parziale per riscrittura del predicato (Sezione 3.2), sia all'approccio ricorsivo nella progettazione
di algoritmi (Capitolo 6).
Nell'articolo di wikipedia sul principio d'induzione si trova un'introduzione divulgativa al concetto di induzione del
quale segnaliamo l'esempio iniziale sulle tessere del domino, interessante perch sembra sottolineare l'aspetto dinamico
che il principio di induzione sembra sottendere.
Per ogni
n N,
n
X
i=
i=0
n(n + 1)
.
2
while (b)
S;
Se
una proposizione che esprime una relazione tra i valori delle variabili che compaiono nell'istruzione
S,
Q(n) P
vera dopo
P (n) P (n + 1),
allora
P (0)
x N.P (x).
Qui una propriet dei numeri naturali una propriet per la quale abbia senso chiedersi se vera o falsa per un
numero naturale. La
dell'implicazione
base
dell'induzione la dimostrazione di
P (n) P (n + 1)
P (0),
mentre il
passo induttivo
induttiva)
e si dimostra che
P (n + 1).
AN
tale che
0A
e, per ogni
P (n)
sia vera
P,
propriet vera:
Se
la dimostrazione
n N, n A n + 1 A ,
allora
A = N.
36
CAPITOLO 3.
Aritmetica
Teorema 1
Per ogni
n N,
n
X
i=
n(n + 1)
.
2
i=
k(k + 1)
.
2
i=0
P (k)
k
X
i=0
La base consiste nel vericare che entrambi i lati dell'induzione hanno valore
0.
assumiamo che
n
X
i=
i=0
n(n + 1)
2
e dimostriamo che
n+1
X
i=0
i=
(n + 1)(n + 2)
.
2
Ora,
n+1
X
i=0
n
X
!
i
+ (n + 1)
i=0
=
=
=
n(n + 1)
+ (n + 1)
2
n(n + 1) 2(n + 1)
+
2
2
(n + 1)(n + 2)
2
Teorema 2
Per ogni
m N:
(m + 1)2 = (m + 1) + 2
m1
X
(m i) .
i=0
P (m)
(m + 1)2 = (m + 1) + 2
m
X
(m i) .
i=0
La base consiste nel vericare che entrambi i lati dell'equazione siano identici quando
01
X
(0 + 1)2 = 1 = (0 + 1) + 2
m = 0:
(m i) .
i=0
Per dimostrare il passo induttivo, assumiamo che:
(m + 1)2 = (m + 1) + 2
m1
X
(m i)
i=0
e dimostriamo che:
(m + 2)2 = (m + 2) + 2
m
X
(m + 1 i) .
i=0
(3.5)
3.4.
37
Cominciamo con l'espandere l'espressione a sinistra e col manipolare la sommatoria, con l'obiettivo di far comparire
esplicitamente (3.5):
(m + 1)2 + 2(m + 1) + 1 = (m + 2) + 2
m
X
(m + 1 i)
i=0
= (m + 2) + 2
m
X
(m i + 1)
i=0
= (m + 2) + 2(m m + 1) + 2
m1
X
(m i + 1)
i=0
= (m + 2) + 2 + 2
= (m + 2) + 2 + 2
m1
X
m1
X
i=0
i=0
(m i) + 2
m1
X
(m i) + 2m .
i=0
Isolando
(m + 1)2
a sinistra, otteniamo:
(m + 1)2 = 2m 2 1 + m + 2 + 2 + 2m + 2
m1
X
(m i)
i=0
= (m + 1) + 2
m1
X
(m i) ,
i=0
che proprio (3.5), vera per ipotesi.
n > 0, n3 n
n 4, n! > 2n .
n > 0,
divisibile per 3.
n(3n 1) X
=
(3i 2).
2
i=1
Esercizio 14 Dimostrare mediante induzione che, per ogni
n 3, n2 > 2n + 1.
n 5, 2n > n2 .
elearning.math.unipd.it
3.4
In questa sezione sviluppiamo una strategia alternativa a quella per riscrittura del predicato invariante della Sezione 3.2. In presenza di una iterazione, procederemo esplicitamente per induzione sul numero di esecuzioni complete del
corpo dell'iterazione, implicitamente usando la precedente tecnica per riscrittura.
3.4.1
Dato:
1:
2:
3:
4:
5:
6:
7:
8:
//
di
//
m contiene un valore
n contiene un valore
ris = m;
i = n;
while (i > 0) do
ris = ris + 1;
i = i - 1;
di
end while
N
N
38
CAPITOLO 3.
ris
m+n.
dell'iterazione:
ris = ris + 1;
i = i - 1;
viene percorso.
ris e i cambiano entrambe valore al termine di ogni ciclo. Ad esempio, dopo k cicli,
ris diverso dal valore assunto dopo k + 1 cicli. Questo suggerisce di identicare come
funzione del numero 0 k di cicli percorsi:
opportuno, il valore di
segue il valore di
ris
in
denotiamo con
risk
denotiamo con
ik
ris
al termine del
al termine del
k -esimo
k -esimo
ciclo,
ciclo.
Lo scopo dimostrare:
Propriet 1
Siano
e
m e n ssati.
ik ==(i-k).
Per ogni
Dim.
Supponiamo
k = 0. Il codice contiene le assegnazioni ris=m e i=n che sono eseguite prima di una
ris0 ==m e i0 ==n e l'enunciato vale banalmente: ris0 +i0 ==m+n e i0 ==(i-0).
qualsiasi
iterazione. Quindi,
Dato
0 < k < n,
ik ==(i-k).
(istruzione 5)
==risk +1+ik -1
==risk +ik
(ipotesi induttiva)
==m+n
ik+1 ==ik +1
(ipotesi induttiva)
==(i-k)+1
==i-(k+1) .
Corollario 1
Siano
ssati. Dopo
cicli,
m+n==ris.
Dim.
(Propriet 1 e
m+n==risn +in
k==n)
(Propriet 1)
==risn +0
==risn
==ris .
Esercizio 16 (Correttezza parziale per induzione con soluzioni)
parziale del problema MSIP che, dati due numeri naturali
iterata per
volte.
n,
m*n
come somma di
3.4.
39
Dato:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: i = n;
5: while (i > 0) do
6:
ris = ris + m;
7:
i = i - 1;
8: end while
di
di
N
N
ris
x*y.
dell'iterazione:
ris = ris + x;
i = i - 1;
viene percorso.
ris e i cambiano entrambe valore al termine di ogni ciclo. Ad esempio, dopo k cicli, per
ris diverso dal valore assunto dopo k + 1 cicli. Questo suggerisce di identicare il valore
del numero 0 k di cicli percorsi:
k
di
opportuno, il valore di
ris
in funzione
denotiamo con
denotiamo con
Propriet 2
Siano
volte, allora
Dim.
k = 0. Il codice contiene le assegnazioni ris=0 e i=n che sono eseguite prima di una qualsiasi
ris0 ==0 e i0 ==n e l'enunciato vale banalmente: ris0 +(i0 *m)==m*n e i0 ==(n-0).
0 < k <n, per ipotesi induttiva assumiamo risk +(ik *m)==m*n e ik ==(n-k). La prima equivalenza
Supponiamo
iterazione. Quindi,
Dato
(istruzione 5)
(ipotesi induttiva)
ik+1 ==ik +1
==(i-k)+1
(istruzione 4)
(ipotesi induttiva)
==i-(k+1) .
Corollario 2
Siano
ssati. Dopo
cicli,
m*n==ris.
Dim.
m*n==risk +ik *m
==risn +ny *m
(Propriet 2 e
k==n)
(Propriet 2)
==risn +0*m
==risn
==ris .
Prendendo spunto dalla dimostrazione di correttezza parziale per induzione di MSIP, risolvere i seguenti esercizi:
40
CAPITOLO 3.
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: i = 0;
5: while (i < n) do
6:
ris = ris + m;
7:
i = i + 1;
8: end while
di
di
N
N
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: i = 0;
5: while (i <= n) do
6:
ris = ris + m;
7:
i = i + 1;
8: end while
di
di
N
N
ris == m*n
2. Per induzione sul numero di iterazioni eettuate, dimostrare la correttezza parziale del problema PMIP che, dati due
numeri naturali
n,
mn
come moltiplicazione di
iterata per
volte.
Dato:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 1;
4: i = n;
5: while (i > 0) do
6:
ris = ris * m;
7:
i = i - 1;
8: end while
di
di
N
N
ris
mn.
dell'iterazione:
ris = ris * m;
i = i - 1;
viene percorso.
ris e i cambiano entrambe valore al termine di ogni ciclo. Ad esempio, dopo k cicli, per
ris diverso dal valore assunto dopo k + 1 cicli. Questo suggerisce di identicare come
i in funzione del numero 0 k di cicli percorsi:
opportuno, il valore di
segue il valore di
ris
denotiamo con
risk
denotiamo con
ik
ris
al termine del
al termine del
k -esimo
k -esimo
ciclo,
ciclo.
Propriet 3
Siano
volte, allora
Dim.
Supponiamo
k = 0. Il codice contiene le assegnazioni ris=1 e i=n che sono eseguite prima di una qualsiasi
ris0 ==1 e i0 ==n e l'enunciato vale banalmente: ris0 *(mi0 )==mn e i0 ==(n-0).
iterazione. Quindi,
3.4.
Dato
0 < k < n,
41
ik ==(n-k).
La prima equivalenza
(istruzione 5)
==risk *(mik )
(ipotesi induttiva)
==mn
ik+1 ==ik -1
(ipotesi induttiva)
==(n-k)-1
==n-(k+1) .
Corollario 3
Siano
ssati. Dopo
cicli,
mn==ris.
Dim.
(Propriet 3 e
mn==risk *(mik )
k==n))
(Propriet 3)
==risn *(mnn )
==risn *(m0)
==risn *1
==ris .
Prendendo spunto dalla precedente dimostrazione di correttezza parziale di PMIP, risolvere i seguenti esercizi:
(a) Scambiare le istruzioni 6 e 7 e dimostrare la correttezza parziale.
(b) Dimostrare la correttezza parziale del seguente algoritmo alternativo che risolve PMIP:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 1;
4: i = 0;
5: while (i < n) do
6:
ris = ris * m;
7:
i = i + 1;
8: end while
di
di
N
N
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: i = 0;
5: while (i < n) do
6:
ris = ris * m;
7:
i = i + 1;
8: end while
di
di
N
N
ris == mn
3. Per induzione, dimostrare la correttezza parziale del problema DPIP che, dati due numeri naturali
calcolare il valore
m-n
Dato:
1: // m contiene un valore
2: // n contiene un valore
3: ris = 0;
4: if (m > n) then
di
di
N
N
volte ad
m.
n,
impone di
42
CAPITOLO 3.
5:
6:
7:
8:
9:
10:
11:
ris = m;
i = n;
while (i > 0) do
ris = ris - 1;
i = i - 1;
end while
end if
ris
m-n.
dell'iterazione:
ris = ris - 1;
i = i - 1;
viene percorso.
ris ed i cambiano entrambe valore al termine di ogni ciclo. Ad esempio, dopo k cicli,
ris diverso dal valore assunto dopo k + 1 cicli. Questo suggerisce di identicare il
funzione del numero 0 k di cicli percorsi:
opportuno, il valore di
valore di
ris
ed
in
denotiamo con
risk
denotiamo con
ik
ris
al termine del
al termine del
k -esimo
k -esimo
ciclo,
ciclo.
Lo scopo dimostrare:
Propriet 4
Siano
volte, allora
Dim.
Supponiamo
k = 0. Il codice contiene le assegnazioni ris=m e i=n che sono eseguite prima di una qualsiasi
ris0 ==m e i0 ==n e l'enunciato vale banalmente: ris0 -i0 ==m-n e i0 ==(n-0).
iterazione. Quindi,
Dato
0 < k < n,
ik ==(n-k).
(istruzione 8)
(ipotesi induttiva)
ik+1 ==ik -1
==(n-k)-1
(istruzione 8)
(ipotesi induttiva)
==n-(k+1) .
Corollario 4
Siano
ssati. Dopo
cicli,
m-n==ris.
Dim.
m-n==risn -in
==risn -0
(Propriet 4 e
k==n)
(Propriet 4)
==risn
==ris .
3.4.
3.4.2
43
Richiamiami il seguente algoritmo per il problema QPNP, gi introdotto nella Sottosezione 3.2.2:
1:
2:
3:
4:
5:
6:
7:
//
x contiene un valore di N
n = 0;
ris = 0;
while (n < x) do
n = n + 1;
ris = ris + 2 * n - 1;
end while
Per induzione sul numero di iterazioni, dimostriamo
(ii)
nk e risk
nk , nk+1 , risk
sono i valori di
e
risk+1
ris,
nk+1 == nk + 1
risk+1 == risk + 2 * nk+1 - 1
Dim.
P (0)
k.
vero perch
Assumiamo valga
n0 * n0 == 0 ==
P (k). Allora:
nk * nk
ris
vero, implica
nk * nk == risk
+ 2 * nk + 1 == risk + 2 * nk + 1
(nk + 1)2 == risk + (nk + 1) + nk
vero, implica
vero, implica
vero, implica
== risk + 2 * (nk + 1) - 1
== risk + 2 * nk+1 - 1
vero, implica
(nk + 1)
(nk + 1)
n2k+1
n2k+1
vero, implica
== risk+1 .
3.4.3
Questo esempio ha, tra l'altro, lo scopo di presentare due stili diversi nell'esposizione di una dimostrazione di correttezza
parziale, entrambi accettabili: il primo pi narrativo, il secondo pi formale.
Stile narrativo.
Consideriamo il problema di calcolare il quoziente
ed il resto
XD
X < D,
poni
X;
aumenta
di 1
r = X.
2
4
6
8
10
problema da risolvere: La condizione di ingresso del programma, cio la propriet che i dati in ingresso
soddisfare, che
X0
D>0
devono
(la seconda propriet serve ad evitare casi di divisione per 0). La condizione di uscita
ed
X = q D + r,
con
r < D.
Questa
44
CAPITOLO 3.
ed
per
correttezza parziale)
D.
La
asserisce
che:
per ogni dato in ingresso che soddisfa la condizione di ingresso, se il programma termina, allora i dati in
uscita soddisfano la condizione di uscita.
Una condizione pi esigente di correttezza quella che si chiama
correttezza totale:
per ogni dato in ingresso che soddisfa la condizione di ingresso, il programma termina e i dati in uscita
soddisfano la condizione di uscita.
Per stabilire che un programma soddisfa la specica vi sono vari modi, ma la tecnica pi conveniente consiste nel
trovare quello che si chiama un
invariante
(di ciclo):
invariante (di un ciclo) una propriet che lega (tutte o alcune del)le variabili coinvolte nel ciclo, e che
vera dopo un numero arbitrario di iterazioni del ciclo. In particolare, vera all'ingresso nel ciclo (cio
dopo 0 iterazioni).
Ci sono molte propriet invarianti del ciclo
1 while (r >= D) {
r = r - D;
q = q + 1;
q 0.
X =qD+r
(3.6)
che molto simile alla condizione di uscita del programma. Che si tratti veramente di un invariante qualcosa che
deve ancora essere dimostrato, ma per il momento assumiamo che lo sia. Quando il ciclo termina (e prima o poi deve
terminare, perch ad ogni iterazione a
> 0,
r < D)
abbiamo che
r < D,
che proprio la condizione di uscita del programma. L'uso dell'invariante ci permette quindi di dimostrare che
il programma (parzialmente) corretto. In questo caso abbiamo gi implicitamente dimostrato che il programma
anche totalmente corretto, perch abbiamo gi visto che il ciclo deve terminare. Resta da dimostrare che la propriet
(3.6) proprio invariante. Questo si pu fare per induzione sul numero di iterazioni del ciclo. Supponiamo che questo
numero sia 0 (base dell'induzione) (ovviamente, la dimostrazione che (3.6) invariante vale in generale, non solo per
gli specici valori di
X = qD+r
n volte, e che la propriet (3.6) sia vera (ipotesi induttiva); vogliamo dimostrare ora che
(n + 1)-esima iterazione. Durante questa iterazione vengono modicati i valori di q e di r,
Allora
il ciclo
resta vera
anche dopo la
ottenendo
valori
q0 = q + 1
r0 = r D
dove
q0
ed
r0
ed
r = r - D;
q = q + 1;
Allora calcoliamo:
q 0 D + r0 = (q + 1) D + (r D)
=qD+D+rD
=qD+r =X
dove l'ultimo passaggio sfrutta l'ipotesi induttiva. Per induzione si conclude allora che la propriet (3.6) vera per
qualsiasi numero di iterazioni del ciclo, quindi (3.6) invariante.
3.4.
45
Stile formale
Per induzione sul numero di iterazioni eettuate, dimostrare la correttezza parziale del problema QRDIP che, dati
due numeri naturali
di
per mezzo di
d,
il divisore, ed
r,
s,
e resto
s.
Dato il dividendo
da
d ed il divisore s il predicato
d, avr la forma seguente:
Dato:
1:
2:
3:
4:
5:
6:
7:
8:
//
di
//
d contiene un valore
s contiene un valore
r = d;
q = 0;
while (r >= s) do
r = r - s;
q = q + 1;
di
end while
vogliamo dimostrare
r,
N
N
k.P (k)
istruzioni 4 e 5 dell'algoritmo:
rk+1 == rk - s
qk+1 == qk + 1
Dim.
P (0)
k.
vero perch
Assumiamo valga
q == q0 * s + d == d.
P (k). Allora:
d == qk+1 * s + rk+1
vero se
d == (qk + 1) * s + (rk - s)
d == qk * s + rk - s + s
vero se
vero se
vero per ipotesi induttiva
d == qk * s + rk
.
3.4.4
Ancora adottando lo stile narrativo, vediamo un altro esempio della tecnica appena usata per dimostrare la correttezza
del programma per la divisione intera, utilizzandola questa volta per sintetizzare un programma per calcolare il
quadrato di un numero naturale
Y = X X
X=N
dove
N.
il dato in uscita ed
N 0,
Y = X X.
Inizialmente avremo dunque
X=0
Y = 0:
2
4
6
8
10
12
class quadrato {
public static void main (String[] args) {
int N, X, Y;
N = ? ; // inizializzazione
X = 0;
Y = 0;
while (X < N) {
Y = Y + 2 * X + 1;
X = X + 1;
}
System.out.println ("Quadrato = " + Y);
}
}
Y =X X
(3.7)
46
CAPITOLO 3.
Y = Y + 2 X + 1,
mentre
calcolare
Y0 =Y +2X +1
= (X X) + 2 X + 1
= (X + 1) (X + 1)
= X0 X0
da cui si conclude che (3.7) proprio invariante. Poich il valore di
N X
ciclo deve terminare (perch non ci pu essere una sequenza innita di numeri naturali
dal ciclo avremo
che
X=N
all'uscita
(perch la condizione del while diventata falsa e sappiamo, per come fatto il programma,
uscita
Esercizio 17
1. Partendo dalla dimostrazione precedente, vericare per induzione sul numero di iterazioni se il predi-
cato:
x2 = x + 2
x
X
(x i)
i=1
(Dicile.)
P (k)
3.5
QuadratoConRaddoppiEtc.java.
QuadratoDiStefanoMerlo.java
individuando un predicato
che, usando un ragionamento per induzione sul numero di cicli percorsi, sia l'invariante.
X
false
false
true
true
XY
true
true
false
true
Y
false
true
false
true
La quanticazione universale vera se la congiunzione di tutte le istanze del predicato su cui si quantica vera.
Particolare interesse per noi ha il modo in cui si verichi la validit di un predicato che quantichi
universalmente su una implicazione.
k N.0 k < 3 3 k 0
00<3300
01<3310
02<3320
03<3330
04<3340
05<3350
.
.
.
true true
true true
true true
false true
false false
false false
true
true
true
true
true
true
.
.
.
.
.
.
true .
La congiunzione pi a sinistra nell'Esempio 4 ottenuta eliminando la quanticazione universale da
k < 3 3 k 0.
in
0 k < 3 3k 0
k N.0
k
stesso possa assumere. Le congiunzioni centrale e seguenti risultano dalla valutazione dei predicati che compongono
ciascuna implicazione.
L'aspetto interessante che la congiunzione innita vera non perch in essa compaiano
solo implicazioni con premessa e conclusione vere. Al contrario, essa contiene sia implicazioni
implicazioni
false true,
false false.
La possibilit di scrivere predicati in cui l'implicazione sia vera grazie al fatto che la premessa falsa gioca
un ruolo fondamentale nelle dimostrazioni di correttezza parziale che illustreremo in questa sezione.
sia
3.6.
47
3.6
Per denizione, il problema SNNP consiste nel voler stampare l'intera sequenza di numeri naturali tra
ed un valore
Proponiamo l'Algoritmo 12
1: // n contiene un valore
2: i = 0;
3: while (i < n) do
4:
stampa i;
5:
i = i + 1;
6: end while
di
deve
i = 0; e stiamo per,
stampa il valore in i;;
i <= n
i = i + 1;,
n.
e argomentiamo sul perch esso possa esser un candidato per dimostrare la correttezza parziale
il valore
P (i)
gi stato stampato
(3.8)
00<i0
01<i1
gi stato stampato
gi stato stampato
.
.
.
Sia
P (i)
true true
true true
true true
false true
false false
false false
true
true
true
true
true
true
.
.
.
.
.
.
i.
true .
pari a 0. Allora:
00<00
01<01
02<02
.
.
.
gi stato stampato
gi stato stampato
gi stato stampato
false 0
false 1
false 2
gi stato stampato
gi stato stampato
gi stato stampato
.
.
.
true
true
true true .
.
.
.
In questo caso, ogni implicazione vera grazie alla tavola di verit dell'implicazione, anche se nessuno
0, 1, 2,
dei valori
etc. sia stato eettivamente stampato. Questo utile perch, ad esempio, interpretando la linea 3 per
48
CAPITOLO 3.
Sia
pari a 1. Allora:
00<10
01<11
02<12
gi stato stampato
gi stato stampato
gi stato stampato
.
.
.
true
true
true true .
.
.
.
.
.
.
i,
constatando che
P (i)
i.P (i)
per
P (i) sembra descrivere convincentemente un processo di stampa incrementale di un certo numero di valori,
constatiamo che esso si adatta a descrivere le congurazioni dell'Algoritmo 12. Riprendiamo quest'ultimo qui sotto,
includendo la dimostrazione di correttezza parziale per riscrittura:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
//
n contiene un valore di N
i = 0;
// P (0)
while (i < n) do
// P (i)
stampa i;
/* P (i) && i stato stampato.
Quindi P (i+1) perch ora sullo schermo
ci sono i valori 0, 1, ..., i-1, i
*/
i = i + 1;
// P (i) perch i un nome per i+1.
end while
1: // n contiene un valore
2: i = 0;
3: while (i < n) do
4:
leggi v;
5:
stampa v;
6:
i = i + 1;
7: end while
Esso legge
di
valori recuperati attraverso un qualche mezzo, ad esempio la tastiera, e, man mano che la lettura
procede, stampa il valore appena letto. Vericare che la dimostrazione di correttezza parziale per riscrittura possa
essere portata a termine usando il seguente predicato:
1: // n contiene un valore
2: i = 0;
3: r = 0;
4: while (i < n) do
5:
leggi v;
6:
r = r + v;
7:
i = i + 1;
8: end while
di
valore
3.6.
Esso legge
somma in
n
r.
49
valori recuperati attraverso un qualche mezzo, ad esempio la tastiera, e, man mano, ne accumula la
Vericare che la dimostrazione di correttezza parziale per riscrittura possa essere portata a termine
P (i) r ==
i-1
X
k -esimo
valore
letto
k=0
Esercizio 20 (Correttezza parziale con predicati implicativi e lettura da tastiera) Per svolgere agevolmente l'esercizio anche a livello di programmazione, pu servire una l'abitudine all'uso della classe
SIn.java,
documentata in
SIn Javadoc.
1. Scrivere in algoritmo che, ssato un valore
n 1,
legge
MassimoTraInteri.java.
0.
Man mano che i numeri vengono letti, occorre contare sia le occorrenze di numeri pari, sia quelle di numeri
dispari. Al termine della lettura occorre essere in grado di sapere, ad esempio stampandoli, quanti dispari e quanti
pari sono stati incontrati, escludendo lo
3.6.1
ContaPariDispari.java.
Iterazioni annidate
ed
n,
composta da
righe ed
colonne.
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
//
inN
//
m contiene un valore
n contiene un valore
i = 0;
j = 0;
while (i < m) do
while (j < n) do
stampa *;
j = j + 1;
in
end while
i = i + 1;
j = 0;
end while
L'algoritmo caratterizzato da un paio di iterazioni annidate. L'iterazione pi interna ha l'obiettivo di stampare
asterischi. Tale riga sotto un certo numero di righe gi completamente stampate. Il ciclo
pi esterno ha il compito di cambiare riga di stampa non appena il ciclo pi interno ha terminato una riga.
Genericamente, nel mezzo della sua interpretazione, l'algoritmo ha generato la seguente illustrazione:
}|
{
z
* * * * 01
*.
*
**
.
.
(3.9)
* * * * i-2
* *
* * i-1
i
*
*
| {z }
j
i-1
in via di
asterischi.
e
e
50
CAPITOLO 3.
P (i, j)
per ogni
se
k,
0 <= k < i, allora la k-esima riga gi
la i-esima riga contiene j occorrenze di *
P (0, 0)
P (i, j)
completamente stampata
.
vero e descrive una situazione in cui non sia stato stampato alcun
j:
per ogni
k,
0 <= k < 0, allora la k-esima riga gi completamente stampata
e la 0-esima riga contiene 0 occorrenze di *
equivale a: per ogni k,
se false, allora la k-esima riga gi completamente stampata
e la 0-esima riga contiene 0 occorrenze di *
equivale a: per ogni k, true e true
che equivale a: true .
se
P (i, 0)
vero e descrive la situazione in cui tutte le righe con indice comprese nell'intervallo [0,i) sono state
completate. In particolare,
P (m, 0)
j
righe.
nell'illustrazione (3.9).
Il predicato
P (i, j)
si presta ad essere l'invariante del seguente algoritmo che, passo dopo passo, incrementa sia la
quantit di asterischi stampati nella riga ancora da completare, sia l'insieme delle righe completate:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
14:
15:
16:
17:
18:
19:
20:
j = 0;
21:
// P (i, j) perch ora j un nome per 0
22: end while
23: // i == m && j == 0 && P (m, 0).
Esercizio 21 (Correttezza parziale con implicazioni ed iterazioni annidate)
1: i = 1;
2: j = 1;
3: while (i<11) do
4:
while (j<11) do
5:
stampa i*j;
6:
j = j + 1;
7:
end while
8:
i = i + 1;
9:
cambia riga;
10:
j = 1;
11: end while
Il seguente algoritmo:
3.6.
51
stampa una matrice che costituisce la tavola pitagorica dei primi dieci numeri interi.
Vericare che il seguente predicato possa essere usato per dimostrarne la correttezza parziale:
Il seguente algoritmo:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
end while
i = i + 1;
cambia riga;
j = 0;
end while
ed
ed
*.
n.
Vericare che il seguente predicato possa essere usato per dimostrarne la correttezza parziale:
P (i, j, m)
per ogni
TriangoloDiStelle.java
k,
se
0 <= k < i,
allora
k-esima
riga ha
occorrenze di
Il seguente algoritmo:
1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:
while (i < m) do
while (j < i && i < n) do
stampa *;
j = j + 1;
end while
i = i + 1;
cambia riga;
j = 0;
end while
Vericare che il seguente predicato possa essere usato per dimostrarne la correttezza parziale:
se
i<=n,
allora
per ogni
se
se
k,
0<=k<i, allora
k-esima riga ha k occorrenze di *
e i-esima riga ha j occorrenze di *
n<i, allora
per ogni k,
se 0<=k<n, allora
k-esima riga ha k occorrenze di *
e se n<=k<i, allora k-esima riga ha
e i-esima riga ha j occorrenze di *
n occorrenze di
.
Altrimenti,
52
CAPITOLO 3.
TrapezioDiStelle.java
3.6.2
La sottosezione precedente sembra indicare che la correttezza di un algoritmo con iterazioni annidate sia possibile
essenzialmente solo sfruttando predicati che contengano implicazioni.
Per completezza vediamo la correttezza di un algoritmo con iterazioni annidate che non si basi su un predicato
contenente un'implicazione. Riprendiamo il problema MSDIP il cui scopo era moltiplicare due numeri naturali, il
moltiplicando
Sfruttando l'esperienza
maturata sinora nell'individuare la congurazione nel mezzo, possiamo descrivere quest'ultima come segue:
mn
z }| {
z }| { z }| {
z }| {
z }| {
z }| {
(1 + . . . + 1) + . . . + (1 + . . . + 1) +(1 + . . . + 1 + 1 + . . . + 1) + (1 + . . . + 1) + . . . + (1 + . . . + 1)
|
{z
}
|
{z
}
N
M (N +1)
|
{z
}
r
nella quale
m M = r + (m n) + m (M (N + 1)) r = m N + n
(3.10)
nel risultato
ci sono
MSIDPCicliAnnidati
r,
ovvero
m (M (N + 1))
n<m
uni,
m M.
la classe Java che implementa un algoritmo corrispondente alle con regole di riscrittura
elencate nei commenti e la dimostrazione di correttezza parziale. Possiamo osservare che, al contrario di altre versioni
di MSDIP, usiamo solo incrementi di una unit.
3.6.3
Approfondimenti facoltativi
I seguenti riferimenti sono letture integrative per i pi curiosi a proposito di correttezza parziale degli algoritmi:
while-do.
Capitolo 4
javac
class,
java e
java.
Vedremo che le classi possono contenere pi metodi tra i quali non necessariamente si trova
main.
Parleremo di passaggio di parametri che abbiano un tipo di base, distinguendo tra quelli formali e quelli attuali.
Descriveremo la struttura frame, associata a ciascun metodo per mantenerne le variabili locali, e la struttura frame
stack costituita da una sequenza di frame, gestita in accordo con una politica last-in-rst-out (LIFO).
Gli argomenti sono trattati anche in [SM14, Capitolo 5], Sezioni 5.1, 5.2 e 5.3.
4.1
Supponiamo di aver compilato la classe seguente, per ipotesi memorizzata nel le
SoloMain.java:
int
a = 1;
boolean b = (a == a);
System.out.println(b);
9
}
}
java SoloClass
SoloMain.class.
SoloMain.class
corrispondente al
main.
Allocazione.
main
durante l'interpretazione.
main
ha la seguente
struttura:
...
b
a
frame :
main
args
null
...
testo [SM14]. Per ora, senza ulteriori dettagli, suciente dire che:
54
CAPITOLO 4.
b.
a,
durante l'interpretazione e
main.
main
un parametro formale una variabile il cui valore dipende dal mondo esterno al metodo. Vedremo in
seguito quando e come il valore di un parametro formale venga ssato. L'importante cominciare a ricordare
che viene allocato lo spazio necessario a contenere i valori per ogni parametro formale di un dato metodo.
come, ad esempio, in
int a = 1;
dove il presso
Per i tipi non primitivi, il meccanismo di allocazione dello spazio per contenere valori appropriati non
diretto come quello usato per l'allocazione di valori di tipi base. Vedremo in seguito i dettagli.
Per ora, intuitivamente, la parola chiave
null
esplicita che
args
utilizzabile
Interpretazione.
Solo ad allocazione avvenuta, comincia l'interpretazione vera e propria delle linee di codice in
SoloMain.class
corrispondenti a quelle in
SoloMain.java.
a,
=,
ottenendo il valore
1.
...
...
a
frame :
main
args
null
1
frame :
main
args
null
...
...
=.
meno immediata di quella precedente perch siamo di fronte ad una espressione complessa
Per valutarla dobbiamo risalire al valore della variabile
a,
La valutazione
a == a.
attivo.
Nel caso in questione, il frame attivo relativo al metodo
main.
In esso troviamo
l'espressione
a e da essa ricaviamo il valore 1. Ora che conosciamo il valore associato ad a possiamo valutare
1 == 1. Siccome i valori confrontati sono identici, il risultato true.
4.2.
55
...
...
b
a
1
frame :
frame :
main
args
...
1
null
...
main
args
null
true
esiste e contiene
System.out.println
b.
a == a, cerchiamo l'esistenza
true, possiamo interpretare:
nel
System.out.println(true);
ottenendo la pubblicazione della costante
true
del PC in uso.
La situazione del frame attivo non cambia, rimanendo:
...
frame :
true
main
null
args
*
...
Disallocazione.
Al di sotto della riga 8 non rimangono istruzioni signicative da interpretare. Il compito della java
virtual machine disallocare lo spazio riservato al frame attivo, ripristinando lo stato quasi identico a quello precedente
all'allocazione. Si ritorna, infatti, alla situazione:
...
true
a
args
null
...
main.
memorizzare valori, non appena siano nuovamente coinvolte nella formazione di un frame.
4.2
MetodoArgomento.java:
6
8
56
CAPITOLO 4.
m(b + 3);
b = b + 1;
10
}
12 }
MetodoArgomento.class
javac.
main, che diventa quello attivo, e prosegue con
M(b + 3);. Il metodo m interpretato in maniera
main.
Occorre allocare un frame, che diventa quello attivo, e che deve essere adatto a contenere
m.
Allocazione
main.
...
...
b
a
frame :
main
args
null
...
...
Interpretazione
main.
...
b
a
frame :
frame :
main
m.
dell'espressione
a
args
null
...
Allocazione
main
args
null
...
m,
b + 3.
M(3);
essa conterrebbe una chiamata al metodo
m,
3.
la prima azione consiste nel valutare il parametro attuale della chiamata, ovvero l'espressione che si trova tra le
parentesi che individuano l'argomento del metodo chiamato.
Nel nostro caso, la riga chiamante
M(b + 3);
b.
b + 3.
vale
2,
la seconda azione ancora sul frame attivo del metodo chiamante, nel nostro caso
cui occorrer riprendere l'interpretazione del codice di
dalla congurazione di sinistra a quella sulla destra:
main
main.
b + 3,
5.
Si memorizza la riga da
*.
Passiamo
4.2.
57
...
frame :
...
a
frame :
main
a
args
null
...
main
args
null
...
la terza azione l'operazione naturale, ovvero l'allocazione del frame relativo al metodo chiamato che, nel nostro
caso,
m. m
non potrebbe gestire correttamente le variabili di sua competenza senza frame dedicato che diventa
a
...
frame :
b
/
frame :
main
args
null
frame :
main
args
null
...
/
...
il frame di
main.
Il frame di
main.
In realt le
variabili non sono omonime perch, per individuarle, quel che conta anche il frame nel quale sono denite.
di
args
di
main,
m.
nella signature
nel frame di
m,
b + 3
in
M(int b) di m.
M(b + 3);, 5.
...
a
frame :
b
/
frame :
a
frame :
a
args
/
null
...
main
args
null
attivo.
5
/
frame :
main
Inoltre,
...
m,
guardando al suo
frame
come a quello
58
CAPITOLO 4.
Interpretazione di
m.
dell'espressione a destra del simbolo uguale che dipende dal valore della variabile
La variabile
b,
b.
che nel nostro caso anche il parametro formale, va cercata tra quelle disponibili nel frame attivo.
Insistiamo nel sottolineare che il frame attivo sempre quello costruito per ultimo in cima al frame stack.
Ne consegue che il valore recuperato da
e non
2,
main
che
pari a
a.
Come per
b,
nel frame attivo in cima al frame stack, per passare dalla congurazione di sinistra a quella sulla destra:
...
...
a
frame :
5
/
frame :
frame :
b
/
a
frame :
main
m.
b
/
a
args
null
...
Disallocazione di
main
args
null
...
celle ad esso dedicate per ulteriori utilizzi, passando dalla congurazione di sinistra a quella sulla destra:
...
frame :
frame :
...
a
frame :
main
a
args
null
...
Interpretazione
main
args
null
...
Dopo la chiamata di
5,
sua interpretazione e
disallocazione del frame, occorre continuare l'interpretazione delle eventuali righe di codice sorgente di
da cui continuare noto. Esso il numero di linea della cella
b.
La variabile
Quindi a
main.
Il punto
main.
quella che troviamo nel frame attivo in cima al frame stack : quello di
3,
2 + 1,
4.3.
59
...
...
frame :
a
frame :
main
main.
a
args
null
...
Disallocazione
main
args
null
...
Non esistendo ulteriori istruzioni signicative al di sotto della riga 10, il frame di
main viene
frame :
...
main
args
null
args
null
...
...
MetodoArgomento.java il programma java che, attraverso la stampa dei valori nelle variabili, evidenzia quelle
accessibili durante l'interpretazione descritta.
4.3
MetodoArgomentoRisultato.java:
8
10
12 }
MetodoArgomentoRisulatato.class prodotto
MetodoArgomentoRisultato.java sono:
per mezzo di
javac.
nella dichiarazione
public static int M(int b) del metodo m il tipo void sostituito col tipo int.
m terminer con la restituzione al metodo chiamante di un valore di tipo
int.
L'ultima istruzione
return a;
di
m.
conterr al
60
CAPITOLO 4.
Anche la chiamata di
in
main
modicata.
Essendo essa
il valore restituito
c.
Vediamo il dettaglio su come venga gestita la memoria e di quali siano le dierenze rispetto al caso precedente nel
quale il metodo chiamato non restituisca alcun valore.
Allocazione
main.
...
c
b
frame :
main
args
null
...
...
Interpretazione
main.
c,
...
b
frame :
frame :
main
args
null
main
m.
m,
a
args
...
null
...
Allocazione
b + 3
m
b + 3,
M(b + 3) + 1;.
m,
attivo il valore
in
b,
ottenendo
b + 3.
b + 3.
M(b + 3);, ovvero M(5). Serve memorizzare in * la
riga da cui occorrer riprendere l'interpretazione del codice di main. Passiamo dalla congurazione di sinistra a quella
frame
occorre chiamare
sulla destra:
...
...
frame :
main
...
frame :
main
args
null
a
args
null
10
...
4.3.
formale
e la variabile locale
a.
61
m, piazzandolo sopra
il frame di
a
...
frame :
c
/
*
c
frame :
main
frame :
/
10
args
null
main
*
args
null
...
/
10
...
5.
...
a
frame :
a
frame :
b
/
5
/
b
/
frame :
main
10
frame :
main
args
null
in
a
args
/
10
m,
lo interpretiamo.
m. L'assegnazione a riga 3 usa b nel frame attivo che contiene 5 e che, sommato con 4, permette
a, sempre del frame attivo. Passiamo dalla congurazione di sinistra a quella sulla destra:
...
...
a
frame :
5
/
b
/
frame :
frame :
...
di memorizzare
null
...
Interpretazione di
main
10
...
frame :
main
args
null
a
args
null
10
...
62
CAPITOLO 4.
return <espressione>
* del metodo chiamante.
scrive il valore di
<espressione>
del
...
frame :
b
/
frame :
frame :
main
frame :
main
m.
a,
a
args
null
10
...
Disallocazione di
valore di
args
null
10
9,
...
scriviamo
...
m, disallochiamo.
...
frame :
...
frame :
main
10
frame :
main
args
null
perch presente in
di
a
args
null
10
...
Interpretazione
...
Conclusa la chiamata di
m,
della riga 9 del metodo chiamante. Questo signica che occorre terminare la valutazione dell'espressione
+ 1;,
ovvero di
M(5) + 1,
Concludiamo che a
sulla destra:
10.
=
M(b + 3)
9 + 1.
4.4.
CAMPI STATICI E
63
FINAL
...
frame :
...
main
10
10
frame :
main
args
9
args
null
10
...
main.
null
Disallocazione
...
Non esistendo ulteriori istruzioni signicative al di sotto della riga 10, il frame di
main viene
...
frame :
main
10
10
args
null
10
args
null
10
...
...
MetodoArgomentoRisultato.java,
4.4
Campi statici e
final
Le variabili locali al frame attivo non sono le uniche disponibili per l'utilizzo da parte dei metodi. Le variabili statiche,
denibili in una classe, possono essere usate come memoria condivisa cui pi metodi possono accedere per leggere o
per scrivere. Campi condivisi ma in sola lettura sono quelli statici di natura
Vediamo la natura dei campi statici, grazie al sorgente
memoria:
final.
VarStatiche.java, di
64
CAPITOLO 4.
18
20
22 }
Rispetto a quanto visto sinora, la novit sta nella necessit di gestire una nuova zona di memoria.
L'interpretazione della classe
VarStatiche
DELTA
statica
...
1
DELTA
statica
Static memory
Frame stack
main.
Trovandolo, lo interpreta,
allocando lo spazio necessario per i parametri formali e le variabili. La situazione al termine dell'interpretazione della
riga 7 diventa:
...
DELTA
1
0
0
main
statica
args
null
Static memory
Frame stack
mA
0,
presente
nel frame attivo. L'interpretazione della chiamata modica la congurazione della memoria come segue:
...
x
0
mA
1
DELTA
statica
Static memory
args
null
main
Frame stack
nella quale il valore del parametro attuale gi stato assegnato al parametro formale.
Interpretare la riga 13 richiede la valutazione dell'espressione a destra del simbolo
DELTA.
x,
sia
DELTA
0.
DELTA
1.
4.4.
CAMPI STATICI E
65
FINAL
...
x
1
mA
1
DELTA
statica
*
x
Static memory
args
null
main
Frame stack
statica
DELTA.
statica,
sia
in
Nessuna di esse appartiene al frame attivo, quindi vanno cercate nella memoria statica la quale contiene entrambi
i campi da cui recuperare
1,
rispettivamente.
...
x
1
mA
1
DELTA
statica
*
x
Static memory
args
null
main
Frame stack
mA
ha modicato il valore di
statica,
mA,
...
x
1
1
DELTA
statica
*
x
Static memory
main
args
null
Frame stack
e l'interpretazione rientra alla riga indicata dalla prima componente della cella
Siccome la riga 8 non contiene null'altro se non la chiamata a
contiene la chiamata
mB
mA
del
main,
ovvero alla 9.
main,
mA.
66
CAPITOLO 4.
...
5
DELTA
mB
1
DELTA
statica
Static memory
0
main
args
null
Frame stack
nella quale l'indirizzo della riga in cui rientrare al termine della chiamata in
final DELTA
gi stata interpretata.
attivo di
mB.
x + y.
3.
statica + DELTA. Entrambi i nomi vengono cercati nel frame
attivo di mB. DELTA vi appartiene e contiene il valore 5, distinto, in questo caso, dal valore del campo statico omonimo,
presente nel preambolo della classe. Il nome statica esiste solo tra quelli dei campi statici e contiene il valore ottenuto
dopo il precedente accesso del metodo mA.
x
diventa quindi
DELTA
mB
1
DELTA
statica
Static memory
0
main
args
null
Frame stack
L'osservazione fondamentale la condivisione del campo statico di nome
statica
l'unico modo che, eventualmente, impareremo per condividere informazioni tra metodi distinti.
Terminata l'interpretazione della riga 20, il frame di
main,
mB
DELTA
statica
DELTA
Static memory
args
null
Frame stack
La nota nale sul signicato del modicatore
final.
volta ssato non pu cambiare durante l'interpretazione per eetto di una assegnazione.
codice di una classe compare, ad esempio,
classe pu comparire un'assegnazione
Equivalentemente, se nel
4.5.
VarStatiche.java
67
il programma java che, attraverso la stampa dei valori nelle variabili, evidenzia quelle
4.5
Per denizione, la signature di un metodo comprende nome ed elenco dei tipi dei parametri formali. L'estensione della
dichiarazione.
signature serva a distinguere tra le dichiara-
4
6
8
10
12
14
16
18
20
22
24
m.
prese le dichiarazioni due a due, le liste dei parametri formali dieriscono o per numero di componenti, o per tipi,
che occupano la stessa posizione nella lista: ad esempio, il tipo nella prima posizione della signature
boolean)
main,
M(int,
M(float, boolean).
...
c
main
false
a
args
null
La riga 22 un'assegnazione che contempla la valutazione di espressioni, usate come parametro attuale di
chiamata alla corretta dichiarazione di
false.
e la
int
il
seconda
relativo
alla dichiarazione che troviamo alla linea 8. La congurazione della memoria, ad allocazione completata, senza aver
interpretato la linea 9, la seguente:
68
CAPITOLO 4.
...
c
false
*
c
main
false
a
args
null
22
...
2
false
*
c
main
false
a
args
null
22
Il valore in
...
a
1
m
false
10
*
c
main
false
a
args
null
22
In essa vediamo che riprenderemo l'interpretazione dalla linea 10, potendo usare il valore
m,
appena interpretato. Interpretando sino in fondo le righe 10 e 11, appena prima della disallocazione del frame attivo,
la congurazione della memoria diventa:
4.5.
69
...
a
false
10
*
c
false
main
args
null
3
22
main.
nel frame attivo stato scritto nella opportuna componente della cella
del
otteniamo:
...
a
false
10
*
c
false
main
args
null
3
22
Riprendiamo dalla linea 22, come indicato dalla componente della cella
appena rientrati per assegnare a
il valore
*.
main
in cui siamo
La congurazione diventa:
...
a
/
3
false
16
main
float.
false
a
args
null
22
alla linea 23. Questa volta, il primo parametro attuale una costante il cui
return c;
a linea 16 :
m,
70
CAPITOLO 4.
...
a
990
false
1000f
main
false
a
args
null
23
990
il valore di
di tipo
float,
ma quello di
int
risultato dell'espressione condizionale alla riga 15. Senza di essa, il compilatore segnala una possibile perdita di
precisione.
Il valore
990
m.
main
...
a
/
990
false
1000f
main
Il valore
990
false
a
args
null
23
990
non viene utilizzato per alcuna assegnazione e possiamo procedere con la disallocazione nale:
4.6.
JELIOT
71
...
a
/
990
false
1000f
MetodoOverloading.java
false
a
args
null
23
990
il programma java che, attraverso la stampa dei valori nelle variabili, evidenzia
4.6
Jeliot
Un utile interprete visuale per vericare la comprensione del meccanismo di gestione della memoria da parte della
JVM,
ed in particolare del modo in cui il frame stack evolva, durante l'interpretazione di metodi ricorsivi,
Jeliot
con cui, ad esempio, si pu osservare la gestione della memoria durante quando si fa emergere il massimo in un array.
72
CAPITOLO 4.
Capitolo 5
parzialmente corretto e
termina.
Riconsideriamo un algoritmo che risolva il problema QRDIP, proposto nell'Esercizio 8 ed il cui scopo sia calcolare
quoziente e resto della divisione tra due numeri naturali:
1:
2:
3:
4:
5:
6:
7:
8:
//
di
//
d contiene un valore
s contiene un valore
r = d;
q = 0;
while (r >= s) do
r = r - s;
q = q + 1;
di
N
N
end while
Cosa succede se
requires d >= s;
requires s != 0;
5.1
ed
SIDP uno dei problemi usati sinora come come riferimento. Esso richiede di sommare due numeri interi, iterando
successore e predecessore. Sappiamo che risolto dal semplice algoritmo:
1: // m contiene un valore
2: // n contiene un valore
3: ris = m;
4: i = n;
5: while (i > 0) do
6:
ris = ris + 1;
7:
i = i - 1;
di
di
N
N
74
CAPITOLO 5.
8: end while
Perch l'algoritmo dato per risolvere SIDP termina per una qualsiasi coppia di valori in
ed
n?
La risposta che orienti nella giusta direzione leggermente meno immediata di quel che si possa immaginare:
l'iterazione modica il valore
cala strettamente e
Siccome
N.
minimo presente in
N,
ovvero il valore
0.
ik
N,
indichi il valore di
Dimostrazione che
i cala strettamente. Deniamo il predicato T (k) ik+1 < ik che formalizza il decrescere
k . Lo scopo dimostrare (T (0) (k.T (k) T (k + 1))) k.T (k).
caso base, T (0) vero perch i1 = i0 1 < i0 .
il caso induttivo, assumiamo che T (k) sia vero, cio che ik+1 < ik . Allora, la tesi ik+2 < ik+1 vera perch
del valore di
Nel
Per
al crescere di
ik+1 1 < ik 1
ik+1 < ik
Dimostrazione che
N.
Lo scopo dimostrare
ik > 0 .
(5.1)
5.2
ik+1 N
ik 1 N
ik N
Lo scopo dimostrare la terminazione del seguente algoritmo per SIDP, alternativo a quello nella precedente sezione:
1:
2:
3:
4:
5:
6:
7:
8:
//
di
//
m contiene un valore
n contiene un valore
ris = m;
i = 0;
while (i < n) do
ris = ris + 1;
i = i + 1;
di
N
N
end while .
La strategia di dimostrazione sar analoga, ma non perfettamente identica, a quella usata nella precedente Sezione 5.1.
Dimostreremo che:
l'iterazione modica un valore
iterazione:
cala strettamente e
N (i),
inizialmente in
N,
che dipende da
5.2.
Siccome
75
N.
N (i) assume solo valori in N e cala ad ogni iterazione, quest'ultima dovr terminare, perch N (i)
0.
l'osservazione dei meccanismi di variazione delle variabili nel corpo di una iterazione.
Nel caso in esame deniamo:
N (i) n i .
(5.2)
Il motivo della scelta dovrebbe essere evidente: sembra essenzialmente ovvio dimostrare per induzione che (5.2) cali
ad ogni iterazione completata e che appartenga sempre ad
Procediamo per induzione sul numero
eseguito completamente
iterazioni, con
N.
k di iterazioni completate,
0 k.
assumendo che
ik
indichi il valore di
i dopo aver
Dimostrazione che
N (i) cala strettamente. Deniamo il predicato T (k) N (ik+1 ) < N (ik ) che formalizza
N (i) al crescere di k . Lo scopo dimostrare (T (0) (k.T (k) T (k + 1))) k.T (k).
T (0) vero perch:
il
N (i1 ) = n i0 1
=nn1
= 1
<0
=nn
= n i0
= N (i0 ) .
Per il caso induttivo, assumiamo
T (k)
vero, cio
Allora, la tesi
vera
N (ik+2 ) = n ik+2
= (n ik+1 ) 1
= N (ik+1 ) 1
ip. induttiva
< N (ik ) 1
= n (ik + 1)
= n ik+1
= N (ik+1 ) .
Dimostrazione che
di
n ik
ad
N.
N (i) N .
Deniamo il predicato
Lo scopo dimostrare
V (0)
vero perch
n > ik .
(5.3)
altrimenti non sarebbe possibile aver interpretato il corpo dell'iterazione, completando almeno una iterazione. Valgono,
perci le seguenti relazioni:
N (ik+1 ) N
equivalente a
n ik+1 N
istruzione 7
(n ik ) 1 N
n ik N
equivalente a
N (ik ) N
76
CAPITOLO 5.
5.3
La terminazione di un algoritmo dipende da quella delle iterazioni in esso eventualmente presenti. La forma sintattica
di una iterazione generica :
1: while (C) do
2:
B
3: end while .
Assumiamo che l'argomento
almeno
una volta. Sotto questa ipotesi, condizione necessaria, ma non suciente, anch il ciclo termini, che le istruzioni
in
C.
0,
allora
C,
ma
i > 0
Se la condizione suciente soddisfatta, per dimostrare la terminazione, occorre denire una opportuna misura
tale che:
B,
N.
5.4
0.
N,
N,
N.
Ovvero, data
pu terminare quando il
0.
Richiamiamo qui per comodit l'algoritmo per QRDIP dato come soluzione nell'Esercizio 8.
alcune assunzioni sui valori contenuti in
1:
2:
3:
4:
5:
6:
7:
8:
9:
N , se essa
N debba
ed
d contiene un valore di N
s contiene un valore di N \ {0} <--// d s <--- NOTARE
r = d;
q = 0;
while (r >= s) do
r = r - s;
q = q + 1;
In esso aggiungiamo
s:
//
//
NOTARE
end while
N (r) = r .
Procediamo per induzione sul numero
eseguito completamente
iterazioni, con
k di iterazioni completate,
0 k.
Dimostrazione che
(5.4)
assumendo che
rk
indichi il valore di
r dopo aver
Deniamo il predicato
scopo dimostrare
il
N (r1 ) = r1
= r0 s
ipotesi
s>0
< r0
= N (r0 ) .
Per il caso induttivo, assumiamo
T (k)
vero, cio
Allora, la tesi
N (rk+2 ) = rk+2
= rk+1 s
< rk+1
= N (rk+1 ) .
ipotesi
s>0
vera
5.4.
77
Dimostrazione che N (r) N . Deniamo il predicato V (k) rk N. Lo scopo dimostrare (V (0) (k.V (k)
V (k + 1))) k.V (k).
Nel caso base, V (0) vero perch r0 N grazie all'ipotesi r0 = d N.
Per il caso induttivo, assumiamo V (k) vero, cio rk N, con k > 0. Siccome k > 0, l'iterazione stata completata
almeno una volta. Questo implica:
rk s ,
(5.5)
altrimenti non sarebbe possibile aver interpretato il corpo dell'iterazione, completando almeno una iterazione. Valgono,
perci le seguenti relazioni:
N (rk+1 ) N
rk+1 N
rk s N
rk N
N (ik ) N
Nota 3
In [Wir73, Capitolo 6]. viene trattato il problema della terminazione degli algoritmi. Un esempio sviluppa la terminazione di QRDIP, fornendo
condizione (a) if
N (d, s) d s, diversa dalla nostra. Osserviamo che tale misura richieda di rilassare la
N > 0 a pagina 27, enunciata come parte del criterio generale di terminazione,
is satised, then
Esercizio 22 (Terminazione di problemi di riferimento e varianti) Dimostrare la terminazione dei seguenti algoritmi:
78
CAPITOLO 5.
Capitolo 6
impera. Essa consiste nello scomporre un problema dato in modo che esso, il problema, si ripresenti, ma in situazioni
pi semplici da risolvere. Una volta ottenute le soluzioni alle versioni pi semplici, le si compongono opportunamente
per ottenere la soluzione alla versione meno banale del problema dato.
Immaginiamo di volere denire una funzione
f : N A,
dove
un insieme
f (0) = a
f (n + 1) = E(f (n))
a un elemento di A e con la notazione E(f (n)) si indica che l'espressione E pu utilizzare al suo interno il valore
f (n). Una giusticazione intuitiva di questo schema si pu ottenere considerando la struttura dei numeri naturali: la
funzione f denita per 0 perch la prima clausola dello schema ne fornisce il valore a; supponiamo invece che k sia
un numero positivo, e che quindi k = n + 1 per qualche numero naturale n. Si pu immaginare di avere gi calcolato il
valore di f (n) (la funzione f viene calcolata dal basso, partendo dall'argomento 0), e si pu quindi calcolare E(f (n))
che d il valore di f (n + 1).
dove
Capitolo 7] parla del ragionamento induttivo che sta alla base della progettazione di programmi ricorsivi.
6.1
6.1.1
Fattoriale
n!
di
n.
n!
elencare
n! = n (n 1) (n 2) (n (n 2)) (n (n 1)) .
Se ci chiediamo quanto valga
di
(n 1)!,
ponendo
n 1 = m,
(6.1)
al posto
n:
m! = m (m 1) (m 2) (m (m 2)) (m (m 1)) .
Sostituendo
n1
ad
(6.2)
n.
(6.3)
n! = n (n 1)! .
(6.4)
80
CAPITOLO 6.
In 6.4 il problema di calcolare il valore
n!
n 1.
(n 1)!.
(n 1)!, sia n, sappiamo anche risolvere il secondo problema
che consiste nel calcolare n (n 1)!, ovvero n!.
Si dice che abbiamo ridotto il calcolo di n! a quello di (n 1)!.
Si potrebbe avere l'impressione che il meccanismo che stiamo impostando non sia ecace perch senza ne.
Sperimentalmente, possibile rendersi conto che questo non sia il caso.
Supponiamo, ad esempio, di voler calcolare concretamente il valore
3!,
Per denizione,
3!
Calcoliamo, quindi,
Per denizione,
2!
Calcoliamo, quindi,
Per denizione,
1!
Calcoliamo, quindi,
0!
0 (1) = 0,
Il valore di
richiede di calcolare
2!.
2!,
lo moltiplichiamo per
ed otteniamo
3!.
1!.
1!,
lo moltiplichiamo per
ed otteniamo
2!.
0!.
0!,
lo moltiplichiamo per
ed otteniamo
1!.
2!.
richiede di calcolare
1!.
richiede di calcolare
0!.
0!
0! = 0 (0 1) =
1, valore
deve essere
0! = 1,
possiamo, passo dopo passo, eseguire tutte le moltiplicazioni rimaste in sospeso nei passi
precedenti.
Il valore
1!
1 0! = 1 1 = 1.
Il valore
2!
2 1! = 2 1 = 2.
Il valore
3!
3 2! = 3 2 = 6
ed il calcolo concluso.
3! = 3 2!
= 3 (2 1!)
= 3 (2 (1 0!))
= 3 (2 (1 1))
= 3 (2 1)
=32
=6 .
n! in forma ricorsiva, come
(
1
se x = 0
x! =
.
x (x 1)! se x > 0
segue:
(6.5)
La denizione ricorsiva perch la funzione che stiamo denendo, compare nella denizione stessa, ma applicata a
valori decrescenti dell'argomento.
La denizione 6.5 si presta alla traduzione immediata nel programma
comodit riportiamo il sorgente, in forma essenziale:
9
11
13 }
FattorialeRec.java
6.1.
fatt,
81
l'impressione di una denizione mal fondata. Un primo mezzo per convincerci del contrario simulare una interpretazione. Riportiamo una sequenza rilevante di istantanee del frame stack conseguente alla chiamata
fatt(3);
del
main:
...
...
2
fatt
...
fatt
nil
args
main
fatt
nil
args
main
nil
args
main
...
...
...
n
fatt
fatt
*
fatt
n
1
fatt
fatt
*
fatt
n
2
fatt
fatt
*
fatt
nil
args
3
fatt
main
nil
1
7
*
n
...
...
...
0
args
main
3
nil
args
main
*
n
fatt
7
*
n
*
n
*
n
fatt
fatt
nil
nil
args
main
*
args
main
3
nil
args
main
main
fatt
disallocato.
Esercizio 23 (
Stack
della chiamata
6.1.2
Quadrato
Esempio 5 (La funzione quadrato) Si pu denire ricorsivamente il quadrato di un numero naturale mediante le clausole:
q(0) = 0
q(n + 1) = q(n) + 2 n + 1
82
CAPITOLO 6.
Vediamo che le clausole precedenti deniscono eettivamente la funzione desiderata, dimostrando per induzione che la
propriet
q(n) = n n
(Base dell'induzione)
q(0) = 0
(per denizione)
=00
(Passo indutttivo)
q(n + 1) = q(n) + 2 n + 1
(per denizione)
=nn+2n+1
= (n + 1) (n + 1)
q(n)
q(5) = q(4 + 1)
= q(4) + 2 4 + 1
= q(3 + 1) + 2 4 + 1
= q(3) + 2 3 + 1 + 2 4 + 1
= q(2 + 1) + 2 3 + 1 + 2 4 + 1
= q(2) + 2 2 + 1 + 2 3 + 1 + 2 4 + 1
= q(1 + 1) + 2 2 + 1 + 2 3 + 1 + 2 4 + 1
= q(1) + 2 1 + 1 + 2 2 + 1 + 2 3 + 1 + 2 4 + 1
= q(0 + 1) + 2 1 + 1 + 2 2 + 1 + 2 3 + 1 + 2 4 + 1
= q(0) + 2 0 + 1 + 2 1 + 1 + 2 2 + 1 + 2 3 + 1 + 2 4 + 1
=0+20+1+21+1+22+1+23+1+24+1
=1+2+1+4+1+6+1+8+1
= 25
Esempio 6 Dato un insieme
f (n) : A A,
Si vede che
(
f (0, y, z)
=zy
f (x + 1, y, z) = z + f (x, y, z)
tale che, per ogni
x, y, z N:
f (x, y, z) = z (x + y)
f (0)
=1
f (n + 1) = f (n) + f (n)
n N:
f (n) = 2n .
6.2.
83
f (0) =
0
f (n + 1) = (n + 1) + f (n)
n N:
f (n) =
n
X
i=0
s(0) = 1
s(n + 1) = 2/s(n)
tale che, per ogni
Esercizio 28 Il numero
A(n)
di modi in cui
A(1) = 1
A(n + 1) = A(n) (n + 1)
Si dimostri per induzione che, per ogni
n 1, A(n) = n!.
di
FibonacciRec.java.
di correttezza.
6.2
6.2.1
McCarthy91.java
interessante per il suo comportamento oscillatorio che costituisce una sda nei confronti degli
6.2.2
Funzione di Ackermann
Ackermann.java interessante per i suoi legami con la teoria della computazione e per i valori che pu assumere:
essi
crescono cos rapidamente che per calcolarli si ottiene uno stack overow error, ovvero si esaurisce lo spazio disponibile
per il frame stack.
6.3
6.3.1
La seguente funzione
f (1) = 1
f (2m) = f (m) + f (m)
f (2m + 1) = f (m) + f (m) + 1
tra numeri naturali costituisce uno schema di riferimento per impostare programmi ricorsivi che, in linea di principio,
possano essere interpretati parallelamente.
Tra poco dimostreremo che
x, f (x) = x.
L'aspetto
rilevante, per, lo schema in accordo col quale essa opera e che pu essere rappresentato con strutture ad albero
come le seguenti:
84
CAPITOLO 6.
f (1) = 1 f (1) = 1
f (1) = 1 f (1) = 1
f (1) = 1 f (1) = 1
f (1) = 1 f (1) = 1
nelle quai ogni sotto-albero produce il risultato indipendentemente dagli altri sotto-alberi.
Dimostriamo ora la propriet
n.P (n),
in cui:
f,
(6.6)
possiamo dimostrare:
Caso induttivo.
P (1)
equivale a dimostrare
f (1) = 1,
(6.7)
f.
Necessariamente,
se
n>1
pari, da
se
n>1
dispari, da
6.3.2
Sia
n > 1.
f (m) = m
otteniamo
f (m) = m
otteniamo
Funzione successore
la seguente funzione:
g(0) = 1
g(n) = g(dn/2e 1) + g(bn/2c) .
Al contrario della funzione
problema di discriminare se l'argomento sia pari o dispari. Al contrario, adiamo alle operazioni
dxe
bxc
il compito
n.Q(n)
in cui:
g,
(6.8)
possiamo dimostrare:
Caso induttivo.
ssato di
consegue
Q(0)
equivale a dimostrare
g(0) = 1,
(ipotesi
induttiva)
= dn/2e 1 + 1 + bn/2c + 1
= dn/2e + bn/2c + 1
(propriet
=n+1 .
Esercizio 29 (Funzioni parallelizzabili)
1. Data la funzione:
f (0) = 1
f (2m + 1) = 2 f (m)
f (2m) = 2 f (m) 1
in cui
m 0,
g.
(6.9)
dideebc)
6.4.
85
2. Data la funzione:
f (0) = 0
f (1) = 1
f (n) = f (dn/2e) + f (bn/2c)
in cui
m 0,
3. Data la funzione:
1
f (l, r) = f (l, dn/2e) + f (bn/2c, r) 1
l, r 0,
se
se
se
l+r =2l
l + r dispari
l + r pari
l r + 1.
4. Data la funzione:
0
f (l, r) = f (l, dn/2e) + f (bn/2c, r) + 1
l, r 0,
se
se
se
l+r =2l
l + r dispari
l + r pari
l r.
5. Data la funzione:
l
f (l, r) = f (l, dn/2e) + f (bn/2c, r)
in cui
dei
6.4
essa calcola
se
se
se
Pr
l+r =2l
l + r dispari
l + r pari
f (l, r)
Il programma
sXYRec.java
n, m N. P (m, n),
in cui:
P (m, n) sXY(m, n) = m + n .
Procedendo per induzione sul valore del secondo argomento di
sXY,
(6.10)
possiamo dimostrare:
Fissiamo
Caso induttivo.
(6.11)
m + n + 1,
Fissiamo
assumendo
sXY(m, n + 1) = 1 + sXY(m, (n + 1) 1)
= 1 + sXY(m, n)
= 1 + (m + n)
= m + (n + 1) .
86
CAPITOLO 6.
corsivo che risolva il problema MSDP, ovvero la moltiplicazione tra due numeri naturali, usando solo somme e
predecessore. Dimostrare la sua correttezza parziale per induzione.
(mXYRec.java una possibile soluzione.)
Scrivere un algoritmo ricorsivo che risolva il problema PPID, ovvero che, dati due numeri naturali
di
elevato a
b,
a e b, calcoli il valore
sotto l'ipotesi di saper solo calcolare il prodotto tra due numeri ed il predecessore di un numero.
Scrivere un algoritmo ricorsivo che risolva il problema QPNP, ovvero che calcoli il quadrato di un numero intero
supponendo di non saper calcolare la moltiplicazione tra numeri arbitrari ed assumendo che se
1.
n = 0,
allora
n0
6.4.1
n,
valga
Torre di Hanoi
Il problema della Torre di Hanoi su Wikipedia ha una natura squisitamente combinatoria. Il metodo Java:
1
3
5
7
l'esistenza di un metodo
al piolo destinazione
muoviDisco
d,
che
che
che
aus
s,
Il motivo per cui la descrizione dell'algoritmo estremamente compatta dipende proprio dal fatto che il problema
in cui occorra spostare
n1
dischi e che
tali problemi pi piccoli siano istanze della Torre di Hanoi stessa, ma con un uso dierente dei pioli.
Leggiamo il meccanismo di soluzione assumendo
n = 3.
a quella nale a destra, nella quali A il disco con diametro inferiore, B quello intermedio e C il pi grande,=:
A
......
A
B
Ragioniamo per come se il disco C non esistesse. Sotto questa ipotesi, ci troviamo a dover risolvere la Torre di
Hanoi con soli due dischi. In particolare, possiamo decidere di risolverlo, usando il piolo 2 come appoggio ed il 1 come
piolo nale; risolviamo quindi il problema
muoviTorre(2,0,1,2).
colpo dalla congurazione a sinistra, che anche quella iniziale, a quella a destra, seguenti:
A
...
Nella congurazione illustrata ci rendiamo conto che il disco C libero di essere mosso dalla sua sede al piolo 2, perch
nulla lo impedisce. Possiamo, quindi passare dall'ultima congurazione che abbiamo supposto di sapere raggiungere
a quella successiva, con C al piolo 2:
|
6.4.
87
L'ultima congurazione ci ripropone di risolvere la Torre di Hanoi con due dischi che, dal piolo centrale 1 devono
essere spostati al piolo 2, usando il piolo 0 come appoggio; risolviamo quindi il problema
muoviTorre(2,1,3,1).
...
Quando la soluzione alla Torre di Hanoi ovvia? Quando c' un solo disco da spostare dal piolo cui inlato al
piolo che in quel frangente, ovvero nella congurazione cui ci troviamo, assume il ruolo di destinazione.
n sia m.
2n 1 =
Pn
i
i=0 2 .
TorreHanoi.java
muoviTorre,
6.4.2
3
5
per induzione sul valore assunto dal parametro formale. Ovvero, se ssiamo
P (n) sommatoriaTraZeroEd(n) =
n N. P (n),
n
X
in cui:
i ,
(6.12)
i=0
procediamo per induzione sul valore dell'argomento di
sommatoriaTraZeroEd,
per dimostrare:
vale
0,
Dimostrare
(6.13)
Caso induttivo.
i,
Sia
assumendo
sommatoriaTraZeroEd(n + 1) = (n + 1) + sommatoriaTraZeroEd(n)
n
X
= (n + 1) +
i=0
= (n + 1) + n + (n 1) + . . . + 2 + 1 + 0
=
n+1
X
i .
i=0
La classe
SommatoriaPrimiInteriRec.java
88
CAPITOLO 6.
6.4.3
Lo scopo di questa sezione insistere sul fatto che le dimostrazioni di correttezza parziale si possono applicare anche
a metodi ricorsivi che eseguono operazioni di input ed output di dati.
La classe
LeggeStampaEnneInteriRec.java
2
4
6
8
n1
per
n,
x,
assunto da
x,
dimostriamo
n N. P (n),
valori interi.
in cui:
ha letto e stampato
i-esimo
intero)
(6.14)
ovvero, dimostriamo:
Sia
In tal caso
leggeStampaInteri(0)
i-esimo
(6.15)
intero).
non stampa
P (0)
situazione nella quale sono stati letti e stampati i numeri interi che contraddistinguiamo per mezzo di indici compresi
tra 0 incluso e 0 escluso. Nell'intervallo descritto non esistono indici. Quindi nulla stato stampato. In pi,
vero proprio perch l'intervallo descritto vuoto, quindi
0i<0
P (0)
vera.
Caso induttivo.
1)
ha letto e stampato
Guardiamo al
0-esimo
1-mo
2-do
intero
intero
intero
.
.
.
2)-esimo
intero
1)-esimo
intero
leggeStampaInteri(n + 1)
vn+1 .
i precedenti
0-esimo
1-mo
2-do
intero
intero
intero
.
.
.
2)-esimo
intero
1)-esimo
intero
ha letto e stampato
i-esimo
intero).
stamp
6.5.
RICORSIONE DI CODA
89
induzione per
il cui metodo
legge
6.5
n 1, usare il principio di
EnneInteriMaxRec.java
Ricorsione di coda
Questo argomento inserito per completezza. La ragione per parlare di ricorsione di coda una possibile osservazione
sul costo che gli algoritmi ricorsivi hanno, in termini di occupazione di memoria.
dimensione, opportunamente misurata, dei dati di input, cresce, anche pi che proporzionalmente, lo spazio necessario
ad allocare la pila di frame.
possibile non rinunciare n alla denizione ricorsiva di metodi, n ad un buon utilizzo della memoria mirato a
contenere l'estendersi del frame stack, conseguente alle chiamate ricorsive.
La strategia consiste nello scrivere metodi ricorsivi di coda :
Un metodo ricorsivo detto di coda se denito in modo che ogni richiamo
ricorsivo non sia seguito da alcuna ulteriore istruzione da interpretare.
Il confronto tra metodi ricorsivi che calcolino
n!
2
4
6
2
4
6
Il metodo
fattCoda
per passarlo al chiamato, dopo le opportune manipolazioni. Il nuovo parametro sposta il calcolo della moltiplicazione
per
fatt
Una dimostrazione di correttezza parziale un modo per vedere il meccanismo all'opera. Dimostriamo, quindi,
m N, n N\{0}. P (m, n)
in cui:
P (m, n) fattRec(m, n) = m! n .
Una volta ssato
n 1,
m,
(6.16)
fattCoda,
per dimostrare:
Per denizione,
Caso induttivo.
fattCoda(0, n)
restituisce
(6.17)
n = 1 n = 0! 1.
Per denizione,
fattCoda(m + 1, n) = fattCoda((m + 1) 1, (m + 1) n)
= fattCoda((m + 1) 1, (m + 1) n)
= (m + 1)! n .
fattCoda(m, 1) = m!.
FattorialeRecCoda.java una classe completa
In particolare, otteniamo
coda.
Nonostante l'introduzione del nuovo parametro formale
a, se non peggiore di, quello per
fattCoda(3,2)
fatt.
in ipotetico, metodo
f,
fattCoda
analoga
main:
90
CAPITOLO 6.
...
3
...
fattCoda
1
...
/
fattCoda
nil
args
main
fattCoda
args
main
ind.1
ind.
nil
nil
args
main
ind.
...
...
...
fattCoda
6
/
fattCoda
/
6
ind.1
ind.1
1
/
nil
fattCoda
ind.1
fattCoda
ind.1
nil
args
args
main
ind.
ind.
...
...
...
ind.1
ind.1
ind.1
ind.1
/
nil
ind.1
ind.1
fattCoda
ind.1
nil
args
main
nil
args
ind.1
ind.1
main
ind.
*
main
fattCoda
1
ind.1
ind.1
fattCoda
args
main
ind.
*
fattCoda
nil
fattCoda
ind.1
ind.1
*
fattCoda
fattCoda
*
fattCoda
fattCoda
*
args
main
ind.
ind.
Riguardo ai vantaggi oerti dalla ricorsione di cosa, quel che occorre realizzare quanto segue:
il solo impostare
memoria.
fattCoda
come metodo di coda non suciente ad ottenere una gestione parsimoniosa della
6.6.
fattCoda
91
in programmi iterativi equivalenti che, proprio perch iterativi, non estendono l'occupazione della memoria da
parte del frame stack oltre il necessario.
Il metodo iterativo, che automaticamente possiamo ricavare da
fattCoda
2
4
Osserviamo che:
x - 1
al parametro formale
in un'assegnazione
x = x - 1
x * f
al parametro formale
in un'assegnazione
f = x * f
siccome il valore di
1,
x * f
x -
Dovrebbe essere intuitivo sia come procedere in maniera automatica, sia che vale la pena denire metodi ricorsivi
di coda in modo che il parametro che guida la ricorsione sia l'ultimo elencato. Con questo ultimo accorgimento, non
c' il problema di dover ordinare le assegnazioni diversamente dagli argomenti.
ed
n,
(Soluzione possibile
Scrivere una classe con un metodo ricorsivo di coda che, presi due numeri
sXYRecCoda.java.)
Scrivere una classe con un metodo ricorsivo che calcoli il prodotto tra due numeri naturali.
(Soluzione possibile
mXYRecCoda.java.)
Scrivere una classe con un metodo ricorsivo di coda che calcoli l'elevamento a potenza di un numero naturale per un
altro naturale.
(Soluzione possibile
Scrivere una classe con un metodo ricorsivo di coda che, presi due numeri naturali
divisione intera di
(Soluzione possibile
eXYRecCoda.java.)
per
ed
n,
RestoRecCoda.java.)
n 1,
legga
il programma sia ricorsivo di coda va organizzato in modo che sia l'ultimo passo della ricorsione a restituire la media.
(Soluzione possibile
6.6
MediaRecCoda.java.)
tale che
Se la propriet
P (n).
k < n. P (k).
Una conseguenza
fondamentale del principio del minimo la seguente propriet, che si esprime dicendo che la relazione d'ordine stretta
<
ben fondata:
(6.18)
92
CAPITOLO 6.
{n0 , n1 , n2 , . . .}
L'importanza di questa propriet dei numeri naturali risiede tra l'altro nell'utilizzo che se ne pu fare per dimostrare
la terminazione di programmi. Si ricordi che implicitamente questa propriet era gi stata utilizzata, per esempio,
nella dimostrazione della correttezza totale del programma per la divisione intera. La terminazione del ciclo sul quale
alla congurazione
di numeri naturali
progressiva
se
Prog(P )
Se
Prog(P ),
allora
n N.P (n).
Usiamo subito questa formulazione del principio di induzione nella dimostrazione di correttezza del seguente metodo
ricorsivo:
1 static int filter (int[] vet, int k, int sx, int dx){
3
5
7
9
11 }
Proposizione 1
Per ogni vettore di interi
maggiori di
vet
ed ogni intero
sx
k , filter (vet,k,sx,dx)
dx di vet.
vet
e la posizione
k = dx sx.
filter(vet,k,sx,(sx+dx)/2)
filter(vet,k,(sx+dx)/2 + 1,dx)
Caso 1:
Abbiamo
e analogamente
Capitolo 7
7.1
array
Nell'Esercizio 4 abbiamo risolto il problema di riorganizzare i valori, inizialmente contenuti in quattro variabili
d,
a, b, c
abcd .
(7.1)
Un modo pi compatto per scrivere 7.1 utilizzare nomi indicizzati per le variabili.
ridenominare
come
a0 , b
come
a1 , c
come
a2
come
a3 .
a, b,
Immaginiamo, infatti, di
ai ai+1
(0 i < 4) .
(7.2)
Una prima conseguenza la compattezza della descrizione che sfrutta la variabilit dell'indice nell'intervallo dato.
Una seconda conseguenza che l'intervallo entro cui
a0 , . . . , a9 ,
arrivare allo stesso insieme di variabili in cui i valori iniziali siano stati risistemati in modo che:
ai ai+1
(0 i < 5) .
(7.3)
La terza conseguenza che la descrizione stessa della riorganizzazione dei valori diventa generalizzabile, perch
dipende dal valore massimo dei valori assumibili dall'indice che usiamo per identicare le variabili. Possiamo scrivere
il seguente algoritmo:
while (i
if ai >
end if
end while
Java fornisce la sintassi per individuare variabili attraverso un indice.
appena dato in righe di codice Java, che, per ora, assumiamo siano parte del metodo
main,
diventa:
94
CAPITOLO 7.
La corrispondenza tra algoritmo e programma quasi uno-a-uno. Gli elementi discordanti stanno nella dichiarazione
dell'array
a:
int[] a = 15, 2, -3, 16, 9;
a.length
al valore esplicito
5.
Per comprendere a fondo quel che succede occorre illustrare la gestione della memoria a fronte dell'introduzione
degli array.
7.2
L'introduzione della struttura dati array richiede una estensione del modello di memoria sviluppato sinora. Alla static
memory e al frame stack occorre ora aancare la zona di memoria identicata come heap.
In generale, la heap deputata a contenere strutture dati la cui dimensione non detto possa essere prevista sin
dalla fase di compilazione.
La motivazione appena addotta per giusticare l'introduzione della heap non sar immediatamente evidente dai
primi esempi.
Riprendiamo il programma precedente:
3
5
7
9
11
13 }
ed interpretiamolo.
Prima dell'interpretazione di riga 2, tranne che per un aspetto, la memoria organizzata come ce la aspettiamo:
...
i
tmp
a
main
args
null
/
*
Heap
Frame stack
Il frame di
main
args, i
ed
a,
sta nella comparsa della zona di memoria heap, per ora vuota.
Al termine dell'interpretazione della riga 2, la memoria si trova nella seguente situazione:
...
int[]
i
tmp
a
main
args
null
/
int.
int[]
[0]
[1]
- 3
[2]
16
[3]
[4]
Heap
valori di tipo
length
15
Frame stack
a:
7.2.
length
L'elemento di indice
nel frame di
95
main
5,
estremi inclusi.
[0],
quello di indice
con etichetta
[1]
e cos via.
usuale usare
i, ma non tmp:
struttura di tipo
int[]
appena allocata.
...
int[]
i
tmp
a
main
args
null
/
length
15
[0]
[1]
- 3
[2]
16
[3]
[4]
*
Heap
Frame stack
L'interpretazione del predicato argomento del costrutto iterativo while produce il valore true siccome i == 0
< a.length - 1 == 4. Segue l'interpretazione dell'espressione a[0] > a[1] che vera perch a[0] == 15 e
a[1] == 2. L'interpretazione delle istruzioni 8, . . . , 11 portano la memoria nella seguente situazione:
...
int[]
tmp
15
main
args
null
/
length
[0]
15
[1]
- 3
[2]
16
[3]
[4]
*
Heap
Frame stack
A questo punto valutiamo nuovamente l'espressione argomento del costrutto iterativo, ovvero
- 1 == 4.
true.
true
reinterpretiamo l'espressione
1 == i < a.length
a[1] > a[2] argomento della sele-
...
int[]
tmp
15
main
args
null
/
length
[0]
- 3
[1]
15
[2]
16
[3]
[4]
*
Heap
Frame stack
true
e possiamo valutare
ottenendo
false.
i
tmp
15
int[]
...
main
args
null
/
length
[0]
- 3
[1]
15
[2]
16
[3]
[4]
*
Heap
Frame stack
true
e possiamo valutare
ottenendo
true.
...
i
tmp
16
main
args
null
/
Frame stack
3 == i < a.length - 1 == 4
Le istruzioni 8, . . . , 11 restituiscono:
int[]
2 == i < a.length - 1 == 4
length
[0]
- 3
[1]
15
[2]
[3]
16
[4]
*
Heap
96
CAPITOLO 7.
7.3
false.
Creazione di
4 == i < a.length - 1 == 4
for,
modello di memoria.
final int L = 2;
int j;
int[] e;
e = new int[L];
for (j = 0; j < L; j++) {
e[j] = SIn.readInt();
}
for (j = 0; j < e.length; j++)
System.out.print(e[j]);
13 }
Codice interpretato
final int L = 2;
int j;
int[] e;
j,
...
e
j
main
args
null
/
*
Heap
Frame stack
In
int[]
e = new int[L];
...
int[].
length
[0]
[1]
j
main
args
null
/
*
Heap
Frame stack
-1,
...
main
length
-1
[0]
[1]
*
Heap
Frame stack
e[j] = SIn.readInt();
j++;
args
null
/
int[]
j = 0;
e[j] = SIn.readInt();
j++;
1,
97
...
main
j == 2
[0]
[1]
Heap
si procede all'interpretazione delle istruzioni alle linee 11 e 12, il cui eetto di reinizializzare
a.
in
Alla linea 13, la situazione della memoria identica a quella della linea 10.
length
-1
Frame stack
Quando
args
null
/
int[]
7.3.
array )
array a
abbiano lo stesso numero di elementi. Dimostrare che la correttezza parziale del seguente codice:
i = 0;
3 while (i < a.length - 1 && uguali) {
5
7
invariante:
// per ogni k, se 0 <= k < i allora a[k]==b[k] e vero perche la premessa e vacua.
4 while (i < a.length && uguali) {
// per ogni k, se 0 <= k < i allora a[k]==b[k] vero per ipotesi (****)
uguali = a[i] == b[i];
if (uguali) {
// (per ogni k, se 0 <= k < i allora a[k]==b[k]) e a[i]==b[i]
// implica che
// per ogni k, se 0 <= k < i+1 allora a[k]==b[k]
i = i + 1;
// per ogni k, se 0 <= k < i allora a[k]==b[k]
// che coincide con (****)
}
6
8
10
12
14
}
16 // Supponiamo uguali == true. In tal caso necesssariamente i == a.length.
// a e b sono diversi.
7.3.1
Perch gli
array
sono nella
final int L;
int[] e;
int j;
L = SIn.readInt();
e = new int[L];
for (j = 0; j < e.length; j++) {
e[j] = SIn.readInt();
}
3
5
7
9
heap ?
98
CAPITOLO 7.
Codice interpretato
final int L;
int[] e;
int j;
...
main
null
L
args
null
/
*
Heap
Frame stack
Il valore di
e.
int[]
L = SIn.readInt();
e = new int[L];
...
length
[0]
main
L
args
null
/
*
Heap
Frame stack
main.
zione di e
e nel frame
dato di tipo primitivo, invece, lo spazio necessario noto durante la compilazione ed possibile
riservare nel frame lo spazio necessario.
int[]
e[j] = SIn.readInt();
j++;
...
1
10
length
[0]
main
L
args
null
/
Frame stack
7.4
Eguaglianza tra
*
Heap
array e aliasing
Per le strutture dati non primitive come gli array necessaria una denizione esplicita di eguaglianza. Il motivo
evidente dall'interpretazione dei metodi nella classe seguente:
8
10
7.4.
12
99
return a;
}
14
int[] a = { 1, 2, 3 };
int[] b = new int[a.length];
int[] c;
arrayClone(a, b);
c = arrayClone(b);
boolean t1 = (a == b);
boolean t2 = (c == b);
boolean t3 = (a == c);
18
20
22
24
}
26 }
Il codice organizzato anche per evidenziare il fenomeno dell'aliasing. Con aliasing si intende l'esistenza di almeno
a e b, anche in frame distinti, tali che un'azione su una, corrisponda ad agire anche sull'altra. Il fenomeno
a e b possono essere reference alla stessa struttura che si trova nello heap.
seguente interpretazione della classe ArrayCopieCloni evidenzia sia l'inecacia dell'operatore == nello
due variabili
Codice interpretato
int[] a = {1,2,3};
int[] b = new int[a.length];
int[] c;
length
3
int[]
...
t3
t2
[0]
[1]
[2]
t1
c
main
int[]
b
a
args
null
/
20
length
[0]
[1]
[2]
length
[0]
[1]
[2]
length
[0]
[1]
[2]
in
veste
Heap
Frame stack
arrayClone(a, b);
...
i
b
a
/
int[]
t3
t2
t1
c
main
int[]
b
a
args
null
20
*
Heap
Frame stack
In
cima
al
frame
riutilizzabile,
stack,
seppure
di
spazio
arrayClone(int[], int[]).
Inoltre, evidente che a e b nel
per
frame di
arrayClone
main. Ovvero, mo-
arrayClone,
la variabile
in
main
b.
100
CAPITOLO 7.
c = arrayClone(b);
i
a
int[]
...
b
/
length
[0]
[1]
[2]
length
[0]
[1]
[2]
length
[0]
[1]
[2]
b
*
int[]
11
t3
t2
t1
b
a
int[]
c
main
args
null
/
20
*
Heap
Frame stack
int[])
arrayClone(in[],
arrayClone(in[]).
boolean t1 = (a == b);
boolean t2 = (c == b);
boolean t3 = (a == c);
i
a
int[]
...
b
/
*
false
t3
false
t2
false
t1
int[]
b
a
int[]
c
main
args
null
20
==
Classe
[1]
[2]
length
[0]
[1]
[2]
length
[0]
[1]
[2]
Heap
7.4.1
[0]
Frame stack
length
*
a
11
t1, t2
Array
Array,
java.utils
tale che
Array.equals,
stesso tipo ne verica l'eguaglianza strutturale che, almeno per array che contengano elementi di tipo base, quella
ovvia: ovvero, due array, ad esempio di tipo
int[],
per elemento.
Array.equals.)
import java.util.Arrays;
2
7.5.
101
8
10
12
14
}
16
int[] a
int[] b
boolean
boolean
20
22
= {1};
= {1};
test1 = ArrayIntEquals(a, b);
test2 = Arrays.equals(a, b);
}
24 }
int[] b = 1;
test1
test2
al variare di
int[] a = 1;
, come segue:
int[] a = null;
int[] b = null;
oppure
int[] a = 1;
int[] b = null;
oppure
int[] a = 1;
int[] b = 1, 2;
oppure
int[] a = 1;
int[] b = 2; .
Disegnare l'evoluzione della memoria, scegliendo tra una o pi delle combinazioni appena date per inizializzare
7.5
Operazioni fondamentali su
7.5.1
b.
array
Ricerca lineare
RicercaLineare.java
per la quale naturale denire il costo, contando il numero di confronti eseguiti prima di
individuare l'elemento, se esiste. Nel caso peggiore occorre visitare tutti gli elementi, ed il costo pari a
- 1
a.length
confronti. Quindi, in generale, il costo della ricerca lineare cresce linearmente con la dimensione dell'array cui
applicata.
7.5.2
Inserimenti e cancellazioni
Inserimento.java, InserimentoTest.java forniscono due soluzioni iterative per inserire il valore e in posizione
p di un array a.
La correttezza parziale di entrambe le soluzioni descritta in Invariante dell'inserimento di un elemento
La seconda soluzione pi eciente della prima, se contiamo il numero di assegnazioni eseguite.
caso si eseguono sia le
a.length
in
b,
sia le
Nel primo
a.length - (p +
1) assegnazioni per spostare verso destra gli elementi, prima dell'inserimento. Il totale delle assegnazioni diventa
a.length + a.length - (p + 1) + 1 = 2 * a.length - p.
Nel secondo caso si eseguono p assegnazioni degli elementi a sinistra di p cui aggiungere la a.length - (p +
1) assegnazioni degli elementi alla sua destra, pi l'inserimento. Il totale diventa p + a.length - (p + 1) + 1
= a.length.
In entrambi i casi, il numero di assegnazioni cresce linearmente al crescere della lunghezza dell'array. Quindi, al
crescere della lunghezza, la dierenza in ecienza perde rilevanza.
Cancellazione.java e CancellazioneTest.java
di indice p in un array a.
102
CAPITOLO 7.
a,
tranne l'ultimo, in
in
a.length - 2
b.
a.length - 2
a,
b.
In entrambi i casi, il numero di assegnazioni cresce linearmente al crescere della lunghezza dell'array.
7.5.3
Filtri
Filtri.java, FiltriTest.java
propriet, da un array.
denito iterativamente.
7.6
array
ArraySemipieni.java il primo esempio ragionevolmente completo di tipo di dato astratto, ovvero di una struttura
che fornisce un universo di elementi su cui operare con un insieme di operazioni predenite. Essa costituita da una
coppia di campi statici ad accesso privato. Proprio perch privati, possibile agire sui campi solo attraverso operazioni
pubbliche rese disponibili dalla classe.
Si pu osservare che se, per un qualche motivo, si renda necessario progettare una classe
parzialmente riempiti, ciascuno libero di avere la propria evoluzione, allora sar ncessario duplicare la classe di array
parzialmente riempiti.
Lo scopo avere due istanze di coppie di campi privati, ciascuna con il proprio nome, per
2
4
6
8
10
12
14
16
18
20
22
24
public class C {
ArraySemipieno0.init(); // prima "istanza"
ArraySemipieno1.init(); // seconda "istanza"
}
Pila
Opportune restrizioni e ridenomine sulla struttura degli array semi-pieni permettono di realizzare il tipo di dato
astratto
PilaInt.java
Esercizio 37
f,
stack
(, )
di caratteri
char,
(())(()(())f
(())(((())f
no.
(ParentesiOKPilaChar.java.)
StrutturaDatiArrayStatico.java e StrutturaDatiArrayStaticoTest.jav
per avere un esempio essenziale di come usare campi statici privati di per codicare tipi di dato astratto.
7.7.
103
array
(Soluzione
non esiste in
valore di
valore
k
-1.
in
a.
Generalizzare
allora il valore
false,
RicercaLineareCoppia.java.)
se il booleano
true,
Se il booleano
false,
(Soluzione
Se il booleano
array
allora l'
allora
array
non occorre in
in
a.
a.
RicercaLineareEsaustivaCoppia.java.)
Denire una classe che realizzi il tipo di dato astratto Multi-insieme, corredato, come da denizione, delle apposite
operazioni su multi-insiemi.
(Proposta, certamente migliorabile, di implementazione
7.7
MultiInsiemiInt.java.)
BufferCircolare.java.)
Ordinamenti ed operazioni su
array ordinati
d'ordine
R,
P : D D
Bubble sort
Progettazione e giusticazione intuitiva del costo quadratico nella dimensione dell'array da ordinare, avendo come
riferimento le versioni iterative e ricorsive in
BubbleJeliot.java.
Selection sort
SelectionSort.java
e Correttezza e costo de
SelectionSort
Insertion sort
104
CAPITOLO 7.
Ricerca dicotomica
RicercaDicotomicaRec.java
come caso particolare dello schema ricorsivo la cui essenza stata raccolta dalla denizione della funzione
f : N N:
f (1) = 1
(7.4)
f (2m) = 2f (m)
(7.5)
f (2m + 1) = 2f (m) + 1 ,
gi introdotta nella Sezione 6.3. Ricordiamo che
non vuoto con
(7.6)
di elementi.
EserciziDicotomici.java,
(Possibili soluzioni in
f,
EserciziDicotomiciProCorrettezza.java.)
Merge.java,
Correttezza del
Merge.java,
Merge sort
per completare il lavoro ad ogni livello il costo globale lineare nella dimensione
Osservando che i livelli dell'albero sono
ln2 n
n(ln2 n)
in
CrivelloEratostene.java
che individua numeri primi tra gli indici di un array di booleani di lunghezza ssata.
CrivelloEratostene.java
in cui il ciclo:
EratosteneAlternativo.java.)
CountingSort.java
In particolare, si pu osservare che il cui costo del Cointing sort il massimo tra il massimo valore contenuto
nell'array
7.8
da ordinare e la lunghezza di
a.
Matrici bidimensionali
Matrici.java.
Problemi decisionali
ProblemiDecisionaliSuMatrici.java
i,
i,
esiste un elemento
i,
i,
m[i][j]
ogni elemento
m[i][j]
m[i][j]
tale che . . . ,
tale che . . . ,
m[i][j]
tale che . . . ,
tale che . . . .
7.8.
MATRICI BIDIMENSIONALI
105
Esercizi
Scrivere un metodo
String toString(int[][] m) che riversi l'intero contenuto della matrice in una stringa,
m su righe diverse devono risultare su righe diverse anche
Scrivere un metodo
ragged,
boolean quadrata(int[][] m)
che restituisca
true
nel caso
altrimenti.
false
(Soluzione possibile in
OperazioniSuMatrici.java.)
Scrivere un metodo
OperazioniSuMatrici.java.)
Scrivere un metodo
OperazioniSuMatrici.java.)
OperazioniSuMatrici.java.)
ne produca
Una matrice ragged (sfrangiata) se contiene righe di lunghezze diverse. Scrivere un metodo
m)
che restituisca
true
(Soluzione possibile in
OperazioniSuMatrici.java.)
nel caso
sia ragged,
false
boolean ragged(int[][]
altrimenti.
OperazioniSuMatrici.java.)
mD
mD che sta per mono dimensionale. Creare una matrice bidimensionale bD con un numero di
nC pressato ed abbastanza righe nella quale copiare, riga per riga, tutte gli elementi di mD. Ad esempio,
l'array {2, 4, 6, 7, 8, 1, 9} e nC ssato al valore 3, allora bD la matrice bidimensionale:
{{2, 4, 6}
{7, 8, 1}
{9, 0, 0}}
nella quale le due occorrenze di
0 sono i valori di default assegnati alle celle di un array, se non altrimenti denite.
mD con un indice, diciamo
i e bD con due indici, ad esempio r e c. I tre metodi si dierenziano l'un l'altro per il modo in cui vengono
aggiornati i valori di r e c, al variare di i. Nel primo metodo non si usa alcuna operazione modulare. Nel
secondo si applicano operazioni modulari per aggiornare correttamente r. Nel terzo, le operazioni modulari sono
usate per aggiornare i valori di entrambi r e c. Si sottolinea che le operazioni modulari permettono di eliminare
l'uso del costrutto selezione if-then-else. (Soluzione MonoBidimensionale.java.)
Per creare
bD
che verichino le previsioni, ovvero che segnalino errore quando il caso che succeda.
106
CAPITOLO 7.
Bibliograa
[SM14]
W. Savitch and D. Micucci. Programmazione di base e avanzata con Java. Pearson, 2014.