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