UNIVERSITA' DEGLI STUDI DI ROMA “TOR VERGATA” Attività Formativa anno 2005/2006 Algoritmi genetici e giochi… PEG SOLITAIRE Docente: Roberto Tauraso Studenti: Luca Burini Enzo Ferrazzano Introduzione Con l’aumentare del sapere scientifico sono aumentate anche le dimensioni delle istanze da risolvere. Se è compito del matematico determinare l’eventuale esistenza di soluzioni per un dato problema, spetta al calcolatore (e a chi lo programma) trovarle! Esistono varie filosofie di approccio ai problemi numerici. E’ possibile ad esempio effettuare una ricerca esaustiva che esamini tutti i casi possibili, oppure seguire una strategia, dettata dalle particolarità del problema. Purtroppo, molti dei quesiti più significativi sono matematicamente intrattabili, cioè diventano computazionalmente ingestibili non appena la dimensione del problema diviene significativa. Il ricorso ad algoritmi di approssimazione con complessità polinomiale é di conseguenza indispensabile: è necessario sacrificare la precisione della soluzione in favore di una ragionevole velocità di calcolo. Tuttavia anche gli algoritmi euristici sono spesso caratterizzati da complessità computazionali onerose, tali da impedire il raggiungimento di “buone” soluzioni in tempi ragionevoli. Per ovviare a questa limitazione nascono gli algoritmi genetici (d’ora in poi GA) . I GA sono algoritmi nei quali la ricerca delle soluzioni è condotta imitando il comportamento degli organismi viventi con una riproduzione sessuata. Vengono generate delle popolazioni di soluzioni e si selezionano i migliori individui, analogamente a quanto avviene in natura secondo la teoria evolutiva di Darwin. Inoltre vengono sfruttati i concetti di ereditarietà e sopravvivenza dell'individuo più adatto (survival of the fittest) per la determinazione della soluzione. In natura, per la maggior parte degli organismi viventi, l'evoluzione avviene attraverso due processi fondamentali: la selezione naturale e la riproduzione sessuale. La prima determina quali elementi di una popolazione sopravvivono per riprodursi, la seconda garantisce la ricombinazione dei geni dei loro discendenti. Le soluzioni trattate da un algoritmo genetico vengono dunque combinate tra loro attraverso alcuni operatori genetici, che agiscono sulla popolazione di soluzioni generandone continuamente di nuove, in un ciclo simile a quello delle specie viventi sessuate. Le soluzioni che sembrano essere più adatte sopravvivono e si riproducono, mentre le soluzioni ritenute peggiori vengono scartate: non viene permesso alle caratteristiche peggiori di diffondersi nelle generazioni successive. In questo modo, statisticamente si migliorano le soluzioni. Il processo ha termine quando gli operatori genetici non riescono a migliorare ulteriormente le soluzioni trovate. Si può quindi procedere con un algoritmo esatto per la ricerca dell’ottimo (algoritmo genetico ibrido) , contando sul fatto che il processo genetico abbia ridotto anche di parecchi ordini di grandezza i calcoli necessari per la ricerca della soluzione. La peculiarità degli algoritmi genetici non risiede nel fatto che possano trovare soluzioni ottime globali, ma nella loro capacità di produrre molte soluzioni ammissibili. In questa attività formativa illustreremo il concetto di algoritmo genetico con le sue basi, un esempio ed infine un algoritmo genetico da noi ideato per la soluzione di un gioco, il peg solitarie. 1 Gli algoritmi genetici Un GA è una tecnica di ricerca e di ottimizzazione basata sui principi dell’evoluzione e della selezione naturale che simula l’evoluzione di una popolazione caratterizzata dalla riproduzione sessuata. Dallo spazio di ricerca (l’insieme di tutte le soluzioni possibili al problema, anche se non ottime ) si estrae un sottoinsieme di soluzioni che forma la popolazione iniziale all’istante 0. Ogni individuo di questa popolazione possiede vettore di parametri (cromosomi) che contiene le informazioni relative alla soluzione in questione, il suo Codice Genetico (o DNA). I cromosomi sono codificati dall’elaboratore sotto forma di stringhe di bit, che sono i geni del cromosoma. Si inizia decidendo una funzione costo (fitness) che, dato un individuo, fornisca un indice della qualità delle informazioni contenute nel suo codice genetico; le soluzioni vengono quindi ordinate in base al fitness. Secondo un opportuno criterio, le soluzioni migliori vengono prese ed accoppiate. In pratica, si genera un figlio il cui DNA è composto dall’unione di cromosomi scelti a caso appartenenti a tutti e due i genitori. (riproduzione sessuata o Crossover) Questo figlio non va a sostituire i genitori (che sono tra le migliori soluzioni ) ma prende il posto dell’ultimo in classifica, il meno adatto. (selezione naturale) Prenderemo in considerazione il caso di popolazioni discrete, nel caso in cui una popolazione differisca dalla precedente per un numero finito di elementi, cioè i figli che l’algoritmo ha generato per sostituire gli elementi meno adatti della sua popolazione. La scelta di quanti figli generare ad ogni iterazione del processo ( e di conseguenza la memoria che ha l’algoritmo della generazione precedente ) è una variabile decisionale scelta in base a considerazioni empiriche sulle prestazioni dell’algoritmo. Nel caso in cui il livello di generazione di nuove soluzione dell’algoritmo si rivelasse insoddisfacente, si possono aggiungere altri due operatori genetici, l’Inversione e la Mutazione. Nella mutazione i geni di ogni individuo generato hanno un probabilità pm (ragionevolmente con pm 1 ) di mutare, cioè che il valore di un gene venga negato ( 0 → 1,1 → 0 ) . Nell’inversione ogni individuo generato ha un probabilità pi che vengano scelti a caso due geni nel suo Dna, e che i geni compresi vengano scambiati di posto tramite una simmetria centrale. Per esempio: (1,1, 0,|1, 0,1,1, 0,| 0,1, 0 ) → (1,1, 0,| 0,1,1, 0,1,| 0,1, 0 ) in questo modo si diminuisce il livello di determinismo all’interno dell’algoritmo, il che porta potenzialmente ad esplorare una porzione maggiore dello spazio di ricerca. Solitamente pm e pi vengono scelti in maniera empirica in base alle prestazioni dell’algoritmo. 2 I GA e il Prisoner’s Dilemma Il dilemma del prigioniero è un gioco per due persone che è stato molto studiato in matematica, economia e politica poiché è considerato un modello per molti fenomeni di tipo umano, come ad esempio la corsa agli armamenti nucleari. Il gioco, ideato da M.Flood e M.Dresher negli anni 50, può essere così descritto: due ladri complici vengono arrestati, ma non colti sul fatto. Vengono portati in celle separati ed ad ognuno viene proposto un accordo. Se il ladro A testimonia contro il ladro B , A guadagna la libertà e B viene punito con 5 anni di reclusione. Viene fatta la stessa proposta a B, e tutti e due sanno della proposta che viene fatta al complice; inoltre il ladri sanno che se testimoniano tutti e due, si prendono 4 anni di carcere, se invece non testimoniano nessuno dei due, si prendono solo 2 anni di carcere a testa per reati minori (poiché solo la testimonianza può farli condannare per il misfatto). In maniera più astratta può essere così formulato: ogni giocatore può decidere di fare due mosse: “cooperare” col complice (cioè non testimoniare) o tradire (testimoniare). A seconda della scelta, i due giocatori guadagnano fino a 5 punti ( un punto per ogni anno di riduzione della pena), quindi le possibili mosse possono essere così riassunte: Giocatore B Giocatore A Cooperare Tradire Cooperare 3,3 0,5 Tradire 5,0 1,1 Quando ogni giocatore ha effettuato la sua mossa, si determinano i punti e dopo un certo numero di partite, chi fa più punti vince il match. Ovviamente il dilemma è costituito dalla strategia da adottare in questo tipo di problema, visto che confessare sempre non è detto che sia vantaggioso, così come non lo è collaborare.. Robert Axelrod dell’università del Michigan ha studiato il problema della ricerca di una strategia vincente. Per fare questo organizzò un torneo di Prisoner’s Dilemma, cioè una gara prima con 14 e poi con 63 partecipanti, cioè dei programmi che decidevano le proprie mosse in base ad una strategia scelta a priori , con la possibilità di tenere in memoria le decisioni prese nelle 3 partite precedenti per ogni giocatore. Il partecipanti sono stati programmati da vari ricercatori americani, alcuni utilizzando strategie elaborate basate sull’inferenza statistica, altri più semplici come una scelta random delle mosse. Ma a risultare vincente fu la strategia più semplice:il TIT FOR TAT, proposto da Anatol Rapoport. Il programma propone all’inizio di collaborare, e offre collaborazione finché l’altro programma collabora. Ma appena viene tradito, il TIT FOR TAT “punisce” l’altro giocatore tradendolo finche non riprende a collaborare. Da questa esperienza Axelrod ha ideato un GA in grado di generare strategie migliori a partire da un numero finito di strategie. Prendiamo ad esempio il TIT FOR TAT, che memorizza la partita precedente. Schematizzando, in una partita si possono avere 4 risultati possibili: 3 Collaborare,Collaborare (o CC); Tradire,Tradire (o TT) ; CT; TC. Allora, considerando che con la notazione XY → Z indichiamo che TIT FOR TAT reagirà alla precedente partita XY con la mossa Z , le possibili decisioni della strategia sono: 1) CC → C 2) CT → T 3) TC → C 4) TT → T Quindi, associando un numero ad ognuno dei casi possibili, possiamo associare alla strategia una stringa contenente alla posizione i la reazione della strategia al caso i; Quindi per il TIT FOR TAT la stringa associata è CTCT. Visto che per le regole del torneo si possono memorizzare le tre partite precedenti, sono possibili 64 terne di partite precedenti (potendo un gene assumere 2 diversi valori, C e T, e essendo 3 le partite, quindi 6 lettere , tutte le combinazioni sono 26 ). Essendo 64 i casi possibili, ogni strategia che tiene conto delle 3 partite precedenti può essere descritta da una stringa di 64 caratteri, che costituisce il Dna di questo individuo. A questi 64 cromosomi se ne aggiungono altri 6 che servono per immagazzinare le 3 ipotetiche partite precedenti per poter svolgere la prima mossa , per un totale di 70 cromosomi. Quindi per questo tipo di gioco sono possibili 270 ≅ 1.18 ⋅1021 strategie diverse, effettivamente troppe per essere provate tutte. Axelrod verificò che facendo giocare una strategia contro 8 delle 63 strategie originali, si ottenevano in media gli stessi risultati che facendole giocare contro tutte e 63. Tra queste 8 non figura il TIT FOR TAT. Il punteggio medio ottenuto facendo giocare una popolazione contro queste 8 strategie di riferimento viene preso a valore di fitness della strategia in esame il punteggio medio ottenuto contro tutte queste popolazioni. Operando geneticamente su di una popolazione iniziale di 20 strategie per 50 generazioni (cioè esplorando al massimo 20 ⋅ 50 = 1000 strategie diverse, i risultati sono decisamente interessanti. La maggior parte delle strategie trovate sono molto simili al TIT FOR TAT (cioè puniscono un tradimento con un tradimento e ricompensano una collaborazione con una collaborazione) ed addirittura alcune strategie finali risultarono migliori del TIT FOR TAT, che era la migliore strategia ideata dall’uomo. Questi risultati vanno interpretati alla luce del fatto che l’evoluzione dell’algoritmo ( così come quella naturale) è molto dipendente dalle condizioni ambientali. Infatti prerogativa dell’evoluzione è quella di generare individui molto specializzati con elevate prestazioni solo nel loro proprio ambiente naturale ( cioè in condizioni simili al quelle dell’ambiente che li ha generati). Axelrod confermò questo facendo vari esperimenti, cambiando le regole del gioco. Per esempio inserì una funzione di fitness dinamica, cioè facendo giocare tra di loro le strategie ad ogni generazione. Sotto queste ipotesi le strategie con tendenze cooperative soccombono all’inizio sotto la spinta di strategie più aggressive, ma a lungo andare le strategie tipo TIT FOR TAT prendono il sopravvento. 4 Un po’ di storia e di “teoria”: il Peg solitarie Il peg solitarie è una tipologia di gioco per un solo giocatore (solitario, appunto) di origine europea. L’origine del puzzle si fa risalire ad un nobile francese prigioniero nella Bastiglia, che lo ideò come passatempo; di certo era ben noto in Francia già nel XVII secolo, poiché risale al 1697 l’incisione della principessa de Soubise, ritratta mentre è intenta a risolvere il rompicapo. Figura 1 Vennero create varie tipologie di questo gioco, diverse per la geometria della scacchiera ma tutte con la regola principale identica: cioè una pedina deve scavalcarne un'altra lungo le 4 direzioni principali per posizionarsi in un uno spazio vuoto, eliminando la pedina scavalcata finché non ne rimane una sola. Addirittura Leibnitz nel 1710 propone una variante di questo gioco, cioè di partire da una sola pedina e cercare di riempire la plancia. La variante più conosciuta è quella inglese (o Hi-Q, nome con il quale venne commercializzato negli anni ‘80), con la plancia a forma di croce e 33 posizioni tutte occupate da pedine , ad esclusione di quella centrale. Chiameremo d’ora in poi soluzioni del peg solitarie una serie di mosse che ci consentono di non poter più muovere nessuna pedina, e saranno ottime quelle soluzione che lasceranno una sola pedina in posizione centrale. Figura 2 Il gioco, in apparenza semplice, contempla 577 116 156 815 309 849 672 diverse soluzioni ( circa 5.77 ⋅ 1020 , in una comoda notazione esponenziale) e fino a 40 861 647 040 079 968 ( ∼ 40 ⋅ 1015 ) possibili soluzioni ottime, molte delle quali sono riflessioni e rotazioni della stessa. Viste le dimensioni dello spazio della ricerca, è evidente il vantaggio di ricorrere ad un algoritmo approssimato per la ricerca di una soluzione, che fornisca una buona soluzione in un tempo accettabile dal punto di vista computazionale. Non è scontato trovare una soluzione ottima di questo gioco, visto che una partita può concludersi con più di una pedina rimanente sulla scacchiera. In questa attività formativa abbiamo implementato un algoritmo genetico che ci ha consentito di risolvere in breve tempo questo rompicapo. 5 Ma esistono delle soluzioni ottime per questo gioco? Supponiamo di colorare la scacchiera come descritto nella figura 3. si nota che qualsiasi mossa vogliamo compiere, essa non fa altro eliminare due pedine posizionate su due colori, e posizionarne una nel terzo colore. Chiamiamo Vt ( C ) il numero di pedine alla mossa t sul colore C. Allora nella configurazione iniziale risulta V0 ( Rosso ) = 11 V0 (Verde ) = 10 V0 ( Blu ) = 11 Figura 3 Chiamiamo mossa di tipo r la mossa che aumenta di 1 il numero di pedine sul rosso, e diminuisce di 1 il numero di pedine sugli altri colori. Similmente definiamo v e b. Quindi: V31 ( Rosso ) = 11 + R − V − B V31 (Verde ) = 10 − R + V − B V31 ( Blu ) = 11 − R − V + B Dove R, V, B sono il numero (intero non negativo) di mosse r, v, b compiute. Le possibili configurazioni finali ⎡⎣V31 ( Rosso ) , V31 (Verde ) , V31 ( Blu ) ⎤⎦ sono [1, 0, 0] , [ 0,1, 0] , [ 0, 0,1] . In generale la relazione tra la situazione iniziale e una situazione al tempo generico t può essere espresso con un sistema lineare del tipo: ⎛ 1 −1 −1⎞⎛ R ⎞ ⎛ −11 ⎞ ⎛ Vt ( Rosso ) ⎞ ⎟ ⎜ ⎟⎜ ⎟ ⎜ ⎟ ⎜ ⎜ −1 1 −1⎟⎜ V ⎟ = ⎜ −10 ⎟ + ⎜ Vt (Verde ) ⎟ ⎜ −1 −1 1 ⎟⎜ B ⎟ ⎜ −11 ⎟ ⎜ ⎟ ⎝ ⎠⎝ ⎠ ⎝ ⎠ ⎝ Vt ( Blu ) ⎠ La cui matrice associata è: ⎛ 1 −1 − 1 ⎞ ⎜ ⎟ ⎜ −1 1 −1⎟ = H ⎜ −1 −1 1 ⎟ ⎝ ⎠ Che di rango massimo, quindi la soluzione se esiste è unica. Per il tipo di problema che abbiamo i valori di R, V, B devono essere interi. 6 Troviamo l’inversa di H: ⎛ ⎜ 0 ⎜ 1 −1 H = ⎜− ⎜ 2 ⎜ ⎜⎜ − 1 ⎝ 2 − 1 2 0 − 1 2 1⎞ − ⎟ 2 ⎟ 1⎟ − ; 2⎟ ⎟ 0 ⎟⎟ ⎠ Risolviamo il sistema: ⎛ ⎜ 0 ⎛ R⎞ ⎜ ⎜ ⎟ ⎜ 1 ⎜V ⎟ = ⎜ − 2 ⎜ B⎟ ⎜ ⎝ ⎠ ⎜⎜ − 1 ⎝ 2 − 1 2 0 − 1 2 1⎞ ⎛ 0 − ⎟ 2 ⎛ −11 ⎞ ⎜ ⎟ ⎜ 1 ⎟⎜ ⎟ ⎜ 1 − ⎜ −10 ⎟ + − ⎜ 2 2 ⎟⎜ ⎟ ⎝ −11 ⎟⎠ ⎜ ⎜⎜ − 1 0 ⎟⎟ ⎠ ⎝ 2 − 1 2 0 − 1 2 1⎞ − ⎟ 2 ⎛ Vt ( Rosso ) ⎞ ⎟ ⎟ 1 ⎟⎜ − ⎜ Vt (Verde ) ⎟ 2 ⎟⎜ ⎟ Vt ( Blu ) ⎟ ⎝ ⎠ 0 ⎟⎟ ⎠ ⎛ Vt (Verde ) + Vt ( Blu ) ⎞ ⎟ ⎛ 21 ⎞ ⎜ 2 ⎟ ⎛ R⎞ ⎜ 2 ⎟ ⎜ ⎜ ⎟ ⎜ ⎟ ⎜ Vt ( Rosso ) + Vt ( Blu ) ⎟ ⎟ ⎜ V ⎟ = ⎜11 ⎟ − ⎜ 2 ⎜ B ⎟ ⎜ 21 ⎟ ⎜ ⎟ ⎝ ⎠ ⎜ ⎟ ⎜ V ( Rosso ) + V (Verde ) ⎟ t t ⎝ 2⎠ ⎜ ⎟ 2 ⎝ ⎠ Affinché R, V, B siano interi (e la soluzione ammissibile) a) Vt (Verde ) + Vt ( Blu ) e Vt ( Rosso ) + Vt (Verde ) deve essere dispari, quindi se un addendo è pari , l’altro è dispari e viceversa. b) Vt ( Rosso ) + Vt ( Blu ) deve essere pari, quindi entrambi gli addendi devono essere contemporaneamente pari o dispari. Fissando uno dei valori sono fissati immediatamente gli altri. Quindi si ottengono due condizioni sulle posizioni di arrivo: ⎛ Vt ( Rosso ) ⎞ ⎛ Pari ⎞ ⎛ Vt ( Rosso ) ⎞ ⎛ Dispari ⎞ ⎜ ⎟ ⎜ ⎟ ⎜ ⎟ oppure ⎜ ⎟ ⎜ Vt (Verde ) ⎟ = ⎜ Dispari ⎟ ⎜ Vt (Verde ) ⎟ = ⎜ Pari ⎟ ⎜ ⎟ ⎜ Pari ⎟ ⎜ ⎟ ⎜ Dispari ⎟ ⎠ ⎠ ⎝ Vt ( Blu ) ⎠ ⎝ ⎝ Vt ( Blu ) ⎠ ⎝ Per quanto riguarda la situazione finale, cioè con una sola pedina, [ 0,1, 0] è l’unica configurazione che soddisfa una delle condizioni di cui sopra. Quindi la nostra pedina finale, ammettendo che esista una combinazione di mosse per ottenerla, deve stare necessariamente su di una casella verde. 7 Ma non tutte le caselle verdi possono ospitare l’ultima pedina! Per ragioni di simmetria della configurazione iniziale e della scacchiera qualsiasi strategia (che porti ad una soluzione ottima o meno) può essere svolta a partire da qualsiasi lato della scacchiera. Ma abbiamo dimostrato che una qualsiasi soluzione deve portare l’ultima pedina in una casella verde, e il nostro ragionamento ci dice che una strategia ottima deve essere invariante rispetto ad una rotazione di 90° gradi della scacchiera. Perciò delle 11 caselle verdi disponibili, 6 non sono raggiungibili perché una rotazione di 90° o una simmetria rispetto agli assi principali, come si vede in figura 4, manda le pedine in una posizione rossa o blu, e quindi non accettabile. Risulta che allora solo le 5 posizioni contrassegnate dalle pedine di colore arancione possono ospitare l’ultima pedina. Figura 4 Una ulteriore curiosità è che le possibili soluzioni ottime possono essere raggiunte tutte da posizioni simili (pedine in verde), quindi la reale differenza tra una soluzione ottima ed una soluzione qualunque con una sola pedina è semplicemente una scelta arbitraria dell’ultima mossa. Figura 5 8 Il nostro algoritmo Per poter risolvere il gioco con un GA si deve per prima cosa cercare di ricondurre il problema in una forma da esso trattabile , cioè tradurre il problema nella struttura principale tipica dell’algoritmo genetico: il DNA. Una qualsiasi strategia in una partita del peg solitarie può essere schematizzata con una scacchiera di pesi. Quindi prendiamo come DNA della soluzione (che sarà il nostro individuo) la scacchiera dei pesi che descrive questa soluzione. Per generare ogni individuo della popolazione iniziale abbiamo riempito il suo codice genetico con dei numeri uniformemente distribuiti nell’intervallo 0-99, organizzati in maniera tale da avere il genoma simmetrico, per questioni di simmetria 1 2 1 intrinseca del gioco. La mossa più promettente viene scelta in base al valore 3 4 3 dell’espressione A+B-C, dove A è il peso della casella d’arrivo, B è il peso della casella della pedina che 1 3 5 6 5 3 1 verrebbe “mangiata” e C è il peso della casella dov’è il 2 4 6 7 6 4 2 pezzo da muovere. Abbiamo preso questo criterio di scelta poiché è quello 1 3 5 6 5 3 1 che ha fornito le prestazioni migliori tra tutti i criteri da 3 4 3 noi valutati. La scelta di un DNA bidimensionale a valori discreti interi 1 2 1 ci porta ad una maggiore facilità di implementazione, a discapito della flessibilità della parte genetica (non ci Figura 6 :Disposizione delle classi di possiamo avvalere degli operatori di mutazione e di equivalenza dei pesi. inversione così come li abbiamo definiti prima). Per valutare il fitness di ogni singolo individuo si svolge la strategia che esso descrive e si memorizza il numero di pedine rimaste sulla scacchiera. Ovviamente un minor numero di pedine sulla scacchiera corrisponde ad un fitness più elevato. La popolazione iniziale è costituita da 15 individui dotati di un genoma casuale. Viene calcolato il fitness della popolazione iniziale e le soluzioni ordinate per fitness crescente. I due candidati migliori sono accoppiati scambiando con probabilità 1 2 i pesi e il loro figlio va a sostituire la soluzione peggiore. Questo procedimento viene ripetuto 30 volte, in maniera tale che avvenga un consistente rimescolamento dei geni della popolazione, rimuovendo le soluzioni caratterizzate da fitness basso. Quindi il procedimento genetico viene ripetuto per altre 30 volte. In questa seconda fase ogni individuo viene “giocato” secondo la strategia descritta nel suo DNA fino a che non rimangono 12 pedine sulla scacchiera. Poi la partita viene continuata utilizzando un algoritmo esaustivo, sino al ritrovamento dell’ottimo. Se 30 iterazioni della seconda fase non portano all’ottimo, viene generata una nuova popolazione iniziale, facendo ripartire l’algoritmo. 9 La scelta di un algoritmo genetico misto ( cioè con un prima fase euristica di tipo genetico e una seconda fase esaustiva) è dovuta al fatto che il processo genetico contribuisce al miglioramento delle soluzione (il fitness medio aumenta) ma risulta incapace di fornire una soluzione ottima in tempi ragionevoli. Quindi la prima fase puramente genetica consente di sfoltire lo spazio di ricerca fino ad una dimensione trattabile da una ricerca esaustiva dell’ottimo. Una possibile strada per rendere più efficace l’algoritmo è l’implementazione di un DNA binario unidimensionale, che consentirebbe l’utilizzo degli operatori genetici di mutazione e inversione. Questo, come documentato dai risultati teorici, consentirebbe di spaziare maggiormente all’interno dello spazio di ricerca. Prestazioni Abbiamo voluto valutare la scalabilità dell’algoritmo, cioè la variazione delle prestazioni al variare dei parametri di esecuzione dell’algoritmo. Il parametro che abbiamo deciso di variare (poiché genera una variazione sensibile dei risultati) è il numero massimo di volte che viene ripetuto l’algoritmo su di una determinata istanza. Ricordiamo che il nostro algoritmo è composto di due fasi: una prima fase puramente genetica, composta da un numero fisso di 30 iterazioni, e una seconda fase genetico / esaustiva. Variando il numero massimo di iterazioni, tenendo fisso il numero fisso di iterazioni puramente genetiche, si opera di fatto sul numero di iterazioni genetico / esaustive. Indichiamo con l’apice ( k ) il fatto che il valore indicato si riferisce all’algoritmo con un numero di iterazioni pari a k . I parametri di valutazione che abbiamo scelto sono stati: Probabilità di successo: ottenuta con il rapporto p ( k ) = Tempo medio per trovare una soluzione. t (k ) 1 = (k ) n k n( ) Soluzioni trovate . Partite giocate ∑ t ( ) , dove t ( ) i =1 k k i i è il tempo impiegato per trovare la soluzione i-esima. (k ) Deviazione standard sul tempo. σ t 1 = n( k ) k n( ) ∑ (t ( ) − t ( ) ) k i i =1 k 2 . Sono state compiute un numero elevato di partite , tra le 6000 e le 10000 per ogni valore del parametro. k p( k ) t (k ) σ t( k ) 40 45 50 60 65 70 0,445722 0,462339 0,474158 0,498598 0,502652 0,505976 0,656537 s 0,719447 s 0,852206 s 1,01548 s 1,06209 s 1,13447 s 0,718544 s 0,886434 s 1,16918 s 1,60329 s 1,7593 s 2,0148 s 10 Probabilità Tempo medio Prestazioni 1,2 1,1 1 0,9 0,8 0,7 0,6 0,5 0,4 0,3 0,2 0,1 0 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 k Tempo medio Tempo medio 3,5 3 2,5 2 1,5 1 0,5 0 -0,5 -1 -1,5 0 5 10 15 20 25 30 35 40 45 50 55 60 65 70 75 k Come si può notare dai risultati sperimentali, l’aumentare delle iterazioni genetico / esaustive porta ad un incremento di tutte le caratteristiche prese in considerazione. Il tempo aumenta poiché aumentato le iterazioni genetico / esaustive aumentiamo di molto l’onere computazionale dell’algoritmo. 11 La probabilità aumenta poiché un maggior numero di iterazioni comporta un maggiore esplorazione dello spazio di ricerca, anche se sembra saturarsi intorno al valore di 50% L’incremento più cospicuo si ottiene sulla deviazione dei tempi di esecuzione, soprattutto per i numeri più elevati del valore k . Questa cosa può essere attribuita al fatto che, aumentando l’esplorazione dello spazio di ricerca, si riescono a trovare delle soluzioni caratterizzate da un tempo molto maggiore rispetto al tempo medio, dato che la ricerca esaustiva è esponenziale rispetto al numero di iterazioni. Conclusioni e ulteriori sviluppi Questa attività formativa ci ha consentito di conoscere e approfondire una tecnica originale e innovativa per risolvere dei problemi con il calcolatore: gli algoritmi genetici. Abbiamo deciso di applicare queste nozioni acquisite ad un gioco, il peg solitarie, che nonostante il suo aspetto poco “serio” , rappresenta un buon banco di prova per un algoritmo , visto il numero estremamente elevato di mosse possibili e di diverse soluzioni. L’adozione di questo algoritmo ha portato a degli ottimi risultati, cioè il ritrovamento di una soluzione ottima in un tempo di esecuzione nell’ordine dei secondi! Per confronto, ammettendo di possedere un calcolatore che può effettuare un miliardo di partite al secondo, per poter effettuare tutte le partite possibili sarebbe necessario un tempo pari a circa 18 297 anni! Questa esperienza si potrebbe espandere nei seguenti modi: • Convertire il DNA da una struttura bidimensionale a valori interi, ad una stringa binaria. Ciò consentirebbe di implementare gli operatori di inversione e di mutazione così come li abbiamo definiti, aumentando lo spazio di ricerca e di avvalersi di molti risultati teorici, poiché l’algoritmo genetico a DNA binario unidimensionale è stato il primo ad essere ideato e studiato. • Elaborare delle strategie di risoluzione ben progettate, e poi effettuare l’algoritmo genetico a partire da questa popolazione, per poter osservare a cosa porta l’evoluzione, sulla falsariga dell’esperienza di Axelrod. • Variare altri parametri, come il valore da assegnare ad ogni mossa oppure la simmetria della scacchiera, per verificare quanto le soluzioni siano sensibili alla variazione di questi parametri. 12 Codice C++ #include <time.h> #include "randomc.h" #include "userintf.cpp" #include "ranrotw.cpp" /* DEFINIZIONE DELLA SCACCHIERA ----------------------------------------------------------------------------------------------------------------------------------------------------*/ //scacchiera quadrata 7 x 7 class peg { public: int peso; // classe di equivalenza della pedina bool is_pedina; //controlla le caselle occupate dalle pedine }scacchiera [7][7]; //sacchiera quadrata, coordinate (x,y) della pedina #include <iostream> #include <stdlib.h> #include <cstdlib> #include <string.h> #include <stdio.h> #include <fstream> using namespace std; int peg_finali=12; /* CAMPO DI GIOCO ------------------------------------------------------------------------posizioni (2,6) (3,6) (4,6) (2,5) (3,5) (4,5) (0,4)(1,4) (2,4) (3,4) (4,4) (5,4)(6,4) (0,3)(1,3) (2,3) (3,3) (4,3) (5,3)(6,3) (0,2)(1,2) (2,2) (3,2) (4,2) (5,2)(6,2) (2,1) (3,1) (4,1) (2,0) (3,0) (4,0) /* /* INIZIALIZZAZIONE DELLA SCACCHIERA --------------------------------------------------------------------------------------------------------------------------------------------------*/ void inizia_peg (peg scacchiera[][7]){ for (int i=0; i<=6; i++) { for (int j=0; j<=6; j++) { //inizializzazione pedine if (((i==0)&&(j==0))||((i==1)&&(j==0))||((i==0)&&(j==1))||((i==1)&& (j==1))|| //quadrato in basso a sx classi di equivalenza 1 2 1 3 4 3 1 3 5 6 5 3 1 2 4 6 7 6 4 2 1 3 5 6 5 3 1 3 4 3 1 2 1 ((i==5)&&(j==0))||((i==6)&&(j==0))||((i==5)&&(j==1))||((i==6)&&( j==1))|| //quadrato in basso a dx ((i==0)&&(j==5))||((i==0)&&(j==6))||((i==1)&&(j==5))||((i==1)&&( j==6))|| //quadrato in alto a sx */ CODICE GENETICO ---------------------------------------------------------------------------------------------------------------------------------------------------*/ ((i==5)&&(j==5))||((i==6)&&(j==5))||((i==5)&&(j==6))||((i==6)&&( j==6))) //quadrato in alto a dx scacchiera[i][j].is_pedina = false; else scacchiera[i][j].is_pedina = true; //Matrice di pesi random //inizializzazione classi di equivalenza class GENETICset { public: int32 Rmatrix[15][7];//matrice random /*1*/if (((i==2)&&(j==6))||((i==4)&&(j==6))||((i==0)&&(j==4))||((i==6)&& (j==4))|| /* GENETICset(){//inizializzazione random dei pesi int32 seed = time(0); TRanrotWGenerator rg(seed); int i; int j; int32 ir; for(i=0; i<15; i++){ for(j=0; j<7; j++){ ir = rg.IRandom(0,99); Rmatrix[i][j]=ir; ((i==0)&&(j==2))||((i==6)&&(j==2))||((i==2)&&(j==0))||((i==4)&&( j==0))) {scacchiera[i][j].peso=0;} /*2*/if (((i==3)&&(j==6))||((i==0)&&(j==3))||((i==6)&&(j==3))||((i==3)&& (j==0))) {scacchiera[i][j].peso=1;} /*3*/if (((i==2)&&(j==5))||((i==4)&&(j==5))||((i==1)&&(j==4))||((i==5)&& (j==4))|| } } } }Genoma; ((i==1)&&(j==2))||((i==5)&&(j==2))||((i==2)&&(j==1))||((i==4)&&( j==1))) {scacchiera[i][j].peso=2;} 13 int pedine_rimaste=0; int i; int j; for (i=0; i<=6; i++) for ( j=0; j<=6; j++) if ((rispetta_condizioni(i,j))&&(scacchiera[i][j].is_pedina))pedine_rim aste++; /*4*/if (((i==3)&&(j==5))||((i==1)&&(j==3))||((i==5)&&(j==3))||((i==3)&& (j==1))) {scacchiera[i][j].peso=3;} /*5*/if (((i==2)&&(j==4))||((i==4)&&(j==4))||((i==2)&&(j==2))||((i==4)&& (j==2))) {scacchiera[i][j].peso=4;} /*6*/if (((i==3)&&(j==4))||((i==2)&&(j==3))||((i==3)&&(j==2))||((i==4)&& (j==3))) {scacchiera[i][j].peso=5;} /*7*/if ((i==3)&&(j==3)){scacchiera[i][j].peso=6;} return pedine_rimaste;} //.............................................................................. //.............................................................................. /* MEMORIZZAZIONE DELLE MOSSE ---------------------------------------------------------------------------------*/ //array genetico int start_i[31]; int start_j[31]; int end_i[31]; int end_j[31]; int l; } } //inizializza condizioni del gioco scacchiera[3][3].is_pedina=false; } //.............................................................................. //puntatore alla matrice random int32 *p[15][7]; int countPOPOLAZIONE=0;//contatore per la popolazione corrente //.............................................................................. //array di buffer int Bstart_i[31]; int Bstart_j[31]; int Bend_i[31]; int Bend_j[31]; //funzione che inizializza e aggiorna il puntare alla matrice random void SETpunt(int countP){ int j; for(j=0; j<7; j++){ p[countP][j]=&(Genoma.Rmatrix[countP][j]); } } //.............................................................................. //.............................................................................. //azzera la lista void azzera_mosse(int start_i[],int start_j[],int end_i[],int end_j[]){ for(int k=0;k<=30;k++){ start_i[k]=0; start_j[k]=0; end_i[k]=0; end_j[k]=0; } } //copia il vettore b in a e il vettore d in c void copia(int a[],int b[], int c[], int d[]){ for(int k=0;k<=30;k++){ a[k]=b[k]; c[k]=d[k]; } } /* FUNZIONI VISUALIZZATRICI ----------------------------------------------------------------------------------*/ /* FUNZIONI DI CONTROLLO APPARTENENZA ALLA SCACCHIERA ---------------------------------------------------------------------------------------------------------------------------------------------------------*/ //funzione che controlla che una pedina sia nella scacchiera bool rispetta_condizioni(int i,int j){ if (((((i==0)&&(j==0))||((i==1)&&(j==0))||((i==0)&&(j==1))||((i==1)& &(j==1))|| //quadrato in basso a sx //visualizza le pedine sulla scacchiera void visualizza_situazione(peg scacchiera[][7]){ for (int j=6; j>=0; j--) {for (int i=0; i<=6; i++) { if (i==0)cout<<"\n"; if ((rispetta_condizioni(i,j))&&(scacchiera[i][j].is_pedina))cout<<"|+| "; if ((rispetta_condizioni(i,j))&&!(scacchiera[i][j].is_pedina))cout<<"| |"; if (!(rispetta_condizioni(i,j)))cout<<" "; } } cout<<"\n\n";} ((i==5)&&(j==0))||((i==6)&&(j==0))||((i==5)&&(j==1))||((i==6)&&( j==1))|| //quadrato in basso a dx ((i==0)&&(j==5))||((i==0)&&(j==6))||((i==1)&&(j==5))||((i==1)&&( j==6))|| //quadrato in alto a sx ((i==5)&&(j==5))||((i==6)&&(j==5))||((i==5)&&(j==6))||((i==6)&&( j==6))))|| //quadrato in alto a dx ((i<0)||(j<0)||(i>6)||(j>6))) //spazio non consentito return false; else return true;} //scrive la lista delle mosse effettuate su un file //uniformato in notazione per il programmino di tauraso void scrivi_lista_mosse(int start_i[],int start_j[],int end_i[], int end_j[]){ //............................................................................... //conta le pedine rimaste int peg_rimasti(peg scacchiera[][7]){ 14 cout<<"\n"; if(!(k==31)){cout<<"\n"<<k<<". ("<<start_i[k]<<","<<start_j[k]<<") ---> ("<<end_i[k]<<","<<end_j[k]<<")\n";} ofstream lista_mosse ("lista_mosse.txt",ios::out|ios::trunc); if(!lista_mosse){ cout<<"\n Impossibile aprire il file. \n"; } B[(start_i[k])][(start_j[k])].is_pedina=false; B[(end_i[k])][(end_j[k])].is_pedina=true; lista_mosse<<"\nLista delle mosse\n"; lista_mosse<<"\nYour moves:"; for (int k=0; k<=8;k++){ if((start_i[k])==(end_i[k])){ if ((start_j[k])>(end_j[k])){ B[(start_i[k])][(start_j[k]-1)].is_pedina=false; } else{ B[(start_i[k])][(start_j[k]+1)].is_pedina=false; } } if((start_j[k])==(end_j[k])){ if((start_i[k])>(end_i[k])){ B[(start_i[k]-1)][(start_j[k])].is_pedina=false; } else{ B[(start_i[k]+1)][(start_j[k])].is_pedina=false; } } system("PAUSE"); lista_mosse<<"\n"<<"0"<<k+1<<". ("<<start_i[k]+1<<","<<start_j[k]+1<<")>("<<end_i[k]+1<<","<<end_j[k]+1<<")"; } for (int k=9; k<=30;k++){ lista_mosse<<"\n"<<k+1<<". ("<<start_i[k]+1<<","<<start_j[k]+1<<")>("<<end_i[k]+1<<","<<end_j[k]+1<<")"; } } //visualizza la lista delle mosse effettuate void visualizza_mosse(int start_i[],int start_j[],int end_i[], int end_j[]){ cout<<"\nLista delle mosse\n"; for (int k=0; k<=31;k++){ cout<<"\n"<<k<<". ("<<start_i[k]<<","<<start_j[k]<<") ---> ("<<end_i[k]<<","<<end_j[k]<<")\n"; } } //visualizza percorso void visualizza_percorso(int start_i[],int start_j[],int end_i[], int end_j[]){ peg B[7][7]; inizia_peg(B); for(int k=0; k<=30;k++){ visualizza_situazione(B); B[(start_i[k])][(start_j[k])].is_pedina=false; B[(end_i[k])][(end_j[k])].is_pedina=true; } } //.............................................................................. //.............................................................................. /* DEFINIZIONE PEDINE CANDIDATE AD ESSERE MOSSE ----------------------------------------------------------------------*/ //classe "candidati" class candidati { public: bool is_candidato; int i; int j; int peso; candidati(); }candidato[7][7]; if((start_i[k])==(end_i[k])){ if ((start_j[k])>(end_j[k])){ B[(start_i[k])][(start_j[k]-1)].is_pedina=false; } else{ B[(start_i[k])][(start_j[k]+1)].is_pedina=false; } } if((start_j[k])==(end_j[k])){ if((start_i[k])>(end_i[k])){ B[(start_i[k]-1)][(start_j[k])].is_pedina=false; } else{ B[(start_i[k]+1)][(start_j[k])].is_pedina=false; } } system("PAUSE"); } } //costruttore della classe "candidati" candidati::candidati( ){ for (int i=0; i<=6; i++) { for (int j=0; j<=6; j++){ candidato[i][j].is_candidato=false; candidato[i][j].i=-1; candidato[i][j].j=-1;} } } // contiene le informazioni sulla pedina da muovere, class mossa { public: int peso; int i,j, dir, fitness; mossa(); }pedina_da_muovere[15]; //VERIFICARE: i candidati per ogni mossa sicuramente non // più di 15; (15 forse è anche tanto!) //visualizza mosse e percorso void visualizza_mosse_percorso(int start_i[],int start_j[],int end_i[], int end_j[]){ peg B[7][7]; inizia_peg(B); cout<<"\nLista delle mosse\n"; for (int k=0; k<=31;k++){ cout<<"\n"; visualizza_situazione(B); //costruttore pedina_da_muovere mossa::mossa(){ for (int k=0; k<15; k++){ pedina_da_muovere[k].i=-1; pedina_da_muovere[k].j=-1; 15 end_j[l]=j; l++; break;} } case 4:{controllo++; if ((scacchiera[i-1][j].is_pedina)&&(rispetta_condizioni(i1,j))&&!(scacchiera[i-2][j].is_pedina)&& (rispetta_condizioni(i-2,j))) {scacchiera[i-2][j].is_pedina=true; scacchiera[i][j].is_pedina=false; scacchiera[i-1][j].is_pedina=false; gia_mosso=true; pedina_da_muovere[k].dir=0; pedina_da_muovere[k].fitness=0; } } //............................................................................. //............................................................................. /* FUNZIONI OPERATIVE -------------------------------------------------------------------------*/ /* --------------------------------MUOVI----------------------------------*/ //Muove le pedine (in input le coordinate) //se una pedina si può muovere in più direzioni ne sceglie un a caso void muovi (int i, int j, int direzione){ if ((!rispetta_condizioni(i,j))||!(scacchiera[i][j].is_pedina)){ int errore;throw errore;} bool gia_mosso=false;//controlla che ci sia una sola mossa while (!gia_mosso) { int controllo=0; start_i[l]=i; start_j[l]=j; end_i[l]=i-2; end_j[l]=j; l++; break;} } if (!(controllo<=4)) cout<<" Errore!! NON RIESCO A MUOVERE UN CANDIDATO!! \a"; } } } switch (direzione) { case 1:{ controllo++; if ((scacchiera[i][j+1].is_pedina)&&(rispetta_condizioni(i,j+1))&&!(s cacchiera[i][j+2].is_pedina)&& (rispetta_condizioni(i,j+2))) {scacchiera[i][j+2].is_pedina=true; scacchiera[i][j].is_pedina=false; scacchiera[i][j+1].is_pedina=false; gia_mosso=true; int num_peg_esau;//memorizza il numero minimo di pedine a cui arriva la ricerca esaustiva //...................................................................................................... // RICERCA ESAUSTIVA //prende in input tutta la scacchiera e fa ricorsivamente tutte le mosse possibili void ricerca_esaustiva(peg scacchieraTEMP[][7],int lc){ start_i[l]=i; start_j[l]=j; end_i[l]=i; end_j[l]=j+2; l++; break;} //genetico in buffer copia (Bstart_i,start_i,Bstart_j,start_j); copia (Bend_i,end_i,Bend_j,end_j); for ( int i=0; i<=6; i++) { for (int j=0; j<=6; j++){ } case 3:{controllo++; if ((scacchiera[i][j-1].is_pedina)&&(rispetta_condizioni(i,j1))&&!(scacchiera[i][j-2].is_pedina)&& (rispetta_condizioni(i,j-2))) {scacchiera[i][j-2].is_pedina=true; scacchiera[i][j].is_pedina=false; scacchiera[i][j-1].is_pedina=false; gia_mosso=true; if ((scacchieraTEMP[i][j].is_pedina)&&(rispetta_condizioni(i,j))){ //verso alto if ((scacchieraTEMP[i][j+1].is_pedina)&&(rispetta_condizioni(i,j+1)) &&!(scacchieraTEMP[i][j+2].is_pedina)&& (rispetta_condizioni(i,j+2))){ peg A[7][7]; for ( int s=0; s<=6; s++) { for (int h=0; h<=6; h++){ A[s][h].is_pedina=scacchieraTEMP[s][h].is_pedina;}} start_i[l]=i; start_j[l]=j; end_i[l]=i; end_j[l]=j-2; l++; break;} } case 2:{controllo++; if ((scacchiera[i+1][j].is_pedina)&&(rispetta_condizioni(i+1,j))&&!(s cacchiera[i+2][j].is_pedina)&& (rispetta_condizioni(i+2,j))) {scacchiera[i+2][j].is_pedina=true; scacchiera[i][j].is_pedina=false; scacchiera[i+1][j].is_pedina=false; gia_mosso=true; A[i][j+2].is_pedina=true; A[i][j].is_pedina=false; A[i][j+1].is_pedina=false; int Cstart_i[31]; int Cstart_j[31]; int Cend_i[31]; int Cend_j[31]; int lr; lr=lc; start_i[l]=i; start_j[l]=j; end_i[l]=i+2; 16 peg A[7][7]; for ( int s=0; s<=6; s++) for (int h=0; h<=6; h++) A[s][h].is_pedina=scacchieraTEMP[s][h].is_pedina; start_i[lr]=i; start_j[lr]=j; end_i[lr]=i; end_j[lr]=j+2; A[i-2][j].is_pedina=true; A[i][j].is_pedina=false; A[i-1][j].is_pedina=false; lr++; int Cstart_i[31]; int Cstart_j[31]; int Cend_i[31]; int Cend_j[31]; int lr; lr=lc; if (!(peg_rimasti(A)==1)){ if(peg_rimasti(A)<=num_peg_esau) {num_peg_esau=peg_rimasti(A);} ricerca_esaustiva(A,lr);} else { cout<<"\a"; start_i[lr]=i; start_j[lr]=j; end_i[lr]=i-2; end_j[lr]=j; visualizza_situazione(A); lr++; if (!(peg_rimasti(A)==1)){ if(peg_rimasti(A)<=num_peg_esau) {num_peg_esau=peg_rimasti(A);} ricerca_esaustiva(A,lr);} else { cout<<"\a"; int esci; throw esci; } } //verso destra if ((rispetta_condizioni(i+1,j))&&(scacchieraTEMP[i+1][j].is_pedina) &&!(scacchieraTEMP[i+2][j].is_pedina)&& (rispetta_condizioni(i+2,j))){ peg A[7][7]; for ( int s=0; s<=6; s++) for (int h=0; h<=6; h++) A[s][h].is_pedina=scacchieraTEMP[s][h].is_pedina; visualizza_situazione(A); int esci; throw esci; } } //verso basso if ((rispetta_condizioni(i,j-1))&&(scacchieraTEMP[i][j1].is_pedina)&&!(scacchieraTEMP[i][j-2].is_pedina)&& (rispetta_condizioni(i,j-2))){ peg A[7][7]; for ( int s=0; s<=6; s++) for (int h=0; h<=6; h++) A[s][h].is_pedina=scacchieraTEMP[s][h].is_pedina; A[i+2][j].is_pedina=true; A[i][j].is_pedina=false; A[i+1][j].is_pedina=false; int Cstart_i[31]; int Cstart_j[31]; int Cend_i[31]; int Cend_j[31]; int lr; lr=lc; A[i][j-2].is_pedina=true; A[i][j].is_pedina=false; A[i][j-1].is_pedina=false; start_i[lr]=i; start_j[lr]=j; end_i[lr]=i+2; end_j[lr]=j; int Cstart_i[31]; int Cstart_j[31]; int Cend_i[31]; int Cend_j[31]; int lr; lr=lc; lr++; if (!(peg_rimasti(A)==1)){ if(peg_rimasti(A)<=num_peg_esau) {num_peg_esau=peg_rimasti(A);} ricerca_esaustiva(A,lr);} else { cout<<"\a"; visualizza_situazione(A); start_i[lr]=i; start_j[lr]=j; end_i[lr]=i; end_j[lr]=j-2; lr++; if (!(peg_rimasti(A)==1)){ if(peg_rimasti(A)<=num_peg_esau) {num_peg_esau=peg_rimasti(A);} ricerca_esaustiva(A,lr);} @@@@@ int esci;throw esci; } } //verso sinistra if ((rispetta_condizioni(i-1,j))&&(scacchieraTEMP[i1][j].is_pedina)&&!(scacchieraTEMP[i-2][j].is_pedina)&& (rispetta_condizioni(i-2,j))){ else { cout<<"\a"; 17 candidato[i][j].is_candidato=true; candidato[i][j].i=i; candidato[i][j].j=j; candidato[i][j].peso=scacchiera[i][j].peso; // cout<<"La pedina("<<i<<","<<j<<") e' candidata a muovere verso destra\n"; pedina_da_muovere[k].i=candidato[i][j].i; pedina_da_muovere[k].j=candidato[i][j].j; pedina_da_muovere[k].dir=2; pedina_da_muovere[k].fitness= visualizza_situazione(A); int esci; throw esci; } } } } } (*p[countPOPOLAZIONE][(scacchiera[i+2][j].peso)]+*p[countPO POLAZIONE][(scacchiera[i+1][j].peso)]*p[countPOPOLAZIONE][(scacchiera[i][j].peso)]); //controllo // cout<<"La pedina_da_muovere["<<k<<"]("<<pedina_da_muovere[k].i<<","< <pedina_da_muovere[k].j<<") e' candidata a muovere verso destra\n"; k++; num_candidati++;} return;} //........................................................................... /* ------------------------RICERCA-SCELTA ( e mossa) CANDIDATI----------------*/ //Scorre la scacchiera, seleziona i candidati ad essere mossi,quindi li ordina //in base al peso. Muove quello con peso maggiore ( a parità di peso l'ultimo //esaminato) void ricerca_scegli_muovi_candidati(int countPOPOLAZIONE){ if ((rispetta_condizioni(i-1,j))&&(scacchiera[i1][j].is_pedina)&&!(scacchiera[i-2][j].is_pedina)&& (rispetta_condizioni(i-2,j))){ candidato[i][j].is_candidato=true; candidato[i][j].i=i; candidato[i][j].j=j; candidato[i][j].peso=scacchiera[i][j].peso; // cout<<"La pedina("<<i<<","<<j<<") e' candidata a muovere verso sinistra\n"; pedina_da_muovere[k].i=candidato[i][j].i; pedina_da_muovere[k].j=candidato[i][j].j; pedina_da_muovere[k].dir=4; pedina_da_muovere[k].fitness= (*p[countPOPOLAZIONE][(scacchiera[i2][j].peso)]+*p[countPOPOLAZIONE][(scacchiera[i-1][j].peso)]*p[countPOPOLAZIONE][(scacchiera[i][j].peso)]); //controllo // cout<<"La pedina_da_muovere["<<k<<"]("<<pedina_da_muovere[k].i<<","< <pedina_da_muovere[k].j<<") e' candidata a muovere verso sinsitra\n"; k++; num_candidati++;} int k=0; int num_candidati=0; int direzione=0; /* -------------------------------ricerca-------------------------------------*/ int i; int j; mossa(); for ( i=0; i<=6; i++) { for (j=0; j<=6; j++){ if ((scacchiera[i][j].is_pedina)&&(rispetta_condizioni(i,j))){ if ((scacchiera[i][j+1].is_pedina)&&(rispetta_condizioni(i,j+1))&&!(s cacchiera[i][j+2].is_pedina)&& (rispetta_condizioni(i,j+2))){ candidato[i][j].is_candidato=true; candidato[i][j].i=i; candidato[i][j].j=j; candidato[i][j].peso=scacchiera[i][j].peso; // cout<<"La pedina("<<i<<","<<j<<") e' candidata a muovere verso l'alto\n"; pedina_da_muovere[k].i=candidato[i][j].i; pedina_da_muovere[k].j=candidato[i][j].j; pedina_da_muovere[k].dir=1; pedina_da_muovere[k].fitness= if ((rispetta_condizioni(i,j-1))&&(scacchiera[i][j1].is_pedina)&&!(scacchiera[i][j-2].is_pedina)&& (rispetta_condizioni(i,j-2))){ candidato[i][j].is_candidato=true; candidato[i][j].i=i; candidato[i][j].j=j; candidato[i][j].peso=scacchiera[i][j].peso; // cout<<"La pedina("<<i<<","<<j<<") e' candidata a muovere verso il basso\n"; pedina_da_muovere[k].i=candidato[i][j].i; pedina_da_muovere[k].j=candidato[i][j].j; pedina_da_muovere[k].dir=3; pedina_da_muovere[k].fitness= ((*p[countPOPOLAZIONE][(scacchiera[i][j2].peso)])+(*p[countPOPOLAZIONE][(scacchiera[i][j-1].peso)])(*p[countPOPOLAZIONE][(scacchiera[i][j].peso)])); //controllo // cout<<"La pedina_da_muovere["<<k<<"]("<<pedina_da_muovere[k].i<<","< <pedina_da_muovere[k].j<<") e' candidata a muovere verso il basso\n"; k++; num_candidati++;} } } } (*p[countPOPOLAZIONE][(scacchiera[i][j+2].peso)]+*p[countPO POLAZIONE][(scacchiera[i][j+1].peso)]*p[countPOPOLAZIONE][(scacchiera[i][j].peso)]); //controllo // cout<<"La pedina_da_muovere["<<k<<"]("<<pedina_da_muovere[k].i<<","< <pedina_da_muovere[k].j<<") e' candidata a muovere verso l'alto\n"; k++; num_candidati++;} if ((rispetta_condizioni(i+1,j))&&(scacchiera[i+1][j].is_pedina)&&!(s cacchiera[i+2][j].is_pedina)&& (rispetta_condizioni(i+2,j))){ 18 // cout<<"Mossa random: scelgo di muovere pedina_da_muovere["<< ir<<"]\n"; //cout<<"\nricerca candidati ok\n"; if ((num_candidati==0)){ // eccezione che fa uscire dal gioco int stop; // cout<<"\nNon ci sono candidati!"; throw stop; } //cout<<"\nI candidati sono "<<num_candidati<<"\n"; muovi(pedina_da_muovere[ir].i,pedina_da_muovere[ir].j,pedina_d a_muovere[ir].dir); /// cout<<"\nok mossa random su pedina da muovere["<<ir<<"]\n"; // visualizza_situazione(); } /* } else //ricerca esaustiva { //visualizza_situazione(scacchiera); /* -------------------------------------ordinamento---------------------bubblesort in base al fitness */ int tempFIT; int tempI; int tempJ; int tempDIR; for(int a=1; a<num_candidati; a++) for(int b=num_candidati-1; b>=a; b--) { if (pedina_da_muovere[b-1].fitness < pedina_da_muovere[b].fitness){ tempFIT=pedina_da_muovere[b-1].fitness; tempI=pedina_da_muovere[b-1].i; tempJ=pedina_da_muovere[b-1].j; tempDIR=pedina_da_muovere[b-1].dir; pedina_da_muovere[b-1].fitness=pedina_da_muovere[b].fitness; pedina_da_muovere[b-1].i=pedina_da_muovere[b].i; pedina_da_muovere[b-1].j=pedina_da_muovere[b].j; pedina_da_muovere[b-1].dir=pedina_da_muovere[b].dir; pedina_da_muovere[b].fitness=tempFIT; pedina_da_muovere[b].i=tempI; pedina_da_muovere[b].j=tempJ; pedina_da_muovere[b].dir=tempDIR; } } //cout<<"\nricerca esaustiva\n"; // system("PAUSE"); ricerca_esaustiva(scacchiera); } */ } //............................................................................ /* OPERAZIONI GENETICHE -------------------------------------------------------------------------------*/ //immagazzina le informazioni sul numero di pedine rimaste sulla scacchiera int num_peg[15]; int index[15]; void accoppia(int m,int f,int u){ int32 ir; int32 seed= time(0);// seme int j; TRanrotWGenerator rg(seed);// generatore di numeri random for(j=0;j<7;j++){ ir = rg.IRandom(0,99); if(ir<50)Genoma.Rmatrix[u][j]=Genoma.Rmatrix[m][j]; else Genoma.Rmatrix[u][j]=Genoma.Rmatrix[f][j];} /*l'array di candidati è ora ordinato in pedina_da_muovere[0] c'è quindi il candidato con il FIT maggiore Prendo in considerazione anche l'eventualità che esistano più candidati con lo stesso peso. In tal caso si sceglierà in modo random */ int count=0; for(int k=1; k<=num_candidati; k++) if (pedina_da_muovere[k].fitness==pedina_da_muovere[0].fitness) count++; /* --------------------------------------mossa-----------------------------*/ countPOPOLAZIONE=m; SETpunt(countPOPOLAZIONE); } //............................................................................ /*ordina l'array num_peg conservando le informazioni sul'indice iniziale del max e min determinando gli indici su cui effettuare l'accoppiamento */ void ordina_accoppia (int num_peg[]){ int tempVALUE; for(int a=1; a<15; a++) for(int b=14; b>=a; b--) { if (num_peg[b-1] > num_peg[b]){ tempVALUE=num_peg[b-1]; num_peg[b-1]=num_peg[b]; index[b-1]=b; num_peg[b]=tempVALUE; index[b]=b-1; } } int ind_M=index[0]; int ind_F=index[1]; int ind_U=index[14]; /*if (num_pedine>10) {*/ //cout<<"Ci sono "<<count+1<<" pedine con il fit maggiore identico\n"; if (count==0) { muovi(pedina_da_muovere[0].i,pedina_da_muovere[0].j,pedina_da _muovere[0].dir); //cout<<"\n ok mossa obbligata su pedina_da_muovere[0]\n"; // visualizza_situazione(); } else { int32 seed = time(0); // random seed TRanrotWGenerator rg(seed);// make instance of random number generator int32 ir; // random integer number ir = rg.IRandom(0,count); accoppia(ind_M,ind_F,ind_U); } //............................................................................ //............................................................................ /* 19 CORPO DEL PROGRAMMA --------------------------------------------------------------------------------------------------------------------------------------------------------------- try */ bool ric_es=false; int countSOL=0; { ricerca_esaustiva(scacchiera,l); num_peg[k]=num_peg_esau; fine_gioco=true; } catch (int esci) { l=32; int num_pedine; int main(int argc, char *argv[]) { for (int megagiri=0; megagiri<20; megagiri++) { int k; int giri=0; bool vinto=false; bool fine_gioco=false; fine_gioco=true; num_peg[k]=1; countSOL++; cout<<"\nTentativo numero "<<countSOL<<"\n"; } } // num_pedine=peg_rimasti(scacchiera); } catch (int stop) {fine_gioco=true; num_peg[k]=peg_rimasti(scacchiera); countSOL++; //cout<<"\nTentativo numero "<<countSOL<<"\n"; } GENETICset(); inizia_peg(scacchiera);//inizializza la scacchiera SETpunt(countPOPOLAZIONE); azzera_mosse(start_i,start_j,end_i,end_j); candidati();//inizializza gli eventuali candidati per le future mosse mossa();//inizializza la pedina da muovere l=0;//contatore della lista delle mosse }//cout<<"\n qui"; if(num_peg[k]==1) { vinto=true; cout<<"\nFine Gioco\n\a"; cout<<"\nTentativo numero "<<countSOL<<"\n"; do{ giri++; for(k=0; k<15; k++) { azzera_mosse(start_i,start_j,end_i,end_j); l=0; while(!fine_gioco) { try { candidati(); mossa(); num_pedine=peg_rimasti(scacchiera); visualizza_mosse_percorso(start_i,start_j,end_i,end_j); scrivi_lista_mosse(start_i,start_j,end_i,end_j); break; } else { fine_gioco=false; countPOPOLAZIONE++; if (countPOPOLAZIONE==15)countPOPOLAZIONE=0; inizia_peg(scacchiera); SETpunt(countPOPOLAZIONE); } if ((num_pedine>peg_finali)) { } /*operazioni genetiche*/ ordina_accoppia(num_peg);//mescola i valori della matrice random e rinizia a giocare dal figlio cout<<"\ngiri "<<giri<<"\n"; if(giri>30) ric_es=true; ricerca_scegli_muovi_candidati(countPOPOLAZIONE); } num_pedine=peg_rimasti(scacchiera); num_peg_esau=peg_rimasti(scacchiera); if ((num_pedine<=peg_finali)&&(!ric_es)) { }while((!vinto)&&(giri<=60)); if(vinto){ cout<<"\nE dajee!\n"; break;} } system("PAUSE"); return 0; } ricerca_scegli_muovi_candidati(countPOPOLAZIONE); } num_pedine=peg_rimasti(scacchiera); num_peg_esau=peg_rimasti(scacchiera); if ((num_pedine<=peg_finali)&&(ric_es)) { 20 Bibliografia: R.L. Haupt – S.E. Haupt : Pratical Genetic Algorithms. ( Wiley & Sons – 2004 ) M. Mitchell : An Introduction to Genetic Algorithms. ( MIT press – 1999 ) M. Bucci – D. Cirielli : Algoritmi genetici e simulated annealing per la soluzione parallela di problemi di ottimizzazione combinatoriale. 21