You are on page 1of 107

DISPENSE

Programmazione 1  Dip. di Informatica di Torino

Roversi, L. and Cardone, F.

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

Il posto della programmazione tra le capacit di un informatico . . . . . . . . . . . . . . . . . . . . .

1.2

Algoritmo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2.1

Die Hard Problem (DHP)

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

1.2.2

Programmazione-come-trasformazione-di-congurazioni-attraverso-azioni . . . . . . . . . . . .

1.3

Semplici problemi di riferimento

2.2
2.3

1.3.1

Problema: massimo tra due valori (MAX2P)

1.3.2

Problema: somma di due numeri per incrementi/decrementi successivi (SIDP)

. . . . . . . . . . . . . . . . . . . . . . . . . . . .
. . . . . . . . .

9
9
10

13

Assegnazione e programmazione strutturata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

13

2.1.1

13

Assegnazione e congurazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Selezione e azioni condizionate

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

15

Ripetizione di azioni e costrutti iterativi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

16

2.3.1

Quadrato di un numero intero basato sui prodotti notevoli . . . . . . . . . . . . . . . . . . . . .

20

2.3.2

Iterazioni di iterazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

21

2.3.3

Riferimenti bibliograci

22

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Elementi di base per la Correttezza parziale

23

3.1

24

Correttezza parziale all'opera


3.1.1

3.2

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Introduzione dell'invariante di ciclo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Correttezza parziale per riscrittura del predicato invariante

26

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

26

Correttezza parziale di SIDP

3.2.2

Correttezza parziale per QPNP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

32

3.2.3

Correttezza parziale per SelP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

33

3.2.4

Correttezza parziale per MCDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

34

Il principio di induzione

3.4

Correttezza parziale per induzione

3.6

25

. . . . . . . . . . . . . . . . . . . . . . .

3.2.1

3.3

3.5

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Programmazione strutturata iterativa di base


2.1

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

35

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

3.4.1

Correttezza parziale di SIDP

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

37

3.4.2

Correttezza parziale di QPNP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

3.4.3

Correttezza parziale di QRDIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

43

3.4.4

Correttezza parziale per un algoritmo di calcolo del quadrato

. . . . . . . . . . . . . . . . . . .

45

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

46

Correttezza parziale con predicati implicativi

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

47

3.6.1

Correttezza parziale per SNNP

Iterazioni annidate . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

49

3.6.2

Iterazioni annidate e correttezza senza predicati implicativi

. . . . . . . . . . . . . . . . . . . .

52

3.6.3

Approfondimenti facoltativi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

52

Metodi e Modello di gestione della memoria


4.1

Frame e variabili locali al

4.2

Metodi senza risultato, ma con parametri

4.3

Metodi con parametri e risultato

main

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

53
53

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

55

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

59

4.4

Campi statici e

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

63

4.5

Signature, overloading, cast, etc. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

67

4.6

Jeliot . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

71

final

INDICE

Elementi di base per la Terminazione


Terminazione per SIDP

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

73

5.2

Terminazione di un algoritmo alternativo per SIDP . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

74

5.3

Criterio generale per la terminazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

76

5.4

Terminazione di un algoritmo per QRDIP . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

76

Programmazione ricorsiva di base


6.1

6.2

6.3

6.4

73

5.1

79

Denizioni e computazioni ricorsive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

79

6.1.1

Fattoriale

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

79

6.1.2

Quadrato

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Funzioni ricorsive famose

83

6.2.1

Funzione di McCarty . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

6.2.2

Funzione di Ackermann

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

Schemi rilevanti di calcolo ricorsivo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

6.3.1

Funzione identit . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

83

6.3.2

Funzione successore

84

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Ricorsione e correttezza parziale per induzione

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

85

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

86

6.4.1

Torre di Hanoi

6.4.2

Sommatoria di un segmento di naturali

6.4.3

Lettura e stampa di una sequenza di valori

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

88

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

89

Ricorsione di coda

6.6

Approfondimento: generalizzazioni del principio di induzione

. . . . . . . . . . . . . . . . . . . . . . .

Programmazione con array


Una scusa per introdurre gli array

7.2

Array e gestione della memoria: Heap

7.4

7.4.1
7.5

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

93

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

94

Creazione di array, inizializzazione ed eguaglianza

. . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Perch gli array sono nella heap ? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

Eguaglianza tra array e aliasing


Classe

Array

91

93

7.1

7.3.1

87

. . . . . . . . . . . . . . . . . . . . . . . . . . . . .

6.5

7.3

81

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

96
97

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

98

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

100

Operazioni fondamentali su array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

101

7.5.1

Ricerca lineare

101

7.5.2

Inserimenti e cancellazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

101

7.5.3

Filtri . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

102

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

7.6

Strutture dati con array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

102

7.7

Ordinamenti ed operazioni su array ordinati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

103

7.8

Matrici bidimensionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

104

Parte I

IN AULA

Capitolo 1

Pensiero computazionale e algoritmi


1.1

Il posto della programmazione tra le capacit di un informatico

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.

Un processo computazionale quindi, richiede

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

Partiamo da un problema che i due protagonisti di

1.2.1

Die Hard (1988)

si trovano a dover risolvere.

Die Hard Problem (DHP)

Sono dati due boccioni (jugs ) utilizzati per i distributori d'acqua, utilizzati negli uci, ad esempio.
primo, che identichiamo con

(grande), contiene al pi 5 litri. Il secondo, che

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

o (non in maniera esclusiva)

p.

Lo scopo partire con

vuoto, eventualmente travasando acqua da

p,

PENSIERO COMPUTAZIONALE E ALGORITMI

vuoti e terminare con

contenente 4 litri, e

o viceversa.

Congurazioni di DHP: struttura e interpretazione.

Avendo lo scopo di dare almeno una soluzione a DHP,

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

etc. fondamentale assegnare un signicato non ambiguo ad una congurazione,

indica

contenente

m5

litri d'acqua, e

(m, n)

contenente

n3

nel modo seguente:


litri d'acqua.

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

Operazioni sulle congurazioni di DHP.

(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:

indica che riempiamo

col rubinetto,

indica che riempiamo

col rubinetto,

g n p
p m g

indica che travasiamo


indica che travasiamo

n5

litri di

m3

litri di

in

p,

in

g,

indica che svuotiamo

g,

facendo perdere l'acqua a terra,

indica che svuotiamo

p,

facendo perdere l'acqua a terra.

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).

Soluzioni possibili di DHP come sequenza di operazioni sulle congurazioni.


Esistono almeno due sequenze di azioni da

(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)

La speranza ben posta.

1.3.

SEMPLICI PROBLEMI DI RIFERIMENTO

Commento 1
Il problema DHP ha catturato l'attenzione di ricercatori, come testimonia l'articolo scientico

algorithms and complexity.

Jug measuring:

La soluzione del problema, che pu essere enunciato anche considerando pi di

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

Problema dello zaino (Knapsac problem).

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,

Java, C++, C, Php, Python,

etc. .

Piuttosto, programmare richiede di individuare algoritmi, i quali:

agiscono sugli elementi costituenti le congurazioni,

usando operazioni, o azioni, interpretabili senza ambiguit ed in tempo nito,

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

Semplici problemi di riferimento

Il nostro scopo ora lavorare alla Programmazione-come-trasformazione-di-congurazioni-attraverso-azioni. Useremo


problemi basilari. Ci abitueranno a denire sia le componenti essenziali delle congurazioni necessarie a contenere i
valori in ingresso ed in uscita, sia le azioni su di esse per ottenere soluzioni ai problemi dati.

1.3.1

Problema: massimo tra due valori (MAX2P)

Deniamo MAX2P come segue:


Dati

m, n N,

Supponiamo

vogliamo conoscere quale sia il massimo tra

m, n

siano

2, 3,

rispettivamente.

ed

n.

La congurazione iniziale dovr necessariamente contenere 2 e 3,

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

r come il nome di un qualsiasi

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

In un sol passo (2, 3,

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, 3), siccome

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).

Il problema di MIN3P richiede di individuare il minimo valore tra

congurazioni siano quadruple di numeri


possano, ad esempio, portare alla

(1.1)

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)

se

mn

mo

se

nm

no

se

om

on .

10

CAPITOLO 1.

PENSIERO COMPUTAZIONALE E ALGORITMI

Il problema di MIN3P' una variante di MIN3P, denita come segue:

(i) ogni azione pu confrontare solo due

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 .

spazio delle congurazioni, partendo da (1, 3, 2, 5):

dello

(1, 3, 2, 5)
x

z
(1, 3, 5, 2)
x

(3, 1, 2, 5)
y
z

(3, 2, 1, 5) (3, 1, 5, 2) (3, 1, 5, 2)

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,

che si trova in qualsiasi linguaggio di programmazione

e che impareremo ad utilizzare in seguito.

1.3.2

Problema: somma di due numeri per incrementi/decrementi successivi (SIDP)

Concentriamo la nostra attenzione sui numeri naturali

N.

Per ogni numero

eseguire solo le operazioni di incremento e decremento. Ad esempio, se

n N,

supponiamo di saper

3, allora assumiamo di sapere

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 }

Pi in generale, per ogni


delle espressioni

essere calcolati, seguendo la struttura

Il problema individuare operazioni su opportune congurazioni le quali, in analogia col DHP, ssati

m, n N qualsiasi,

possano, alla ne, arrivare a contenere il valore dell'espressione

da una congurazione iniziale in cui appaiano

Possibile soluzione al SIDP.


che la coppia

(m, n)

ed

m + n,

una volta partiti

n.

Siccome i dati in ingresso sono una coppia di numeri interi

m ed n, possiamo assumere

possa costituire la congurazione iniziale. Questo implica che le azioni in grado di modicare una

congurazione in un'altra, debbano agire su coppie di numeri naturali.

In prima istanza, stabiliamo d'avere a disposizione l'azione

col seguente eetto su una qualsiasi congurazione

(m, n):

(m, n) (m + 1, n 1) .
Ad esempio, applicando iterativamente (1.2) alla congurazione iniziale

(1.2)

(3, 2)

otteniamo:

(3, 2) (4, 1) (5, 0) ,

(1.3)

la cui ultima coppia contiene il risultato cercato.


In generale, l'intuizione ci supporta nell'aermare che, usando coppie di numeri naturali e l'azione (1.2) sia possibile
ricavare da un qualsiasi esempio, o istanza di SIDP, la congurazione nale

partendo da una congurazione iniziale

applicando un numero suciente, ma non eccessivo, di volte (1.2):

(m + n, 0):

(m, n),

(m, n) (m + 1, n 1) ((m + 1) + 1, (n 1) 1) . . . (m + n, 0) .

(1.4)

1.3.

SEMPLICI PROBLEMI DI RIFERIMENTO

Esercizio 2 (

11

Problemi come transizioni tra congurazioni.)

1. MIDP un problema analogo a SIDP. In esso

m, n N, supponendo di saper eseguire solo il decremento di una


Ad esempio, se n 3, allora assumiamo di saper dire che 3-1 valga 2. Tuttavia,

l'obiettivo ottenere la dierenza tra due interi


singola unit ad ogni numero naturale.

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

deve essere eettuata solo se

m n.

2. SIDP' ha lo stesso enunciato di SIDP. Occorre individuare l'operazione che, partendo da una opportuna congurazione,
contenente

m, n N,

permetta di ottenerne una nale contenente

m + n,

sempre assumendo di saper solo eseguire

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

del problema e di sequenze di azioni che producano il risultato voluto.

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

m ed n di cui calcolare la somma.

Fornire

del problema e di sequenze di azioni che producano il risultato voluto.

4. MSDP relativo alla moltiplicazione tra numeri. Dati


necessarie a permettere il calcolo di

n m,

m, n N, individuare la struttura delle congurazioni e le azioni

assumendo di saper eseguire

somme generiche

tra numeri, e decrementi di

m 3 pu solo essere calcolata come (m + m) + m. Come in SIDP,


di (n 1) 1. Fornire un paio di istanze del problema e di sequenze

una singola unit su ogni numero. Ad esempio,


ad esempio,

n2

pu solo essere il risultato

d'azioni che producano il risultato voluto.


5. MSDP' analogo a MSDP, ma con una limitazione ulteriore.

L'unico modo per sommare due numeri applicare

incrementi o decrementi di una singola unit ai valori coinvolti nelle congurazioni. Fornire un paio di

istanze

del

problema e di sequenze di azioni che producano il risultato voluto.

Quando terminare l'applicazione

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.

Tuttavia, le azioni, sono ancora applicabili indenitamente a

(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.

Propriet invariante dell'applicazione iterata di azioni


Senza voler anticipare alcuni degli aspetti fondanti la buona programmazione, possibile osservare quanto segue in
ogni congurazione de SIDP, usando indierentemente una tra (1.2) e (1.6):
La somma dei valori di tutte le componenti in ciascuna delle congurazioni prodotte da (1.2) o (1.6) pari
al risultato che si vuole ottenere.

Esempio 1 Se la congurazione iniziale di SIDP

(3, 2),

allora in entrambe le sequenze:

(3, 2) (4, 1) (5, 0) (6, 1) (7, 2) . . .


(3, 2) (4, 1) (5, 0)
abbiamo che la somma delle componenti di una qualsiasi congurazione, anche quelle oltre il limite sensato, sia 5.
Siccome l'aermazione:

12

CAPITOLO 1.

Se

(m, n)

PENSIERO COMPUTAZIONALE E ALGORITMI

una congurazione nella ricerca della soluzione a SIDP, allora

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:

 ogni simbolo ha un signicato dicilmente misinterpretabile,


 le strutture manipolate e le regole per la manipolazione si possono descrivere per mezzo di una grammatica,
strumento concettuale identico a quello che usiamo per descrivere linguaggi naturali.

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.

Il [Wir73, Capitolo 2] parla ancora di algoritmi.

Il cammino per arrivare alla denizione di algoritmo, passa

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

Programmazione strutturata iterativa di


base
La sintesi di algoritmi attraverso l'identicazione di opportune congurazioni tra le quali muoversi per mezzo di
azioni la premessa per scrivere programmi composti da speciche strutture linguistiche. Vedremo quali esse siano,
come si relazionano con congurazioni ed azioni e come siano la premessa alla scrittura di programmi in linguaggi di
programmazione veri e propri.

2.1

Assegnazione e programmazione strutturata

2.1.1

Assegnazione e congurazioni

Sinora abbiamo indicato ogni congurazione per mezzo di una tupla:

(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:

A sinistra del simbolo  = compaiono identicatori di variabili o pi semplicemente variabili.

xZero, xUno,

Ciascuno tra

. . . va pensato come nome di un contenitore.

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.

Le variabili sono tali perch il loro valore pu mutare.

La mutazione del valore conseguenza di una

assegnazione. Supponiamo, infatti che:

xZero = 25;

(2.3)

xUno = xUno + 1;

(2.4)

14

CAPITOLO 2.

PROGRAMMAZIONE STRUTTURATA ITERATIVA DI BASE

siano due assegnazioni scritte ed interpretate immediatamente dopo l'ultima assegnazione

xEnne = mn ; che compare

in (2.2). Ovvero, supponiamo di scrivere:

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:

(?, ?, . . . , ?) /* configurazione da inizializzare */


xZero = m0 ;
(m0 , ?, . . . , ?) /* prima componente inizializzata */
xUno = m1 ;
(m0 , m1 , . . . , ?)

(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.

Notazione a tuple e assegnazioni non permettono le stesse azioni


Poniamo di voler risolvere il seguente problema di scambio di valori EXC2P:
Data una coppia

(m, n) di valori
(n, m).

interi produrre la coppia nella quale i valori siano scambiati. Ovvero, il

risultato deve essere

L'azione ovvia da denire :

(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

la prima posizione della congurazione e

la seconda. Per avere il contenuto di

in

occorrer,

prima o poi interpretare:

b = a; .
Tuttavia, non appena interpretassimo tale assegnazione otterremmo la congurazione
la possibilit di spostare

in

a.

(m, m) scomparendo per sempre

Lo stesso succederebbe col tentativo simmetrico.

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.

SELEZIONE E AZIONI CONDIZIONATE

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,

per ottenere l'azione globale voluta, attraverso una sequenza

di assegnazioni:

// (m, n, ?) in cui compaiono anche i valori iniziali da scambiare


tmp = a;
// (m, n, m)
a = b;
// (n, n, m)

(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

Selezione e azioni condizionate

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

le due azioni (1.1) in termini del seguente Algoritmo:

Algoritmo 1 per MAX2P

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

L'Algoritmo 1 si legge come segue:

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

a > b true  il valore in a strettamente maggiore di quello in b , allora si interpreta


max = a;. Si dice che si segue il ramo then della selezione.

 Altrimenti, se il risultato di
interpreta l'assegnazione

a > b false  il valore in a minore o uguale a quello


max = b;. Si dice che si segue il ramo else della selezione.

in

, allora si

All'ingresso di ciascun ramo la congurazione non ancora cambiata perch non sono state interpretate assegnazioni.

Al termine di ciascun ramo la congurazione cambiata in accordo con l'idea che


posizione. In essa c' il valore di
ramo

se abbiam seguito il ramo

max

then, oppure il valore di

sia il nome della terza

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

e stampando su standard output il massimo.

16

CAPITOLO 2.

PROGRAMMAZIONE STRUTTURATA ITERATIVA DI BASE

Algoritmo 2 per MAX2P con selezioni ad un solo ramo

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

ovvero quello interpretato solo se l'espressione argomento della

vera.

Esercizio 4 (Scambi di valori controllati da selezioni)


assume siano dati quattro numeri naturali

MAX4P una generalizzazione di MAX2P. MAX4P

m, n, o, p.

L'obiettivo conoscere il massimo tra i quattro.


Un algoritmo che risolva il problema, potr assumere di manipolare congurazioni con almeno quattro posizioni

a, b,

che conterranno i quattro valori dati.

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.

Nel caso il primo superi il secondo si scambiano i contenuti di

contenuto di

con quello di

c.

e di

e di

b;

con quello di

d.

con quello di

(ii) la seconda azione confronta il

Nel caso il primo superi il secondo si scambiano i contenuti di

la terza azione confronta il contenuto di


contenuti di

e di

c;

(iii)

Nel caso il primo superi il secondo si scambiano i

d.

(EmersioneDelMassimoQuattroVars.java una classe Java che si comporta in accordo con la strategia


appena descritta.)

 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

Se il primo supera il secondo allora il contenuto di

(iv) La quarta azione confronta il contenuto di

secondo allora il contenuto di

max.

(ii) La seconda azione confronta il contenuto

Se il primo supera il secondo allora il contenuto di

azione confronta il contenuto di


copiato in

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

da sinistra verso destra.

(BubbleQuattroVars.java una classe Java che risolve BS4P.)

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

Ripetizione di azioni e costrutti iterativi

Riprendiamo il problema SIDP nella Sottosezione 1.3.2. Lo abbiamo risolto in due passi:

fornendo l'azione (1.2),

assumendo di poter iterare liberamente, ma anche indenitamente, l'azione (1.2).

2.3.

RIPETIZIONE DI AZIONI E COSTRUTTI ITERATIVI

17

N.ro di cicli percorsi

Conf. prima di 3

Conf. dopo 3 e prima di 4

Conf. dopo 4

(2, 3)
(3, 2)
(4, 1)

(3, 3)
(4, 2)
(5, 1)

(3, 2)
(4, 1)
(5, 0)

1
2

Tabella 2.1: Evolversi delle congurazioni nell'Algoritmo 3, partendo col valore 2 in

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.

Continuare sarebbe controproducente:

la soluzione cercata verrebbe

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 .

se la seconda posizione contiene un valore

Ad esempio, applicare (2.9), o (2.10), alla congurazione

(2, 3)

(2.10)

genera la sequenza:

(2, 3) (3, 2) (4, 1) (5, 0) .

(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).

Algoritmo 3 per SIDP

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

rappresentano le congurazioni di SIDP e che

e la variabile

il valore

3.

Questo signica che

identica la seconda componente. Il meccanismo di iterazione

controllata di una tra (2.9) e (2.10) viene espresso come nell'Algoritmo 3, che va letto come segue:

Il costrutto iterativo comincia con la parola riservata


caso, confronta il valore associato a

 Se il risultato di

b > 0

b,

true

col valore

0,

 il valore in

while, seguita da un'espressione che, in questo specico

per mezzo dell'espressione

b > 0:

strettamente maggiore di

, allora:

while e che precedono


end while. Nella nostra specica situazione, siccome tra while e end while esistono esattamente

1. nell'ordine in cui compaiano, si interpretano tutte le istruzioni che seguono


due assegnazioni, interpretiamo prima
interpreta, il corpo del

a = a + 1;,

2. . . . si riprende il usso interpretando l'espressione

 Altrimenti, se il risultato di

quindi

b = b - 1;.

Si dice che si esegue, o si

while e . . .

b > 0

false

l'intepretazione dalla prima riga che segue

b > 0,

 il valore in

argomento della parola chiave

while.

minore o uguale a 0 , allora si ricomincia

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,

come condizione di terminazione dell'iterazione.

while vengano ripetutamente interpretate prima che l'espreswhile diventi false.

, quindi, possibile che le istruzioni nel corpo del


sione argomento della parola chiave

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.

PROGRAMMAZIONE STRUTTURATA ITERATIVA DI BASE

while stata interpretata esattamente 1 volta, non essendo mai ripartiti


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.

di tutte le istruzioni nel corpo del


dalla parola chiave

La Tabella 2.1 non contiene ulteriori righe perch, una volta raggiunta la congurazione

(5,0)

vale 0. Quindi,

end while. Siccome

vale

b > 0

e si deve proseguire dalla prima istruzione che segue

false

nessuna istruzione esiste al di sotto di

end while la congurazione consegnataci dall'Algoritmo 3

cui la prima componente, individuata da

SommaSuccessoreIterato.java
Esercizio 5

signica che

a,

(5, 0)

b
in

contiene il risultato cercato.

un programma Java che implementa l'Algoritmo 3 per SIDP.

1. Riprendere le soluzioni fornite ai problemi dell'Esercizio 2, in termini trasformazione di congurazioni e

riformularle, usando opportunamente assegnazioni, selezioni o iterazioni.


2. Fornire un algoritmo che risolva il problema PPID, ovvero che calcoli il valore di

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.

Quindi, occorre tradurla, usando opportunamente assegnazioni,

selezioni o iterazioni.

Quoziente e resto per iterazioni successive di sottrazioni


Progettiamo daccapo un algoritmo per risolvere il seguente problema QRSP:
Siano dati i valori

d,

rispettivamente numeratore e denominatore. L'obiettivo fornire un algoritmo

che restituisca il quoziente


Ad esempio, se

n=7

ed il resto

d = 2,

della divisione intera tra

allora la divisione intera

7/2

d.

deve restituire quoziente

q =3

e resto

r = 1.

Un

algoritmo utilizzabile quello che, alle scuole elementari, viene utilizzato per illustrare il meccanismo della divisione.

7 steccoline

Ovvero la divisione una distribuzione di elementi tra destinatari possibili:


a

2 destinatari,

vanno distribuite equamente

senza creare dierenze, ovvero, non assegnando steccoline rimanenti se non ne esistono abbastanza per

essere distribuite a tutti.


Segue una rappresentazione schematica del processo di distribuzione:
Steccoline

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

conclusivo di distribuzioni eettuate rappresenta il quoziente. La singola steccolina indistribuibile il resto.


Possiamo ridescrivere il processo appena illustrato per mezzo di tuple e azioni.

Le tuple sono quadruple.

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)

se il resto non inferiore al denominatore

(2.14)

2.3.

RIPETIZIONE DI AZIONI E COSTRUTTI ITERATIVI

19

N.ro di cicli gi percorsi

Conf. prima di 5

Conf. dopo 5 e prima di 6

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

Tabella 2.2: Evolversi delle congurazioni nell'Algoritmo 4, partendo col valore 7 in

e 2 in

b.

Usando (2.14) la sequenza di distribuzioni :

q/r

q/r

q/r

(7, 2, 0, 7) (7, 2, 1, 5) (7, 2, 2, 3) (7, 2, 3, 1) .

(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.

Algoritmo 4 per QRSP

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

sia il nome della variabile col valore

del numeratore e

il nome della variabile

del denominatore. L'Algoritmo (4) costruisce le congurazioni di QRSP trasformandole attraverso una

opportuna implementazione dell'azione (2.14).


L'Algoritmo (4) si legge come segue:

L'assunzione sui valori contenuti in

n,

pi le assegnazioni

q = 0;

creano la congurazione

r = n;

iniziale che leggiamo alla linea 3.

L'espressione di terminazione

r >= d
r non

eseguito solo quando il valore in

 Se il risultato di

r >= d

true

del costrutto iterativo


sia inferiore a quello in
 il valore in

while, implica che il corpo dell'iterazione venga

d.

Quindi:

non strettamente inferiore a

1. nell'ordine in cui compaiano, interpretiamo tutte e sole le assegnazioni

d;

nel corpo del

, allora:

q = q + 1;,

quindi

r = r -

while e . . .

2. . . . riprendiamo il usso dell'interpretazione dall'espressione

 Altrimenti, se il risultato di
corpo del while.

r >= d

false

 il valore in

r >= d,

argomento di

minore di quello in

while.

, allora si esce dal

while vengano ripetutamente interpretate prima che l'espreswhile diventi false.

, quindi, possibile che le istruzioni nel corpo del


sione argomento della parola chiave

Nel caso in questione, l'evolversi delle congurazioni rappresentato dalla Tabella 4.


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. 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 )

in cui la terza componente, individuata da

contiene in resto della divisione intera del valore in

q,

contiene il quoziente e la

per il valore in

n.

20

CAPITOLO 2.

PROGRAMMAZIONE STRUTTURATA ITERATIVA DI BASE

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,

determinare se esso sia pari o dispari, iterando solo sottrazioni.

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

3. SPNP che, ssato un naturale

n,

divisibile per 2 senza resto, altrimenti vale 1.

calcoli la somma dei primi numeri naturali, senza sfruttare la nota formula

4. MSMP che, ssati due numeri naturali

n(n+1)
.
2

m ed n determina il minore, supponendo d'avere a disposizione solo le seguenti


= 0?, m = 0?, n 6= 0?, m 6=

operazioni di base: decremento di una unit, risposta positiva o negativa alla domande  n

0?.
2.3.1

Quadrato di un numero intero basato sui prodotti notevoli

Deniamo il problema QPNP (Quadrato attraverso prodotti notevoli).


Vogliamo calcolare il quadrato di un numero intero

n = 0,

tra numeri arbitrari ed assumendo che se


calcolare

n2

semplicemente usando

n, supponendo di non saper calcolare la moltiplicazione


0
allora n valga 1. Questo signica che non possiamo

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)

(almeno per ora)  della validit di (2.16) usando qualche esempio.

Allora:

32 = (2 + 1)2
= 22 + 2 2 + 1 .
Ma

coincide con

(1 + 1)

(2.17)

, quindi possiamo riapplicare (2.16):

(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.

RIPETIZIONE DI AZIONI E COSTRUTTI ITERATIVI

21

N.ro di cicli gi percorsi

Conf. prima di 5

Conf. dopo 5 e prima di 6

Conf. dopo 6

(3, 1, 0)
(3, 2, 2)

(3, 1, 2)
(3, 2, 3)

(3, 2, 2)
(3, 3, 3)

Tabella 2.3: Evolversi delle congurazioni nell'Algoritmo 5, partendo col valore 3 in

La sommatoria

Pn1
i=0

(n i)

esprime la necessit di accumulare la somma dei valori

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.

Una volta che

il valore e vi sommiamo il contenuto di

si trovi in

s,

seguendo (2.19), ne raddoppiamo

Algoritmo 5 per QPNP

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: //

(n, n, n + (n 1) + (n 2) + . . . + (n (n 2)) + (n (n 1)))


Pn1
ovvero (n, n,
i=0 (n i))
= n + 2 * s; P
n1
(n, n, n + 2 i=0 (n i))

L'Algoritmo 5 implementa quanto descritto, assumendo che la variabile

il quadrato, la seconda ad

e la terza ad

n. La linea
n, col valore n di cui calcolare

contenga un valore naturale

3, illustra la congurazione iniziale in cui la prima componente corrisponde alla variabile

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.

La terza contiene il valore 3 della sommatoria.

sommandovi 3. La congurazione nale

(3, 3, 9),

L'assegnazione alla linea 10 modica 3, , raddoppiandolo e


la cui ultima componente contiene 9, ovvero il valore cercato.

Nota 2
La potenziale rilevanza della strategia di soluzione per QPNP sta nel fatto che utilizzi operazioni le quali, viste al
livello della

CPU, sono poco costose da eseguire.

Infatti, nella rappresentazione in base 2 dei numeri, la moltiplicazione

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.

Con le congurazioni date, le azioni possono essere le seguenti:

(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.

Le azioni date assicurano che, per

PROGRAMMAZIONE STRUTTURATA ITERATIVA DI BASE

n volte, la terza componente tiene traccia di quanti incrementi abbiamo applicato


m.

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.

Esempio 2 Supponiamo la congurazione iniziale sia

(3, 2, 0, 0).

Le azioni (2.20) e (2.21) generano la seguente sequenza di congurazioni:

(3, 2, 0, 0) (3, 1, 3, 0) (3, 1, 2, 1) (3, 1, 1, 2) (3, 1, 0, 3)


(3, 0, 3, 3) (3, 0, 2, 4) (3, 0, 1, 5) (3, 0, 0, 6) .
Siccome

n=0

i = 0,

non pi possibile applicare alcuna azione alla congurazione nale

(2.22)

(3, 0, 0, 6).

Inoltre, per due

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).

Algoritmo 6 per MSDP'

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

N.ro di cicli esterni

Conf. prima di 5

Conf. dopo 6

N.ro di cicli interni

(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,

Tabella 2.4: Evolversi delle congurazioni nell'Algoritmo 6, partendo col valore 3 in

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.

Il corpo del ciclo pi interno contiene solo due assegnazioni.

Esse sono interpretate un numero di volte che

dipende dal valore in

ogni volta che si re-interpreta daccapo il corpo

i,

il quale reinizializzato al valore di

del ciclo pi esterno.

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

Elementi di base per la Correttezza parziale


Riprendiamo il problema SIDP, introdotto nella Sottosezione 1.3.2.

SIDP richiede di calcolare la somma di due

naturali, usando iterativamente incrementi e decrementi di opportune variabili. L'Algoritmo 7 quello gi visto nella
Sottosezione 1.3.2

Algoritmo 7 SIDP richiamato per parlare di correttezza parziale.

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,

usassimo l'Algoritmo 8 come soluzione a SIDP?

Algoritmo 8 errato per SIDP

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

dell'Algoritmo (8)) Assumiamo che, inizialmente,

vericare che, all'uscita dall'iterazione,

valga

valga

1.

Potremmo ripetere il testing per pi coppie di valori in

Ovvero
e

b.

contenga

ed

per

contenga

b.
2.

Possiamo

non conterrebbe il valore desiderato.

Ad un certo punto, siccome le coppie da usare per

il tewsting sono innite, giungeremmo inevitabilmente a denire la questione in termini pi generali:


L'Algoritmo 8 fornirebbe un valore sbagliato in
ad

a,

per una qualsiasi coppia di valori inizialmente assegnati

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,

per una qualsiasi coppia di valori assegnati ad

b?

sia innito, senza speranza l'immaginare di portare a termine un testing

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.

La domanda fondamentale diventa:


Esistono strumenti in grado di dimostrare, quindi in grado di assicurare, che, ssato un algoritmo
qualsiasi combinazione di valori iniziali,

restituisca il risultato voluto?

A,

per

24

CAPITOLO 3.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

Il capitolo sviluppa gli strumenti concettuali e formali per rispondere positivamente, ovvero per parlare di dimo-

strazione (verica) della Correttezza parziale di Algoritmi.


Gli strumenti concettuali e formali che vedremo sono la base per l'automazione della verica di correttezza
parziale. La verica di correttezza parziale surclassa la qualit oerta dal testing sulla certicazione del
buon funzionamento di un algoritmo.

3.1

Correttezza parziale all'opera

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

quite simply, because

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

dagli strumenti disponibili in Key.

3.1.

CORRETTEZZA PARZIALE ALL'OPERA

25

Uno strumento analogo a Key, ma di pi immediato utilizzo,

Dafny@rise4fun from Microsoft

di cui

illustriamo brevemente le potenzialit tramite un esempio che conosciamo bene.


Riprendiamo SIDP e consideriamo due algoritmi per esso, scritti nel linguaggio

Algoritmo 9 per SIDP in

Dafny@rise4fun from Microsoft:

Dafny

1 method s(m: nat, n: nat)

{
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

Algoritmo 10 errato per SIDP in


2
4
6
8
10
12

Dafny

method s(m: nat, n: nat) {


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;
}

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.

evidente che l'esistenza

dell'errore scovata automaticamente. Il motivo la presenza dei seguenti elementi sintattici, che chiamiamo direttive :

invariant m + n == a + b;

(3.1)

invariant 0 <= b && b <= n;

(3.2)

assert m + n == a && b == 0;

(3.3)

Lo scopo delle direttive (3.1) e (3.2) descrivere propriet che i valori in

m, n, a

soddisfano sia prima che

l'assegnazione iniziale nel corpo dell'iterazione venga interpretata, sia appena l'ultima assegnazione nale del corpo
dell'iterazione venga eseguita. In particolare:

(3.1) aerma che la somma dei valori in

(3.2) aerma che il valore in

ed

coincide con la somma dei valori in

sempre compreso tra

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

Introduzione dell'invariante di ciclo

Supponiamo, per esempio, che, inizialmente, nell'Algoritmo 7,


contenuto ad

m valga 4 ed n valga 3. Una volta assegnato il loro


a e b, rispettivamente, possiamo svolgere l'interpretazione dell'iterazione come segue, nch l'espressione

26

CAPITOLO 3.

b > 0

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

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

Otteniamo la seguente successione di assegnazioni:

// (4, 3, 4, 3): valori allinizio


delliterazione 0 con i quali abbiamo
m+n==4+3==7==4+3==a+b e b==3<=3.
a = a + 1;
b = b - 1;
// (4, 3, 5, 2): valori alla fine
delliterazione 0 con i quali abbiamo
m+n==4+3==7==5+2==a+b e b==2<=3.
// (4, 3, 5, 2) sono anche i valori allinizio
delliterazione 1.
a = a + 1;
b = b - 1;
// (4, 3, 6, 1): valori alla fine
delliterazione 1 con i quali abbiamo
m+n==4+3==7==6+1==a+b e b==1<=3.
// (4, 3, 6, 1) sono anche i valori allinizio
delliterazione 2.
a = a + 1;
b = b - 1;
// (4, 3, 7, 0): valori alla fine
delliterazione 2 con i quali abbiamo
m+n==4+3==7==7+0==a+b e b==0<=3.
I commenti inframezzati alle assegnazioni evidenziano come all'inizio ed alla ne di ciascun ciclo, la relazione tra i
valori di

m, n, a

rimanga invariata e descrivibile col predicato

m+n==a+b.

Analogamente,

sempre contenuta

nell'intervallo indicato.

a e b anch in a si accumuli, ciclo dopo ciclo, il valore cercato. Inne, con b a


m ed n risulta accumulata in a. Questo esattamente quanto asserisce (3.3), direttiva
da interpretare strategicamente all'uscita dell'iterazione per confermare proprio che, con b = 0, il valore in a sia m+n.
Quel che cambia, sono i valori di

valore nullo, tutta la somma tra

L'aspetto fondamentale che il meccanismo di mantenimento delle relazioni tra


dentemente dai valori che inizialmente decidiamo di assegnare ad

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

predicati, o propriet, invarianti di ciclo.


Un invariante di ciclo, assieme alle condizioni di terminazione del ciclo in questione, permette di dimostrare
che il ciclo sia in grado di produrre il risultato cercato.

Esercizio 7

Copiare

QuozienteRestoDifferenzaIterata.dafny in Dafny@rise4fun from Microsoft

e ripetere esperimenti di verica di correttezza parziale.

Usando opportunamente le congurazioni, vericare che all'inizio ed alla ne di ogni ciclo la relazione tra

3.2

sia quella indicata da

n, d, q

ed

invariant n == (d * q + r);.

Correttezza parziale per riscrittura del predicato invariante


In ordine crescente di dicolt e generalit, scopo di questa sezione :
1. illustrare come dimostrare che il predicato invariante usato nell'esempio della Sottosezione 3.1.1 sia
indipendente dai valori inizialmente assegnati a

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

Correttezza parziale di SIDP

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

il corpo dell'iterazione. I valori in


e dopo il corpo dell'iterazione per

3.2.

CORRETTEZZA PARZIALE PER RISCRITTURA DEL PREDICATO INVARIANTE

27

Algoritmo 11 per SIDP

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:

// m contiene un qualche numero in N


// n contiene un qualche numero in N
a = m;
b = n;
while (b > 0) do
// m+n == a+b
a = a + 1;
b = b - 1;
// m+n == a+b
end while

valori generici di

ed

n.

Esprimendoci formalmente, vogliamo dimostrare che i predicati, espressi come commenti

nel seguente algoritmo:


siano veri nel punto esatto in cui essi compaiono, utilizzando i valori delle variabili di cui essi descrivono le propriet.
La dimostrazione consiste nel trovare manipolazioni algebriche dei valori

anch lo stesso predicato possa

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,

non cambi e continui a valere.

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:

// m contiene un qualche numero in N


// n contiene un qualche numero in N
a = m;
b = n;
while (b > 0) do
/* Supponiamo che m+n == a+b sia vero.
Allora m+n == a+b+1-1 == (a+1)+(b-1) vero.
*/
a = a + 1;
/* Lassegnazione a = a + 1, d il nome a al valore,
presente in a prima dellassegnazione,
ma incrementato di 1.
Siccome in m+n == (a+1)+(b-1) compare
lespressione a + 1, ed a un nuovo nome per
essa, possiamo rimpiazzare a + 1 con a in
m+n == (a+1)+(b-1). Otteniamo cos un nuovo
predicato vero m+n == a+(b-1).
*/
b = b - 1;
/* Lassegnazione b = b - 1, d il nome b al valore,
presente in b prima dellassegnazione,
ma decrementato di 1.
Siccome in m+n == a+(b-1) compare
lespressione b - 1, e b un nuovo nome per
essa, possiamo rimpiazzare b - 1 con b in
m+n == a+(b-1). Otteniamo cos un nuovo
predicato vero m+n == a+b. Il nuovo predicato
coincide con quello iniziale m+n == a+b.
Proprio per il fatto che il predicato appena
ottenuto sia identico a quello di partenza.
alla linea 6, possiamo affermare che
m+n == a+b sia invariante, ovvero che la sua
forma sitattica non cambi da inizio a fine ciclo.
*/
end while
Una volta comprese le manipolazioni su

che permettano di riscrivere

m+n == a+b a linea 6 nello stesso


m+n == a+b sia vero

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

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

vero al termine di ogni iterazione vero anche quando il corpo dell'iterazione non pu

pi essere eseguito, ovvero quando


a

in

m+n == a+b,

b == 0 vero. Essendo sia b == 0 sia m+n == a+b veri, possiamo sostituire 0


m+n == a+0 == a. Ovvero in a, indipendentemente dai valori iniziali di m ed n, al
compare il valore m+n, che la somma cercata.

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.

Esercizio 8 (Correttezza parziale per riscrittura)

1. Problemi legati alla dimostrazione della correttezza par-

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:

// m contiene un qualche numero in N


// n contiene un qualche numero in N
a = m;
b = 0;
while (b < n) do
a = a + 1;
b = b + 1;

end while

(c) Dimostrare che il seguente programma:

1:
2:
3:
4:
5:
6:
7:
8:

// m contiene un qualche numero in N


// n contiene un qualche numero in N
a = m;
b = 0;
while (b <= n) do
a = a + 1;
b = b + 1;

end while

non corretto.
2. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema MSIP che, dati due numeri naturali

n,

impone di calcolare il valore

Soluzione per MSIP.


della somma di

per

n,

m*n

come somma di

iterata per

volte.

Un metodo possibile per impostare una soluzione osservare che

m*n, vista come iterazione

si pu esprimere come segue:

m*n == |m+...+m
{z } .
n volte

L'equazione precedente pu essere riscritta come:

m*n == ris + m+...+m


| {z }
k volte

ris == |m+...+m
{z } ,
n-k volte

assumendo che

vari tra

valori ottenuti addizionando

m.

0.

Questa seconda coppia di equazioni indica che il risultato

ris accumula
ris:

Il seguente algoritmo descrive il processo di accumulo dei valori in

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.

CORRETTEZZA PARZIALE PER RISCRITTURA DEL PREDICATO INVARIANTE

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:

(a) Scambiare le istruzioni 12 e 22 e dimostrare la correttezza parziale.


(b) Dimostrare la correttezza parziale de:

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

(c) Dimostrare che il seguente programma:

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

non corretto siccome il predicato

ris == m*n

non vale una volta terminato.

30

CAPITOLO 3.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

3. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema PMIP che, dati due numeri naturali

n,

impone di calcolare il valore

Soluzione per PMIP.


della moltiplicazione di

mn

come moltiplicazione di

iterata per

volte.

Un metodo possibile per impostare una soluzione osservare che


per

n,

mn, vista come iterazione

si pu esprimere come segue:

mn == |m*...
{z *m} .
n volte

L'equazione precedente pu essere riscritta come:

mn == ris |*m*...
{z *m}
k volte

ris == |m*...
{z *m} ,
n-k volte

assumendo che

vari tra

valori ottenuti moltiplicando

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

Questa seconda coppia di equazioni indica che il risultato

ris

seguente algoritmo descrive il processo di accumulo dei valori in

accumula via via

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

(c) Dimostrare che il seguente algoritmo, scritto per risolvere PMIP:

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.

CORRETTEZZA PARZIALE PER RISCRITTURA DEL PREDICATO INVARIANTE

8:

31

end while

non corretto, siccome il predicato

ris == mn

non vale una volta terminato.

4. Usando la tecnica per riscrittura, dimostrare la correttezza parziale del problema DPIP che, dati due numeri naturali

n,

impone di calcolare il valore

Soluzione per DPIP.


di

m-n

iterando l'applicazione per

volte ad

m.

Un metodo possibile per impostare una soluzione osservare che

volte del predecessore su

m-n, vista come iterazione

si pu esprimere come segue:

m-n == m |-1-...-1
{z
} .
n volte

L'equazione precedente pu essere riscritta come:

m-n == ris |-1-...-1


{z }
k volte

ris == m |-1-...-1
{z } ,
n-k volte

assumendo che

vari tra

valori ottenuti sottraendo

n
1.

0.

Questa seconda coppia di equazioni indica che il risultato

ris accumula
ris:

via via

Il seguente algoritmo descrive il processo di accumulo dei valori in

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,

il dividendo, impone di calcolare quoziente

e resto

della divisione intera di

per mezzo di

s.

32

CAPITOLO 3.

Soluzione per QRDIP.


e resto

r,

Dato il dividendo

usando sottrazioni successive di

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

d ed il divisore s il predicato
d, avr la forma seguente:

che esprime la relazione tra quoziente

da

d== |s+...+s
{z } +r

con

r < s .

q volte

Il seguente algoritmo descrive il processo di accumulo dei valori in

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

/* d == q*s + r && r < s il predicato che vale


in questo punto del codice e coincide con la relazione
tra d, s, q, ed r che cerchiamo.
*/

Usando la dimostrazione precedente come spunto e scambiando le istruzioni 10 e 13, dimostrare la correttezza parziale
dell'algoritmo ottenuto.


3.2.2

Correttezza parziale per QPNP

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

x*x, ovvero esiste una relazione ricorsiva

tra

(x+1)2 e x2.

(x+1)*(x+1)

Vedremo in seguito la rilevanza del concetto ricorsione.

Segue l'algoritmo dai cui commenti possibile ricavare la relazione tra

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

/* n*n == ris && n = x


implica
x*x == ris
*/

x, sfruttando

si pu esprimere in termini del valore di

(x+1)2

x2:

3.2.

CORRETTEZZA PARZIALE PER RISCRITTURA DEL PREDICATO INVARIANTE

Esercizio 9 (Correttezza parziale legata a QPNP)

33

Dimostrare la correttezza parziale del seguente algorit-

mo, alternativo al precedente, per QPNP:

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

Correttezza parziale per SelP

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.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

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

Correttezza parziale per MCDP

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)

MCD(x, y): nch x ed y sono


MCD tra l'argomento appena diminuito

Le equivalenze espresse in (3.4) suggeriscono una lettura algoritmica per ricavare


diversi decrementiamo l'argomento maggiore e continuiamo a cercare il valore
di valore e l'altro, lasciato invariato.
Il seguente algoritmo formalizza quanto descritto:

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

/* a==b && MCD(a,b)==MCD(x,y) implica


MCD(a,a)==MCD(x,y) che, assieme al terzo assioma a==MCD(a,a), implica a==MCD(x,y)
/
*

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.

Qui di seguito riassumiamo i punti essenziali per le applicazioni

nell'ambito della programmazione.


Una trattazione pi completa verr fatta nel corso di Matematica discreta e Logica.
In matematica (ed in informatica) spesso necessario dimostrare che una certa propriet vera per tutti i numeri
naturali. Alcuni esempi:

Per ogni

n N,
n
X

i=

i=0

n(n + 1)
.
2

Consideriamo un frammento di codice della forma:

while (b)
S;
Se

una proposizione che esprime una relazione tra i valori delle variabili che compaiono nell'istruzione

S,

allora si pu denire un'altra propriet

Q(n) P

vera dopo

iterazioni del ciclo while.

Propriet di questo tipo sono utilizzate per stabilire che

una propriet invariante del ciclo in questione.

La formulazione pi generalmente nota del principio di induzione la seguente:

principio di dimostrazione per induzione (PI):

P (n) P (n + 1),

allora

Data una propriet

dei numeri naturali, se

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)

(questa detta l'ipotesi

P (0),

mentre il

passo induttivo

induttiva)

e si dimostra che

P (n + 1).

prima, del principio di induzione, usa l'estensione della propriet

AN

tale che

0A

e, per ogni

P (n)

sia vera

Un'altra formulazione, del tutto equivalente alla

P,

cio l'insieme dei numeri naturali per i quali la

propriet vera:

Se

la dimostrazione

che, normalmente, si articola nel modo seguente: si assume che

n N, n A n + 1 A ,

allora

A = N.

36

CAPITOLO 3.

Aritmetica

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

Vediamo subito il primo esempio di utilizzo del principio di induzione:

Teorema 1
Per ogni

n N,
n
X

i=

n(n + 1)
.
2

i=

k(k + 1)
.
2

i=0

Dim. 1 Qui la propriet

P (k)

k
X
i=0

La base consiste nel vericare che entrambi i lati dell'induzione hanno valore

0.

Per dimostrare il passo induttivo,

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

per l'ipotesi induttiva

e mediante un'applicazione del principio di induzione si ottiene la conclusione.

Teorema 2
Per ogni

m N:
(m + 1)2 = (m + 1) + 2

m1
X

(m i) .

i=0

Dim. 2 Qui la propriet

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.

CORRETTEZZA PARZIALE PER INDUZIONE

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.

Esercizio 11 Dimostrare mediante induzione che, per ogni

n > 0, n3 n

Esercizio 12 Dimostrare mediante induzione che, per ogni

n 4, n! > 2n .

Esercizio 13 Dimostrare mediante induzione che, per ogni

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.

Esercizio 15 Dimostrare mediante induzione che, per ogni

n 5, 2n > n2 .

elearning.math.unipd.it

3.4

contiene alcune soluzioni esplicite agli esercizi precedenti.

Correttezza parziale per induzione

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

Correttezza parziale di SIDP

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.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

vogliamo dimostrare che, se il ciclo termina, allora la variabile


La dimostrazione procede per induzione sul numero

ris

contiene un valore pari a

m+n.

di iterazioni, ovvero, sul numero di volte che l'intero corpo

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:

Osserviamo che le variabili


per

opportuno, il valore di

segue il valore di

ris

in

denotiamo con

risk

denotiamo con

ik

il valore della variabile

il valore della variabile

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

0 k n, se la sequenza di istruzioni 6 e 7 stata percorsa k volte, allora risk +ik ==m+n

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,

per ipotesi induttiva assumiamo

risk +ik ==m+n

ik ==(i-k).

La prima equivalenza vale

grazie ai seguenti passi:


(istruzione 4)

risk+1 +ik+1 ==risk +1+ik+1

(istruzione 5)

==risk +1+ik -1
==risk +ik

(ipotesi induttiva)

==m+n

La seconda equivalenza vale grazie ai seguenti passi:


(istruzione 4)

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,

1. Per induzione, dimostrare la correttezza

impone di calcolare il valore

m*n

come somma di

3.4.

CORRETTEZZA PARZIALE PER INDUZIONE

Soluzione per MSIP.

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

vogliamo dimostrare che, se il ciclo termina, allora la variabile


La dimostrazione procede per induzione sul numero

ris

contiene un valore pari a

x*y.

di iterazioni, ovvero, sul numero di volte che l'intero corpo

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:

Osserviamo che le variabili

k
di

opportuno, il valore di

ris

in funzione

denotiamo con
denotiamo con

risk il valore della variabile ris al termine del k -esimo ciclo,


ik il valore della variabile i al termine del k -esimo ciclo.

Propriet 2

m e n ssati. Per ogni 0 k n,


risk +(ik *m)==m*n e ik ==(n-k).

Siano

se la sequenza di istruzioni 6 e 7 stata percorsa

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

vale grazie ai seguenti passi:


(istruzione 4)

risk+1 +(nk+1 *m)==risk +m+(ik+1 *m)

(istruzione 5)

==risk +m+((ik -1)*m)


==risk +m+(ik *m)-m
==risk +ik
==m*n

(ipotesi induttiva)

La seconda equivalenza vale grazie ai seguenti passi:

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.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

(a) Scambiare le istruzioni 6 e 7 e dimostrare la correttezza parziale.


(b) Dimostrare la correttezza parziale de:

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

(c) Dimostrare che il seguente programma:

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

non corretto siccome il predicato

ris == m*n

non vale una volta terminato.

2. Per induzione sul numero di iterazioni eettuate, dimostrare la correttezza parziale del problema PMIP che, dati due
numeri naturali

n,

impone di calcolare il valore

Soluzione per PMIP.

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

vogliamo dimostrare che, se il ciclo termina, allora la variabile


La dimostrazione procede per induzione sul numero

ris

contiene un valore pari a

mn.

di iterazioni, ovvero, sul numero di volte che l'intero corpo

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:

Osserviamo che le variabili

opportuno, il valore di

segue il valore di

ris

denotiamo con

risk

denotiamo con

ik

il valore della variabile

il valore della variabile

ris

al termine del

al termine del

k -esimo

k -esimo

ciclo,

ciclo.

Propriet 3

m e n ssati. Per ogni 0 k n,


risk *(mnk )==mn e ik ==(n-k).

Siano

se la sequenza di istruzioni 6 e 7 stata percorsa

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.

CORRETTEZZA PARZIALE PER INDUZIONE

Dato

0 < k < n,

per ipotesi induttiva assumiamo

41

risk *(mik )==mn

ik ==(n-k).

La prima equivalenza

vale grazie ai seguenti passi:


(istruzione 4)

risk+1 *(mik+1 )==(risk *m)*mik+1


==(risk *m)*m(ik 1)

(istruzione 5)

==risk *(mik )
(ipotesi induttiva)

==mn

La seconda equivalenza vale grazie ai seguenti passi:


(istruzione 4)

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

(c) Dimostrare che il seguente algoritmo, scritto per risolvere PMIP:

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

non corretto, siccome il predicato

ris == mn

non vale una volta terminato.

3. Per induzione, dimostrare la correttezza parziale del problema DPIP che, dati due numeri naturali
calcolare il valore

m-n

iterando un decremento unitario per

Soluzione per DPIP.

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:

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

ris = m;
i = n;
while (i > 0) do
ris = ris - 1;
i = i - 1;

end while
end if

vogliamo dimostrare che, se il ciclo termina, allora la variabile


La dimostrazione procede per induzione sul numero

ris

contiene un valore pari a

m-n.

di iterazioni, ovvero, sul numero di volte che l'intero corpo

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:

Osserviamo che le variabili


per

opportuno, il valore di

valore di

ris

ed

in

denotiamo con

risk

denotiamo con

ik

il valore della variabile

il valore della variabile

ris

al termine del

al termine del

k -esimo

k -esimo

ciclo,

ciclo.

Lo scopo dimostrare:

Propriet 4

m e n ssati. Per ogni 0 k n,


risk -ik ==m-n e ik ==(n-k).

Siano

se la sequenza di istruzioni 8 e 9 stata percorsa

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,

per ipotesi induttiva assumiamo

risk -ik ==m-n

ik ==(n-k).

La prima equivalenza vale

grazie ai seguenti passi:


(istruzione 7)

risk+1 -ik+1 ==risk -1-ik+1

(istruzione 8)

==risk -1-(ik -1)


==risk -1-ik +1
==risk -ik
==m-n

(ipotesi induttiva)

La seconda equivalenza vale grazie ai seguenti passi:

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.

CORRETTEZZA PARZIALE PER INDUZIONE

3.4.2

43

Correttezza parziale di QPNP

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,

sia determinato dalle istruzioni 5 e 6 dell'algoritmo:

nk+1 == nk + 1
risk+1 == risk + 2 * nk+1 - 1
Dim.

P (0)

k.

La dimostrazione procede per induzione su

vero perch

Assumiamo valga

n0 * n0 == 0 ==
P (k). Allora:

nk * nk

k.P (k) in cui (i), per denizione, P (k) nk * nk == risk ,


k volte, (iii) il legame tra

rispettivamente, dopo aver percorso il ciclo per

ris

vero, implica

nk * nk == risk
+ 2 * nk + 1 == risk + 2 * nk + 1
(nk + 1)2 == risk + (nk + 1) + nk

vero, implica
vero, implica

== risk + (nk + 1) + (nk + 1) - 1

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

Correttezza parziale di QRDIP

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

r della divisione di due numeri interi X 0 e D > 0.


D a X , aumentando ogni volta di 1 il valore di q che inizialmente

ed il resto

L'algoritmo usuale consiste nel sottrarre ripetutamente

ha valore 0. Schematicamente, l'algoritmo il seguente:


1. no a quando
2. quando

XD

X < D,

poni

esegui le seguenti azioni: sottrai

X;

aumenta

di 1

r = X.

Un metodo Java che realizza questo algoritmo il seguente:

2
4
6
8
10

public static void main (String[] args) {


int X, D, q, r;
X = 14;
D = 3;
q = 0;
r = X;
while (r >= D) {
r = r - D;
q = q + 1;
}
}

Come si pu dimostrare che il programma precedente corretto?

Prima di tutto, serve una specica precisa del

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

del programma, cio la propriet che i dati in uscita

ed

devono soddisfare, che

X = q D + r,

con

r < D.

Questa

44

CAPITOLO 3.

propriet dice proprio che

ed

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

sono, rispettivamente, il quoziente ed il resto della divisione intera di

correttezza del programma (qualche volta si parla di questa condizione come di

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;

nel programma precedente, per esempio la propriet

q 0.

Tra tutte le possibili propriet ce ne sono alcune che sono

pi interessanti di altre. Consideriamo ora la propriet:

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

propriet sia invariante, ed inoltre si esce dal ciclo perch

r < D,

D che, per la condizione di ingresso, un numero


X = q D + r perch abbiamo assunto che questa
r < D. Ma allora vera la propriet X = q D + r, con

viene sottratto il valore

quindi prima o poi deve accadere che

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

q = 0 (perch q non viene incrementato) e r = X .


X = 0 D + X , che ovviamente vero. Supponiamo che

che abbiamo scelto). Allora

perch questo si riduce a dire che

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

sia stato eseguito

resta vera

anche dopo la

ottenendo

valori

q0 = q + 1
r0 = r D
dove

q0

ed

r0

sono i valori delle variabili

ed

dopo l'esecuzione delle istruzioni

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.

CORRETTEZZA PARZIALE PER INDUZIONE

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

Soluzione per QRDIP.


resto

r,

s,

il dividendo, impone di calcolare quoziente

e resto

della divisione intera

s.
Dato il dividendo

usando sottrazioni successive di

da

d ed il divisore s il predicato
d, avr la forma seguente:

che esprime la relazione tra quoziente

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)

in cui (i), per denizione,

rispettivamente, dopo aver percorso il ciclo per

P (k) d == qk * s + rk , (ii) qk e rk sono i valori di q e


qk , qk+1 , rk e rk+1 sia determinato dalle

volte, (iii) il legame tra

istruzioni 4 e 5 dell'algoritmo:

rk+1 == rk - s
qk+1 == qk + 1
Dim.

P (0)

La dimostrazione procede per induzione su

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

Correttezza parziale per un algoritmo di calcolo del quadrato

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.

La condizione di ingresso sar dunque

il dato in uscita ed

N 0,

mentre la condizione di uscita sar

una variabile ausiliaria utilizzata come contatore. L'invariante

appropriato in questo caso la formula

Y = X X.
Inizialmente avremo dunque

X=0

Y = 0:

l'invariante ovviamente vero in questo caso, e questo stabilisce la base

della dimostrazione induttiva che la propriet (3.7) invariante.

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);
}
}

Per quanto riguarda il passo induttivo, l'ipotesi induttiva

Y =X X

dopo l'n-esima iterazione;

(3.7)

46

CAPITOLO 3.

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

(n + 1)-esima iterazione. Se Y 0 il valore di Y dopo l'esecuzione


il valore di X dopo l'esecuzione dell'istruzione X = X + 1, possiamo

bisogna dimostrare che (3.7) resta vera dopo la


dell'istruzione

Y = Y + 2 X + 1,

mentre

calcolare

Y0 =Y +2X +1
= (X X) + 2 X + 1

(per ipotesi induttiva)

= (X + 1) (X + 1)
= X0 X0
da cui si conclude che (3.7) proprio invariante. Poich il valore di

N X

decresce strettamente ad ogni iterazione, il

ciclo deve terminare (perch non ci pu essere una sequenza innita di numeri naturali
dal ciclo avremo
che

X=N

k0 > k1 > k2 > . . .)

all'uscita

(perch la condizione del while diventata falsa e sappiamo, per come fatto il programma,

X N ) quindi, per l'invariante, Y = N N .


Y , perci il programma corretto.

Questo mostra che la condizione di uscita soddisfatta dal dato in

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

sia un invariante di ciclo per


2.

(Dicile.)
P (k)

3.5

QuadratoConRaddoppiEtc.java.

Dimostrare la correttezza parziale de

QuadratoDiStefanoMerlo.java

individuando un predicato

che, usando un ragionamento per induzione sul numero di cicli percorsi, sia l'invariante.

Correttezza parziale con predicati implicativi

Partiamo ricordando la tavola di verit di implicazione e quanticazione universale.


L'implicazione sempre vera, tranne quando la premessa vera e la conclusione falsa:

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.

Esempio 4 (Validit di una quanticazione universale su un'implicazione)

k N.0 k < 3 3 k 0

vero, perch equivalente a quanto segue:

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.

L'eliminazione consiste nel sostituire a

in

0 k < 3 3k 0

k N.0
k

ogni possibile valore che

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.

CORRETTEZZA PARZIALE PER SNNP

47

Esercizio 18 Seguendo l'Esempio 4 determinare il valore di verit dei seguenti predicati:

Q k N.0 k < 2 2 k > 0.


R k N.0 k < 2 2 k 0.
S k N.0 k 1 1 k 1.

3.6

Correttezza parziale per SNNP

Per denizione, il problema SNNP consiste nel voler stampare l'intera sequenza di numeri naturali tra

ed un valore

pressato, primo estremo incluso e secondo escluso.


Se il valore pressato

2, un algoritmo che risolva SNNP dovr produrre la sequenza 01.

Proponiamo l'Algoritmo 12

seguente come soluzione.

Algoritmo 12 per SNNP

1: // n contiene un valore
2: i = 0;
3: while (i < n) do
4:
stampa i;
5:
i = i + 1;
6: end while

di

Il nostro problema dimostrare che l'Algoritmo 12 sia parzialmente corretto.


A tal ne, occorre sintetizzare un predicato

che sia vero in ogni congurazione del programma. Ovvero,

deve

valere in tutti i casi seguenti:


1. l'Algoritmo 12 non ha ancora stampato nulla. Questo corrisponde alla situazione in cui abbiamo interpretato
solo l'assegnazione
comando

i = 0; e stiamo per,
stampa il valore in i;;

ma non lo abbiamo ancora fatto, interpretare per la prima volta il

2. l'Algoritmo 12 ha stampato il valore 0, ha interpretato


volta l'espressione

i <= n

3. e cos via no a quando


Deniamo

i = i + 1;,

ma non ha ancora valutato per la seconda

per decidere se rieseguire il corpo dell'iterazione;

non supera il valore in

n.

e argomentiamo sul perch esso possa esser un candidato per dimostrare la correttezza parziale

dell'Algoritmo 12. Sia:

P (i) k.0 k < i


Sappiamo che il valore di verit del predicato

il valore

P (i)

gi stato stampato

(3.8)

si determina sviluppandolo in una congiunzione innita e

valutando ogni implicazione in ogni congiunto:

00<i0
01<i1

gi stato stampato
gi stato stampato

.
.
.

0 i-1 < i i-1 gi stato stampato


0 i < i i gi stato stampato
0 i+1 < i i+1 gi stato stampato
.
.
.
Dovrebbe essere evidente che il valore di

Sia

P (i)

true true
true true
true true
false true
false false
false false

true
true
true
true
true
true

.
.
.

.
.
.

pu dipendere dal valore di

i.

true .

Proviamo alcuni casi:

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

la prima volta, nulla stato stampato, ma il predicato deve essere vero.

48

CAPITOLO 3.

Sia

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

pari a 1. Allora:

00<10
01<11
02<12

gi stato stampato
gi stato stampato
gi stato stampato

.
.
.

true 0 gi stato stampato


false 1 gi stato stampato
false 2 gi stato stampato

true
true
true true .

.
.
.

.
.
.

In questo caso la prima implicazione vera solo se il valore

0 stato eettivamente stampato. Tutte le implicazioni


P (1) vera perch stato stampato

seguenti, invece, valgono semplicemente perch la premessa falsa. Quindi,


il valore

e descrive la congurazione subito dopo aver interpretato la riga 5.

Potremmo continuare per ogni valore di

i,

constatando che

di valori sono stati eettivamente stampati.

P (i)

vero grazie al fatto che un numero crescente

Il modo corretto di procedere sarebbe dimostrare

i.P (i)

per

induzione, ovvero occorrerebbe dimostrare:

(P (0) (i.P (i) P (i+1)) (i.P (i)) .


Siccome

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

/* P (i) && i == n implica P (n).


Quindi, sullo schermo ci sono i valori 0, 1, ..., n-1.
*/

Esercizio 19 (Correttezza parziale con predicati implicativi)

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

1. Sia dato il seguente algoritmo:

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:

P (i) k.0 k < i k -esimo


2. Sia dato il seguente algoritmo:

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

stato letto e scritto

3.6.

CORRETTEZZA PARZIALE PER SNNP

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

usando il seguente predicato:

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

interi e stampa il massimo tra essi incontrato. Quindi,

dimostrarne la correttezza parziale.


Una soluzione espressa nel linguaggio di riferimento in

MassimoTraInteri.java.

2. Scrivere un algoritmo che legga iterativamente numeri naturali da tastiera.


valore

0.

La lettura termina all'inserimento del

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

nale. Qiundi, dimostrarne la correttezza parziale.

Una soluzione espressa nel linguaggio di riferimento in

3.6.1

ContaPariDispari.java.

Iterazioni annidate

Fissati due numeri naturali

ed

n,

il seguente algoritmo stampa una matrice di simboli

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

una riga completa con

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

Sono state stampate completamente


stampa e conterr

i-1

righe, ciascuna con

asterischi. La riga pi in basso di indice

in via di

asterischi.

La situazione descrivibile con il seguente predicato:

k == 0, allora la k-esima riga gi completamente stampata e


k == 1, allora la k-esima riga gi completamente stampata e
e se k == i-2, allora la k-esima riga gi completamente stampata
se k == i-1, allora la k-esima riga gi completamente stampata
la i-esima riga contiene j occorrenze di * .
se
se

e
e

possibile comprimere la formalizzazione del predicato precedente, sfruttando un quanticazione universale:

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 *

Per comprende bene di cosa predichi il predicato

P (0, 0)

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

P (i, j)

completamente stampata
.

ssiamo alcune combinazioni di valori per

vero e descrive una situazione in cui non sia stato stampato alcun

j:

in alcuna riga perch:

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,

Per valori intermedi di

P (m, 0)
j

indica che sono state completate esattamente

righe.

il predicato vero e descrive una situazione generica come quella rappresentata

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:

// m contiene un qualche valore in N


// n contiene un qualche valore in N
i = 0;
j = 0;
// P (0, 0)
while (i < m) do
// P (i, j)
while (j < n) do
// P (i, j)
stampa *;
// P (i, j + 1) perch appena stato stampato * di posizione j nella riga i-esima
j = j + 1;
// P (i, j) perch j rappresenta lespressione j+1
end while

/* j == n perch il ciclo pi interno terminato.


Quindi stata completamente stampata la i-esima riga. Allora P (i + 1, j) vero
*/
i = i + 1; cambia riga;
// P (i, 0) perch i rappresenta lespressione i + 1 e la riga con tale indice non ha
*

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.

CORRETTEZZA PARZIALE PER SNNP

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:

P (i, j) per ogni k,


se 1 <= k < i, allora k-esima tabellina completamente stampata
e per ogni k, se 1 <= k < j , allora il i*k stato stampato

TavolaPitagoricaWhile.java un programma nel linguaggio di riferimento, commentato con la dimostrazione


di correttezza parziale per riscrittura.

Il seguente algoritmo:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:

// m contiene un qualche valore in N


// n contiene un qualche valore in N
i = 0;
j = 0;

while (i < m && i < n) do


while (j < i) do
stampa *;
j = j + 1;

end while

i = i + 1;
cambia riga;
j = 0;

end while

ssa due numeri naturali

ed

del lato pari al minimo tra

e stampa un triangolo, usando il carattere

ed

*.

Il triangolo equilatero. La lunghezza

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

e i-esima riga ha j occorrenze di *

occorrenze di

un programma nel linguaggio di riferimento, commentato con la dimostrazione di

correttezza parziale per riscrittura.

Il seguente algoritmo:

1:
2:
3:
4:
5:
6:
7:
8:
9:
10:
11:
12:
13:

// m contiene un qualche valore in N


// n contiene un qualche valore in N
i = 0;
j = 0;

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

ssa due numeri naturali

m ed n e, usando il simbolo *, stampa un triangolo equilatero di base m, se m<=n.


m righe e base minore di m-n righe.

stampa un trapezio con base maggiore di

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

ELEMENTI DI BASE PER LA CORRETTEZZA PARZIALE

un programma nel linguaggio di riferimento, commentato con la dimostrazione di

correttezza parziale per riscrittura.


3.6.2

Iterazioni annidate e correttezza senza predicati implicativi

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

m ed il moltiplicatore M , utilizzando solo incrementi e decrementi di una unit.

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

indica il risultato. Si pu descrivere la congurazione col predicato:

m M = r + (m n) + m (M (N + 1)) r = m N + n

(3.10)

che si pu leggere come segue:

nel risultato

Il resto delle unit non in

ci sono

MSIDPCicliAnnidati

blocchi gi completi, cio con

r,

ovvero

m (M (N + 1))

uni, ed uno parziale con solo

n<m

sono quelle che mancano per ottenere

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:

[Wir73, Capitolo 5] che introduce la notazione a ow chart dei programmi.


essere il rendere pi leggibile la natura iterativa del costrutto

Hoare logic  wikipedia.

while-do.

Il vantaggio della notazione pu

Capitolo 4

Metodi e Modello di gestione della memoria


Illustreremo aspetti della gestione della memoria da parte della java virtual machine, che risponde al comando
che usiamo per interpretare un qualsiasi le oggetto con estensione

javac

ad un le sorgente con estensione

class,

java e

prodotto dall'applicazione del compilatore

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.

Frame e variabili locali al main

4.1

Supponiamo di aver compilato la classe seguente, per ipotesi memorizzata nel le

SoloMain.java:

1 public class SoloMain {


3

public static void main (String[] args) {

int
a = 1;
boolean b = (a == a);

System.out.println(b);
9

}
}

Questo signica avere a disposizione il le oggetto


Il comando
metodo

java SoloClass

SoloMain.class.

guida il PC all'interpretazione del codice in

SoloMain.class

corrispondente al

main.

Allocazione.

Prima di qualsiasi altra operazione avviene l'allocazione di un insieme di Byte.

L'insieme detto frame attivo.


Allocare Byte  signica riservare Byte  per uno specico scopo. Lo scopo memorizzare i valori assunti dalle
variabili che compaiono nel

main

durante l'interpretazione.

Quindi, il frame allocato per il

main

ha la seguente

struttura:
...

b
a
frame :

main
args

null

...

che analizziamo nel dettaglio, partendo dal basso:

la cella di indirizzo * la prima allocata.

Essa logicamente divisa in due parti, seguendo la notazione del

testo [SM14]. Per ora, senza ulteriori dettagli, suciente dire che:

54

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

 una parte permetter di recuperare un valore calcolato,


 l'altra parte conterr, idealmente, un numero di linea del sorgente.

Anche le altre celle hanno un indirizzo.


Al nostro livello di dettaglio, come indirizzo usiamo il nome della variabile che ciascuna cella rappresenta:

indica la cella che sar usata per contenere i valori assunti da

indica la cella che sar usata per contenere i valori assunti da

b.

Notiamo che n la cella

a,

contengano alcun valore.

dell'interpretazione delle righe che compongono il metodo

Rimane la cella di indirizzo

durante l'interpretazione e

I valori saranno scritti solo come conseguenza

main.

args. Essa rappresenta il parametro


String[] nella signature :

formale del metodo

main

che troviamo scritto

tra parentesi, assieme al suo tipo

main (String[] args)


contenuta nella dichiarazione del metodo:

public static void main (String[] args)


Per ora anticipiamo solo il necessario per comprendere quel che sta succedendo:

 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.

String[] indica il tipo del parametro esattamente


int specica il tipo della variabile a.
Il punto che String[] non un tipo primitivo.

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

non assume alcun valore (non primitivo)

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.

Quel che succede esattamente quel che possiamo

aspettarci, interpretando linea per linea il sorgente, come segue:

Linea 5. Occorre interpretare un'assegnazione. Valutiamo l'espressione a destra del simbolo


Esso va memorizzato nella cella di indirizzo

a,

=,

ottenendo il valore

1.

omonimo alla variabile.

Partendo dalla congurazione a sinistra, terminiamo col frame sulla destra:

...

...

a
frame :

main

args

null

1
frame :

main

args

null

...

...

Linea 6. Occorre interpretare un'assegnazione. Valutiamo l'espressione a destra del simbolo

=.

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.

cominciando a cercarne l'esistenza proprio dal frame

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.

Partendo dalla congurazione a sinistra, terminiamo col frame sulla destra:

4.2.

METODI SENZA RISULTATO, MA CON PARAMETRI

55

...

...

b
a

1
frame :

frame :

main

args

...

Linea 8. Occorre interpretare il richiamo al metodo il cui nome


argomento il valore contenuto in una variabile di nome
Come per la valutazione dell'espressione

1
null

...

frame attivo. Siccome

main

args

null

true

esiste e contiene

System.out.println

cui passiamo come

b.

a == a, cerchiamo l'esistenza
true, possiamo interpretare:

di una qualche variabile di nome

nel

System.out.println(true);
ottenendo la pubblicazione della costante

true

sullo standard output, ovvero, tendenzialmente, sullo schermo

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

...

Le celle contengono ancora i valori risultanti dall'interpretazione del metodo

main.

Tuttavia, esse sono disponibili per

memorizzare valori, non appena siano nuovamente coinvolte nella formazione di un frame.

4.2

Metodi senza risultato, ma con parametri

Interpretiamo ora il sorgente

MetodoArgomento.java:

public class MetodoArgomento {


public static void m (int b) {
int a = b + 4;
4
}
2

6
8

public static void main (String[] args) {


int a = 1;
int b = 2;

56

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

m(b + 3);
b = b + 1;

10

}
12 }

cui corrisponder l'oggetto

MetodoArgomento.class

prodotto per mezzo di

javac.
main, che diventa quello attivo, e prosegue con
M(b + 3);. Il metodo m interpretato in maniera

L'interpretazione comincia con l'allocazione del frame relativo al


l'interpretazione delle sue linee di codice sino a quella contenente
del tutto analoga a

main.

Occorre allocare un frame, che diventa quello attivo, e che deve essere adatto a contenere

m c' anche il parametro formale b che dovr


m se ne interpretano tutte le istruzioni, in analogia a
quanto faremmo con le linee di codice nel main. Al termine di m si disalloca il frame ad esso relativo. Quindi, il frame
attivo ridiventa quello di main. Il punto di rientro la stessa istruzione che ha generato l'allocazione del frame per m,
ovvero la chiamata M(b + 3) al metodo m. Quindi, si procede, interpretando la parte di main che segue il richiamo
del metodo m.
i valori delle variabili necessarie ad
assumere il corretto valore.

m.

Tra le variabili del frame per

Una volta allocato il frame per

Discutiamo il dettaglio, illustrando l'evoluzione dell'utilizzo della memoria.

Allocazione

main.

Interpretando la riga 6 passiamo dalla congurazione a sinistra a quella destra:

...

...

b
a
frame :

main
args

null

...

...

Interpretazione

main.

L'interpretazione delle righe 7 ed 8 modicano il frame, passando dalla congurazione di

sinistra a quella sulla destra:


...

...

b
a
frame :

frame :

main

m.

dell'espressione

a
args

null

...

Allocazione

main

args

null

...

La riga 9 contiene, in gergo, una chiamata al metodo

m,

usando come parametro attuale il risultato

b + 3.

Se la riga 9 fosse, ad esempio:

M(3);
essa conterrebbe una chiamata al metodo

m,

usando come parametro attuale il risultato dell'espressione

3.

L'interpretazione corretta di una chiamata procede in accordo con i seguenti passi:

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);

e l'espressione che costituisce il parametro attuale

La sua valutazione richiede di individuare il valore della variabile

b.

b + 3.

La cerchiamo a partire dal frame attivo che

anche l'unico disponibile, per il momento.


Siccome, nel frame attivo,

vale

2,

il valore del parametro attuale, ovvero dell'espressione

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

in una delle due posizioni della cella

*.

Passiamo

4.2.

METODI SENZA RISULTATO, MA CON PARAMETRI

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

quello attivo. Passiamo dalla congurazione di sinistra a quella sulla destra:


...

a
...

frame :

b
/

frame :

main

args

null

frame :

main
args

null
...
/

...

Osservazioni cruciali sono:

 il frame di

idealmente sopra quello del

main.

Questo signica rispettare la politica di allocazione

FIFO dei frame, gi accennata. Ne vedremo tra poco il signicato.

 Il frame di

pu contenere variabili che sembrano avere lo stesso nome di quelle del

main.

In realt le

variabili non sono omonime perch, per individuarle, quel che conta anche il frame nel quale sono denite.

 Esattamente come per il parametro formale


formale

di

args

di

main,

stato allocato dello spazio per il parametro

m.

Una volta allocato il frame per

si assegna al parametro formale il valore del parametro attuale.

Nel nostro caso il parametro formale l'argomento tra parentesi

nella signature

sappiamo gi che il valore del parametro attuale, ovvero dell'espressione


Assegniamo

nel frame di

m,

b + 3

in

M(int b) di m.
M(b + 3);, 5.

passando dalla congurazione di sinistra a quella sulla destra:


...

...

a
frame :

b
/

frame :

a
frame :

a
args
/

completa, possiamo interpretare

null

...

main

args

null

attivo.

5
/

frame :

main

Ora che l'allocazione del frame di

Inoltre,

...

m,

guardando al suo

frame

come a quello

58

CAPITOLO 4.

Interpretazione di

m.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

Consiste nell'interpretare l'unica assegnazione a riga 3. Il primo passo l'interpretazione

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,

valore associato all'occorrenza di

nel frame del

main

che

non quello attivo.


La somma tra

pari a

va scritta nella variabile

a.

Come per

b,

essa va cercata tra le variabili disponibili

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

...

Nessuna riga signicativa segue la numero 3 di

ed il frame viene disallocato, lasciando le

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

...

main (parte restante).

Dopo la chiamata di

con parametro attuale

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

stack, ovvero il frame di

b.

La variabile

Quindi a

main.

Il punto

nel frame attivo che quello rimasto in cima al frame

main.

La riga 10 richiede d'assegnare alla variabile


in

b il valore di una espressione, usando il valore che si trova attualmente


main.

quella che troviamo nel frame attivo in cima al frame stack : quello di

viene assegnato il valore

sinistra a quella sulla destra:

3,

che risulta dalla valutazione di

2 + 1,

passando dalla congurazione di

4.3.

METODI CON PARAMETRI E RISULTATO

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

disallocato, passando dalla congurazione di sinistra a quella sulla destra:


...

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

Metodi con parametri e risultato

Interpretiamo ora il sorgente

MetodoArgomentoRisultato.java:

public class MetodoArgomento {


public static int m (int b) {
int a = b + 4;
4
return a;
}
2

public static void main (String[] args) {


int a = 1;
int b = 2;
int c = m(b + 3) + 1;
}

8
10
12 }

cui corrisponder l'oggetto

MetodoArgomentoRisulatato.class prodotto
MetodoArgomentoRisultato.java sono:

per mezzo di

javac.

Le dierenze sostanziali con

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

Questo signica che l'interpretazione di

int.

L'ultima istruzione

return a;

di

lo strumento che permette la restituzione del valore che

termine dell'interpretazione del corpo del metodo

m.

conterr al

60

CAPITOLO 4.

Anche la chiamata di

in

main

modicata.

dalla chiamata sar memorizzato in

METODI E MODELLO DI GESTIONE DELLA MEMORIA

Essendo essa

int c = M(b + 3) + 1;,

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.

Interpretando la riga 7, passiamo dalla congurazione a sinistra a quella destra:


...

...

c
b
frame :

main

args

null

...
...

Esiste la necessit di allocare spazio anche per la variabile

Interpretazione

main.

c,

siccome, per essa, esiste una dichiarazione.

Come in precedenza, l'interpretazione delle righe 8 e 9 permette di passare dalla congu-

razione di sinistra a quella sulla destra:


...

...

b
frame :

frame :

main

args

null

main

m.

m,

a
args

...

L'assegnazione alla riga 10 richiede di valutare l'espressione

dipende dal quello che

null

...

Allocazione

applicato al valore dell'espressione

valutiamo il valore del parametro attuale

usiamo il valore restituito dalla chiamata ad

b + 3
m

b + 3,

M(b + 3) + 1;.

Il suo valore nale

produce. Nell'ordine, occorre che:

da utilizzarsi per la chiamata ad

m,

per determinare il valore dell'intera espressione a destra dell'as-

segnazione alla riga 10.


Cominciamo col valutare il parametro attuale
dal

attivo il valore

in

b,

ottenendo

b + 3.

Meccanicamente, come nel caso precedente, recuperiamo

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

Per valutare il valore da assegnare a

come valore per

occorre chiamare

sulla destra:
...

...

frame :

main

...

frame :

main

args

null

a
args

null

10
...

4.3.

METODI CON PARAMETRI E RISULTATO

Allochiamo il frame del metodo chiamato

formale

e la variabile locale

a.

61

m, piazzandolo sopra

il frame di

main e allocando spazio per il parametro

Passiamo dalla congurazione di sinistra a quella sulla destra:


...

a
...

frame :

c
/

*
c

frame :

main

frame :
/

10

args

null

main

*
args

null
...
/

10

...

Una volta allocato il frame per

si assegna al parametro formale il valore del parametro attuale che vale

5.

Passiamo dalla congurazione di sinistra a quella sulla destra:


...

...

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 :

...

Una volta completata l'allocazione del frame di

di memorizzare

null

...

Interpretazione di

main

10
...

frame :

main

args

null

a
args

null

10
...

62

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

A dierenza del caso precedente, occorre ancora valutare la riga 4.


Per denizione,

return <espressione>
* del metodo chiamante.

scrive il valore di

<espressione>

nella parte non ancora utilizzata

della cella di indirizzo

<espressione> coincide con la variabile a del frame attivo. Quindi,


main, che il metodo chiamante. Passiamo dalla congurazione di sinistra

Nel nostro caso,


nella cella

del

...

frame :

b
/

frame :

frame :

main

frame :

main

m.

a,

a
args

null

10

...

Disallocazione di

valore di

args

null

10

9,

a quella sulla destra:

...

scriviamo

...

Non esistendo pi righe nel corpo di

m, disallochiamo.

Passiamo dalla congurazione di sinistra

a quella sulla destra:

...

frame :

...

frame :

main

10

frame :

main

args

null

perch presente in
di

main (parte restante).


* del frame del main, ora

a
args

null

10

...

Interpretazione

...

Conclusa la chiamata di

m,

il valore da esso prodotto recuperabile

nuovamente attivo. Tale valore non invece pi recuperabile dal frame

perch non pi raggiungibile.

fondamentale ricordare che la chiamata ad

parte della valutazione dell'espressione a destra del simbolo

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:

che ora sappiamo essere il risultato della valutazione di

del frame attivo assegniamo il valore

10.

=
M(b + 3)

9 + 1.

Passiamo dalla congurazione di sinistra a quella

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

disallocato, passando dalla congurazione di sinistra a quella sulla destra:


...

...

frame :

main

10

10

args

null

10

args

null

10

...

...

MetodoArgomentoRisultato.java,

il programma java che, attraverso la stampa dei valori nelle variabili,

evidenzi quelle accessibili durante l'interpretazione descritta.

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:

public class VarStatiche {


2
4
6
8
10
12
14
16

static final int DELTA = 1;


static int statica = 0;
public static void main(String[] args) {
int x = 0;
mA(x);
mB(x,3);
}
public static void mA(int x) {
x = x + DELTA;
statica = statica + DELTA;
}

final.
VarStatiche.java, di

cui simuliamo la gestione della

64

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

public static void mB(int x, int y) {


final int DELTA = 5;
x = x + y;
statica = statica + DELTA;
}

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

comincia dalle prime righe, tra le quali troviamo:

static final int DELTA = 1;


static int statica = 0;
Esse allocano le due variabili

DELTA

statica

che compaiono nelle rispettive assegnazioni.

nella memoria statica, inizializzandole con i valori delle espressioni

Siccome il codice di nessuno dei metodi stato interpretato, il frame

stack vuoto ed abbiamo la seguente situazione iniziale:

...
1

DELTA

statica

Static memory

Frame stack

Terminata l'interpretazione del preambolo della classe, l'interprete cerca il metodo

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

La riga 8 un richiamo al metodo

mA

il cui parametro attuale vale

0,

valore recuperato nella variabile

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.

Per recuperare entrambi i valori, occorre cercare sia

La prima esiste nel frame attivo e contiene il valore

x,

sia

DELTA

che dipende dai valori in

nel frame attivo.

0.

La seconda non esiste nel frame attivo.


Quando un nome non esiste nel frame attivo, per denizione, esso viene cercato nella memoria statica: il valore
recuperato in

DELTA

1.

L'interpretazione della riga 13 modica la congurazione della memoria come segue:

4.4.

CAMPI STATICI E

65

FINAL
...
x

1
mA
1

DELTA

statica

*
x

Static memory

args

null

main

Frame stack

Interpretare la riga 14 richiede la valutazione dell'espressione a destra del simbolo

statica

DELTA.

Per recuperare entrambi i valori, occorre cercare sia

statica,

sia

= che dipende dai valori


DELTA nel frame attivo.

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.

L'interpretazione della riga 14 modica la congurazione della memoria come segue:

...
x

1
mA
1

DELTA

statica

*
x

Static memory

args

null

main

Frame stack

fondamentale sottolineare che

mA

ha modicato il valore di

statica,

variabile che non appartiene al suo frame,

ma che esiste nella memoria statica.


Il frame attivo, costruito per

mA,

in mancanza di ulteriori istruzioni da interpretare viene a disallocato:

...
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.

appena interpretata, procediamo con la 9. Essa

che richiede di valutare due parametri attuali.

La valutazione del primo restituisce il valore di


che, in linea di principio, una variabile di nome

presente nel frame attivo del

main,

indipendentemente dal fatto

sia precedentemente stata allocata nel frame del metodo

mA.

La valutazione del secondo parametro immediata perch esso un numero.


Conosciamo gi il processo di allocazione e di assegnazione dei corretti valori al parametro formale. Otteniamo
quindi, la congurazione:

66

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

...
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

* del main e l'assegnazione alla variabile

gi stata interpretata.

Per ipotesi, invece, non abbiamo ancora interpretato le linee 19 e 20.


La linea 19 richiede la valutazione dell'espressione

attivo di

mB.

Il risultato nale memorizzato in

Entrambi i nomi vengono cercati e trovati nel frame

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

La linea 20 richiede la valutazione dell'espressione

L'interpretazione di entrambe le righe produce la congurazione:


...
5

DELTA

mB
1

DELTA

statica

Static memory

0
main

args

null

Frame stack
L'osservazione fondamentale la condivisione del campo statico di nome

statica

tra i due metodi che evidenzia

l'unico modo che, eventualmente, impareremo per condividere informazioni tra metodi distinti.
Terminata l'interpretazione della riga 20, il frame di

main,

mB

viene disallocato, cos come viene disallocato quello del

subito dopo il rientro alla riga 9.

L'ultima congurazione della memoria diventa la seguente:


...

DELTA

statica

DELTA

Static memory

args

null

Frame stack
La nota nale sul signicato del modicatore

final.

Esso contraddistingue variabili o campi il cui valore, una

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

final costante = 1; allora


costante = <espressione>.

Equivalentemente, se nel

in nessun altro punto del codice di quella

4.5.

SIGNATURE, OVERLOADING, CAST, ETC.

VarStatiche.java

67

il programma java che, attraverso la stampa dei valori nelle variabili, evidenzia quelle

accessibili durante l'interpretazione descritta.

Signature, overloading, cast, etc.

4.5

Per denizione, la signature di un metodo comprende nome ed elenco dei tipi dei parametri formali. L'estensione della

signature con il tipo restituito in uscita,


Il seguente sorgente

void incluso, si identica come


MetodoOverloading.java sottolinea come la

dichiarazione.
signature serva a distinguere tra le dichiara-

zioni dei metodi.

public class MetodoOverloading {


2

public static int m () {


int a = 1;
return a;
}

4
6
8

public static int m (int a, boolean b) {


int c = (b) ? a + 1 : a - 1;
c = c + m();
return c;
}

10
12
14

public static int m (float a, boolean b) {


int c = (int)((b) ? a + 10 : a - 10);
return c;
}

16
18

public static void main (String[] args) {


int a = 2;
boolean b = false;
int c = m(a + 1, true && b);
m(1000f, true && b);
}

20
22
24

Esso contiene tre dichiarazioni del metodo

m.

Non sono in conitto perch:

il tipo del valore d'uscita identico e

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)

dierisce dal tipo nella stessa posizione in

L'allocazione del frame per il metodo

main,

M(int,

M(float, boolean).

e l'interpretazione delle righe 21 e 22 producono:

...
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

Entrambe le espressioni usano variabili locali al frame attivo di

false.

e la

tra le tre disponibili.

Siccome il tipo del primo parametro attuale

int

il

main. La prima fornisce il valore 3 e la


frame allocato per la chiamata quello

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.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

...
c
false

*
c

main

false

a
args

null

22

La riga 9 richiede la valutazione di una espressione condizionale

(b) ? a + 1 : a - 1; che si interpreta,


b e, a seconda del suo valore, si interpreta a + 1 o a - 1. Nel nostro caso, b, presente nel frame attivo
di m, vale false. Quindi valutiamo a - 1, usando a presente nel frame attivo di m stesso. Alla ne assegniamo il
valore 2 a c del frame attivo :
valutando

...
2

false

*
c

main

false

a
args

null

22

Il valore in

viene ulteriormente modicato dalla chiamata ad

frame allocato per tale chiamata relativo alla dichiarazione di

nell'espressione dell'assegnazione alla riga 10. Il

che troviamo alla linea 3. La congurazione della

memoria immediatamente prima della disallocazione dell'ultimo frame attivo diventa:

...
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

restituito dal metodo

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.

SIGNATURE, OVERLOADING, CAST, ETC.

69

...
a

false

10

*
c

false

main

args

null
3

22

Possiamo osservare che il valore di


metodo chiamante

main.

nel frame attivo stato scritto nella opportuna componente della cella

Disallocando il frame attivo di

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

del frame attivo del

presente nella seconda componente di

*.

main

in cui siamo

La congurazione diventa:

...
a

/
3

false

16

main

Procediamo, quindi, col richiamare


valore di tipo

float.

false

a
args

null
22

alla linea 23. Questa volta, il primo parametro attuale una costante il cui

Viene quindi allocato il frame della dichiarazione di

che troviamo alla riga 14.

La congurazione della memoria giusto prima della disallocazione del frame di


dell'istruzione

return c;

a linea 16 :

m,

che segue l'interpretazione

70

CAPITOLO 4.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

...
a

990

false

1000f

main

false

a
args

null
23

990

nella quale vale la pena osservare un paio di aspetti:

il valore di

di tipo

float,

ma quello di

int

Il motivo sta nell'aver applicato l'operazione di cast al

risultato dell'espressione condizionale alla riga 15. Senza di essa, il compilatore segnala una possibile perdita di
precisione.

Il frame attivo, relativo all'ultima chiamata di


dierente dichiarazione di

Il valore

990

ha usato le celle in precedenza sfruttate per la chiamata ad una

m.

comparso nella seconda componente del frame di

Disallocando il frame attivo per

main

in cui stiamo per rientrare.

la congurazione della memoria diventa:

...
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

quelle accessibili durante l'interpretazione descritta.

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.

METODI E MODELLO DI GESTIONE DELLA MEMORIA

Capitolo 5

Elementi di base per la Terminazione


In questo capitolo trattiamo gli aspetti fondamentali della prova di terminazione di un programma. Dimostrare che un
programma termini importante tanto quanto lo sia la correttezza parziale. Per denizione, un programma corretto
se, e solo se:

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

s contiene il valore 0 N ed r un valore diverso da 0? L'espressione argomento dell'iterazione rimane


r = r - s non decrementa r.

vera indenitamente, siccome l'assegnazione

L'algoritmo parzialmente corretto, grazie a quanto detto nell'Esercizio 8.

Per, l'algoritmo non

(globalmente) corretto, esistendo almeno un valore per cui non termina.


Per sintetizzare algoritmi corretti, non solo parzialmente, , quindi, necessario conoscere almeno i rudimenti
per la verica della terminazione sui quali si basano strumenti automatici, come quelli, ad esempio, incorporati in

Dafny@rise4fun from Microsoft.


Copiando QuozienteRestoDifferenzaIterata.dafny in Dafny@rise4fun from Microsoft possibile sperimentarne l'utilizzo, con un click sul tasto a sfondo viola, il quale produce un messaggio che segnala l'impossibilit
di dimostrate la terminazione. Il messaggio d'errore sparisce, ad esempio, modicando il preambolo del programma
come segue:

requires d >= s;
requires s != 0;

Esso esclude esplicitamente da quelli utilizzabili i valori di

5.1

ed

che creano problemi.

Terminazione per SIDP

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

ELEMENTI DI BASE PER LA TERMINAZIONE

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

assume solo valori in

Siccome

i il quale, inizialmente, assume un valore in N e, al termine di ogni iterazione:

N.

pu solo calare, assumendo solo e sempre valori in

minimo presente in

N,

ovvero il valore

0.

Possiamo dimostrare le due propriet godute da


ed assumendo, per denizione, che

ik

N,

non potr mai assumere valori inferiori al

Quindi, l'iterazione non potr procedere indenitamente.

i, procedendo per induzione sul numero k di iterazioni completate,


i dopo aver eseguito completamente k iterazioni, con 0 k .

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

valgono le seguenti relazioni:

ik+2 < ik+1

vera perch implicata da

ik+1 1 < ik 1
ik+1 < ik

vera grazie a istruzione 7 ed implicata da


vera per ipotesi induttiva

Dimostrazione che

i N . Deniamo il predicato V (k) ik N che formalizza l'appartenenza del valore di i


(V (0) (k.V (k) V (k + 1))) k.V (k).
Nel caso base, V (0) vero perch i0 = n N.
Per il caso induttivo, assumiamo che V (k) sia vero, cio che ik N, con k > 0. Siccome k > 0, l'iterazione
stata completata almeno una volta. Questo implica n > 0. Altrimenti non sarebbe possibile aver interpretato il corpo
ad

N.

Lo scopo dimostrare

dell'iterazione, terminando almeno una iterazione. Ne consegue che:

ik > 0 .

(5.1)

Valgono, perci, le seguenti implicazioni:

5.2

ik+1 N

vera perch implicata da istruzione 7 e da

ik 1 N

vera perch implicata da (5.1) e da

ik N

vera per ipotesi induttiva

Terminazione di un algoritmo alternativo per SIDP

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

e tale che, al termine di ogni

5.2.

TERMINAZIONE DI UN ALGORITMO ALTERNATIVO PER SIDP

assume solo valori in

Siccome

75

N.

N (i) assume solo valori in N e cala ad ogni iterazione, quest'ultima dovr terminare, perch N (i)
0.

non pu assumere valori inferiori a

importante sottolineare che

N (i) debba essere denita da chi voglia dimostrare la terminazione attraverso

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

decrescere del valore di


Nel caso base,

N (i1 ) = n i0 1
=nn1
= 1
<0
=nn
= n i0
= N (i0 ) .
Per il caso induttivo, assumiamo

T (k)

vero, cio

N (ik+1 ) < N (ik ).

Allora, la tesi

N (ik+2 ) < N (ik+1 )

vera

grazie alle seguenti relazioni:

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 .

V (k) n ik N che formalizza l'appartenenza del valore


(V (0) (k.V (k) V (k + 1))) k.V (k).
n i 0 = n n = 0 N.
V (k) vero, cio n ik N, con k > 0. Siccome k > 0, l'iterazione stata

Deniamo il predicato

Lo scopo dimostrare

Nel caso base,

V (0)

vero perch

Per il caso induttivo, assumiamo

completata almeno una volta. Questo implica:

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

grazie a (5.3) segue da

n ik N

equivalente a

N (ik ) N

vera per ipotesi induttiva

76

CAPITOLO 5.

5.3

ELEMENTI DI BASE PER LA TERMINAZIONE

Criterio generale per la terminazione

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

dell'iterazione sia inizialmente vero, permettendo l'interpretazione del corpo

almeno

una volta. Sotto questa ipotesi, condizione necessaria, ma non suciente, anch il ciclo termini, che le istruzioni
in

modichino almeno una variabile di

C.

La condizione non suciente perch, ad esempio, se

0,

allora

modica una variabile di

C,

ma

i > 0

B contiene solo i = i + 1 e C coincide con l'espressione i >

non diventer mai falsa, impedendo la terminazione dell'iterazione.

Se la condizione suciente soddisfatta, per dimostrare la terminazione, occorre denire una opportuna misura

tale che:

dipenda dalle variabili manipolate dal corpo

decresca strettamente al crescere del numero di iterazioni eseguite,

assuma sempre e solo valori in

B,

N.

Fondamentale che questa denizione si rifaccia alla well-foundedness dell'insieme


una misura di terminazione, allora decresce all'interno di
necessariamente assumere il valore
valore di

5.4

0.

N,

Ovvero, un'iterazione cui associata una misura

assume un valore minimo in

N,

N.

Ovvero, data

non necessariamente pari a

pu terminare quando il

0.

Terminazione di un algoritmo per QRDIP

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

ma questo non richiede che, alla ne,

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

La ragione delle nuove assunzioni sta nell'esperimento con


sia che

debba essere diverso da 0, sia che

Dafny@rise4fun from Microsoft. Da esso sappiamo


s. L'applicazione dell'ipotesi sar cruciale,

non debba essere minore di

nella dimostrazione di terminazione, basata sulle propriet della misura:

N (r) = r .
Procediamo per induzione sul numero
eseguito completamente

iterazioni, con

k di iterazioni completate,
0 k.

Dimostrazione che

N (r) cala strettamente.


N (r) al crescere di k . Lo
T (0) vero perch:

decrescere del valore di


Nel caso base,

(5.4)
assumendo che

rk

indichi il valore di

r dopo aver

T (k) N (rk+1 ) < N (rk ) che formalizza


(T (0) (k.T (k) T (k + 1))) k.T (k).

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

N (rk+1 ) < N (rk ).

Allora, la tesi

N (rk+2 ) < N (rk+1 )

grazie alle seguenti relazioni:

N (rk+2 ) = rk+2
= rk+1 s
< rk+1
= N (rk+1 ) .

ipotesi

s>0

vera

5.4.

TERMINAZIONE DI UN ALGORITMO PER QRDIP

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

vero grazie a(5.5) e

rk N
N (ik ) N

vera per ipotesi induttiva

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

per terminare la dimostrazione di terminazione per QRDIP.

Esercizio 22 (Terminazione di problemi di riferimento e varianti) Dimostrare la terminazione dei seguenti algoritmi:

quelli proposti come soluzione ai problemi dell'Esercizio 8,

quelli che appaiono nelle Sezioni 3.2.2 e 3.2.4,

quelli proposti come soluzione ai problemi dell'Esercizio 10,

quelli che appaiono nella Sezione 3.6.1,

quelli proposti come soluzione ai problemi dell'Esercizio 21.

78

CAPITOLO 5.

ELEMENTI DI BASE PER LA TERMINAZIONE

Capitolo 6

Programmazione ricorsiva di base


Se dovessimo riassumere in maniera essenziale il principio guida alla base della programmazione iterativa potremmo
aermare che essa costruisce un risultato, accumulando, passo dopo passo, sue approssimazioni sempre migliori.
Il principio guida intuitivo alla base della programmazione ricorsiva una particolare visione dello schema divide et

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

l'insieme dei numeri naturali ed

un insieme

qualsiasi. Si pu allora utilizzare il seguente schema di ricorsione:

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

Il riferimento per la programmazione ricorsiva [SM14, Capitolo 7].

In particolare, la Sezione 7.2.1 in [SM14,

Capitolo 7] parla del ragionamento induttivo che sta alla base della progettazione di programmi ricorsivi.

6.1

Denizioni e computazioni ricorsive

Forniamo esempi di base di algoritmi deniti ricorsivamente.

6.1.1

Fattoriale

Focalizziamo la nostra attenzione sul calcolo del fattoriale

n!

di

n.

Un modo per descrivere il valore di

n!

elencare

ogni fattore moltiplicativo che compone la sua denizione:

n! = n (n 1) (n 2) (n (n 2)) (n (n 1)) .
Se ci chiediamo quanto valga
di

(n 1)!,

ponendo

n 1 = m,

possiamo procedere in maniera analoga, con

(6.1)

al posto

n:
m! = m (m 1) (m 2) (m (m 2)) (m (m 1)) .

Sostituendo

n1

ad

(6.2)

in 6.2, lo sviluppo diventa:

(n 1)! = (n 1) (n 2) (n (n 2)) (n (n 1)) .


Osserviamo che 6.3 compare, tale e quale, in 6.1 moltiplicato per

n.

(6.3)

A questo punto, la sostituzione di 6.3 in 6.1

cruciale per comprendere il nocciolo della questione:

n! = n (n 1)! .

(6.4)

80

CAPITOLO 6.
In 6.4 il problema di calcolare il valore

n!

PROGRAMMAZIONE RICORSIVA DI BASE

spezzato in due sotto-problemi.

Il primo consiste nel calcolare il fattoriale di

n 1.

Ovvero, stiamo riproponendo

il problema iniziale, ma in una veste pi semplice, perch relativo ad un valore


strettamente pi piccolo di quello iniziale.

(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)!.

Supponiamo, quindi, di essere in grado di calcolare il


Allora, conoscendo sia

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

come indicato dalla scomposizione vista:

2!.

Una volta ottenuto

2!,

lo moltiplichiamo per

ed otteniamo

3!.

1!.

Una volta ottenuto

1!,

lo moltiplichiamo per

ed otteniamo

2!.

0!.

Una volta ottenuto

0!,

lo moltiplichiamo per

ed otteniamo

1!.

2!.
richiede di calcolare

1!.
richiede di calcolare

0!.

deve essere primitivo.

Se non lo fosse, dovremmo applicare la denizione

risultato che azzererebbe qualsiasi valore moltiplicato per esso. Quindi,

0!

0! = 0 (0 1) =
1, valore

deve essere

neutro per la moltiplicazione.


Stabilito che

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.

Il processo di calcolo descritto riassumibile come segue:

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

Siamo nella condizione di poter riformulare

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:

1 public class FattorialeRec {


3
5
7

public static int fatt (int n) {


if (n == 0)
return 1;
else
return n * fatt(n - 1);
}

9
11
13 }

public static int fattAlt (int n) {


return (n == 0 || n == 1) ? 1 : n * fattAlt(n - 1);
}

FattorialeRec.java

del quale, per

6.1.

DEFINIZIONI E COMPUTAZIONI RICORSIVE

La denizione del metodo

fatt,

81

ad esempio, contiene un richiamo a se stesso. Come in precedenza si pu avere

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

Nell'ultima congurazione attivo solo il frame del

main

*
args

main
3

nil

args

main

main

che contiene il risultato ottenuto dall'ultimo frame di

fatt

disallocato.

Esercizio 23 (

Stack

della chiamata

fattAlt(3).) Disegnare una sequenza rilevante


fattAlt(3) in FattorialeRec.java.

delle congurazioni della me-

moria durante l'interpretazione della chiamata

6.1.2

Quadrato

Riprendiamo un esempio gi trattato quando abbiamo discusso gli invarianti:

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.

PROGRAMMAZIONE RICORSIVA DI BASE

Vediamo che le clausole precedenti deniscono eettivamente la funzione desiderata, dimostrando per induzione che la
propriet

q(n) = n n

vera per ogni valore di 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

(per ipotesi induttiva)

= (n + 1) (n + 1)

(per propriet algebriche)

Si osservi che le clausole della denizione ricorsiva della funzione

q(n)

consentono anche di calcolare il valore di questa

funzione per un valore arbitrario dell'argomento. Per esempio:

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,

una per ogni

A ed una funzione f : A A, si pu denire


n N, per ricorsione come segue, per ogni a A:
f (0) (a) = a
f (n+1) = f (f (n) (a))

Si vede che

f (n) (a) = f (. . . f (a) . . .)


| {z }
n volte

Esercizio 24 Dimostrare per induzione che la funzione denita dalle clausole:

(
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)

Esercizio 25 Dimostrare per induzione che la funzione denita dalle clausole:

tale che, per ogni

f (0)
=1
f (n + 1) = f (n) + f (n)

n N:
f (n) = 2n .

una collezione numerabile di funzioni

6.2.

FUNZIONI RICORSIVE FAMOSE

83

Esercizio 26 Dimostrare per induzione che la funzione denita dalle clausole:

tale che, per ogni

f (0) =
0
f (n + 1) = (n + 1) + f (n)

n N:
f (n) =

n
X

i=0

Esercizio 27 Dimostrare per induzione che la funzione denita dalle clausole:

s(0) = 1
s(n + 1) = 2/s(n)
tale che, per ogni

n N, s(n) {1, 2}.

Esempio 7 (Coeciente binomiale)

il numero di combinazioni composte da

Esercizio 28 Il numero

A(n)

CoeffBinomialeRec.java una funzione interessante almeno perch conta


n.

elementi presi tra

di modi in cui

persone possono essere assegnate a

poltrone pu essere denito

ricorsivamente mediante le clausole:

A(1) = 1
A(n + 1) = A(n) (n + 1)
Si dimostri per induzione che, per ogni

n 1, A(n) = n!.

Esempio 8 (Numero di Fibonacci)

FibonacciRec.java e Simulazione del frame stack

Esempio 9 (Massimo comun divisore)

di

FibonacciRec.java.

MCDRec.java confrontato con MCD.java in termini di ovviet della prova

di correttezza.

6.2
6.2.1

Funzioni ricorsive famose


Funzione di McCarty

McCarthy91.java

interessante per il suo comportamento oscillatorio che costituisce una sda nei confronti degli

strumenti di verica automatica della correttezza.

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

Schemi rilevanti di calcolo ricorsivo


Funzione identit

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

esegue una banale operazione di conteggio, siccome, per ogni

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.

PROGRAMMAZIONE RICORSIVA DI BASE

f (4) = f (2 2) = f (2) + f (2)

f (5) = f (2 2 + 1) = f (2) + f (2) + 1

f (2) = f (2 1) = f (1) + f (1) f (2) = f (2 1) = f (1) + f (1)

f (2) = f (2 1) = f (1) + f (1) f (2) = f (2 1) = f (1) + f (1)

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:

P (n) n.n N\{0} f (n) = n .


Procedendo per induzione forte sul valore dell'argomento di

f,

(6.6)

possiamo dimostrare:

(P (1) (n, k N\{0}. (k < n P (k)) P (n))) (n N\{0}. P (n)) .


Caso base.

Per denizione, dimostrare

Caso induttivo.

P (1)

equivale a dimostrare

f (1) = 1,

(6.7)

equivalenza vera per denizione di

f.

k < n P (k) per un valore


n pari o dispari. Se n pari, allora esiste m > 0 tale che n = 2m. Se n
dispari, allora esiste m >= 1 tale che n = 2m + 1. In entrambi i casi, m < n e per ipotesi induttiva vale P (m), ovvero
f (m) = m. Ma questa equivalenza, permette di concludere che vale P (n), sia con n pari, sia con n dispari:
ssato di

Siccome ragioniamo per induzione completa, assumiamo che valga

Necessariamente,

se

n>1

pari, da

se

n>1

dispari, da

6.3.2
Sia

n > 1.

f (m) = m

otteniamo

f (m) = m

f (n) = f (2m) = 2f (m) = 2m = n,

otteniamo

f (n) = f (2m + 1) = 2f (m) + 1 = 2m + 1 = n.

Funzione successore

la seguente funzione:

g(0) = 1
g(n) = g(dn/2e 1) + g(bn/2c) .
Al contrario della funzione

della precedente sezione,

per stabilire quale tra le due clausole eseguire.

non sfrutta il cos detto meccanismo di pattern matching

Intendiamo che non demandiamo al momento dell'applicazione il

problema di discriminare se l'argomento sia pari o dispari. Al contrario, adiamo alle operazioni

dxe

bxc

il compito

di calcolare il giusto valore dell'argomento da utilizzare nel sotto-problema.


Dimostriamo

n.Q(n)

in cui:

Q(n) n.n N f (n) = n + 1 .


Procedendo per induzione forte sul valore dell'argomento di

g,

(6.8)

possiamo dimostrare:

(Q(0) (n, k N. (k < n P (k)) P (n))) (n N. P (n)) .


Caso base.

Per denizione, dimostrare

Caso induttivo.
ssato di
consegue

Q(0)

equivale a dimostrare

g(0) = 1,

equivalenza vera per denizione di

g(n) = g(dn/2e 1) + g(bn/2c)

(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,

dimostrare che essa calcola il successore.

g.

k < n Q(k) per un valore


bn/2c < n. Per ipotesi induttiva, ne

Siccome ragioniamo per induzione completa, assumiamo che valga

n > 0. Per denizione di de e bc valgono le relazioni dn/2e 1 < n


che g(dn/2e 1) = dn/2e 1 + 1 e g(bn/2c) = bn/2c + 1. Quindi:

(6.9)

dideebc)

6.4.

RICORSIONE E CORRETTEZZA PARZIALE PER INDUZIONE

85

2. Data la funzione:

f (0) = 0
f (1) = 1
f (n) = f (dn/2e) + f (bn/2c)
in cui

m 0,

dimostrare che essa calcola l'identit.

3. Data la funzione:

1
f (l, r) = f (l, dn/2e) + f (bn/2c, r) 1

f (l, dn/2e) f (bn/2c, dn/2e) + f (bn/2c, r)


in cui

l, r 0,

dimostrare che essa calcola

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

f (l, dn/2e) + f (bn/2c, r)


in cui

l, r 0,

dimostrare che essa calcola

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)

f (l, dn/2e) f (bn/2c, dn/2e) + f (bn/2c, r)


l, r 0, dimostrare che
primi n numeri naturali?

in cui
dei

6.4

essa calcola

se
se
se

Pr

l+r =2l
l + r dispari
l + r pari

i=l i. , quindi, possibile aermare che

f (l, r)

possa calcolare la somma

Ricorsione e correttezza parziale per induzione

Il programma

sXYRec.java

riformula ricorsivamente il problema SIDP della somma tra due naturali.

Segue la dimostrazione di correttezza parziale che consiste nel dimostrare

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:

m N. (P (m, 0) (n N. (P (m, n) P (m, n + 1))) (m, n N. P (m, n)) .


Caso base.

Fissiamo

secondo argomento vale

Caso induttivo.

m N. Dimostrare P (m, 0) equivale a dimostrare sXY(m, 0).


0, il programma restituisce m. Quindi, sXY(m, 0) = m = m + 0

(6.11)

Per denizione, quando il


quel che attendiamo.

m N ed n N\{0}. Lo scopo dimostrare P (m, n + 1), che equivale a sXY(m, n + 1) =


P (m, n), che equivale a sXY(m, n) = m + n.
Guardiamo al codice di sXYRec.java. Richiamare sXY(m, n+1) equivale a voler restituire il risultato dell'espressione 1 + sXY(x, y - 1); nella quale il parametro formale x assume il valore m ed il parametro formale y assume
il valore n 1. Quindi, lo scopo valutare l'espressione 1 + sXY(m, (n + 1) 1) che uguale a 1 + sXY(m, n). Per
ipotesi induttiva, sappiamo che sXY(m, n) = m + n, valore che possiamo sostituire a sXY(m, (n + 1) 1), ottenendo
1 + m + n. Quindi, sXY(m, n + 1) = m + n + 1. Il processo formale, senza prosa:

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.

PROGRAMMAZIONE RICORSIVA DI BASE

Esercizio 30 (Dimostrazioni di base della correttezza parziale per induzione)

Scrivere un algoritmo ri-

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.

Dimostrare la sua correttezza parziale per induzione.


(eXYRec.java una soluzione possibile.)

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

Dimostrare la sua correttezza parziale per induzione.

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

public static void muoviTorre (int n, int s, int d, int aus) {


if (n > 0) {
muoviTorre (n-1,s,aus,d);
muoviDisco(s,d);
muoviTorre (n-1,aus,d,s);
}
}

rappresenta una soluzione ricorsiva al problema, assumendo:

l'esistenza di un metodo

al piolo destinazione

che realizzi eettivamente lo spostamento di un disco dal piolo sorgente

muoviDisco
d,

che

indichi il numero di dischi impilati sul piolo sorgente

che

sia il piolo destinazione ,

che

aus

s,

sia il piolo ausiliario, per gli spostamenti intermedi.

Il motivo per cui la descrizione dell'algoritmo estremamente compatta dipende proprio dal fatto che il problema
in cui occorra spostare

dischi scomposto in due problemi pi piccoli, ovvero che operano solo su

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.

Lo scopo passare dalla congurazione iniziale a sinistra

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).

Questo signica supporre di passare in un sol

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.

RICORSIONE E CORRETTEZZA PARZIALE PER INDUZIONE

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).

Passiamo, quindi dall'ultima congurazione a quella nale:


|

...

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.

Esercizio 31 Dimostrare per induzione su

m che muoviTorre esegue l'istruzione muoviDisco(s,d) per 2m 1 volte,

n sia m.

possibile che per portare a termine la dimostrazione occorra anche dimostrare

supponendo che il valore iniziale di


che

2n 1 =

Pn

i
i=0 2 .

TorreHanoi.java

una classe che completa

muoviTorre,

stampando la sequenza di mosse per risolvere il

problema della Torre du Hanoi.

6.4.2

Sommatoria di un segmento di naturali

Dimostriamo la correttezza del seguente metodo sorgente:

public static int sommatoriaTraZeroEd(int x) {


if (x == 0)
return 0;
else
return x + sommatoriaTraZeroEd(x - 1);
}

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:

(P (0) (n N. (P (n) P (n + 1))) (n N. P (n)) .


Caso base.

vale

0,

Dimostrare

(6.13)

P (0) equivale a dimostrare sommatoriaTraZeroEd(0)


= 0. Quando il parametro P
formale
Pn
n
0, dall'altro i=0 i = 0, quindi sommatoriaTraZeroEd(0) = 0 = i=0 i.

da un lato il metodo restituisce

Caso induttivo.

n N\{0}. Lo scopo dimostrare P (n + 1), che equivale a sommatoriaTraZeroEd(n + 1) =


Pn
P
(n)
, che equivale a sommatoriaTraZeroEd(n) =
i=0
i=0 i.
Guardiamo al codice sorgente. Richiamare sommatoriaTraZeroEd(n + 1) equivale a voler restituire il risultato dell'espressione x + sommatoriaTraZeroEd(x - 1) nella quale il parametro formale x assume il valore
n 1. Quindi, lo scopo valutare l'espressione (n + 1) + sommatoriaTraZeroEd((n + 1) P
1) che uguale a
n
(n + 1) + sommatoriaTraZeroEd(n). Per ipotesi induttiva, sommatoriaTraZeroEd(n) =
i=0 i, espressione
Pn
P
n+1
che possiamo sostituire a sommatoriaTraZeroEd((n + 1) 1), ottenendo (n + 1) +
i
=
i=0
i=0 i. Quindi,
Pn+1
sommatoriaTraZeroEd(n + 1) = i=0 i. Il processo formale, senza prosa, il seguente:
Pn+1

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

include sia il metodo appena analizzato, sia una versione alter-

nativa che sfrutta l'espressione condizionale disponibile in Java.

88

CAPITOLO 6.

6.4.3

PROGRAMMAZIONE RICORSIVA DI BASE

Lettura e stampa di una sequenza di valori

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

include il metodo ricorsivo:

public static void leggeStampaInteri(int x) {


if (x == 0) {
} else {
leggeStampaInteri(x - 1);
int numeroLetto = SIn.readInt();
System.out.println(numeroLetto);
}
}

2
4
6
8

che, ssato un valore

n1

Per induzione sul valore

per

n,

x,

legge e stampa una sequenza formata da

assunto da

x,

dimostriamo

n N. P (n),

P (n) (i.0 i < n leggeStampaInteri(n)

valori interi.

in cui:

ha letto e stampato

i-esimo

intero)

(6.14)

ovvero, dimostriamo:

(P (0) (n N. (P (n) P (n + 1))) (n N. P (n)) .


Caso base.

n = 0. Per denizione del codice,


P (0) deve essere vero, descrivendo
(i.0 i < 0 leggeStampaInteri(n) ha letto
nulla.

Sia

In tal caso

nel caso in questione,

leggeStampaInteri(0)

che nulla stato stampato.


e stampato

i-esimo

(6.15)

intero).

non stampa

Per denizione, l'istanza

P (0)

Esso eettivamente descrive una

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)

falso, da cui segue che l'intera implicazione sia

vera.

Caso induttivo.

1)

n N. Lo scopo dimostrare P (n+1), che equivale a (i.0 i < n+1 leggeStampaInteri(n+


i-esimo intero), assumendo P (n), che equivale a (i.0 i < n leggeStampaInteri(n) ha letto e
codice sorgente. Richiamare leggeStampaInteri(n + 1) equivale ad interpretare le linee 4, 5 e 6:
Sia

ha letto e stampato
Guardiamo al

leggeStampaInteri(n + 1), avendo richiamato leggeStampaInteri(n), per


v1 , v2 , . . . , vn1 di valori interi, perch vale il predicato
(i.0 i < n leggeStampaInteri(n) ha letto e stampato i-esimo intero) che pu essere espanso come:

al termine della linea 4 di

ipotesi induttiva, abbiamo letto e stampato una sequenza

0-esimo

stato letto e stampato lo

stato letto e stampato il

1-mo

stato letto e stampato il

2-do

intero
intero

intero

.
.
.

Al termine della linea 5 di

stato letto e stampato l'(n

2)-esimo

intero

stato letto e stampato l'(n

1)-esimo

intero

leggeStampaInteri(n + 1)
vn+1 .

stato letto un valore che segue tutti i precedenti letti

e stampati. Ovvero stato letto

Al termine della linea 6 di

leggeStampaInteri(n + 1) stato stampato un valore che segue tutti


vn+1 . Quindi, globalmente, siamo giunti nella situazione

i precedenti

letti e stampati. Ovvero stato stampato

stato letto e stampato lo

0-esimo

stato letto e stampato il

1-mo

stato letto e stampato il

2-do

intero
intero

intero

.
.
.

stato letto e stampato l'(n

2)-esimo

intero

stato letto e stampato l'(n

1)-esimo

intero

stato letto e stampato l'n-esimo intero

che si pu riassumere proprio con

(i.0 i < n+1 leggeStampaInteri(n+1)

ha letto e stampato

i-esimo

intero).

stamp

6.5.

RICORSIONE DI CODA

89

Esercizio 32 (Correttezza parziale per induzione) Fissato un valore

induzione per

dimostrare la correttezza parziale dei metodi ricorsivi nella seguente classe

il cui metodo

legge

6.5

n 1, usare il principio di
EnneInteriMaxRec.java

interi, e ne restituisce il massimo.

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.

In generale, al crescere della

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!

dovrebbe chiarire il signicato della denizione.

Il primo metodo quello gi visto:

2
4
6

public static int fatt (int x) {


if (x == 0)
return 1;
else
return x * fatt(x - 1);
}

Uno di coda equivalente :

2
4
6

public static int fattCoda (int x, int f) {


if (x == 0)
return f;
else
return fatt(x - 1, x * f);
}

Il metodo

fattCoda

ha un parametro in pi il cui scopo accumulare il risultato, raccogliendolo dal chiamante,

per passarlo al chiamato, dopo le opportune manipolazioni. Il nuovo parametro sposta il calcolo della moltiplicazione
per

da dopo il richiamo ricorsivo, come in

fatt

al momento in cui si valutano i parametri attuali.

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,

procediamo per induzione sul valore

m,

(6.16)

assunto dal primo parametro formale di

fattCoda,

per dimostrare:

n N\{0}. (P (0, n) (m N. P (m, n) P (m + 1, n))) (m N, n N\{0}. P (m, n)) .


Caso base.

Per denizione,

Caso induttivo.

fattCoda(0, n)

restituisce

(6.17)

n = 1 n = 0! 1.

fattCoda(m + 1, n) equivalente a calcolare fattCoda((m + 1) 1, (m + 1) n).


fattCoda((m + 1) 1, (m + 1) n) = (m + 1)! n, quindi:

Per denizione,

Per ipotesi induttiva

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

per cominciare a sperimentare l'uso di metodi ricorsivi di

coda.
Nonostante l'introduzione del nuovo parametro formale
a, se non peggiore di, quello per

fattCoda(3,2)

fatt.

in ipotetico, metodo

f,

l'espansione del frame stack di

fattCoda

analoga

Ad esempio, vediamo l'evoluzione del frame stack relativo ad una chiamata

main:

90

CAPITOLO 6.

PROGRAMMAZIONE RICORSIVA DI BASE

...
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.

APPROFONDIMENTO: GENERALIZZAZIONI DEL PRINCIPIO DI INDUZIONE

Quel che conta che metodi ricorsivi di coda come, ad esempio,

fattCoda

91

sono automaticamente trasformabili

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

public static int fattIterDaCoda (int x, int f) {


while (x > 0) {
f = x * f;
x = x - 1;
}

2
4

Osserviamo che:

abbiamo trasformato il passaggio (del valore) di

x - 1

al parametro formale

in un'assegnazione

x = x - 1

x * f

al parametro formale

in un'assegnazione

f = x * f

abbiamo trasformato il passaggio (del valore) di


e

siccome il valore di

1,

da cui deve dipendere l'espressione

x * f

deve essere quello prima del decremento

x -

l'ordine delle assegnazioni invertito, rispetto a quello dei parametri.

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.

Esercizio 33 (Ricorsione di coda)


naturali

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

calcoli la loro somma per incrementi e decrementi successivi di una unit.

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,

calcoli il resto della

per sottrazioni successive.

RestoRecCoda.java.)

Scrivere una classe con un metodo ricorsivo che, ssato un valore

n 1,

legga

interi e ne calcoli la media. Anch

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.)

Approfondimento: generalizzazioni del principio di induzione

C' un altro principio fondamentale per ragionare sui numeri naturali:

Principio del minimo (PM):


numero naturale
Dire che

tale che

Se la propriet

vera per qualche numero naturale, allora c' un minimo

P (n).

il minimo per il quale la propriet

vale implica, in particolare, che

k < n. P (k).

Una conseguenza

fondamentale del principio del minimo la seguente propriet, che si esprime dicendo che la relazione d'ordine stretta

<

sui numeri naturali


In

ben fondata:

non esiste alcuna successione discendente innita della forma

n0 > n1 > n2 > . . .

(6.18)

92

CAPITOLO 6.

Infatti, se esistesse una successione della forma (6.18), l'insieme

PROGRAMMAZIONE RICORSIVA DI BASE

{n0 , n1 , n2 , . . .}

non avrebbe un minimo elemento.

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

c = (X, D, q, r) assunti dalle


T (c) (nel caso specico r). Si utilizza poi l'osservazione che,
0
0
0 0 0
congurazione c = (X, D, q, r) ad una congurazione c = (X , D , q , r ),

quel programma si basa viene dimostrata assegnando a ciascuna congurazione di valori


corrispondenti variabili del programma un numero naturale
se il programma permette di passare da una
allora

T (c) > T (c0 ).

Questo suciente a stabilire la terminazione: se il programma non terminasse dovrebbe esistere

c0 , c1 , c2 , . . . tale che il programma passa dalla congurazione ci


ci+1 , per ogni i = 0, 1, 2, . . .. Ma allora dovrebbe anche esistere una successione discendente
T (c0 ) > T (c1 ) > T (c2 ) > . . ., contro la buona fondazione di < su N.
una successione di congurazioni

alla congurazione
di numeri naturali

Il principio di induzione forte


Si pu anche dare la seguente formulazione del principio di induzione, che risulter essere equivalente alla prima.
Diciamo che una propriet

dei numeri naturali

progressiva

se

(y < x.P (y)) P (x),


e scriviamo

Prog(P )

per indicare che

una propriet progressiva.

Principio di dimostrazione per induzione forte (PIF):

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){

if (sx < dx) {


return filter(vet,k,sx,(sx+dx)/2) +
filter(vet,k,(sx+dx)/2 + 1,dx);
}
else
if (vet[sx] > k)
return 1;
else
return 0;

3
5
7
9
11 }

Proposizione 1
Per ogni vettore di interi
maggiori di

vet

ed ogni intero

compresi tra la posizione

sx

k , filter (vet,k,sx,dx)
dx di vet.

restituisce il numero degli interi di

vet

e la posizione

Dimostrazione: Per induzione forte sul valore

k = dx sx.

Consideriamo le due chiamate ricorsive

filter(vet,k,sx,(sx+dx)/2)
filter(vet,k,(sx+dx)/2 + 1,dx)
Caso 1:

dx ((sx + dx)/2 + 1) < dx sx. Quindi,


vet maggiori di k compresi tra la posizione
sx e la posizione (sx+dx)/2) di vet, e tra la posizione (sx+dx)/2 + 1 e la posizione dx di vet. La loro somma
allora esattamente il numero degli interi di vet maggiori di k compresi tra la posizione sx e la posizione dx di vet.
Caso 2: sx = dx. In questo caso l'ipotesi non si applica perch (sx + dx)/2 = sx = dx, ma il ramo else conta
l'eventuale unico elemento > k nella singola posizione esaminata.
sx < dx.

Abbiamo

(sx + dx)/2 sx < dx sx,

e analogamente

per ipotesi, queste due chiamate restituiscono il numero degli interi di

Capitolo 7

Programmazione con array


Il riferimento per la maggior parte degli argomenti trattati in questa parte di programma didattico [SM14, Capitolo
6].

7.1

Una scusa per introdurre gli

array

Nell'Esercizio 4 abbiamo risolto il problema di riorganizzare i valori, inizialmente contenuti in quattro variabili

d,

in modo che, al termine della riorganizzazione, i valori in

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,

fossero in ordine non decrescente, ovvero:

Immaginiamo, infatti, di

Allora, 7.1 pu essere riscritto come:

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

possa variare pu essere arbitrario, anche se nito. Questo

signica che potremmo immaginare di partire da un insieme di 10 variabili

a0 , . . . , a9 ,

ciascuna con un valore, per

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:

// a0 , . . . , a4 contengono, ciascuna, un valore numerico


< 5) do
ai+1 then
scambio dei valori in ai e ai+1
incrementa i

while (i
if ai >

end if
end while
Java fornisce la sintassi per individuare variabili attraverso un indice.

Una possibile traduzione dell'algoritmo

appena dato in righe di codice Java, che, per ora, assumiamo siano parte del metodo

1 public static void main(String[] args) {


3
5
7
9
11
13 }

int[] a = {15, 2, -3, 16, 9};


int tmp;
int i = 0;
while (i < a.length - 1) {
if (a[i] > a[i+1]) {
tmp = a[i];
// scambio dei valori in a[i] e a[i + 1]
a[i] = a[i + 1];
a[i + 1] = tmp;
}
i = i + 1;
}

main,

diventa:

94

CAPITOLO 7.

PROGRAMMAZIONE CON ARRAY

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;

e nella sostituzione dell'espressione:

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

Array e gestione della memoria: Heap

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:

1 public static void main(String[] args) {

int[] a = {15, 2, -3, 16, 9};


int tmp;
int i = 0;
while (i < a.length - 1) {
if (a[i] > a[i+1]) {
tmp = a[i];
// scambio dei valori in a[i] e a[i + 1]
a[i] = a[i + 1];
a[i + 1] = tmp;
}
i = i + 1;
}

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

alloca spazio per

args, i

ed

a,

quest'ultima considerata alla stregua delle altre variabili. La novit

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
/

all'intera struttura possiamo associare il tipo

int.

int[]

[0]

[1]

- 3

[2]

16

[3]

[4]

Heap

Nella memoria heap comparsa la rappresentazione dell'array

valori di tipo

length

15

Frame stack

a:

per ricordare che essa un array i cui elementi contengono

7.2.

ARRAY E GESTIONE DELLA MEMORIA: HEAP

Il primo campo di nome

length

indica il numero di elementi indicizzabili. Siccome esso contiene il valore

per convenzione, gli indici possono variare tra

L'elemento di indice

Il contenuto della cella

identicato con l'etichetta

nel frame di

95

main

5,

estremi inclusi.

[0],

quello di indice

con etichetta

[1]

e cos via.

un riferimento adatto a recuperare ogni informazione utile nella

Sinonimi di riferimento sono  indirizzo  e  puntatore .

usuale usare

Al termine dell'istruzione 4 la situazione evolve come ci si pu attendere, inizializzando la variabile

i, ma non tmp:

struttura di tipo

int[]

appena allocata.

anche reference, al posto di riferimento.

...

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.

Siccome l'espressione produce

zione che restituisce

true.

true

reinterpretiamo l'espressione

1 == i < a.length
a[1] > a[2] argomento della sele-

L'interpretazione delle istruzioni 8, . . . , 11 portano la memoria nella seguente situazione:

...

int[]

tmp

15

main

args

null
/

length

[0]

- 3

[1]

15

[2]

16

[3]

[4]

*
Heap

Frame stack

Ricominciamo dall'espressione argomento del costrutto iterativo. Il valore di

true

e possiamo valutare

a[2] > a[3],

ottenendo

false.

i
tmp

15

int[]

...

main

args

null
/

length

[0]

- 3

[1]

15

[2]

16

[3]

[4]

*
Heap

Frame stack

Ricominciamo dall'espressione argomento del costrutto iterativo. Il valore di

true

e possiamo valutare

a[3] > a[4],

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

L'unica istruzione eseguibile la 11, ottenendo:

length

[0]

- 3

[1]

15

[2]

[3]

16

[4]

*
Heap

96

CAPITOLO 7.

PROGRAMMAZIONE CON ARRAY

Ricominciando dall'espressione argomento del costrutto iterativo, valutiamo


che

7.3

false.

L'iterazione termina proprio quando il massimo valore in

Creazione di

4 == i < a.length - 1 == 4

occupa la cella con indice maggiore.

array, inizializzazione ed eguaglianza

Creazione e stampa di array, anche col costrutto ciclico

for,

modello di memoria.

1 public static void main(String[] args) {


3
5
7
9
11

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 }

Segue l'interpretazione di blocchi di righe:

Situazione della memoria

Codice interpretato

final int L = 2;
int j;
int[] e;

j,

sono inizializzate. Per essi solo allocato spazio.

...

e
j
main

args

null
/

*
Heap

Frame stack

In

c' il reference ad una struttura per il tipo

int[]

e = new int[L];

...

int[].

length

[0]

[1]

j
main

args

null
/

*
Heap

Frame stack

Supponendo di inserire il valore

-1,

...

main

length

-1

[0]

[1]

*
Heap

Frame stack

e[j] = SIn.readInt();
j++;

args

null
/

tramite lo standard input.

int[]

j = 0;
e[j] = SIn.readInt();
j++;

Supponendo di inserire il valore

1,

tramite lo standard input.

CREAZIONE DI ARRAY, INIZIALIZZAZIONE ED EGUAGLIANZA

97

...

main

j == 2

[0]

[1]

Heap

si procede all'interpretazione delle istruzioni alle linee 11 e 12, il cui eetto di reinizializzare

modo da percorrere tutti gli elementi di

a.

in

Alla linea 13, la situazione della memoria identica a quella della linea 10.

Esercizio 34 (Correttezza parziale di eguaglianza (intensionale) tra

length

-1

Frame stack

Quando

args

null
/

int[]

7.3.

array )

Assumiamo che i due

array a

abbiano lo stesso numero di elementi. Dimostrare che la correttezza parziale del seguente codice:

1 boolean uguali = true;

i = 0;
3 while (i < a.length - 1 && uguali) {

uguali = a[i] == b[i];


if (uguali) {
i = i + 1;
}

5
7

in cui, al termine dell'iterazione, ci si aspetta che

uguali == true se a e b coincidono indice per indice. Altrimenti,


b dieriscono, deve essere che uguali == false.
predicato per ogni k, se 0 <= k < i allora a[k] == b[k] come

ovvero se esiste un indice per cui gli elementi di


La seguente soluzione possibile usa il

invariante:

boolean uguali = true;


2 i = 0;

// 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.

// Quindi per ogni k, se 0 <= k < a.length allora a[k]==b[k] e vero e


18 // questo significa a[0] == b[0], ... a[a.length-1] == b[a.length-1].

// Supponiamo uguali == false. In tal caso


20 // per ogni k, se 0 <= k < i allora a[k]==b[k] e vero, ma a[i]!=b[i], quindi

// a e b sono diversi.

7.3.1

Perch gli

array

sono nella

1 public static void main(String[] args) {

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

Segue l'interpretazione di blocchi di righe:

heap ?

98

CAPITOLO 7.

Codice interpretato

PROGRAMMAZIONE CON ARRAY

Situazione della memoria

final int L;
int[] e;
int j;
...

main

null

L
args

null
/

*
Heap

Frame stack

Il valore di

quello di default. Non ancora

stato usato per denire il numero di elementi in

e.

int[]

L = SIn.readInt();
e = new int[L];

...

length

[0]

main

L
args

null
/

*
Heap

Frame stack

In questo caso, il valore di

L non pu essere noto

durante la compilazione, quindi non possibile


stabilire quanto spazio riservare per
del

main.
zione di e

e nel frame

Questo il motivo per cui l'allocaavviene nello heap. Per un qualsiasi

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:

public class ArrayCopieCloni {


public static void arrayClone(int[] a, int[] b) {
int i;
4
for (i = 0; i < a.length; i++)
b[i] = a[i];
6 }
2

8
10

public static int[] arrayClone(int[] b) {


int[] a = new int[b.length];
arrayClone(b, a);

7.4.

12

EGUAGLIANZA TRA ARRAY E ALIASING

99

return a;
}

14

public static void main(String[] args) {


16

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

esiste proprio perch


La

stabilire l'eguaglianza tra pi array, sia l'aliasing :

Situazione della memoria

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

esiste intatta la struttura del frame

arrayClone(int[], int[]).
Inoltre, evidente che a e b nel

per

frame di

sono alias delle omonime variabili del

arrayClone
main. Ovvero, mo-

dicando le prime si modicano le seconde. Al termine di

arrayClone,

la variabile

in

main

punta ad una istanza

di array che mantiene le modiche apportate da arrayClone


alla propria istanza di

b.

100

CAPITOLO 7.

PROGRAMMAZIONE CON ARRAY

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

In cima al frame stack, partendo dall'alto, si possono vedere


le strutture dei due frame relativi a

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

==

elementi identici in identica posizione, il valore

Classe

[1]

[2]

length

[0]

[1]

[2]

length

[0]

[1]

[2]

Heap

non sia suciente ad esprimere il concetto di eguaglianza (intensionale) intuitivo e

ragionevole tra array le cui celle abbiano tipo primitivo

7.4.1

[0]

Frame stack

evidente che l'operatore

length

*
a

11

t1, t2

int. Anche se a, b e c hanno identica lunghezza e contengono


e t3 sempre false.

Array

Java fornisce la classe

Array,

parte del package

java.utils

tale che

Array.equals,

applicato a due array dello

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[],

sono eguali se di identica lunghezza, e se coincidenti, elemento

per elemento.

Esercizio 35 (Metodo equivalente ad

Array.equals.)

Data la seguente classe:

import java.util.Arrays;
2

public class ArrayEgualianza {


4

public static boolean ArrayIntEquals(int[] a, int[] b) {

7.5.

OPERAZIONI FONDAMENTALI SU ARRAY

101

boolean uguali = (a == null && b == null) // entrambi vuoti


|| (a != null && b != null && a.length == b.length); // non vuoti, lunghi uguali
int i;
if (a != null && b != null && uguali) {
for (i = 0; i < b.length && uguali; i++)
uguali = (a[i] == b[i]);
}
return uguali;

8
10
12
14

}
16

public static void main(String[] args) {


18

int[] a
int[] b
boolean
boolean

20
22

= {1};
= {1};
test1 = ArrayIntEquals(a, b);
test2 = Arrays.equals(a, b);

}
24 }

compilare ed interpretare, sperimentando la variazione dei valori in

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

assegnazioni per copiare tutti gli elementi di

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.

forniscono due soluzioni per la cancellazione dell'elemento

La correttezza parziale della seconda soluzione descritta in Invariante dell'eliminazione di un elemento.


L'ecienza delle due soluzioni, misurata contando il numero di assegnazioni eseguite dierisce di un valore costante.

102

CAPITOLO 7.

Nel primo caso, si esegue una prima assegnazione, seguita da


elementi di

a,

tranne l'ultimo, in

in

a.length - 2

assegnazioni che copiano tutti gli

b.

Nel secondo caso, si eseguono solo

PROGRAMMAZIONE CON ARRAY

a.length - 2

assegnazioni tutti gli elementi di

a,

tranne quello di posizione

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

forniscono alcuni esempi di proiezioni di elementi, che godono si speciche

propriet, da un array.

Esercizio 36 Sviluppare ricorsivamente il metodo void

pariPrimaDeiDispari(int[] a) che nella classe Filtri.java

denito iterativamente.

7.6

Strutture dati con

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

C.java che usi due array

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

evitare sovrascritture da parte di metodi dell'altra classe. Lo schema di codice sarebbe:

2
4
6
8

public class ArraySemipieno0 {


private static int[] a0;
private static int p0;
public static void init() {
a0 = new int[1];
p0 = 0;
}
....
}

10
12
14
16
18

public class ArraySemipieno1 {


private static int[] a1;
private static int p1;
public static void init() {
a1 = new int[1];
p1 = 0;
}
....
}

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

che impone la gestione di valori interi in accordo con la politica L(ast)I(n)F(irst)O(ut).

Scrivere una classe che, sfruttando uno

di leggere sequenze composte dai soli caratteri


terminata con
Ad esempio,

f,

stack

(, )

di caratteri

char,

abbia un metodo iterativo in grado

e sia in grado di dire se in una sequenza di parentesi,

le parentesi siano correttamente aperte e chiuse.

(())(()(())f

una sequenza corretta, mentre

(())(((())f

no.

(ParentesiOKPilaChar.java.)

Simulare il codice della seguente classe

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.

ORDINAMENTI ED OPERAZIONI SU ARRAY ORDINATI

103

array

parzialmente riempiti, in analogia con quanto detto

Questo esercizio un esempio di come si possano realizzare


in [SM14, Sezione 6.1.6].
Partendo dall'idea in

StrutturaDatiArrayStatico.java denire una classe RicercaLineareCoppia.java


k in un array di interi a. Il metodo memorizza il risultato

con un metodo che realizzi la ricerca lineare del valore in


in una coppia (di campi) tali che:

 un campo booleano e l'altro un intero,


 se il booleano
in

(Soluzione

true, allora l'intero la posizione del


a e il campo intero deve assumere il

non esiste in

valore di
valore

k
-1.

in

a.

Generalizzare

allora il valore

RicercaLineareCoppia.java, scrivendo RicercaLineareEsaustivaCoppia.java, in mok in un array di interi a. Il risultato deve essere

strutturato in modo da contenere almeno un campo booleano ed un campo

false,

RicercaLineareCoppia.java.)

do da realizzare la ricerca lineare di ogni occorrenza del valore in

 se il booleano

true,

 Se il booleano

false,

(Soluzione

Se il booleano

array

allora l'
allora

array

di interi tale che:

contiene la posizione di tutte le occorrenze di

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

Studiare la denizione della struttura


(Soluzione possibile

7.7

MultiInsiemiInt.java.)

Coda ed implementarla per mezzo di un Circular buer




BufferCircolare.java.)

Ordinamenti ed operazioni su

array ordinati

La Sezione 6.3. de [SM14, Capitolo 6] pu essere di riferimento.


Abbiamo introdotto l'algoritmo di ordinamento

BubbleSortIter.java (BubbleSortIterTest.java) come

conseguenza della necessit di generalizzare una sua componente.


Il problema computazionale dell'ordinamento di fondamentale importanza per le conseguenze positive che pu
aver il lavorare su strutture dati i cui valori siano ordinati.
numerici interi.

Ci concentreremo sull'ordinamento di array con valori

D, per il quale esista una relazione


X D, si abbia P (X) = Y tale che
Y una sequenza hy1 , . . . , yn i che contiene tutti e soli gli elementi di X e i.i {1, . . . , n} yi R yi+1 . Ad esempio,
se D coincide con l'insieme dei numeri naturali N, allora R l'ordinamento che conosciamo. Se D fosse l'insieme
di tutte le sequenze nite di caratteri dell'alfabeto, allora R sarebbe l'ordine lessico graco tra sequenze di caratteri,

d'ordine

R,

In generale, ordinare una sequenza di elementi in un dominio

equivale a calcolare una permutazione

P : D D

tale che, per ogni

ovvero l'ordine che usiamo nei dizionari.

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

InsertionSort.java e Correttezza e costo de InsertionSort.


Per i curiosi, Dimostrazione di correttezza originale.
La Sezione 6.3.3 de [SM14, Capitolo 6] un possibile riferimento.

104

CAPITOLO 7.

PROGRAMMAZIONE CON ARRAY

Ricerca dicotomica

RicercaDicotomicaIter.java realizza un algoritmo iterativo per la ricerca dicotomica e Correttezza de RicercaDicotomicaI


parla della sua correttezza.

RicercaDicotomicaRec.java

realizza un algoritmo ricorsivo per la ricerca dicotomica. Essa pu essere vista

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)

organizzata per visitare tutti gli elementi di un ipotetico array

di elementi.

Esercizio 38 (Rivisitazione di programmi ricorsivi in funzione di schemi dati.)


todi in

EserciziDicotomici.java,

(Possibili soluzioni in

seguendo lo schema denitorio di

f,

Riscrivere tutti i me-

introdotto nella Sezione 6.3.

EserciziDicotomiciProCorrettezza.java.)

Riscrivere tutti i metodi gi deniti in

EserciziDicotomici.java, seguendo lo schema denitorio di g , introdotto

nella Sezione 6.3.

Fusione di due array ordinati

Merge.java,

Correttezza del

Merge.java,

Merge sort

MergeSort.java fornisce un algoritmo che ordina due array. Il


cui n la dimensione dell'array da ordinare. Il costo giusticato

costo si comporta come la funzione

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

rappresentando lo spazio di lavoro come albero;

dell'array originale da ordinare.

si pu determinare il costo indicato.

Indirizzamento indiretto, e Bucket/Counting sort


Un esempio di cosa sia l'indirizzamento indiretto fornito dalla simulazione di una possibile implementazione del

CrivelloEratostene.java

che individua numeri primi tra gli indici di un array di booleani di lunghezza ssata.

Esercizio 39 (Riscrittura del Crivello di Eratostene.) Riscrivere il metodo in:

CrivelloEratostene.java

in cui il ciclo:

for (int multiplo = numero * 2; multiplo <= nMax; multiplo += numero)


primi[multiplo] = false;
sia sostituito con uno alternativo, ma equivalente.
(Soluzione

EratosteneAlternativo.java.)

CountingSort.java

presenta programmi che realizzano gli omonimi algoritmi di ordinamento.

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

si possono pensare come problemi riconducibili a quattro schemi ssi, i

quali si possono descrivere per mezzo dei seguenti schemi di predicato:


1. Esiste una riga
2. Per ogni riga

i,

3. Esiste una riga


4. Per ogni riga

i,

esiste un elemento

i,

i,

nella quale esiste un elemento

m[i][j]

tale che ogni elemento

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

strutturandolo in maniera ragionevole: gli elementi di


nel risultato.
(Soluzione possibile in

Scrivere un metodo

ragged,

boolean quadrata(int[][] m)

che restituisca

true

nel caso

sia quadrata, non

altrimenti.

false

(Soluzione possibile in

OperazioniSuMatrici.java.)

Scrivere un metodo

OperazioniSuMatrici.java.)

int[] diagonalePrincipale(int[][] m) che produca un array con tutti gli elementi


a.

della diagonale principale di


(Soluzione possibile in

Scrivere un metodo

OperazioniSuMatrici.java.)

int[] diagonaleSecondaria(int[][] m) che produca un array con tutti gli elementi


a.

della diagonale secondaria di


(Soluzione possibile in

OperazioniSuMatrici.java.)

Scrivere il metodo statico

int[][] per(int[][] a, int[][] b)


a e b.

che date due matrici

ne produca

una terza che rappresenti il prodotto matriciale di


(Soluzione possibile in

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.)

Sia dato un array


colonne
se

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

possibile immaginare tre metodi distinti. Ciascuno di essi scorrer

Simulare l'uso della memoria da parte del metodo

main della classe RaggedArray.java e scrivere assegnazioni




che verichino le previsioni, ovvero che segnalino errore quando il caso che succeda.

106

CAPITOLO 7.

PROGRAMMAZIONE CON ARRAY

Bibliograa
[SM14]

W. Savitch and D. Micucci. Programmazione di base e avanzata con Java. Pearson, 2014.

[Wir73] Niklaus Wirth.


USA, 1973.

Systematic Programming: An Introduction.

Prentice Hall PTR, Upper Saddle River, NJ,

You might also like