Modellistica Combinatoria - Dipartimento di Ingegneria dell

Transcript

Modellistica Combinatoria - Dipartimento di Ingegneria dell
Modellistica Combinatoria
Renzo Sprugnoli
Dipartimento di Sistemi e Informatica
Viale Morgagni, 65 - Firenze (Italia)
26 febbraio 2007
2
Indice
1 Modellistica
1.1 Ciclo della modellazione . . . .
1.2 Modellistica combinatoria . . .
1.3 Gli strumenti della modellistica
1.4 Generatori specifici . . . . . . .
1.5 Il metodo del chi-quadro . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
5
5
7
8
10
11
2 Permutazioni e combinazioni
2.1 Concetti di base . . . . . . . . . .
2.2 La struttura di gruppo . . . . . . .
2.3 Il conteggio delle permutazioni . .
2.4 La generazione delle permutazioni
2.5 Parole, disposizioni e combinazioni
2.6 Un caso di studio . . . . . . . . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
15
15
16
17
18
20
22
3 La ricorsione
3.1 Generare le permutazioni . . .
3.2 Sudoku . . . . . . . . . . . . .
3.3 Gli altri programmi del Sudoku
3.4 Osservazioni da sviluppare . . .
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
.
25
25
26
28
30
.
.
.
.
.
.
.
.
.
.
.
.
.
3
4
INDICE
Capitolo 1
Modellistica
Col termine di Modellistica si intende la scienza (o
l’arte) di creare simulazioni della realtà, allo scopo di
ottenere informazioni sulla realtà stessa. Da un certo
punto di vista, la Matematica e la Fisica fanno parte
della Modellistica, in quanto entrambe queste scienze ci forniscono “modelli”, matematici o fisici, della
realtà. La geometria (cioè la misura della terra), come si sa, è nata come rappresentazione ed elaborazione degli appezzamenti del terreno nell’antico Egitto,
dove le inondazioni del Nilo cancellavano i confini tra
un campo e l’altro. Le equazioni di Keplero, che descrivono il moto dei pianeti intorno al sole, sono un
altro esempio di modello matematico, che ci permette di prevedere fenomeni che possono verificarsi nel
sistema solare, come la congiunzione o l’opposizione
dei pianeti, le eclissi e cosı̀ via. Un grande successo
del modello è stata la scoperta degli ultimi pianeti,
avvenuta osservando proprio le discrepanze fra il moto effettivo dei pianeti conosciuti più lontani e quello
previsto da Keplero.
Quando ero studente, nel Laboratorio avevamo un
complesso apparecchio costituito da varie sfere che
potevano scorrere lungo orbite ellittiche intorno a una
sfera più grossa. Questa rappresentava il sole, mentre
le altre sfere rappresentavano i pianeti che si potevano
veder muovere intorno a quella centrale. Si trattava
di un modello fisico del sistema solare, certo molto
meno preciso del modello matematico di Keplero, ma
senz’altro più intuitivo e spettacolare. Analogamente, la Relatività einsteiniana è un modello matematico della realtà fisica; oggi, Piero Angela e altri abili
divulgatori ci fanno capire la curvatura dello spazio
operata dalla gravità con utili e graziosi modelli fisici. Una lamina di gomma viene tesa orizzontalmente
su un supporto che la lasci libera come la pelle di un
tamburo. Una pesante palla d’acciaio viene messa in
un punto della lamina; col suo peso essa crea una depressione che rappresenta l’azione della gravità intorno a una massa (ad esempio, una stella). Una pallina
leggera viene quindi lanciata sulla lamina in modo
da passare vicino alla stella; la depressione fa curvare
la traiettoria della pallina, il cui percorso non risulta
rettilineo, ma curvo. Questa è la simulazione del-
la famosa osservazione durante l’eclissi del 1919, che
confermò la deviazione dei fotoni in prossimità del
sole e quindi diede ragione ad Einstein relativamente
alla curvatura dello spazio.
I modelli fisici sono molto intuitivi e ci fanno capire come “funziona” la realtà. Purtroppo, di solito, non sono molto precisi e non possono essere usati
quando si voglia “prevedere” il comportamento della
realtà. In questo caso vanno molto meglio i modelli
matematici, che sfidano la nostra intuizione, ma ci
permettono di calcolare l’andamento esatto di un femomeno. Ad esempio, Galileo usò il piano inclinato,
cioè un modello fisico, per studiare la gravità. Questo gli fece capire molte cose, ma dovette passare a un
modello matematico, le equazioni del moto, per poter
fare previsioni: quanto tempo impiega un oggetto dal
peso di 2 kg a cadere da un’altezza di 10 metri? Per
questo motivo, la scienza fa principalmente ricorso
a modelli matematici: equazioni, sistemi di equazioni o disequazioni, equazioni differenziali, sistemi di
equazioni integro-differenziali, e cosı̀ via. I modelli
fisici sono limitati a quelle situazioni in cui il modello
matematico è troppo complesso o non lo si conosce
troppo bene. Ad esempio, una camera a vento è un
modello fisico che sostituisce un complesso sistema di
equazioni differenziali studiato dalla fluidodinamica.
1.1
Ciclo della modellazione
La creazione di un modello, di qualunque specie si
tratti, deve seguire un determinato percorso, noto come il ciclo di vita del modello. Le fasi sono le seguenti,
schematizzate nella Figura 1.1:
1. La realtà ci si presenta come un insieme complesso e nebuloso, nel quale interagiscono innumerevoli fattori, che spesso non riusciamo a tenere
sotto il controllo della nostra mente. Occorre
allora un opportuno studio che ci permetta di
capire quali sono i fattori essenziali che determinano quella parte di fenomeni della realtà che
intendiamo controllare: il nostro problema. Su di
essi concentriamo la nostra attenzione cercando
5
6
CAPITOLO 1. MODELLISTICA
turalmente, essi siano stati rappresentati con un
modello matematico. Anche l’Informatica presenta questo stesso vantaggio e, in certi casi, risulta ancora migliore in quanto può affrontare
problemi anche di natura non numerica: si pensi per esempio alle basi di dati o all’Information
Retrieval.
REALTA’
Astrazione
Rappresentazione
Interpretazione
Elaborazione
Figura 1.1: Ciclo di vita della modellizzazione
di non farci influenzare da quei fattori che solo
apparentemente hanno a che fare con i fenomeni
che ci interessano.
2. Propriamente, questo lavorio intellettuale si dice astrazione. Astrarre significa scegliere, tra gli
innumerevoli parametri che potrebbero intervenire nella parte di realtà sotto osservazione (il
problema), quelli che hanno un’effettiva influenza. Se studiamo la forma aerodinamica di un aereo, possiamo certamente prescindere dal colore
con cui verrà dipinto e dalla forma degli arredi interni. Viceversa, metteremo tutta la nostra
attenzione sulla forma delle ali e della fusoliera.
3. Una volta capito quali sono i parametri da considerare, dobbiamo passare a una rappresentazione del problema, che ci permetta di poterlo affrontare in modo razionale. Talvolta (specie nei
tempi passati) l’unico metodo di rappresentazione era il linguaggio naturale; oggi la Matematica
e l’Informatica offrono metodi molto più adeguati, sia perché più esatti sia perché più adatti alle
fasi successive. Come s’è detto, i modelli fisici sono meno frequenti, perché di regola meno
esatti, ma possono essere validi per aiutare la nostra intuizione. L’Informatica oggi offre un aiuto
sostanziale alla modellistica, sia perché può essere usata direttamente nei modelli informatici,
sia perchè gli elaboratori possono essere utilizzati
per risolvere tanti modelli matematici.
4. La rappresentazione di un problema è tanto più
efficace quanto più può aiutare nella elaborazione
necessaria ad arrivare alla soluzione del problema. Per questo, il linguaggio naturale si rivela
piuttosto scarso, mentre la Matematica, con i
metodi di calcolo da essa sviluppati, è l’approccio più efficace a risolvere i problemi, purché, na-
5. La soluzione di un problema ottenuta dall’elaborazione di un modello non è, di per sé, la
conclusione del lavoro. Infatti, occorre sempre
esercitare il nostro senso critico per controllare o interpretare la soluzione ottenuta. A parte
eventuali sbagli di modellazione (sempre possibili) è bene cercar di capire se i risultati ottenuti sono ragionevoli da un punto di vista umano, cosa non sempre verificata da una soluzione
formale, cioè ottenuta matematicamente o informaticamente. Solo dopo aver controllato questo
aspetto si può passare ad applicare alla realtà i
risultati ottenuti dal modello, cioè la soluzione
del problema.
Ognuno di questi punti meriterebbe un lungo discorso e la discussione di esempi significativi. Ci limitiamo a osservare qualche caso semplice e ben noto.
Per un informatico, non è difficile riconoscere nella
Figura 1.1 schemi familiari. Una base di dati (un
sistema informativo) è un modello della realtà, diciamo della realtà aziendale, e riunisce in un’unica e
complessa struttura tutte le informazioni che l’Azienda utilizza per la sua operatività. I vari archivi che
formano la base dei dati contengono le più svariate informazioni: sul personale, sul magazzino, sulla
produzione, sui fornitori e sui clienti, sulle filiali e
sull’amministrazione.
Queste informazioni sono state raccolte studiando le funzionalità svolte all’interno dell’azienda, registrando i dati utili al loro svolgimento e scartando
quelli superflui, che appesantirebbero la base dei dati
senza fornire indicazioni utili. Questa fase di astrazione è detta in gergo analisi dei requisiti, e la sua
importanza è fondamentale, poiché un errore in questa fase può compromettere il corretto funzionamento
della base dei dati. La fase di rappresentazione fa uso
di quelli che si dicono modelli delle basi di dati; il nome, come si vede, è indicativo e chiama direttamente
in causa il concetto di modello. Il modello utilizzato è di regola quello relazionale, che permette una
rappresentazione dei dati facilmente utilizzabile e, soprattutto, logicamente valida. Una volta tradotto lo
schema logico della base dei dati nello schema fisico,
adatto all’elaboratore, la rappresentazione è utilizzabile direttamente per la fase di elaborazione. Questa
è realizzata tramite il query language che permette
di interrogare la base dei dati e quindi conoscere i
7
1.2. MODELLISTICA COMBINATORIA
dati aziendali rilevanti per un dato scopo e quindi di
organizzare il lavoro di tutta quanta l’azienda. Prima tuttavia di passare alla fase esecutiva è sempre
bene validare criticamente i risultati di ciascuna query per rendersi conto della loro congruenza. Anche
se è vero che le probabilità di errore della macchina
sono scarsissime, bisogna sempre ricordare che i dati
e i programmi sono opera dell’uomo e quindi possono contenere dati sbagliati o elaborazioni imprecise
(interpretazione).
L’esempio delle basi di dati è facilmente generalizzabile a qualsiasi progetto informatico, e lo schema della Figura 1.1 è allora detto ciclo di vita del
software. L’astrazione è lo studio del problema; la
rappresentazione è la sua schematizzazione in dati
e algoritmi; questo produce i programmi (software)
che permettono l’elaborazione dei dati del problema;
i risultati è bene siano opportunamente interpretati,
specie in fase di messa a punto del programma; infine,
quando tutto sembra funzionare, il software realizzato passa alla fase operativa, cioè arriva ad operare
nella realtà.
Naturalmente, lo schema della Figura 1.1, proprio
per la sua generalità, non è limitato alla scienza e alla
tecnica. Il lavoro di uno storico segue più o meno lo
stesso iter. Lo studioso studia i documenti relativi al
periodo che gli interessa e che, per lui, rappresentano
la realtà. Astraendo, da tali documenti egli estrae i
dati che reputa rilevanti e ignora i dettagli inutili. La
rappresentazione dei fatti storici non ha un formalismo specifico, per cui avviene nel linguaggio naturale,
eventualmente con il gergo tecnico sviluppato per designare gli aspetti più specifici della materia. Sui dati
cosı̀ raccolti, lo storico opera le sue elaborazioni, cioè
cerca di organizzare in schemi logici lo svolgimento
dei fatti (la storia è tipicamente una disciplina analitica a posteriori). Naturalmente, l’interpretazione
gioca un ruolo fondamentale in questa fase ed è difficile distinguerla dall’elaborazione, in quanto buona
parte del lavoro dello storico consiste proprio nell’interpretare i fatti storici. Comunque, possiamo pensare a una fase critica che stabilisca almeno la verisimiglianza dell’esposizione conclusiva che lo storico
dà di ciò che è accaduto nella realtà.
1.2
Modellistica combinatoria
La Modellistica Combinatoria fa parte della Modellistica Matematica, in quanto usa gli strumenti messi a disposizione di quella branca della Matematica
che è appunto la Combinatoria o Analisi Combinatoria. Questa disciplina studia gli insiemi finiti e la
loro enumerazione. Ad esempio, le possibili permutazioni di n oggetti sono n!, e questo vale per ogni
n ∈ N, quindi per ogni insieme finito. Non ha invece
senso contare le possibili permutazioni di un insieme
infinito anche se in realtà ha senso considerare tali
permutazioni, che sono semplicemente le corrispondenze biunivoche di N ⇋ N, ad esempio. Si sa che
esse hanno la cardinalità del continuo, ma questo è
un risultato della matematica del transfinito e non ha
praticamente interesse nell’Analisi Combinatoria.
Le permutazioni costituiscono un semplice modello combinatorio. Se mescoliamo un mazzo di 40 carte, le permutazioni contano chiaramente le possibili sequenze assunte dalle carte stesse, che pertanto
risultano essere 40!, cioè:
40! ≈ 0.8159152832 × 1048 ,
un numero di tutto rispetto; esso ci assicura che la
probabilità di incontrare due smazzate uguali è durante la nostra vita è del tutto trascurabile, anche se
giocassimo a carte 16 ore al giorno per cinquant’anni.
Infatti, immaginando una smazzata al minuto, in 16
ore ci sono 16 × 60 = 960 minuti e in cinquant’anni ci
sono 50 × 365 = 18. 250 giorni. Le smazzate ottenute
sono perciò 960 × 18. 250 = 17. 520. 000 = 0.1752 × 108
e quindi la probabilità di ottenere due volte la stessa permutazione è minore di 10−40 , un numero che
risulta difficile anche a pronunciarsi.
Ricordiamo che il calcolo delle probabilità iniziò
come problema combinatorio alla metà del 1600, soprattutto con Pascal. Quello che si voleva era un
modello combinatorio per le varie forme di gioco d’azzardo che andava propagandosi. Il gioco veniva usato
dagli stati per far soldi a spese della povera gente, che
si indebitava per cercar di vincere al lotto e alle lotterie. Nel caso del lotto, un ambo è ciò che in Analisi
Combinatoria si dice una combinazione, ed esattamente una combinazione di due numeri fra i 90 del
gioco. Sono ambi {14, 63} oppure {34, 89}, e {51, 77}
è considerato lo stesso ambo di {77, 51}. La teoria
ci dice che il numero dei possibili ambi è dato da un
coefficiente binomiale:
µ ¶
90
= 4005.
A=
2
Poiché vengono estratti cinque numeri, gli ambi
vincenti sono:
µ ¶
5
= 10
2
e quindi la probabilità di azzeccare un preciso ambo
è:
10
πA =
≈ 0.0246913 ≈ 2.47%
4005
una quantità non certo elevata. Naturalmente, gli
stessi conti si possono fare per terni, quaterne e
cinquine. Nel testo vedremo vari casi.
Questi esempi sono molto semplici e si tratta di applicazioni pressoché immediate, non di veri e propri
8
CAPITOLO 1. MODELLISTICA
esempi di “modellistica”. Per parlare di questi occorre introdurre il concetto di generazione casuale di
oggetti combinatori, dove con il termine di “oggetto”
si intende una qualsiasi configurazione finita che possa essere contata in modo univoco in accordo alla sua
numerosità. Le permutazioni e le combinazioni sono
oggetti combinatori, ma tali sono anche gli alberi binari, le liste ordinate e in generale le strutture dati
introdotte dall’Informatica, cosı̀ come lo sono strutture generate dall’uomo nella sua attività quotidiana,
come le possibili pavimentazioni di una sala, le parole
che si possono formare con lettere assegnate o i modi
di disporre gli ospiti intorno a una tavolo.
dano grosse parti di un’azienda, ad esempio tutto il
reparto della produzione. Tali simulazioni fanno uso
di molti oggetti combinatori, che devono essere generati casualmente; il loro interagire può essere valutato
con opportuni conteggi, e questi forniscono indicazioni precise (almeno in media) sul comportamento reale
del reparto. Cambiando qualche parametro, è allora
possibile verificare situazioni diverse e complesse, che
sarebbe impossibile realizzare nella pratica senza intervenire pesantemente sulla realtà aziendale e senza
sapere quali potrebbero essere i risultati.
Consideriamo due semplici problemi. Gettando m
volte un dado, se m è grande, ci aspettiamo che ognuno dei sei valori 1, 2, 3, 4, 5, e 6 si ottenga lo stesso
numero di volte, cioè m/6, Analogamente, mescolando le carte, ci aspettiamo che l’ultima carta possa
essere indifferentemente una qualsiasi delle quaranta carte del mazzo. In termini più astratti, date m
qualsiasi permutazioni di n oggetti (con m abbastanza alto) ci aspettiamo che la distribuzione degli ultimi
elementi sia uniforme, cioè, ognuno degli n oggetti occorra in fondo con la stessa probabilità m/n. Come è
possibile, ci chiediamo, verificare queste attese? Un
volta, con santa pazienza, i matematici e gli statistici
si mettevano a tavolino, con un vero dado o un vero mazzo di carte, e si divertivano o si annoiavano
(a seconda dei casi) a gettare il dado qualche migliaio di volte o a mescolare le carte ancora più volte.
Chi si interessava di roulette, passava intere serate
al Casinò segnandosi tutte le uscite dei numeri. Oggi, l’uso dell’elaboratore ha semplificato molto queste
procedure.
1.3
L’elaboratore, infatti, può simulare il lancio dei dadi o il mescolamento delle carte, e può fare tante cose,
simulando il comportamento degli oggetti combinatori. Questi possono essere generati in maniera uniforme, cioè in modo tale che ogni oggetto di dimensione
n abbia la stessa probabilità di essere generato di tutti
gli altri oggetti della stessa dimensione. Ciò permette di rilevare le caratteristiche di tale oggetto, cioè
di classificarlo. Ripetendo la generazione migliaia o
decine di migliaia di volte, si riescono a fare accuratissime statistiche sulle caratteristiche di quel tipo
di oggetti combinatori. Questo permette di verificare
le stime teoriche fatte sugli oggetti o, quando queste
manchino, di avere delle stime empiriche. Quello che
però è particolarmente importante è proprio il riscontro con la situazione reale di cui gli oggetti combinatori sono il modello: la simulazione e i dati che se ne
ricavano ci danno, attraverso il modello, indicazioni
sul comportamento reale di ciò che stiamo studiando.
Oltre a questa applicazione molto diretta del modello, oggi si possono creare simulazioni che riguar-
Gli strumenti della modellistica
Un modello simula un sistema reale, nel senso che
il comportamento del modello è analogo a quello
del sistema, dove “analogo” ha un significato tecnico preciso. Se abbiamo stabilito che i parametri
p1 .p2 , . . . , pk del modello corrispondono alle variabili
v1 , v2 , . . . , vk del sistema reale, ogni volta che facciamo assumere ai parametri i valori p01 .p02 , . . . , p0k come
corrispondenti dei valori v10 , v20 , . . . , vk0 , allora il risultato Q della simulazione è il corrispondente del valore U che il sistema assume come risultato del suo
comportamento.
Ciascun parametro pi potrà assumere valori nell’insieme Pi , i = 1, 2, . . . , n. Talvolta, tutti i valori
di Pi possono essere assunti in modo indifferente, talaltra alcuni valori sono assunti più frequentemente
di altri. Si parla allora della distribuzione del parametro pi (nell’insieme Pi ). Il caso di valori con ugual
frequenza corrisponde, nel linguaggio del Calcolo delle Probabilità e della Statistica, a una distribuzione
uniforme. Gli altri casi corrispondono a distribuzioni
talvolta note e ben studiate, talvolta del tutto particolari. Queste ultime richiedono accorgimenti opportuni per essere trattate nella simulazione. Le altre si
possono affrontare con tecniche standard, e fra esse
rientrano la distribuzione binomiale e quella normale,
la distribuzione di Poisson e quella di Pareto-Zipf.
La distribuzione uniforme è, insieme alla normale,
una delle più frequenti; spesso fornisce la base per
trattare anche le altre distribuzioni. Per la sua semplicità e per la sua utilità, è quella che si considera
per prima, e l’elaboratore fornisce di regola dei programmi che simulano tale distribuzione. Cominciamo
anche noi con la distribuzione uniforme.
La causalità e l’elaboratore elettronico sono due
concetti antitetici; questa macchina, infatti, è completamente deterministica e c’è quindi un’obiezione
di fondo all’ottenere quantità casuali attraverso l’uso
di un programma. All’inizio della storia degli elaboratori, vennero immediatamente introdotti program-
9
1.3. GLI STRUMENTI DELLA MODELLISTICA
mi che avrebbero richiesto la generazione di numeri a
caso; l’esempio tipico è il cosı̀ detto metodo di Montecarlo per il calcolo degli integrali definiti. Si cercarono pertanto possibili sorgenti di numeri casuali, prime
tra tutte le tabelle fatte a mano con metodi empirici
come il lancio di monete o dei dadi. Si pensò poi che
le cifre di un numero trascendente come π potessero
essere più o meno casuali. Se ne calcolarono allora
migliaia di cifre, poi decine di migliaia (oggi siamo a
25 milioni). Finalmente, si cercarono procedure più
semplici e che non richiedessero grandi quantità di
memoria come le tabelle.
Fra le varie idee, la più fruttifera fu quella di proporre programmi per generare numeri quasi casuali,
cioè non genuinamente casuali (cosa impossibile, come s’è visto), ma che si comportassero come i numeri
casuali. Trovato il metodo, questi numeri vennero
detti pseudocasuali e son questi che si usano nella
modellistica.
Per spiegare il concetto della pseudocasualità, occorre prima di tutto osservare che un singolo numero
non può essere né casuale, né non casuale. Si deve piuttosto parlare di una sequenza, nella quale la
distribuzione dei numeri è casuale. Ad esempio, la
cifra 4, di per sé non ha senso come “numero casuale”; se si considera la sequenza {4, 4, 4, 4, . . .}, possiamo dire che non è casuale; se invece pensiamo a
{3, 1, 4, 1, 5, 9, . . .} (le cifre di π), allora possiamo ragionevolmente asserire che si tratta di una sequenza
casuale.
Per i nostri scopi, noi cerchiamo delle sequenze casuali uniformi, cioè nelle quali ogni elemento ricorra,
più o meno, con la stessa frequenza di tutti gli altri.
I matematici hanno pensato a come poter stabilire se
una certa sequenza di numeri ha questa proprietà; il
metodo usato è quello dei test, cioè di prove che, se
superate, indicano che una data sequenza è casuale
e uniforme. Senza entrare in troppi dettagli, osserviamo che ogni sottosequenza estratta in modo non
casuale (ad esempio, un elemento sı̀ e uno no) da una
sequenza casuale deve comunque essere casuale; viceversa, se estratta da una sequenza non casuale, deve
essere non casuale. Si noti che estraendo in modo casuale si dovrebbe ottenere sempre una sottosequenza
casuale. Vedremo tra poco come si fa a stabilire se
una sequenza è uniforme, e da quella passeremo ai
test di casualità.
Per ritornare alla generazione di numeri pseudocasuali, o meglio, alla generazione di sequenze pseudocasuali di numeri, diciamo subito che il modo
migliore si è dimostrato essere il cosı̀ detto metodo congruenziale. Si consideri un numero primo p
e l’insieme dei residui modulo p eccetto lo 0, cioè
Z∗p = {1, 2, . . . , p − 1}; come si sa, esso costituisce un
gruppo con l’operazione x × y (mod p), ∀x, y ∈ Z∗p .
Se m è un generatore del gruppo, le sue potenze
m0 , m1 , . . . , mp−1 sono tutte distinte; la cosa interessante è che la distribuzione di queste potenze costituisce una sequenza pseudocasuale. Facciamo un
esempio semplice con p = 11 e m = 7:
m0 = 1, m1 = 7, m2 = 5, m3 = 2, m4 = 3,
m5 = 10, m6 = 4, m7 = 6, m8 = 9, m9 = 8, m10 = 1.
Un esempio più complesso si ottiene con p = 23 ed
m = 15; la sequenza è:
1, 15, 18, 17, 2, 7, 13, 11, 4, 14, 3, 22, 8,
5, 6, 21, 16, 10, 12, 19, 9, 20.
Ad occhio (per ora non possiamo far di meglio) ci
sentiamo soddisfatti di questi due semplici esempi,
anche se, naturalmente, un occhio ipercritico potrebbe trovare varie cose da ridire. Esempi cosı̀ piccoli
non sono molto significativi, poiché dopo p − 1 generazioni la sequenza si ripete, e un programma che
anche solo richieda qualche migliaio di numeri a caso
non potrebbe utilizzarli; infatti, la ciclicità della generazione toglierebbe qualsiasi speranza di casualità.
Comunque, l’idea va indagata più a fondo, scegliendo
valori di p molto più grandi.
Il programma di generazione è molto semplice; si
considerano tre variabili globali:
• p, che contiene il modulo di base;
• a, che contiene il generatore scelto di Z∗p ;
• seed, che contiene il numero casuale man mano
generato; inizialmente è messo ad 1.
Il programma è semplicemente:
random := proc();
seed := irem(a*seed, p)
end proc;
dove irem (cioè, integer remainder) è la funzione che
restituisce il resto della divisione intera (cioè, il modulo). Il termine seed (seme) è usato per mettere
in evidenza l’aspetto generativo di questa procedura.
Ad esempio, in Maple abbiamo la funzione predefinita mostrata nella Figura 1.2. Le variabili a e p sono
dichiarate locali invece che globali. Il valore di p è
l’ultimo numero primo che precede 1012 e il generatore è stato scelto non troppo piccolo per evitare che
all’inizio vi sia una sequenza troppo “evidente” delle sue potenze, ma sia mascherata dall’operazione di
modulo. Con questa funzione, occorre generare circa
1012 numeri casuali prima che la sequenza si ripeta,
e questo può essere considerato soddisfacente.
Come s’è detto, il seme è una variabile globale;
questo è utile quando si deve mettere a punto un
10
CAPITOLO 1. MODELLISTICA
rand := proc()
local a, p; global _seed;
p := 999999999989;
a := 427419669081;
if not assigned(_seed)
then _seed := 1 end if;
_seed := irem(a*_seed, p) end if
end proc;
Figura 1.2: La funzione random in Maple
1.4
Generatori specifici
Da questo momento in avanti, il termine “casuale”
verrà utilizzato al posto di “pseudocasuale” in quanto, salvo avviso contrario, solo di questi numeri parleremo. Avere numeri a caso compresi tra 1 e 1012 (lo
zero, naturalmente, non si può mai ottenere poiché
non fa parte di Z∗p ) non è che soddisfi molte esigenze
pratiche. Di solito, si hanno due tipi di problemi:
1. generare un numero intero casuale compreso tra
0 ed n − 1;
2. generare un numero reale a caso compreso tra 0
ed 1 (un estremo escluso).
programma. Infatti, se si verifica un errore e modifichiamo il programma, vogliamo evere la possibilità di
eseguirlo di nuovo con le stesse modalità che avevano
provocato l’errore. Se cambia qualcosa, o l’errore non
si ripete o, se si ripete, possiamo dubitare che dipenda dalle stesse cause che l’hanno originato la prima
volta. Se ogni volta cambiamo la sequenza di numeri casuali, potremmo non riuscire più a provocare
lo stesso tipo d’errore, e quindi incontrare difficoltà
a correggere il programma. Se però inizializziamo il
generatore, assegnando un numeo preciso a seed, la
sequenza dei numeri pseudocasuali è da questo determinata, e una nuova esecuzione con lo stesso valore di
seed riesegue il programma nello stesso, identico modo. Con l’avvertenza di gestire il seed in questo modo, è facile correggere eventuali errori nei programmi
di simulazione. Questo è uno dei vantaggi dei numeri pseudocasuali nei confronti di quelli veramente
casuali: con questi ultimi, che non ripetono mai la
stessa sequenza, sarebbe quasi impossible correggere
gli errori con le tecniche tradizionali.
Come vedremo, molti problemi sono riconducibili a
questi due, che potrebbero addirittura ridursi al solo
punto 2. Infatti, se r è un numero reale casuale compreso tra 0 ed 1 (0 ≤ r < 1), il numero R = nr è un
numero reale a caso 0 ≤ R < n; se di questo numero prendiamo la parte intera s = ⌊R⌋, otteniamo un
intero 0 ≤ s ≤ n − 1, che è proprio l’intero casuale
cercato. Se gli r sono uniformemente distribuiti tra 0
ed 1, gli s lo sono tra 0 ed n − 1. Tuttavia, il metodo
esposto nella sezione precedente genera numeri interi,
per cui, in pratica, è meglio distinguere i due casi.
Per generare un numero fra 0 ed n − 1 si procede
allora cosı̀:
1. si genera un intero a caso r := random(),
compreso nell’intervallo tra 0 e p − 1;
2. lo si riduce modulo n: s := irem(r, n), cosı̀
che 0 ≤ s < n.
Il programma è allora il seguente (notare la differenza tra random(), random(n) e random(m, n) che
D’altra parte, quando un programma di simulazio- introdurremo tra poco):
ne funziona, ed vogliamo eseguirlo più volte, partire random := proc(n)
dallo stesso seme vuol dire produrre gli stessi risulta- local r,s;
ti. Se questo è bene per verificare i risultati ottenuti,
r := random(); s := irem(r, n)
è controproducente se vogliamo effettuare nuove si- end proc;
mulazioni, ad esempio, per aggiungere nuovi risultati
Nel caso precedente di p = 23 ed m = 15 si hanno
a quelli già ottenuti. Per evitale che il programma usi
sempre la stessa sequenza, l’elaboratore fornisce una le seguenti sequenze, la prima modulo 2 e la seconda
funzione, detta di solito randomize(), che assegna un modulo 3:
valore casuale (non pseudocasuale!) al seme. Per far
1, 1, 0, 1, 0, 1, 1, 1, 0, 0, 1, 0, 0, 1, 0, 1, 0, 0, 0, 1, 1, 0
ciò usa il clock interno, che indica la data e l’ora corrente, fino al millesimo di secondo. Questa quantità,
1, 0, 0, 2, 2, 1, 1, 2, 1, 2, 0, 1, 2, 2, 0, 0, 1, 1, 0, 1, 0, 2.
eventualmente elaborata per portarla al giusto numero di cifre, viene usata come seed. Se due chiamate a Anche se come sequenze casuali sembrano abbastanza
randomize() non arrivano all’elaboratore a distanza buone, ribadiamo che occorre pensare a valori di p
minore di un millesimo di secondo, il generatore verrà molto, ma molto più grandi, per ottenere risultati
attivato con due valori differenti del seme, dando cosı̀ utilizzabili nella pratica.
In generale, possiamo desiderare un’estrazione uniorigine a sequenze diverse. Quindi, in fase operativa,
è bene far iniziare il programma di simulazione con forme fra due interi n ed m, cioè numeri casuali
nell’intervallo [m..n]; questo è presto fatto:
la chiamata a randomize().
11
1.5. IL METODO DEL CHI-QUADRO
random := proc(m, n) local r;
r := random(n-m+1) + m
end proc;
Il programma dovrebbe essere chiaro di per sé; in
particolare, random(1, n) estrae un numero a caso
compreso tra 1 ed n. Ad esempio, la random(1, 6)
simula il lancio di un dado; la random(1, 90) simula
l’estrazione di un numero del Lotto o di un numero
della Tombola; per la roulette si deve invece chiamare
random(37) oppure random(0,36).
Per la generazione dei numeri reali, l’idea di base
è semplice. Supponiamo di voler generare i numeri
compresi tra 0 ed 1, l’1 escluso, la situazione più frequente. Se generiamo un numero intero a caso r tra 1
e p − 1, possiamo sottrarre 1 e dividere per v = p − 1.
Si ottengono numeri razionali, ma due numeri consecutivi differiscono per 10−12 e quindi possiamo senz’altro considerare, quella ottenuta, una vera distribuzione continua; inoltre, se erano uniformemente distribuiti gli r, tali devono essere anche gli r/v. Ecco
il programma di generazione:
rand := proc()
local r; global p;
r:=evalf((random()-1)/(p-1))
end proc;
Qui, evalf è la procedura che valuta il corrispondente
decimale di una frazione.
La generazione dei numeri reali si può sfruttare anche per le distribuzioni discrete. Abbiamo già visto
un caso del genere, ma consideriamo il seguente esempio: vogliamo generare i numeri 1, 2 e 3 in una sequenza casuale, ma non uniforme. Vogliamo che ogni
numero venga generato con una probabilità proporzionale al numero stesso. Questo significa che 1 è
generato con probabilità π1 = 1/(1 + 2 + 3) = 1/6, il
2 con probabilità π2 = 2/(1 + 2 + 3) = 1/3 e 3 con
probabilità π3 = 3/(1 + 2 + 3) = 1/2. La probabilità
cumulate sono pertanto:
1
,
6
1 1
1
+ = ,
6 3
2
1 1 1
+ + = 1.
6 3 2
La strategia che si adotta è allora la seguente:
1. si genera un numero reale pseudocasuale r
nell’intervallo [0..1), cioè 0 ≤ r < 1;
2. se r < 1/6 allora si genera il numero 1;
3. altrimenti, se r < 1/2 si genera il numero 2;
4. altrimenti [se 1/2 ≤ r < 1] si genera il numero 3.
Tradotto in programma, questo algoritmo diventa:
tres := proc()
local r, y;
r := rand();
if r < 1/6 then y := 1 else
if r < 1/2 then y := 2 else y := 3
end if end if;
y
end proc
Questo metodo che sfrutta le probabilità cumulate
è molto generale e potrebbe essere sfruttato anche in
caso di distribuzioni uniformi. Se i casi sono tanti, e
non solo tre come nell’esempio, conviene creare una
tabella ordinata con le probabilità cumulate e quindi,
estratto r, farne una ricerca binaria nella tabella. Se i
casi sono k, questo rende la procedura di complessità
O(ln k) invece che lineare in k.
1.5
Il metodo del chi-quadro
Il problema di controllare se una sequenza di numeri
ha una distribuzione uniforme è un caso del problema
generale di controllare se una distribuzione empirica si conforma a una distribuzione teorica assegnata.
Supponiamo di voler “dimostrare” che il metodo congruenziale produce una distribuzione uniforme; possiamo procedere cosı̀. Fissiamo un certo numero di intervalli, diciamo 10 per semplicità; generiamo 10. 000
numeri col nostro programma random() e restituiamo 1 se il numero generato r è 0 ≤ r < (p − 1)/10;
restituiamo 2 se (p − 1)/10 ≤ r < 2(p − 1)/10, e cosı̀
via. Otteniamo una sequenza di numeri compresi tra
1 e 10. Contiamo le frequenze di ciascun numero e
siano f1 , f2 , . . . , f10 . Ci aspettiamo che, più o meno,
ciascuna di queste frequenze valga 1000. In realtà,
passando al programma ed eseguendolo si ottengono
le frequenze della Tabella 1.1. Ad occhio, possiamo
essere più o meno soddisfatti, ma vorremmo avere un
criterio oggettivo sul quale basarci.
Tale metodo esiste ed è chiamato chi-quadro (χ2 );
vediamo in cosa consiste. Siano f1 , f2 , . . . , fn le frequenze empiriche ottenute da una simulazione; nel
nostro esempio è n = 10, ed è importante che ogni
fi sia maggiore di 4 o anche 5; se cosı̀ non fosse,
occorrerebbe accorpare due o più frequenze, affinché questa condizione venga rispettata. Nel nostro
esempio, fortunatamente, le frequenze sono tutte alte. Siano poi t1 , t2 , . . . , tn le corrispondenti frequenze
teoriche; nel caso di una distribuzione uniforme si ha
t1 = t2 = · · · = tn ; nel nostro caso il valore comune è
1000. Ovviamente, se abbiamo accorpato due o più
frequenze empiriche, dobbiamo operare lo stesso accorpamento fra le frequenze teoriche. Calcoliamo poi
12
CAPITOLO 1. MODELLISTICA
n
fn
1
1017
2
963
3
980
4
1019
5
974
6
980
7
1044
8
996
9
1006
10
1021.
Tabella 1.1: Frequenze per l’uniformità di rand()
g\α
1
2
3
4
5
6
7
8
9
10
12
14
16
18
20
25
30
40
50
60
70
80
90
100
0.995
—
0.010
0.072
0.207
0.412
0.676
0.989
1.344
1.735
2.156
3.074
4.075
5.142
6.265
7.434
10.520
13.787
20.707
27.991
35.534
43.275
51.172
59.196
67.328
0.99
—
0.020
0.115
0.297
0.554
0.872
1.239
1.646
2.088
2.558
3.571
4.660
5.812
7.015
8.260
11.524
14.953
22.164
29.707
37.485
45.442
53.540
61.754
70.065
0.975
0.001
0.051
0.216
0.484
0.831
1.237
1.690
2.180
2.700
3.247
4.404
5.629
6.908
8.231
9.591
13.120
16.791
24.433
32.357
40.482
48.758
57.153
65.647
74.222
0.95
0.004
0.103
0.352
0.711
1.145
1.635
2.167
2.733
3.325
3.940
5.226
6.571
7.962
9.390
10.851
14.611
18.493
26.509
34.764
43.188
51.739
60.391
69.126
77.929
0.90
0.016
0.211
0.584
1.064
1.610
2.204
2.833
3.490
4.168
4.865
6.304
7.790
9.312
10.865
12.443
16.473
20.599
29.051
37.689
46.459
55.329
64.278
73.291
82.358
0.10
2.706
4.605
6.251
7.779
9.236
10.645
12.017
13.362
14.684
15.987
18.549
21.064
23.542
25.989
28.412
34.382
40.256
51.805
63.167
74.397
85.527
96.578
107.565
118.498
0.05
3.841
5.991
7.815
9.488
11.070
12.592
14.067
15.507
16.919
18.307
21.026
23.685
26.296
28.869
31.410
37.652
43.773
55.758
67.505
79.082
90.531
101.879
113.145
124.342
0.025
5.024
7.378
9.348
11.143
12.833
14.449
16.013
17.535
19.023
20.483
23.337
26.119
28.845
31.526
34.170
40.646
46.979
59.342
71.420
83.298
95.023
106.629
118.136
129.561
0.01
6.635
9.210
11.345
13.277
15.086
16.812
18.475
20.090
21.666
23.209
26.217
29.141
32.000
34.805
37.566
44.314
50.892
63.691
76.154
88.379
100.425
112.329
124.116
135.807
0.005
7.879
10.597
12.838
14.860
16.750
18.548
20.278
21.955
23.589
25.188
28.300
31.319
34.267
37.156
39.997
46.928
53.672
66.766
79.490
91.952
104.215
116.321
128.299
140.169
Tabella 1.2: La tabella del χ2
l’espressione:
χ2 =
n
X
(fi − ti )2
i=1
ti
,
che è una specie di deviazione dei valori empirici rispetto ai valori teorici, normalizzata dalla divisione
per i valori teorici. A seconda del valore del χ2 ottenuto possiamo sostenere che la distribuzione empirica
si conforma a quella teorica, oppure no. Nel nostro
esempio abbiamo χ2 = 5.924 e ci chiediamo se la
pretesa uniformità è confermata.
Per questo, occorre munirsi della tabella del χ2 (vedere Tabella 1.2), la cui lettura richiede di stabilire
quanti sono i “gradi di libertà” del problema considerato. Per gradi di libertà g di un problema di
simulazione si intende il numero di frequenze considerate e tra loro indipendenti. Ad esempio, nel nostro caso, f1 , f2 , . . . , f9 possono assumere valori arbitrari (dipendenti solo dal problema), mentre f10 è
determinato dagli altri valori, visto che deve essere
f10 = 10000 − (f1 + f2 + · · · + f9 ). Questo vuol dire
che i gradi di libertà del nostro problema sono g = 9.
In esperienze analoghe, si ha spesso g = n − 1, ma
bisogna stare attenti, perché si possono avere anche
altri valori, come vedremo più avanti.
Stabilito il valore di g, ci interessa la riga g della
Tabella del χ2 . Cerchiamo il valore più vicino al χ2
ottenuto e guardiamo cosa c’è scritto in cima alla colonna: nel nostro caso troviamo α = 0.90. Questa
quantità significa che solo nel 10% degli esperimenti
(1 − α) fatti con una distribuzione [sicuramente] uniforme con g gradi di libertà si è ottenuto un valore del
χ2 maggiore di 5.924. Il parametro α prende il nome
di confidenza; infatti, questo valore ci fa propendere
a credere (con una confidenza appunto del 90%) che
la nostra sia effettivamente una distribuzione uniforme. Se il valore del χ2 fosse stato 20, la tabella ci
avrebbe detto che solo il 2.5% degli esperimenti dà
un valore del χ2 cosı̀ alto; quindi, la nostra confidenza sull’uniformità della distribuzione calerebbe al
1.5. IL METODO DEL CHI-QUADRO
2.5%: in questo caso, saremmo portati a giudicare
non uniforme la nostra sequenza.
Facciamo un altro esempio utilizzando il programma tres per effettuare 60. 000 generazioni; in questo caso le frequenze teoriche sono t1 = 10000, t2 =
20000, t3 = 30000; i risultati empirici sono stati:
k
fk
1
10030
2
19718
3
30252
Il valore del χ2 è pertanto:
χ2 =
2822
2522
302
+
+
= 6.183.
10000 20000 30000
I gradi di libertà sono g = 2. L’uso della tabella ci fa
ancora propendere per un accordo tra la distribuzione
empirica e quella teorica.
Per fare un esempio negativo, immaginiamo di voler controllare se questa è una distribuzione uniforme.
Come frequenze teoriche avremmo t1 = t2 = t3 =
20000 e il χ2 varrebbe:
χ2 =
2822
102522
99702
+
+
= 10229.196,
20000 20000
20000
un valore troppo alto per convincerci a scommettere
sull’uniformità della distribuzione.
Per ritornare al problema della casualità e della
uniformità della procedura rand(), possiamo osservare che un altro modo di procedere potrebbe essere
quello di usare la random(1,n) che usa un modo diverso di estrarre i numeri, ma ci deve dare gli stessi
risultati. Cambiando il valore di n si riesce a indagare
a fondo l’uniformità delle sequenze. Lasciamo al lettore il divertimento di scrivere la relativa procedura
e fare diversi esperimenti.
Infine, vogliamo fare un’importante osservazione
sulla casualità che, fino a questo momento, abbiamo trascurato rispetto all’uniformità. Quanto vale il
χ2 della sequenza 0, 1, 2, 0, 1, 2, . . . , 0, 1, 2? In
questo caso la sequenza è evidentemente non casuale
e se prendiamo 30. 000 termini, i tre numeri hanno
la stessa frequenza e il χ2 è 0. E’ chiaro che il valore del χ2 è tanto più basso quanto più le frequenze
empiriche si avvicinano a quelle teoriche, ed è proprio 0 quando coincidono. Quindi, un valore troppo
vicino a 0 del χ2 può far sospettare che la sequenza
non sia del tutto casuale. Naturalmente, se per altre
vie sappiamo che la sequenza è veramente casuale, un
χ2 troppo piccolo è benvenuto, ma altrimenti è bene ripetere l’esperimento e, se il fenomeno continua
a sussistere, la conclusione deve essere (almeno nel
nostro caso): sı̀, la sequenza è uniforme, ma, ohimè,
non è casuale!
13
14
CAPITOLO 1. MODELLISTICA
Capitolo 2
Permutazioni e combinazioni
2.1
Concetti di base
Secondo la comune definizione, una permutazione di
un insieme di oggetti è una disposizione di tali oggetti in un ordine qualsiasi. Per esempio, tre oggetti,
denotati da a, b, c, possono essere disposti in sei modi
diversi:
(a, b, c), (a, c, b), (b, a, c), (b, c, a), (c, a, b), (c, b, a).
scrivere la seconda linea (le immagini) sotto forma di vettore. pertanto le sei permutazioni sono (1, 2, 3), (1, 3, 2), (2, 1, 3), (2, 3, 1), (3, 1, 2), (3, 2, 1),
rispettivamente.
Consideriamo la permutazione π = (3, 2, 1) per la
quale si ha π(1) = 3, π(2) = 2 e π(3) = 1. Se partiamo con l’elemento 1 e mano mano applichiamo la
funzioneπ, otteniamo π(1) = 3, π(π(1)) = π(3) =
1, π(π(π(1))) = π(1) = 3 e cosı̀ via. Dato che gli elementi in Nn sono in numero finito, cominciando con
qualsiasi k ∈ Nn dobbiamo ottenere una catena finita di numeri, che si ripeteranno sempre nello stesso
ordine. Si dice che tali numeri formano un ciclo e la
permutazione (3, 2, 1) è formata da due cicli, il primo
composto da 1 e 3, il secondo composto solamente da
2. Scriviamo (3, 2, 1) = (1 3)(2), in cui ogni ciclo è
scritto fra parentesi e i numeri sono separati da spazi,
per distinguere un ciclo da un vettore. Per convenzione, un ciclo viene scritto con l’elemento più piccolo
in testa ed i vari cicli sono ordinati secondo il loro
primo elemento. Perciò, in questa rappresentazione a
cicli le sei permutazioni sono:
Un problema informatico molto importante è
l’ordinamento: supponiamo di avere n oggetti di un
insieme ordinato; di solito qualche insieme numerico o qualche insieme di parole (col consueto ordinamento lessicografico); gli oggetti sono dati in un
ordine casuale e il problema consiste nell’ordinarli
secondo l’ordine assegnato. Per esempio, ordinando (60, 51, 80, 77, 44) si ottiene (44, 51, 60, 77, 80) e il
vero problema è quello di ottenere questo ordinamento nel tempo più breve possibile. In altre parole, si
parte con una permutazione casuale degli n oggetti, e vogliamo arrivare al loro ordinamento standard,
quello derivato dalla relazione d’ordine definita nel
loro insieme (per es., “minore di”).
(1)(2)(3), (1)(2 3), (1 2)(3), (1 2 3), (1 3 2), (1 3)(2).
Per poter astrarre dalla natura degli n oggetti, useUn numero k per il quale π(k) = k si dice un punremo i numeri {1, 2, . . . , n} = Nn , e definiamo una
permutazione come una funzione biunivoca π : Nn → to fisso per π. Il ciclo corrispondente, formato da
Nn . Se identifichiamo a con 1, b con 2, c con 3, le sei un solo elemento, viene per convenzione sottinteso,
eccetto che nell’identità (1, 2, . . . , n) = (1)(2) · · · (n),
permutazioni di 3 oggetti si scrivono:
nella quale tutti gli elementi sono punti fissi; l’iden¶
¶ µ
¶ µ
µ
1 2 3
1 2 3
1 2 3
tità si scrive semplicemente (1). Di conseguenza, la
,
,
,
2 1 3
1 3 2
1 2 3
rappresentazione delle sei permutazioni è:
µ
1 2 3
2 3 1
¶ µ
1 2
,
3 1
3
2
(1)
¶
¶ µ
1 2 3
,
,
3 2 1
dove, per convenzione, la prima linea contiene gli
elementi di Nn nel giusto ordine, e la seconda linea contiene le immagini corrispondenti. Queste
è la rappresentazione consueta delle permutazioni, ma poiché la prima linea può essere sottintesa senza creare ambiguità, risulta più comune la
rappresentazione vettoriale. Questa consiste nello
15
(2 3)
(1 2)
(1 2 3)
(1 3 2)
(1 3).
Una permutazione priva di punti fissi si dice un
derangement. Un ciclo con due soli elementi si dice
una trasposizione. Il grado di un ciclo è il numero dei
suoi elementi, più uno; il grado di una permutazione è
la somma dei gradi dei suoi cicli. Le sei permutazioni
hanno grado 2, 3, 3, 4, 4, 3, rispettivamente. Una
permutazione è pari o dispari a seconda che il suo
grado sia pari o dispari.
16
CAPITOLO 2. PERMUTAZIONI E COMBINAZIONI
La permutazione (8, 9, 4, 3, 6, 1, 7, 2, 10, 5), in no- Infatti, con la notazione a cicli abbiamo:
tazione vettoriale, ha come rappresentazione a cicli
(2 5 4 7 3 6)(2 6 3 7 4 5) =
(1 8 2 9 10 5 6)(3 4), e il numero 7 è un punto fisso.
= (2 6 3 7 4 5)(2 5 4 7 3 6) = (1).
Il lungo ciclo (1 8 2 9 10 5 6) ha grado 8; pertanto, il grado della permutazione è 8 + 3 + 2 = 13 e la
E’ facile osservare che l’inversa di un ciclo si ottiepermutazione è dispari.
ne scrivendo il suo primo elemento seguito da tutti
gli altri elementi in ordine inverso. Di conseguenza,
l’inverso di una trasposizione è la trasposizione stessa.
2.2 La struttura di gruppo
Poiché la composizione è associativa, abbiamo provato
che (Pn , ◦) è un gruppo. Tale gruppo non è
Se n ∈ N, Pn indica l’insieme di tutte le permutaziocommutativo,
dal momento che, per esempio:
ni di n elementi, cioè, per quanto detto nella sezione
precedente, l’insieme delle corrispondenze biunivoche
π : Nn → Nn . Se π, ρ ∈ Pn , possiamo eseguire la loro
composizione, il cui risultato è una nuova permutazione σ definita da σ(k) = π(ρ(k)) = (π ◦ ρ)(k). Un
esempio in P7 è:
π◦ρ=
µ
¶µ
¶
1 2 3 4 5 6 7
1 2 3 4 5 6 7
1 5 6 7 4 2 3
4 5 2 1 7 6 3
¶
µ
1 2 3 4 5 6 7
.
=
4 7 6 3 1 5 2
Infatti, per esempio, π(2) = 5 e ρ(5) = 7; perciò σ(2) = ρ(π(2)) = ρ(5) = 7, e cosı̀ via. La
rappresentazione vettoriale delle permutazioni non
si presta molto per la valutazione manuale della
composizione, benché sia particolarmente conveniente sull’elaboratore. Il caso opposto si verifica per la
rappresentazione a cicli:
(2 5 4 7 3 6) ◦ (1 4)(2 5 7 3) = (1 4 3 6 5)(2 7)
I cicli del membro sinistro vengono letti da sinistra a
destra ed esaminando un ciclo dopo l’altro troviamo
l’immagine di ciascun elemento semplicemente ricordando il successore nel ciclo dello stesso elemento.
Per esempio, l’immagine di 4 nel primo ciclo è 7; il
secondo ciclo non contiene 7, ma il terzo ciclo ci dice
che l’immagine di 7 è 3. Perciò, l’immagine di 4 è 3.
I punti fissi vengono ignorati, in accordo con il loro
significato. Il simbolo della composizione ‘◦’ di solito
si sottintende; in effetti, la semplice giustapposizione
dei cicli è sufficiente a denotare la loro composizione.
L’applicazione identica funziona da identità per la
composizione, poiché è composta solo da punti fissi.
Ogni permutazione ha un’inversa, la permutazione
ottenuta leggendo dal basso quella data, cioè, ordinando gli elementi nella seconda linea, che divengono
gli elementi della prima, e quindi trasferendo la prima linea nella seconda. Per esempio, l’inversa della
prima permutazione π dell’esempio precedente è:
µ
¶
1 2 3 4 5 6 7
.
1 6 7 5 2 3 4
ρ◦π
= (1 4)(2 5 7 3) ◦ (2 5 4 7 3 6)
= (1 7 6 2 4)(3 5) 6= π ◦ ρ.
Una involuzione è una permutazione π tale che π 2 =
π◦π = (1). Una involuzione può essere composta solo
da punti fissi e da trasposizioni, poiché per definizione
abbiamo π −1 = π e la precedente osservazione sull’inversa dei cicli dimostra che un ciclo con più di due
elementi ha un’inversa che non può coincidere con il
ciclo stesso.
Fino ad ora, abbiamo supposto che nella rappresentazione a cicli ogni numero sia considerato una volta
sola. Tuttavia, se pensiamo ad una permutazione come a un prodotto di cicli, possiamo immaginare che la
sua rappresentazione non sia unica e che un elemento
k ∈ Nn possa apparire in diversi cicli. La rappresentazione di σ o di π◦ρ confermano questa affermazione.
In particolare, è possibile ottenere la rappresentazione a trasposizioni di una permutazione; osserviamo
che si ha:
(2 6)(6 5)(6 4)(6 7)(6 3) = (2 5 4 7 3 6).
Un ciclo si trasforma in un prodotto di trasposizioni
costruendo una trasposizione con il primo e l’ultimo
elemento del ciclo, e aggiungendo altre trasposizioni sempre con il medesimo primo elemento (l’ultimo
elemento del ciclo) e gli altri elementi del ciclo, nello
stesso ordine, come secondo elemento. Inoltre, facciamo notare che si può sempre aggiungere una coppia di trasposizioni come (2 5)(2 5), corrispondenti ai
due punti fissi (2) e (5), senza cambiare niente nella
permutazione. Queste osservazioni mostrano che:
• ogni permutazione può essere scritta come la
composizione di trasposizioni;
• tale rappresentazione non è unica, ma due qualsiasi rappresentazioni differiscono per un numero
pari di trasposizioni;
• il numero minimo di trasposizioni corrispondenti a un ciclo è il grado del ciclo stesso, (eccezion
fatta eventualmente per i punti fissi, che comunque corrispondono sempre a un numero pari di
trasposizioni).
17
2.3. IL CONTEGGIO DELLE PERMUTAZIONI
Perciò, concludiamo osservando che una permutazione pari [dispari] si può esprimere come la composizione di un numero pari [dispari] di trasposizioni. Poiché la composizione di due permutazioni pari è ancora pari, l’insieme An delle permutazioni pari è un
sottogruppo di Pn ed è detto il sottogruppo alternante, mentre l’intero gruppo Pn è noto come il gruppo
simmetrico.
2.3
Il conteggio delle permutazioni
Poniamo per comodità Pn = |Pn |. Quante permutazioni ci sono in Pn ? Se n = 1, abbiamo l’unica
permutazione (1), e se n = 2 ne abbiamo due, esattamente (1, 2) e (2, 1). Abbiamo già visto che P3 = 6
e se n = 0 consideriamo () come l’unica permutazione possibile, cioè P0 = 1. In questo modo otteniamo
una sequenza {1, 1, 2, 6, . . .} e ci piacerebbe avere una
formula per calcolare Pn , per ogni n ∈ N.
Sia π ∈ Pn una permutazione e (a1 , a2 , ..., an ) la
sua rappresentazione vettoriale. Si può ottenere una
permutazione di Pn+1 aggiungendo il nuovo elemento
n+1 in ogni possibile posizione della rappresentazione
di π:
(n + 1, a1 , a2 , . . . , an )
···
(a1 , n + 1, a2 , . . . , an )
···
(a1 , a2 , . . . , an , n + 1)
Perciò, da ogni permutazione di Pn si ottengono n+1
permutazioni di Pn+1 , tutte differenti. Viceversa, se
si parte con le permutazioni di Pn+1 , e si elimina
l’elemento n + 1, si ottengono n + 1 permutazioni
tutte uguali di Pn . Pertanto, tutte le permutazioni di
Pn+1 si ottengono nel modo descritto e si ottengono
una sola volta. Troviamo quindi:
Il numero n · (n − 1)... · 2 · 1 è detto n fattoriale e si
indica con n!. Per esempio, si ha 10! = 10 · 9 · 8 · 7 · 6 ·
5 · 4 · 3 · 2 · 1 = 3, 680, 800. I fattoriali crescono molto
rapidamente, e risultano essere una delle quantità più
importanti della Matematica.
Quando n ≥ 2, possiamo aggiungere ad ogni permutazione di Pn una trasposizione, diciamo (1 2).
Questo trasforma ogni permutazione pari in una dispari, e viceversa. D’altra parte, poiché (1 2)−1 =
(1 2), la trasformazione è la propria inversa, e perciò
definisce una corrispondenza biunivoca fra le permutazioni pari e quelle dispari. Ciò prova che il numero
di permutazioni pari [dispari] è n!/2.
Un altro problema semplice è come si determina il
numero delle involuzioni su n elementi. Come abbiamo già visto, un’involuzione è composta solamente da
punti fissi e da trasposizioni (senza ripetizione degli
elementi!). Se indichiamo con In l’insieme delle involuzioni di n elementi, possiamo dividere In in due sottoinsiemi: In′ è l’insieme delle involuzioni nelle quali
n è un punto fisso, e In′′ è l’insieme delle involuzioni
nelle quali n appartiene a una trasposizione, diciamo
(k n). Se eliminiamo n dalle involuzioni di In′ , otteniamo un’involuzione di n − 1 elementi, e viceversa
ogni involuzione di In′ si può ottenere aggiungendo il
punto fisso n ad una involuzione di In−1 . Se eliminiamo la trasposizione (k n) da un’involuzione di In′′ ,
otteniamo un’involuzione di In−2 , che contiene l’elemento n − 1, ma non contiene l’elemento k. In tutti i
casi, tuttavia, eliminando (k n) da tutte le involuzioni
che lo contengono, s’ottiene un insieme di involuzioni in corrispondenza biunivoca con In−2 . L’elemento
k può assumere un qualsiasi valore 1, 2, . . . , n − 1, e
perciò si ottengono (n − 1) volte In−2 involuzioni.
Osserviamo ora che tutte le involuzioni di In si
ottengono in questo modo dalle involuzioni di In−1 e
In−2 , e quindi, posto In = |In |, si ha:
In = In−1 + (n − 1)In−2 .
Pn+1 = (n + 1)Pn
che è una relazione di ricorrenza molto semplice.
Espandendo la ricorrenza, cioè sostituendo a Pn la
stessa espressione per Pn+1 , e cosı̀ via, si ottiene:
Pn+1
=
(n + 1)Pn = (n + 1)nPn−1 =
= ··· =
= (n + 1)n(n − 1) · · · 1 × P0 .
Poiché I0 = 1, I1 = 1 e I2 = 2, da questa relazione di
ricorrenza possiamo via via trovare tutti i valori degli
In . Tale sequenza è pertanto:
n
In
0
1
1 2
1 2
3 4
4 10
5
26
6
76
7
232
8
764
Il numero delle involuzioni cresce molto rapidamente e può essere una buona idea considerare la quanDal momento che, come s’è visto, P0 = 1, abbiamo tità in = In /n!. Perciò, modifichiamo la ricorrenza in
dimostrato che il numero delle permutazioni di Pn è modo da renderla valida per ogni n ∈ N, e dividiamo
dato dal prodotto n · (n − 1) · . . . · 2 · 1. Perciò, la tutto per (n + 2)!:
nostra sequenza è:
In+2 = In+1 + (n + 1)In
In+1
1
1 In
In+2
n 0 1 2 3 4
5
6
7
8
=
+
.
Pn 1 1 2 6 24 120 720 5040 40320
(n + 2)!
n + 2 (n + 1)! n + 2 n!
18
CAPITOLO 2. PERMUTAZIONI E COMBINAZIONI
La relazione di ricorrenza per in è:
(n + 2)in+2 = in+1 + in
modi possibili e poi permutando gli n − 2 elementi
restanti. Abbiamo cosı̀ la nuova approssimazione:
µ ¶
n
Dn = n! − n(n − 1)! +
(n − 2)!.
2
e possiamo passare alle funzioni generatrici.
G((n + 2)in+2 ) può esser vista come lo spostamento
di G((n + 1)in+1 ) = i′ (t), se con i(t) indichiamo la In questo modo, però, abbiamo aggiunto due volte le
funzione generatrice di (ik )k∈N = (Ik /k!)k∈N , che permutazioni con almeno tre punti fissi, che devono
pertanto è la funzione generatrice esponenziale di essere di nuovo sottratte. Otteniamo quindi:
(Ik )k∈N . Abbiamo:
µ ¶
µ ¶
n
n
(n − 3)!.
(n
−
2)!
−
D
=
n!
−
n(n
−
1)!
+
n
i′ (t) − 1
i(t) − 1
3
2
=
+ i(t)
t
t
Possiamo continuare con lo stesso metodo, che è detto
per la condizione iniziale i0 = i1 = 1, e quindi:
il principio di inclusione ed esclusione, e finalmente
si
arriva al risultato:
i′ (t) = (1 + t)i(t).
µ ¶
n
Dn = n! − n(n − 1)! +
(n − 2)! −
Questa è un’equazione differenziale semplice a
2
¶
µ
variabili separabili e risolvendola troviamo:
n
(n − 3)! + · · · =
−
µ
¶
2
2
3
t
t
ln i(t) = t + + C o i(t) = exp t + + C
n
X
(−1)k
n! n! n! n!
2
2
−
+
−
+ · · · = n!
.
=
0!
1!
2!
3!
k!
k=0
dove C è la costante di integrazione. Poiché i(0) =
eC = 1, abbiamo C = 0 e concludiamo con la formula:
Questa formula è in accordo ai valori trovati soµ
¶
pra.
Si ottiene la funzione generatrice esponenzia2
t
In = n![tn ] exp t +
.
le G(Dn /n!) osservando che l’elemento generico della
2
somma è il coefficiente [tn ]e−t , e perciò per il teoCome abbiamo detto, un derangement è una per- rema sulla funzione generatrice delle somme parziali
mutazione senza punti fissi. Per n = 0 la permutazio- otteniamo:
µ
¶
Dn
e−t
ne vuota è considerata un derangement, dal momento
G
.
=
n!
1−t
che non esistono punti fissi. Per n = 1, non ci sono
derangement, ma per n = 2 la permutazione (1 2),
Per trovare il valore asintotico di Dn , osserviao che
scritta nella notazione a cicli, è chiaramente un de- il raggio di convergenza di 1/(1 − t) è 1, mentre e−t
rangement. Per n = 3 abbiamo i due derangement converge per ogni valore di t. Per il teorema di Bender
(1 2 3) e (1 3 2), e per n = 4 abbiamo un totale di 9 abbiamo:
derangement.
n!
Dn
Sia Dn il numero di derangement di Pn ; possiamo
∼ e−1 or Dn ∼ .
n!
e
contarli nel modo seguente: cominciamo sottraendo
da n!, il numero totale di permutazioni, il numero
Questo valore è effettivamente una buona approssidi permutazioni che hanno almeno un punto fisso:
mazione di Dn , che può essere calcolato come l’intero
se il punto fisso è 1, abbiamo (n − 1)! permutaziopiù vicino ad n!/e.
ni possibili; se il punto fisso è 2, abbiamo di nuovo
(n − 1)! permutazioni degli altri elementi. Perciò,
abbiamo un totale di n(n − 1)! casi, che ci danno 2.4
La generazione delle perl’approssimazione:
mutazioni
Dn = n! − n(n − 1)!.
Questa quantità è evidentemente 0 e ciò succede perché abbiamo sottratto due volte ogni permutazione
con almeno 2 punti fissi: infatti, l’abbiamo sottratta quando abbiamo considerato il primo e il secondo
punto fisso. Pertanto, dobbiamo ora aggiungere le
permutazioni con almeno due punti fissi. Queste¡ si¢
ottengono scegliendo i due punti fissi in tutti gli n2
Le permutazioni costituiscono un importante modello combinatorio: sono una struttura abbastanza semplice da studiare e facile da trattare; se ne conoscono
moltissime proprietà combinatorie che permettono di
verificare le simulazioni e i risultati empirici ottenuti;
sono un caso di per sé importante come modello di
ogni tipo di “mescolamento”, dalle carte da gioco all’ordine di arrivo di un certo numero di messaggi che
19
2.4. LA GENERAZIONE DELLE PERMUTAZIONI
seguono percorsi casuali. Per questo abbiamo dedicato il presente capitolo alle permutazioni, da vedersi
come esempio di modello e come esempio degli usi
che di un modello combinatorio si posson fare.
Generare una permutazione casuali di Pn non è
difficile. Consideriamo un vettore V di n componenti,
contenente gli oggetti da mescolare. Come abbiamo
detto, la rappresentazione vettoriale è quella più conveniente sul calcolatore. Per comodità, riempiamo il
vettore con i numeri da 1 ad n, ma come vedremo la
procedura è indipendente dal contenuto del vettore e
fa riferimento solo ai suoi indici. Procediamo da sinistra verso destra, considerando il primo elemento;
nella permutazione che vogliamo generare, tale primo
elemento deve essere uno a caso degli elementi di V.
Pertanto, usando la procedura random(1,n), estraiamo a sorte un numero j e scambiamo V[1] con V[j];
osserviamo esplicitamente che potrebbe anche essere
j = 1, e quindi è come se lo scambio non avvenisse;
questo però tiene conto del fatto che il primo elemento dell’insieme può essere davvero il primo della
permutazione.
A questo punto V[1] è a posto, e possiamo sistemare il secondo elemento. Estraiamo a caso un numero
j tra 2 ed n e scambiamo V[2] con V[j]; valgono ancora le considerazioni di prima e quindi anche V[2]
è a posto. Si può continuare cosı̀ fino a mettere a
posto il penultimo elemento V[n-1], dopo di che V[n]
è a posto automaticamente. Il semplice programma è
mostrato nella Figura 2.1; esso è noto col termine inglese di shuffling, mescolamento, e cosı̀ lo chiameremo
talvolta anche noi, per convenzione. Chiaramente, la
complessità della procedura è O(n).
shuffle := proc(n)
local a, i, j, V;
for i:=1 to n do V[i] := i end for;
for i := 1 to n-1 do
j := random(i,n);
a := V[i]; V[i] := V[j]; V[j] := a
end for;
V
end proc;
Figura 2.1: La procedura di mescolamento
Osserviamo esplicitamente che nei sistemi in cui
random(n) genera un numero pseudocasuale compreso tra 1 ed n, può essere più conveniente procedere da destra verso sinistra. Lo stesso capita con la
nostra random(n) se gli indici partono da 0. Lasciamo al lettore interessato apportare le semplici modifiche alla procedura di shuffling per adeguarla a queste
situazioni.
Un modo per verificare che la procedura di me-
scolamento genera veramente permutazioni casuali è
quello di osservare che la distribuzione dei vari elementi in ognuna delle posizioni del vettore deve essere
uniforme. In altre parole, se consideriamo la posizione 3 e generiamo m permutazioni degli elementi da
1 ad n, deve succedere che ognuno di questi elementi
appare con frequenza m/n. Naturalmente, questo si
deve verificare per ognuna delle posizioni 1, 2, . . . , n
in un solo esperimento che cumuli le frequenze posizione per posizione, utilizzando una matrice n × n,
dove F[j,k] conti la frequenza dell’elemento j nella
posizione k. Ottenuta la matrice, basta poi applicare
il metodo del χ2 . Un esperimento con 10. 000 generazioni ed n = 4 ha prodotto la seguente matrice di
frequenze:
2514
2556
2498
2432
2488
2487
2484
2541
2468
2499
2517
2516
2530
2458
2501
2511
Facendo i conti, si trova χ2 = 5.8264. Quanti sono
i gradi di libertà di questo problema? Si osservi che
ogni riga e ogni colonna ha come somma 10. 000 e
quindi il quarto elemento è determinato dai primi tre.
Questo significa che solo 3 × 3 = 9 quantità sono
“libere”, e questo è il valore di g. Dalla Tabella 1.2
del χ2 si vede che l’ipotesi di uniformità è del tutto
ragionevole.
Generare tutte le permutazioni di Pn è un’ardua
impresa anche per n molto piccolo. Già n = 7 o n = 8
danno 5040 e 40320 permutazioni, che solo a scriverle occuperebbero pagine e pagine. Talvolta, però, se
si vuole provare una procedura in modo esaustivo su
tutte le possibili permutazioni, è possibile arrivare fino ad n = 11, purché non si voglia generarle tutte assieme e memorizzarle in memoria centrale. In questi
casi è più opportuno avere un programma che genera
le permutazioni una ad una, in un ordine stabilito.
Si esegue il programma in prova sulla permutazione
generata, si salvano i risultati ottenuti e si passa alla
permutazione successiva.
Ad esempio, si supponga di essere arrivati alla permutazione π = (3, 4, 7, 6, 8, 5, 2, 1) e di voler generare la successiva. Quale ordinamento consideriamo?
In modo naturale possiamo proporre l’ordinamento
lessicografico, quello cioè di vedere la permutazione
come una parola i cui caratteri sono i vari elementi, e di usare l’ordine del vocabolario. Pertanto, la
permutazione π precede (3, 4, 8, 5, 1, 2, 6, 7), ma segue (3, 1, 2, 8, 7, 5, 6, 4). In particolare, ci chiediamo
chi venga dopo π.
Poiché l’ultima permutazione è (8, 7, 6, 5, 4, 3, 2, 1),
osserviamo che elementi in ordine decrescente corrispondono all’ultima sottopermutazione da essi composta. Ad esempio, i due ultimi elementi 1 e 2 formano le permutazioni (1, 2) e (2, 1) e quella entro π è
20
CAPITOLO 2. PERMUTAZIONI E COMBINAZIONI
nextp := proc(Q) local a, finito, j, k, L, n, P, trovato;
P := Q; n := nops(P); j := n + 1; L := [];
finito := false; trovato := false;
while not trovato do
j := j - 1;
L := [op(L), P[j]];
if P[j - 1] < P[j] then trovato := true
else if j = 2 then finito := true; trovato := true
end if end if
end while;
if finito then for k to n do P[k] := k end for
else
j := j - 1;
a := P[j];
k := 1;
while L[k] < a do k := k + 1 end while;
P[j] := L[k];
L[k] := a;
for k to nops(L) do P[j + k] := L[k] end for
end if;
P
end proc
Figura 2.2: Il programma per la permutazione successiva
proprio l’ultima. Analogamente, i tre ultimi elementi
1, 2 e 5 formano 6 permutazioni delle quali (5, 2, 1) è
l’ultima. Cosı̀ (8, 5, 2, 1) è l’ultima fra quelle formate
da questi quattro elementi. Invece, questo non capita
a (6, 8, 5, 2, 1), per cui basta trovare la sottopermutazione successiva a questa per aver risolto il nostro
problema. In questa sottopermutazione, il 6 ha come
unico elemento superiore l’8, che perciò dovrà iniziare la prossima sottopermutazione. D’altra parte, essa
deve essere la prima di quelle che cominciano per 8, e
quindi sarà (8 1 2 5 6), con gli ultimi elementi in ordine crescente. La permutazione successiva a π sarà
pertanto (3, 4, 7, 8, 1, 2, 5, 6).
Cerchiamo di formalizzare un po’ meglio questi
discorsi. Ecco l’algoritmo:
1. partendo da destra si scorrono gli elementi, spostandoli in ordine in una lista L, finché non si
trova un elemento più piccolo del successivo;
l’elemento trovato V[j] non viene spostato;
2. nella lista L (che per costruzione è ordinata in
senso decrescente) si cerca l’elemento L[k] che
è immediatamente superiore a V[j]; un tale elemento deve esistere per forza: almeno l’ultimo
elemento aggiunto ad L è, ancora per costruzione, più grande di V[j]; ce ne possono essere però
anche altri;
3. si scambiano V[j] ed L[k];
4. si inseriscono in coda a V gli elementi di L, in
ordine inverso (cioè decrescente) per formare la
prima sottopermutazione di tali elementi.
Il corrispondente programma è dato nella Figura 2.2.
2.5
Parole, disposizioni e combinazioni
Un oggetto combinatorio interessante, e che si presenta sotto vari aspetti, è quello delle liste; con questo termine si intende ogni sequenza finita i cui elementi appartengono a un insieme fissato (e finito).
Ad esempio, se S = {0, 1, 2}, una lista su S di
lunghezza 4 è [1, 2, 1, 0]; al contrario delle permutazioni, gli elementi possono essere ripetuti o essere
assenti e [2, 2, 2, 2] è ancora una lista legale su S,
di lunghezza 4. La lista vuota si indica con [] ed
è l’unica lista di lunghezza 0. Se |S| = n, diciamo
S = {a1 , a2 , . . . , an }, vi sono n liste di lunghezza 1,
ed esattamente: [a1 ], [a2 ], . . ., [an ]. Vi sono poi n2
liste di lunghezza 2, in quanto un elemento tra n si
trova al primo posto e un elemento tra n al secondo.
In modo analogo, è semplice dimostrare che le liste
di k elementi sono in tutto nk . Se L ed M sono due
liste, sullo stesso insieme S, la giustapposizione di L
ed M si indica con L.M , o anche semplicemente con
LM . Con tale operazione l’insieme delle liste su S
(che si denota S ∗ ), è un monoide, avente come iden-
21
2.5. PAROLE, DISPOSIZIONI E COMBINAZIONI
tità la lista vuota. Se L è una lista, |L| ne indica la
lunghezza; pertanto si ha: |L.M | = |L| + |M |.
A seconda dei contesti, la lista può assumere diversi
nomi. Talvolta, una lista di lunghezza m si dice una
m-upla; anche il termine di sequenza finita è abbastanza frequente. Nella teoria dei linguaggi formali,
l’insieme S si dice alfabeto, gli elementi di S si dicono
caratteri o lettere e le liste su S si dicono parole o
stringhe su S. In questo caso, la lista [a1 , a2 , . . . , am ]
si scrive semplicemente a1 a2 . . . am e la parola vuota
si indica con λ.
La generazione casuale delle liste è banale. Se S
si rappresenta con gli interi [1..n], la procedura si riduce ad estrarre un numero in questo intervallo per
m volte, se m è la lunghezza della lista che si desidera. Altrettanto semplice è generare, in ordine alfabetico o lessicografico, tutte le liste su S di lunghezza m. Più complesso è generare casualmente le
parole di un linguaggio formale, cioè di un sottoinsieme di S ∗ , problema che affronteremo più avanti nelle
applicazioni.
Sia ancora S un insieme finito con |S| = n; una
disposizione degli elementi di S a k a k è un sottoinsieme ordinato di S con k elementi. Ad esempio,
se S = {1, 2, 3, 4}, sono disposizioni a 2 a 2 (1, 2)
e (4, 2), e sono disposizioni diverse (2, 3) e (3, 2).
come si vede, per le disposizioni si usa la medesima
notazione vettoriale delle permutazioni. Ciò è consistente, poiché le disposizioni di S ad n ad n coincidono proprio con le permutazioni di S. Contare le
possibili disposizioni a k a k non è difficile; detta Dn,k
questa quantità, si procede cosı̀: in prima posizione
può stare qualunque elemento di S e vi sono quindi n
possibilità. In seconda posizione possono stare tutti
gli elementi di S eccetto eccetto quello che abbiamo
messo in prima posizione: vi sono quindi n − 1 possibilità; in tutto, abbiamo n(n − 1) possibilità. Analogamente, in terza posizione vi sono n − 2 possibilità,
e cosı̀ via. Nella k-esima posizione avremo n − k + 1
possibilità, per cui in totale si ha:
Dn,k = n(n − 1) · · · (n − k + 1).
La generazione casuale delle disposizioni è una banale generalizzazione della procedura di shuffling (vedi Figura 2.1). Basta infatti osservare che man mano si procede nell’iterazione for i:=1 to n-1 do,
gli elementi fino all’i-esimo risultano correttamente
mescolati. Pertanto, basta fermare l’iterazione al kesimo perché i primi k elementi di V costituiscano
una disposizione degli n numeri [1..n] presi a k a k.
Quindi, è sufficiente sostituire la precedente istruzione iterativa; per comodità, riportiamo la procedura
nella Figura 2.3.
Più importanti delle disposizioni sono le combinazioni. Sia S un insieme di n elementi. Si dicono combinazioni degli elementi di S a k a k i sottoinsiemi
disposiz := proc(n,k)
local a, i, j, V;
for i:=1 to n do V[i] := i end for;
for i := 1 to k do
j := random(i,n);
a := V[i]; V[i] := V[j]; V[j] := a
end for;
V[1..k]
end proc;
Figura 2.3: La generazione delle disposizioni
di S con k elementi. A differenza delle disposizioni,
le combinazioni non tengono conto dell’ordinamento degli elementi; pertanto, la combinazione (1, 2) è
la stessa di (2, 1). Ogni combinazione di k elementi, perciò, corrisponde a k! disposizioni diverse, tutte
quelle che si ottengono mescolando i k elementi della combinazione. Questo ci dà un semplice modo di
contare le combinazioni:
n(n − 1) · · · (n − k + 1)
.
k!
I Cn,k si dicono coefficienti binomiali e sono una delle
quantità più importanti della Matematica. Di regola
si scrivono con una particolare notazione:
µ ¶
n
Cn,k =
k
Cn,k =
universalmente adottata e che in italiano si legge “n
su k”. Moltiplicando numeratore e denominatore
della frazione per (n − k)! si ha la formula:
µ ¶
n
n(n − 1) · · · (n − k + 1) · (n − k)!
n!
=
=
k
k!(n − k)!
k!(n − k)!
che può essere utile in varie circostanze.
Come è noto, i coefficienti binomiali possono essere
disposti in un triangolo infinito, noto come Triangolo
di Pascal o anche Triangolo di Tartaglia, nonostante
fosse noto anche ai matematici cinesi del Medioevo.
La parte iniziale del triangolo è riportata nella Tabella 2.1 e la sua costruzione si basa sulla seguente
proprietà:
µ
¶ µ ¶ µ
¶
n+1
n
n
=
+
.
k+1
k
k+1
Questo significa che l’elemento nella posizione k + 1
della riga n+1 si ottiene come somma di due elementi
della riga precedente n-esima: quello nella sua (k + 1)
e quello nella posizione precedente (k).
Le proprietà dei coefficienti binomiali sono moltissime, ma le principali sono le tre seguenti. La
proprietà di simmetria:
¶
µ ¶ µ
n
n
=
n−k
k
22
CAPITOLO 2. PERMUTAZIONI E COMBINAZIONI
k\n
0
1
2
3
4
5
6
7
0
1
1
1
1
1
1
1
1
1
2
3
4
5
6 7
1
2 1
3 3
4 6
5 10
6 15
7 21
1
4
10
20
35
1
5
15
35
1
6
21
1
7 1
Tabella 2.1: Il Triangolo di Tartaglia-Pascal
la proprietà di negazione
µ ¶ µ
¶
n
−n + k − 1
=
(−1)k sgn(k)
k
k
e il prodotto in croce
¶
µ ¶µ ¶ µ ¶µ
n n−r
n k
=
k−r
r
r
k
La funzione sgn(k) vale 1 se k geq0 e vale −1 se k < 0;
pertanto, la seconda proprietà, permette di estendere
la definizione dei coefficienti binomiali al caso che n
e/o k siano negativi:
µ ¶ µ ¶
−5
7
=
(−1)3 = −35;
3
3
¶
µ ¶
µ ¶ µ
−3
4−6−1
−4
=
(−1)6 sgn(−6) = −
=
−6
−6
−6
¶
µ ¶
µ
−3
−3
=
=−
=−
3
−3 − (−6)
µ
¶
µ ¶
3+3−1
5
=−
(−1)3 sgn(3) =
= 10,
3
3
µ ¶
µ ¶
3/2
3 · (−1)n
2n
= n
;
n
4 (2n − 1)(2n − 3) n
µ ¶
µ
¶
−1/2
(−1)n 2n
;
=
4n
n
n
µ ¶
µ
¶
−3/2
(−1)n−1 (2n + 1) 2n
.
=
4n
n
n
Infine, utilizzando la funzione Γ(x) i coefficienti binomiali possono essere estesi a tutte le coppie di numeri
reali:
µ ¶
r
Γ(r + 1)
=
;
s
Γ(s + 1)Γ(r − s + 1
La funzione Γ(x) generalizza il fattoriale, nel senso
che per x intero non negativo si ha Γ(x + 1) = x!.
Noi però non ci addentreremo per questa strada.
La generazione casuale delle combinazioni è la stessa delle disposizioni; è sufficiente non considerare
l’ordine degli elementi generati e ciò che si ottiene, per definizione, è una combinazione. Vedremo
nella prossima sezione come si utilizzino le relative
procedure.
2.6
Un caso di studio
Le combinazioni forniscono un caso di studio semplice, ma interessante. Vorremmo verificare che la
procedura di generazione delle combinazioni, come
descritta nella sezione precedente, è valida, cioè produce combinazioni casuali e uniformemente distribuite. Per tale verifica seguiremo due strade: vedremo
che generando esaustivamente combinazioni di piccole dimensioni, la loro frequenza si distribuisce uniformemente; in secondo luogo vedremo come anche gli
elementi generati hanno una distribuzione uniforme.
2
dove abbiamo utilizzato anche la proprietà di Naturalmente, faremo uso del metodo del χ .
simmetria.
combin := proc(n, k)
E’ facile estendere i coefficienti binomiali al caso local a, caso, i, j, V, W;
che il numeratore sia un numero reale qualsiasi e il
W := [];
denominatore sia un intero non negativo; infatti, è
for j := 1 to n do V[j] := j end do;
sufficiente applicare la definizione:
for i := 1 to n - 1 do
µ ¶
j := rand(i,n);
r(r − 1) · · · (r − k + 1
r
=
a := V[i]; V[i] := V[j]; V[j] := a
k
k!
end do;
per cui, ad esempio, abbiamo:
for j := 1 to k do
µ ¶
W := append(W, V[j]) end do;
1 · (−1) · (−3)
1/2
1/2(1/2 − 1)(1/2 − 2)
=
=
=
W := sort(W);
3
3!
2 · 3!
3
end proc;
3
1
=
=
.
48
16
Figura 2.4: La generazione delle combinazioni
Si possono in generale dimostrare le seguenti
formule, che ci serviranno nel seguito:
Intanto, nella Figura 2.5 riportiamo la procedura di
µ ¶
¶
µ
generazione, con qualche modifica per renderla frui2n
1/2
(−1)n
;
= n
bile ad altre procedure. La variabile locale W contiene
4 (2n − 1) n
n
23
2.6. UN CASO DI STUDIO
la combinazione generata, con gli elementi ordinati in
senso crescente dalla procedura sort. Inizialmente, W
contiene la lista vuota []; la procedura append concatena le due liste argomento; in questo caso V[j] è
un singolo elemento, visto comunque operativamente
come lista. Come detto, questa procedura è derivata
direttamente dallo shuffling.
Per la prima verifica consideriamo un caso che non
comporti il trattamento di molte combinazioni; se
prendiamo n = 5 e k = 3, le combinazioni sono soltanto 10 (vedi Tabella 2.1). Queste combinazioni si
possono scrivere facilmente e memorizzare in una lista, che abbiamo chiamato U e abbiamo definito come
globale:
> cocomb(20000);
[2079, 1975, 2002, 1977, 1972, 1982,
1975, 2021, 1989, 2028]
chi2 =
5.239
Poiché questa prova coinvolge classi esigue di combinazioni, ci possiamo chiedere cosa succede quando
le classi sono molto più ampie; infatti, dato che è
impossibile trattare nello stesso modo classi con milioni o miliardi di combinazioni diverse, potrebbero
succedere anche cose strane. Per affrontare questo
caso, occorre allora procedere in modo diverso. Si
dice indicatore un parametro che è funzione della dimensione degli oggetti che si vogliono generare. Ad
esempio, nel caso delle permutazioni, il valore del priU := [[1, 2, 3], [1, 2, 4], [1, 2, 5], [1, 3, 4], [1, 3, 5], [1, 4, 5], mo elemento è un indicatore del conteggio delle permutazioni stesse: noi sappiamo che ogni valore deve
apparire per primo con la stessa frequenza di tutti gli
[2, 3, 4], [2, 3, 5], [2, 4, 5], [3, 4, 5]].
altri e quindi se in un esperimento rileviamo tale freGenerata una permutazione casuale, la si va a cercare quenza dobbiamo trovare una distribuzione uniforme.
in questa lista di modo da poter trattare l’indice cor- E’ chiaro che questo potrebbe essere un caso particorispondente, invece della combinazione. Il program- lare, cioè, se la frequenza degli elementi è uniforme,
ma di generazione è riportato nella Figura ??, con il potrebbe essere che le permutazioni, nel loro insienome cocomb, cioè Conta Combinazioni.
me, non hanno la distribuzione che ci aspettiamo. Si
tratta cioè di una prova indiretta, ma è comunque
cocomb := proc(m)
un’indicazione importante, quando, appunto, vi sono
local F, i, j, mu, Q, trovato, Y;
difficoltà oggettive nel rilevare la vera distribuzione
global U;
di una classe di oggetti combinatori.
for j := 1 to 10 do F[j] := 0 end do;
Nel caso delle combinazioni possiamo pensare a vafor j := 1 to m do
ri indicatori. Abbiamo qui considerato la frequenza
Y := combin(5, 3);
dei singoli elementi nelle combinazioni: chiaramente,
i := 0;
tale frequenza deve essere la stessa per tutti, cioè ci
trovato := false;
aspettiamo una distribuzione uniforme. L’esperimenwhile not trovato do i := i + 1;
to che abbiamo allestito riguarda le cinquine (della
trovato := Y = U[i] end do;
Tombola o del Lotto), che sono in tutto:
F[i] := F[i] + 1
µ ¶
end do;
90
= 43949268
scriviV(F, 10);
5
mu := m/10; Q := 0;
una quantità abbastanza elevata: se generassimo anfor i := 1 to 10 do
che 50 milioni di tali combinazioni, molte non verQ := Q + (F[i] - mu)^2/mu end do;
rebbero mai prodotte, escludendo cosı̀ ogni speranza
printf("chi2 = %10.5g\n", Q)
di verifica diretta. Vediamo allora cosa succede geend proc;
nerando un numero molto minore di combinazioni e
contando la frequenza dei vari elementi da 1 a 90 che
Figura 2.5: La generazione delle combinazioni
man mano vengono estratti. Il programma cinquina
è presentato nella Figura ??.
Il programma è abbastanza lineare. Il vettore F
Poiché i numeri possibili sono 90, per restringere
serve a contare le frequenze di ciascuna combinazio- i valori possibili abbiamo raggruppato gli elementi
ne delle 10 considerate; viene quindi azzerato all’i- delle combinazioni in 9 sottoclassi, ognuna comprennizio. Il parametro m conta il numero di prove da dente una decina di valori: da 1 a 10, da 11 a 20, e
effettuare; poiché pensiamo che la distribuzione sia cosı̀ via. Quindi, abbiamo ridotto la variabilità del
uniforme, la frequenza teorica comune delle 10 com- nostro indicatore, che però deve rimanere uniformebinazioni è m/10, e con questo valore si calcola il χ2 , mente distribuito. L’istruzione k := trunc((Y[i] che salviamo nella variabile Q. La procedura scriviV 1)/10 + 1); non fa altro che trovare la sottoclasse
serve semplicemente a scrivere in uscita un vettore. di appartenenza del singolo elemento Y[i]. Questa
volta, la frequenza teorica comune è data da 5m/9,
Il risultato è quello che ci aspettavamo:
24
CAPITOLO 2. PERMUTAZIONI E COMBINAZIONI
cinquina := proc(m)
local F, i, j, k, mu, Q, Y;
for j := 1 to 9 do F[j] := 0 end do;
for j := 1 to m do Y := combin(90, 5);
for i to 5 do
k := trunc((Y[i] - 1)/10 + 1);
F[k] := F[k] + 1 end do
end do;
scriviV(F, 9);
mu := 5/9*m;
Q := 0;
for i to 9 do
Q := Q + (F[i] - mu)^2/mu end do;
printf("chi2 = %10.5g\n", Q)
end proc
Figura 2.6: La generazione delle cinquine
visto che ognuna della m combinazioni generate ha
5 elementi e questi 5m elementi vanno ripartiti in 9
sottoclassi. Un’esecuzione con 9 mila generazioni ha
dato questi risultati:
> cinquina(9000);
[ 5037, 5048, 4912, 4902, 5058, 5075,
4952, 5025, 4991]
chi2 =
6.604
che confermano, con le cautele descritte, l’uniformità
della distribuzione.
Capitolo 3
La ricorsione
Una delle tecniche fondamentali della programmazione è senz’altro la ricorsione. Oggi, la maggior parte
dei linguaggi di programmazione mette a disposizione
dell’utente questa tecnica, che semplifica la soluzione
di molti problemi, sia concettualmente che praticamente. I programmi ricorsivi sono molto brevi e molto chiari e, una volta entrati nel meccanismo mentale
che richiedono se ne può apprezzare l’eleganza logica
e strutturale. Poiché la ricorsione riconduce un problema di grosse dimensioni a problemi di dimensioni
sempre più ridotte, può essere utile quando il problema presenta proprio tale caratteristica. Nel campo della simulazione non è particolarmente frequente, ma in alcuni giochi si presta bene ad affrontare in
modo sistematico le difficoltà della esecuzione.
Presenteremo qui due problemi. Il primo, che abbiamo già affrontato in un altro modo, riguarda la generazione di tutte le permutazioni di n oggetti. Questo problema, una volta impostato in maniera ricorsiva, è di facilissima soluzione. Bisogna però aver
chiaro cosa si vuole e cosa si può fare. Il secondo
problema, apparentemente più complesso, è il Sudoku, la cui soluzione risulta abbastanza complessa da
un punto di vista umano. Per l’elaboratore, invece,
una volta impostata la soluzione ricorsiva, si tratta
di una facile procedura, che permette di risolvere in
modo esaustivo il gioco.
Le procedure che presentiamo in formato standard,
sono state realizzate in Maple. Questo linguaggio presenta una caratteristica specifica, che qui abbiamo
conservato. Per realizzare correttamente la ricorsione, Maple non consente di assegnare valori specifici ai
parametri formali delle procedure. Pertanto, per modificarne uno occorre assegnarlo a una variabile locale, il che permette di mantenere la copia precedente
nel parametro attuale. Una volta copito questo, la
ricorsione diviene molto semplice.
3.1
Generare le permutazioni
perm := proc (V, M)
local j, N, W;
if M = {} then scrivi(V)
else for j to nops(M) do
W := [op(V), M[j]];
N := M minus‘{M[j]};
perm(W, N) end do
end if
end proc;
Il parametro V, una lista, contiene la parte della
permutazione che si è formata fino a questo momento. Il parametro M, un insieme, contiene gli elementi
che non sono ancora stati usati. Se M è l’insieme vuoto, indicato da {}, vuol dire che abbiamo disposto
tutti gli elementi in V, che quindi è pronto per essere
stampato con la procedura scrivi. Questa è la condizione di terminazione della procedura; al rientro da
scrivi il programma termina e comincia il processo
di backtracking. Nel caso invece che M non sia vuoto,
viene effettuato un ciclo per aggiungere nella successiva posizione di V gli elementi di M, uno alla volta.
Per questo si usano le variabili locali W ed N; W contiene la nuova struttura di V, con l’elemento aggiunto;
N contiene l’insieme M, meno l’elemento aggiunto a V.
Sono questi i parametri passati alla procedura perm,
che prosegue nella ricorsione. I parametri V ed M, non
modificabili dalla procedura, assicurano il mantenimento dei valori iniziali, e quindi la corretta costruzione di W ed N ad ogni rientro dalla istanza di perm
chiamata nel ciclo.
Questo è il cuore della procedura di generazione
esaustiva. La chiamata della procedura avviene con
il seguente programma:
permutazioni := proc (n)
local j, S, V; global cont;
cont := 0; V := []; S := {};
for j:=1 to n do S := S union {j} end do;
perm(V, S)
end proc;
Il parametro n indica il numero di elementi da permutare; in altre parole permutazioni(n) genera tut-
comicnciamo col riportare il programma ricorsivo che
genera le permutazioni:
25
26
CAPITOLO 3. LA RICORSIONE
te le permutazioni di Pn . La procedura provvede a
inizializzare le variabili: V, che conterrà il risultato, è
inizialmente vuoto. L’insieme S contiene invece tutti
i valori [1..n], poiché nessuno è ancora stato utilizzato e trasferito in V. La variabile globale cont serve
a contare le permutazioni generate, come ora vedremo nell’esempio. La chiamata di perm dà inizio alla
ricorsione.
Il programma di stampa è del tutto ovvio. Stampa il numero d’ordine contenuto in cont e quindi
la permutazione generata utilizzando la notazione
vettoriale:
scrivi := proc (Y) local j; global cont;
cont := cont + 1;
printf("%5d (", cont);
j := 1;
while j < nops(Y) do
printf("%d,", Y[j]);
j := j+1 end do;
printf("%d)\n", Y[j])
end proc;
Un semplice esempio di esecuzione è il seguente:
> permutazioni(3);
1 (1,2,3)
2 (1,3,2)
3 (2,1,3)
4 (2,3,1)
5 (3,1,2)
6 (3,2,1)
3.2
Sudoku
Le seguenti sezioni vogliono descrivere il metodo generale per la risoluzione del Sudoku. Dato uno schema di Sudoku, definito da una matrice numerica 9×9,
il programma trova la soluzione del gioco supponendo
che esista uno e un solo modo di arrivare alla conclusione. Il programma funziona anche se vi sono più
soluzioni, trovando la prima, in un certo ordine che
vedremo; inoltre avverte con il messaggio NEY nel
caso non vi siano soluzioni. Il metodo usato è rigorosamente ricorsivo. Sia dato ad esempio il seguente
schema:
1 0 0
4 2 8
3 0 0
3 0 0
0 0 0
0 0 0
0 0 7
0 0 0
0 0 0
5 0 0
0 8 4
0 0 0
6 0 0
5 3 7
0 4 0
0 8 0
0 0 6
5 0 0
0 0 0
4 8 2
0 6 0
0 0 5
6 1 2
0 0 0
0 9 0
0 7 0
0 3 0
dove gli 0 rappresentano le caselle vuote; queste caselle inizialmente vuote verranno dette caselle attive.
Il programma procede da sinistra a destra e dall’alto
in basso, cercando la prima casella contenente 0; nell’esempio è la seconda, dopo l’1. In tale casella può
inserire solo i numeri che non si trovano nella stessa riga (esclude cosı̀ 1, 3 e 7), nella stessa colonna
(esclude perciò 1, 2 e 8) o nella stessa casa, cioè nel
riquadro 3 × 3 che gli compete (escludendo ancora 1,
2, 3, 4 e 8). In definitiva, i numeri da non considerare
sono l’unione di quelli elencati, cioè 1, 2, 3, 4, 7, 8, e
quindi quelli da considerare sono 5, 6 e 9.
Dei tre numeri [5, 6, 9] trovati, il programma assegna alla casella ordinatamente il primo, cioè 5, e
passa alla casella successiva che contiene 0. In questo caso è la terza e procede come per la precedente,
escludendo i numeri che si trovano nella stessa prima
riga, nella stessa terza colonna e nella stessa prima
casa. Naturalmente deve considerare il fatto che è
stato (almeno provvisoriamente) assegnato il valore
5 alla seconda casella. Pertanto i valori da escludere
sono 1, 3, 5, 7 nella prima riga, 2, 4, 5, 8 nella terza
colonna e 1, 2, 3, 4, 8 nella prima casa; rimangono
6 e 9 e quindi il 6 viene tentativamente assegnato a
questa posizione.
Si va cosı̀ avanti assegnando il primo dei possibili
valori ad ogni casella che contiene 0. L’ultimo elemento di ciascuna riga, colonna o casa è determinato dai
precedenti e quindi ha un valore obbligato. Possono
succedere due casi:
1. gli assegnamenti possibili vanno avanti senza
problemi fino al completamento dello schema: in
questo caso si è trovata la soluzione del gioco, ovvero si è trovata la prima soluzione nel caso che
il gioco abbia più soluzioni. Questo fa terminare
il programma;
2. ad un certo punto non si ha nessun valore che
possa essere assegnato ad una casella. In questo
caso la parte di soluzione creata fino a questo
punto non porta a niente e quindi va iniziata
l’operazione di backtracking per modificare gli
assegnamenti precedenti. Se il rientro è eccessivo
e si ritorna al programma principale, allora il
problema non ammette soluzione.
Vediamo come deve avvenire il backtracking. Prima di tutto, la casella che ha provocato il backtracking stesso deve essere messa a zero. Se il backtracking è stato causato al primo tentativo di assegnamento, il valore è di per sé 0, ma se il tentativo non
è il primo si ha senz’altro un valore diverso da zero.
3.2. SUDOKU
Quindi, per sicurezza, la casella viene posta a 0. Secondariamente, si ritorna alla casella attiva precedente; se questa ha la possibilità di cambiare il proprio
valore con uno possibile, si effettua il cambiamento
e si procede di nuovo in avanti. Se invece la casella
ha esuarito le proprie possibilità, il becktracking va
continuato. Questo significa che la casella va posta a
0 e si deve cercare la casella attiva ancora precedente. Se a forza di tornare indietro si dovesse cercare di
modificare la casella che precede la prima attiva (la
casella 2 dell’esempio), allora vuol dire che non esiste
alcuna soluzione al gioco.
Riprendiamo il nostro esempio. Supponiamo che
non vi siano valori da assegnare alla quinta casella
dello schema. Essa viene messa a zero e si torna alla
terza casella, seconda casella attiva. Come si è visto,
i possibili valori per questa casella sono 6 e 9; il 6
è già stato assegnato e quindi si passa al valore 9,
riprendendo poi il cammino in avanti, ritornando cioè
alla quinta casella. Quando questa dovesse non avere
più alternativa, il backtracking ci porterebbe ancora
alla terza casella. Questa volta, però, non vi sono più
valori da assegnarle; quindi viene messa a 0 e si passa
alla seconda casella, la prima attiva. Qui i valori
possibili sono 5, 6 e 9 e perciò il valore successivo, 6,
viene assegnato alla casella. Il programma, esaurito
il backtracking, va avanti alla terza casella che, si
osservi, questa volta avrà come casi possibili 5 e 9;
infatti il 6 si trova nella casella precedente, che però
non contiene più il 5.
La seconda casella sopporterebbe un’ulteriore fase
di backtracking, durante la quale le verrebbe assegnato il valore 9, il terzo dei valori possibili. Se però si dovesse passare a un’altra fase di backtracking, avendo
esaurito tutti i valori possibili e non potendo tornare
ad una casella precedente, si avrebbe la conclusione
del programma col risultato negativo che il gioco non
ammette soluzioni. Tecnicamente, questo programma è stato realizzato con una procedura MOSSA che
riportiamo nella Figura 3.1.
Alcune osservazioni tecniche sono necessarie:
27
3. La variabile h contiene il valore da assegnare alla prossima casella attiva. Sarà un parametro
della procedura LEGALE, che determina i valori
possibili da assegnare a una casella attiva.
4. La variabile booleana trabocco dice se si è scandita la matrice fino in fondo; in questo caso viene
messa al valore vero e ciò termina il programma,
come ora vedremo.
5. La variabile booleana trovato dice se è stato
possibile trovare un valore da assegnare alla casella corrente; se assume il valore falso, si deve
procedere al backtracking. Inizialmente, alla variabile è assegnato il valore di trabocco, di modo
che se questo è vero il programma termina.
6. La variabile booleana zero dice se si è trovata
una casella attiva (cioè inizialmente 0).
7. La variabile globale W contiene il risultato finale
del gioco. Per evitare di dover eseguire all’indietro gli assegnamenti del risultato, questo viene messo una volta per tutte in questa variabile
globale.
8. La variabile globale booleana ass serve per evitare l’assegnamento multiplo del risultato a W;
viene quindi messa al valore vero quando si arriva
al risultato e controllata in fase di backtracking.
9. La variabile globale cont serve a contare il numero di assegnamenti alle caselle effettuati dal
programma. Indica pertanto la complessità della
procedura.
Il programma si divide in due parti: la prima determina il successivo elemento della matrice con valore
0, cioè trova la prima casella attiva dopo la casella
corrente. La seconda parte esegue un ciclo per assegnare alla casella attiva trovata tutti i valori “legali”,
cioè che non si trovano nella sua stessa riga, nella sua
stessa colonna o nella sua stessa casa. Se l’assegna1. MOSSA è definito come procedura con i tre para- mento è possibile, il programma va avanti chiamando
metri r (riga), c (colonna), M (matrice del gioco). ricorsivamente sé stesso con la posizione della casella
I primi due parametri individuano la posizione attiva; se l’assegnamento è impossibile, allora inizia
della matrice dalla quale partire per la ricerca il processo di backtracking. In dettaglio:
della successiva casella attiva (che cioè contenga
1. La variabile zero è messa inizialmente al valore
0).
falso; questo permette di eseguire correttamente
2. Le tre variabili locali i, j, N sono i corrisponil ciclo successivo;
denti di r, c, M. Nel linguaggio considerato
(Maple) non è consentito assegnare valori ai pa2. Viene incrementata la posizione corrente della
rametri formali perché il loro valore non è salvamatrice; si incrementa il numero della colonna
to nello stack. A ciò si rimedia definendo delle
e se questo supera 9 si passa alla riga successivariabili locali, alle quali vengono assegnati i vava. Se si esce dai limiti 9 × 9 si ha il trabocco
lori dei parametri. Cosı̀ è stato fatto nella prima
e quindi la terminazione del programma dopo
linea del programma.
l’assegnamento: trovato := trabocco.
28
CAPITOLO 3. LA RICORSIONE
MOSSA := proc(r, c, M) local h, i, j, N, trabocco, trovato, zero; global ass, cont, W;
N := M; i := r; j := c; zero := false;
while not zero do
j := j + 1;
if 9 < j then j := 1; i := i + 1 end if;
if 9 < i then trabocco := true; zero := true
else trabocco := false end if;
if not trabocco then zero := M[i, j] = 0 end if
end do {while};
trovato := trabocco;
if not trovato then
h := 1;
while h <= 9 do
if LEGALE(h, i, j, N) then
N[i, j] := h;
cont := cont + 1;
trovato := MOSSA(i, j, N)
end if;
if trovato then
if not ass then W := N; ass := true end if;
h := 10
else N[i, j] := 0; h := h + 1
end if
end do {while}
end if;
trovato
end proc;
Figura 3.1: Il programma esecutivo
3. La variabile zero è vera in corrispondenza di una
casella attiva e falsa in corrispondenza delle altre caselle; ciò permette di eseguire il ciclo per
trovare la successiva casella attiva.
4. Se non si è avuto il trabocco, si cerca il valore da
assegnare alla prossima casella attiva; ciò è fatto
con un ciclo che incrementa h fino a superare
il valore 9. A questo punto deve cominciare il
backtracking. Questo è determinato dal valore
di trovato.
5. Si osservi che la variabile h è messa artificialmente al valore 10 per poter uscire correttamente dal ciclo. La variabile ass è messa al valore
vero quando si scopre la soluzione e ciò permette
di evitare l’assegnamento a W in fase del ritorno
indietro conclusivo.
6. Ad ogni assegnamento di un valore a una casella,
il contatore è incrementato di 1.
3.3
Gli altri programmi del
Sudoku
Il più importante dei programmi di contorno è
LEGALE, che stabilisce se il valore di h è un valore
che può essere assegnato alla posizione considerata;
tale programma è mostrato nella Figura 3.2. Il programma è molto semplice: la posizione da controllare
è M[r,c], per cui vengono fatti tre cicli: il primo lungo la riga r, il secondo lungo la colonna c e il terzo
(un po’ più complesso) relativamente alla casa dell’elemento. Una semplice formula matematica determina le coordinate della posizione iniziale di una casa che naturalmente deve essere scandita usando due
variabili. Questi brevi commenti dovrebbero essere
sufficienti a comprendere la procedura.
Veniamo ora al programma principale, mostrato
nella Figura 3.3. Esso si compone delle assegnazioni
iniziali alle variabili globali e della chiamata a MOSSA
con gli opportuni parametri: si osservi che la posizione “precedente” la M[1,1] è proprio la M[0,9]. Il
programma stampa il numero di passi (assegnamenti) effettuati e la matrice risultante. Se il programma
ha fallito (non ci sono soluzioni) viene stampato il
29
3.3. GLI ALTRI PROGRAMMI DEL SUDOKU
LEGALE := proc(h, r, c, M) local cc, cr, i,
ris := true;
for j to 9 do
if M[r, j] = h then ris := false end
for j to 9 do
if M[j, c] = h then ris := false end
cr := 3*iquo(r - 1, 3) + 1;
cc := 3*iquo(c - 1, 3) + 1;
for i from cr to cr + 2 do
for j from cc to cc + 2 do
if M[i, j] = h then ris := false
end do end do;
ris
end proc;
j, ris;
if end do;
if end do;
end if
Figura 3.2: Il programma di controllo
sudoku := proc(M) local x; global ass, cont, W;
ass := false;
cont := 0;
x := MOSSA(0, 9, M);
printf(‘‘passi \%7d\n’’, cont);
if x then stampa(W)
else printf(‘‘NEY\n’’) end if
end proc;
Figura 3.3: Il programma principale
A2 := [[0, 0, 2, 0, 0, 9, 1,
messaggio convenzionale NEY.
[0, 3, 4, 0, 6, 0, 0,
Il programma di stampa, mostrato nella Figura
[0, 7, 0, 0, 5, 0, 0,
3.4, è alquanto noioso, ma permette di veder bene
[0, 0, 5, 0, 0, 0, 7,
la matrice del gioco. Unico commento è un esem[0, 0, 6, 0, 0, 0, 3,
pio di stampa, costituito dalla matrice che abbiamo
[2, 9, 8, 0, 0, 0, 6,
usato come esempio fin dall’inizio di questa nota; la
[0, 0, 0, 0, 4, 0, 0,
matrice, detta C3, produce:
[0, 0, 0, 0, 2, 0, 8,
[0, 0, 1, 8, 0, 0, 5,
> sudoku(C3);
passi
1 6 9
4 2 8
3 5 7
26430
3 5 4
9 7 6
2 1 8
8 2 7
3 5 1
6 4 9
5 3 1
9 8 4
2 7 6
6 2 9
5 3 7
8 4 1
7 8 4
2 1 6
5 9 3
7 9 3
8 4 5
6 1 2
4 8 2
1 6 3
7 9 5
1 6 5
9 7 2
4 3 8
>
0
0
0
stampa(A2);
0 2
0 0 9
3 4
0 6 0
7 0
0 5 0
0 0 5
0 0 6
2 9 8
1 0 0
0 0 0
0 0 0
0 0 0
0 0 0
0 0 0
7 4 9
3 0 0
6 0 0
0 0 0
0 4 0
0 0 0
0 2 0
0 0 1
8 0 0
Il numero di passi è molto elevato; un esempio > sudoku(A2);
più contenuto è dato dal seguente caso, del qua- passi
3851
le mostriamo anche la definizione della matrice di 6 8 2
4 7 9
partenza:
5 3 4
1 6 8
0 6 0
8 3 0
5 0 0
1 5 3
2 9 7
0,
0,
0,
4,
0,
0,
6,
3,
0,
0],
0],
0],
9],
0],
0],
0],
0],
0]]
30
CAPITOLO 3. LA RICORSIONE
stampa := proc(M) local i, j;
for i to 3 do
for j to 3 do printf(‘‘\%d ’’, M[i, j]) end do;
printf(‘‘ ’’);
for j from 4 to 6 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘ ’’);
for j from 7 to 9 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘\n’’)
end do;
printf(‘‘\n’’);
for i from 4 to 6 do
for j to 3 do printf(‘‘\%d ’’, M[i, j]) end do;
printf(‘‘ ’’);
for j from 4 to 6 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘ ’’);
for j from 7 to 9 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘\n’’)
end do;
printf(‘‘\n’’);
for i from 7 to 9 do
for j to 3 do printf(‘‘\%d ’’, M[i, j]) end do;
printf(‘‘ ’’);
for j from 4 to 6 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘ ’’);
for j from 7 to 9 do printf(‘‘\%d ’’, M[i, j]) end
printf(‘‘\n’’)
end do
end proc;
do;
do;
do;
do;
do;
do;
Figura 3.4: Uscita di una matrice
1 7 9
3 5 2
4 8 6
3 1 5
7 4 6
2 9 8
2 8 6
9 1 5
7 3 4
7 4 9
3 2 8
6 1 5
8 2 3
9 5 7
4 6 1
5 4 7
6 2 1
8 9 3
9 6 1
8 3 4
5 7 2
3.4
Osservazioni da sviluppare
• La complessità della procedura dipende molto
dalla posizione degli 0 iniziali nella matrice. Se
molti di questi sono in alto e a sinistra, le possibilità divengono molto elevate e quindi tanti sono
i tentativi che il programma è costretto a fare.
Se invece all’inizio vi sono molti valori diversi da
zero, le possibilità diminuiscono e i tentativi sono in numero minore. Infatti, le scelte successive
sono limitate dalle prime. Il caso peggiore sarebbe quello in cui la prima riga, la prima colonna
e la prima casa fossero tutte 0.
• Per provare una cosa del genere, basta prendere
una matrice ed eseguire il programma anche con
la trasposta o con una matrice ottenuta dall’originale per simmetria. Le simmetrie possibili sono: rotazione intorno alla diagonale secondaria;
rotazione intorno alla riga o alla colonna centrale; rotazione sul piano di 90◦ in senso orario o
antiorario. Tutte queste trasformazioni si realizzano con semplici programmi e mostrano la dipendenza della complessità dalla posizione delle
caselle attive.
• Il programma è molto lineare e perciò molto banale. Si possono facilmente pensare metodi più
complessi. Ad esempio, si possono stabilire a
priori, per ogni casella attiva, i possibili valori
iniziali. Si tratta poi di lavorare su questi invece
che su tutti e nove i possibili valori. Questo complica il ciclo centrale: while h ≤ 9, ma dovrebbe sostanzialmente ridurre il numero di chiamate
a LEGALE.
• Un altro accorgimento è quello di eliminare dalla riga, dalla colonna e dalla casa di un elemento
3.4. OSSERVAZIONI DA SVILUPPARE
il valore che ad esso viene assegnato. Tuttavia,
specie nella parte iniziale, non so se questo sia
molto utile, perché il ciclo da eseguire è più complesso di quello relativo ai singoli elementi, come
visto ai punti precedenti.
• Potrebbe invece essere utile ripetere più volte il
ciclo iniziale (v. punto 1) di eliminazione dei valori inutili o impossibili. Questo può far sı̀ che
certe posizioni possano solo assumere un valore
obbligato, e quindi divengano caselle non attive,
diminuendo di fatto le possibilità da indagare.
Questa era stato la mia impostazione iniziale.
31