Attività Formativa Algoritmi genetici e giochi… PEG SOLITAIRE

annuncio pubblicitario
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
Scarica