© Harry Broeders.
Deze pagina is bestemd voor studenten van de THRijswijk groep EH3 C&D en IH3 SYSO.
Als je de beste zet in een spel (met 2 spelers, die elk afwisselend aan de beurt zijn) wilt vinden kun een boom tekenen met alle mogelijke posities. Stel dat deze boom er (voor een denkbeeldig spel) als volgt uitziet:
In deze figuur is een vierkant getekend voor een positie waarbij de computer aan de beurt is en een rondje als de tegenstander (mens) aan de beurt is. De waarde van een positie is 15 als de computer wint en 0 als de mens wint. De computer moet dus een positie met een zo hoog mogelijke waarde zien te bereiken.
De posities zijn genummerd van boven naar beneden en van links naar rechts. Voor elke positie 15<=p<31 geldt dat de waarde van deze positie bekend is. Voor elke positie 0<=p<15 geldt dat de twee mogelijke volgende posities p*2+1 en p*2+2 zijn. Bijvoorbeeld: positie 4 heeft als mogelijke volgende posities 9 (2*4+1) en 10 (2*4+2).
De computer moet de hoogst haalbare waarde zien te bereiken. In de figuur lijkt dit een van de twee posities met de waarde 14 maar deze zijn onbereikbaar denk maar even na. Bij zijn eerste beurt gaat de computer naar rechts (richting 14). De tegenstander (mens) zal dan echter naar links gaan. De computer zal nu bij zijn volgende beurt naar rechts gaan (richting 11) maar zijn tegenstander zal bij de volgende beurt weer voor links kiezen waardoor de bereikte positie de waarde 2 heeft (niet best voor de computer dus).
Is dit de hoogst haalbare waarde voor de computer?
De hoogst haalbare waarde kan bepaald worden door van onder naar boven door de boom te wandelen en de waarde van elk knooppunt als volgt te bepalen:
Omdat je dus afwisselend kiest voor de maximale (als de computer aan de beurt is) en de minimale (als de tegenstander aan de beurt is) waarde wordt dit algoritme het min-max algoritme genoemd.
In het onderstaande programma
MinMax0.cpp
wordt de boom uit de bovenstaande
tekening gebruikt en wordt de hoogst haalbare waarde berekend met het het
min-max algoritme. Er wordt daarbij gebruik gemaakt van 2 functies die elkaar
(recursief) aanroepen.
#include <iostream>
#include <iomanip>
using namespace std;
int positionValue(int pos);
void printTree(int pos, int level);
int valueMoveComputer(int pos);
int valueMoveHuman(int pos);
int positionValue(int pos) {
static const int value[16]={4,5,3,2,6,7,8,9,1,10,2,11,12,13,14,14};
if (pos>=15 && pos<31)
return value[pos-15];
return -1;
}
int valueMoveComputer(int pos) {
int value(positionValue(pos));
if (value==-1) {
value=0;
for (int i(1); i<3; ++i) {
int reply(valueMoveHuman(2*pos+i));
if (reply>value) {
value=reply;
}
}
}
return value;
}
int valueMoveHuman(int pos) {
int value(positionValue(pos));
if (value==-1) {
value=15;
for (int i(1); i<3; ++i) {
int reply(valueMoveComputer(2*pos+i));
if (reply<value) {
value=reply;
}
}
}
return value;
}
int main() {
cout<<"Pos: 0"<<" Value: "<<setw(2)<<valueMoveComputer(0)<<endl;
for (int p(1); p<3; ++p)
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<valueMoveHuman(p)<<endl;
for (int p(3); p<7; ++p)
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<valueMoveComputer(p)<<endl;
for (int p(7); p<15; ++p)
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<valueMoveHuman(p)<<endl;
cin.get();
}
De functie positionValue
geeft -1
terug als
de waarde van de positie berekend moet worden. Als de positie zich in de
onderste laag van de boom bevindt en de waarde dus bekend is, geeft
positionValue
deze waarde terug.
De functie valueMoveComputer
wordt aangeroepen om te bepalen
wat de waarde van de positie pos
is als de computer aan de beurt
is in deze positie. Deze functie moet dus het maximum bepalen van
de (in dit geval twee) posities die vanuit de positie pos
bereikt
kunnen worden. In deze binaire boom zijn dat de posities:
2*pos+1
en 2*pos+2
. Bij binnenkomst van de functie
valueMoveComputer
wordt eerst de functie
positionValue
aangeroepen. De returnwaarde van deze functie
wordt opgeslagen in de lokale variabele value
. Als de waarde
van value
ongelijk is aan -1
kan de functie
valueMoveComputer
deze waarde meteen teruggeven. De positie
pos
bevindt zich in dat geval in de onderste laag van de boom.
Als de positie pos
waarmee de functie
valueMoveComputer
is aangeroepen zich niet in de onderste laag
van de boom bevindt geeft de functie positionValue
de waarde
-1
terug en wordt de waarde van de variabele value
gelijk gemaakt aan het absolute minimum (in dit geval 0
).
Het is logisch dat een functie die het maximum moet bepalen begint met het
(tot dan toe gevonden) maximum gelijk te maken aan het absolute minimum.
Vervolgens wordt in de for
lus de waarde van de (in dit geval
twee) posities die vanuit de positie pos
bereikt kunnen worden
bepaald. Dit gebeurt door het aanroepen van de functie
valueMoveHuman
met als argument 2*pos+i
waarbij
i
achtereenvolgens de waarde 1
en 2
heeft. We moeten hier de functie valueMoveHuman
aanroepen omdat
als in positie pos
de computer aan de beurt is, in de posities
die vanuit de positie pos
bereikt kunnen worden de tegenstander
(mens) aan de beurt zal zijn. De returnwaarde van deze functie wordt opgeslagen
in de lokale variabele reply
. Als de waarde van
reply
groter is dan het tot nu toe gevonden maximum (opgeslagen
in de variabele value
) dan wordt het tot nu toe gevonden maximum
gelijk gemaakt aan reply
. Aan het einde van de for
lus zal de variabele value
dus het maximum bevatten van de (in
dit geval twee) posities die vanuit de positie pos
bereikt kunnen
worden. De waarde van value
wordt teruggegeven aan de aanroepende
functie.
De functie valueMoveHuman
wordt aangeroepen als de mens aan
de beurt is en lijkt erg veel op de functie valueMoveComputer
,
in plaats van het maximum wordt echter het minimum bepaald.
De functie valueMoveHuman
wordt aangeroepen om te bepalen wat
de waarde van de positie pos
is als de mens aan de beurt is
in deze positie. Deze functie moet dus het minimum bepalen van de
(in dit geval twee) posities die vanuit de positie pos
bereikt
kunnen worden. In deze binaire boom zijn dat de posities:
2*pos+1
en 2*pos+2
. Bij binnenkomst van de functie
valueMoveHuman
wordt eerst de functie positionValue
aangeroepen. De returnwaarde van deze functie wordt opgeslagen in de lokale
variabele value
. Als de waarde van value
ongelijk
is aan -1
kan de functie valueMoveHuman
deze waarde
meteen teruggeven. De positie pos
bevindt zich in dat geval
in de onderste laag van de boom. Als de positie pos
waarmee
de functie valueMoveHuman
is aangeroepen zich niet in de onderste
laag van de boom bevindt geeft de functie positionValue
de waarde
-1
terug en wordt de waarde van de variabele value
gelijk gemaakt aan het absolute maximum (in dit geval
15
). Het is logisch dat een functie die het minimum moet bepalen
begint met het (tot dan toe gevonden) minimum gelijk te maken aan het absolute
maximum. Vervolgens wordt in de for
lus de waarde van de (in
dit geval twee) posities die vanuit de positie pos
bereikt kunnen
worden bepaald. Dit gebeurt door het aanroepen van de functie
valueMoveComputer
met als argument 2*pos+i
waarbij
i
achtereenvolgens de waarde 1
en 2
heeft. We moeten hier de functie valueMoveComputer
aanroepen
omdat als in positie pos
de mens aan de beurt is, in de posities
die vanuit de positie pos
bereikt kunnen worden de computer
aan de beurt zal zijn. De returnwaarde van deze functie wordt opgeslagen
in de lokale variabele reply
. Als de waarde van
reply
kleiner is dan het tot nu toe gevonden minimum (opgeslagen
in de variabele value
) dan wordt het tot nu toe gevonden minimum
gelijk gemaakt aan reply
. Aan het einde van de for
lus zal de variabele value
dus het minimum bevatten van de (in
dit geval twee) posities die vanuit de positie pos
bereikt kunnen
worden. De waarde van value
wordt teruggegeven aan de aanroepende
functie.
In de functie main
wordt ervan uitgegaan dat de computer in
positie 0
aan de beurt is. In de volgende laag van de boom (posities
1
en 2
) zal dus de mens aan de beurt zijn enz.
Er vanuitgaande dat de computer in positie 0
aan de beurt is
worden de waarden van alle posities in de boom bepaald en afgedrukt. De uitvoer
van dit programma is:
Pos: 0 Value: 4 Pos: 1 Value: 4 Pos: 2 Value: 2 Pos: 3 Value: 4 Pos: 4 Value: 8 Pos: 5 Value: 2 Pos: 6 Value: 14 Pos: 7 Value: 4 Pos: 8 Value: 2 Pos: 9 Value: 6 Pos: 10 Value: 8 Pos: 11 Value: 1 Pos: 12 Value: 2 Pos: 13 Value: 12 Pos: 14 Value: 14
In de bovenstaande figuur kun je zien dat met behulp van het min-max algoritme de best bereikbare positie voor de computer wordt gevonden.
Het is ook mogelijk om de functies valueMoveComputer
en
valueMoveHuman
te combineren tot 1 functie:
enum Side {HUMAN, COMPUTER};
int valueMove(Side s, int pos) {
int value(positionValue(pos));
if (value==-1) {
value=(s==COMPUTER)?0:15;
for (int i(1); i<3; ++i) {
int reply(valueMove((s==COMPUTER)?HUMAN:COMPUTER, 2*pos+i));
if (s==COMPUTER&&reply>value || s==HUMAN&&reply<value) {
value=reply;
}
}
}
return value;
}
Er zijn echter 2 redenen om dit niet te doen:
In het bovenstaande programma wordt alleen de waarde van de positie bepaald.
Dit is uiterraard niet voldoende. We moeten ook weten welke zet we moeten
doen om deze waarde te bereiken! Dit kan "eenvoudig" worden bereikt door
bij het zoeken naar de maximale of minimale waarde de beste positie te onthouden
in de lokale variabele bestNextPos
en aan het einde van
de functie terug te geven. Omdat een functie echter maar één
variabele kan teruggeven is het noodzakelijk om de twee returnwaarden (de
waarde van de positie en de beste volgende positie) in te pakken in een standaard
pair<int, int>
object.
#include <iostream>
#include <iomanip>
using namespace std;
int positionValue(int pos);
pair<int, int> chooseMoveComputer(int pos);
pair<int, int> chooseMoveHuman(int pos);
int positionValue(int pos) {
static const int value[16]={4,5,3,2,6,7,8,9,1,10,2,11,12,13,14,14};
if (pos>=15 && pos<31)
return value[pos-15];
return -1;
}
pair<int, int> chooseMoveComputer(int pos) {
int value(positionValue(pos));
int bestNextPos(pos);
if (value==-1) {
value=0;
for (int i(1); i<3; ++i) {
pair<int, int> reply(chooseMoveHuman(2*pos+i));
if (reply.first>value) {
value=reply.first;
bestNextPos=2*pos+i;
}
}
}
return make_pair(value, bestNextPos);
}
pair<int, int> chooseMoveHuman(int pos) {
int value(positionValue(pos));
int bestNextPos(pos);
if (value==-1) {
value=15;
for (int i(1); i<3; ++i) {
pair<int, int> reply(chooseMoveComputer(2*pos+i));
if (reply.first<value) {
value=reply.first;
bestNextPos=2*pos+i;
}
}
}
return make_pair(value, bestNextPos);
}
int main() {
pair<int, int> res(chooseMoveComputer(0));
cout<<"Pos: 0"
<<" Value: "<<setw(2)<<res.first
<<" BestPos: "<<setw(2)<<res.second<<endl;
for (int p(1); p<3; ++p) {
res=chooseMoveHuman(p);
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<res.first
<<" BestPos: "<<setw(2)<<res.second<<endl;
}
for (int p(3); p<7; ++p) {
res=chooseMoveComputer(p);
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<res.first
<<" BestPos: "<<setw(2)<<res.second<<endl;
}
for (int p(7); p<15; ++p) {
res=chooseMoveHuman(p);
cout<<"Pos: "<<setw(2)<<p
<<" Value: "<<setw(2)<<res.first
<<" BestPos: "<<setw(2)<<res.second<<endl;
}
cin.get();
}
De uitvoer van dit programma
MinMax1.cpp
is als volgt:
Pos: 0 Value: 4 BestPos: 1 Pos: 1 Value: 4 BestPos: 3 Pos: 2 Value: 2 BestPos: 5 Pos: 3 Value: 4 BestPos: 7 Pos: 4 Value: 8 BestPos: 10 Pos: 5 Value: 2 BestPos: 12 Pos: 6 Value: 14 BestPos: 14 Pos: 7 Value: 4 BestPos: 15 Pos: 8 Value: 2 BestPos: 18 Pos: 9 Value: 6 BestPos: 19 Pos: 10 Value: 8 BestPos: 21 Pos: 11 Value: 1 BestPos: 23 Pos: 12 Value: 2 BestPos: 25 Pos: 13 Value: 12 BestPos: 27 Pos: 14 Value: 14 BestPos: 29
Bij het zoeken naar een minimale waarde op een bepaalde positie kun je stoppen zodra je een waarde hebt gevonden die kleiner of gelijk is aan de tot dan toe gevonden maximale waarde in de bovenliggende positie.
In de bovenstaande figuur is de waarde van positie 3 gelijk aan het maximum van de waarden van de posities 7 en 8. De waarde van positie 7 is het minimum van de waarden van de posities 15 en 16. De waarde van positie 7 is dus 4 (het minimum van 4 en 5). Het tijdelijke maximum in positie 3 wordt dus (na het berekenen van de waarde van positie 7) gelijk gemaakt aan 4. De waarde van positie 8 is het minimum van positie 17 en 18. Bij het zoeken naar dit minimum wordt eerst de waarde 3 voor positie 17 gevonden. Omdat deze waarde kleiner of gelijk is aan het tot nu toe gevonden maximum in de bovenliggende positie (de waarde 4) kunnen we meteen stoppen! Het is dus niet nodig om de waarde van positie 18 te bepalen. Denk maar mee. Als deze waarde <3 (b.v. 0) is dan wordt deze waarde gekozen als minimale waarde. Echter in de bovenliggende positie (positie 3) wordt toch voor de maximale waarde 4 gekozen. Als de waarde van positie 18 >3 (b.v. 15) is dan wordt de waarde 3 (van positie 17) gekozen als minimale waarde. Echter in de bovenliggende positie (positie 3) wordt toch voor de maximum waarde 4 gekozen. De waarde van positie 18 hoeft dus helemaal niet bepaald te worden. Dit deel van de boom hoeft niet "doorzocht" te worden en we kunnen als het ware de zoekboom snoeien (Engels: to prune). Om dit mogelijk te maken moet wel de tot dan toe gevonden maximale waarde van de bovenliggende positie worden doorgegeven aan de onderliggende posities. Dit gebreurt in het onderstaande programma door middel van de parameter alpha. Deze naam is toevallig zo gekozen door de bedenkers van dit algoritme en heeft verder geen betekenis.
Bij het zoeken naar een maximale waarde op een bepaalde positie kun je stoppen zodra je een waarde hebt gevonden die groter of gelijk is aan de tot dan toe gevonden minimale waarde in de bovenliggende positie.
In de bovenstaande figuur is de waarde van positie 1 gelijk aan het minimum
van de waarden van de posities 3 en 4. De waarde van positie 3 is hiervoor
al bepaald (4). Het tijdelijke minimum in positie 1 wordt dus (na het berekenen
van de waarde van positie 3) gelijk gemaakt aan 4. De waarde van positie
4 is het maximum van positie 9 en 10. Bij het zoeken naar dit maximum wordt
eerst de waarde 6 voor positie 9 gevonden (door het minimum te bepalen van
positie 19 en 20). Omdat het tot nu toe gevonden maximum in positie 4 (de
waarde 6) groter of gelijk is aan het tot nu toe gevonden minimum in de
bovenliggende positie (de waarde 4) kunnen we meteen stoppen! Het is dus
niet nodig om de waarden van posities 10, 21 en 22 te bepalen. Denk maar
mee. Als de waarde van positie 10 >6 (b.v. 15) is dan wordt deze waarde
gekozen als maximale waarde. Echter in de bovenliggende positie (positie
1) wordt toch als minimale waarde 4 gekozen. Als de waarde van positie 10
<6 (b.v. 0) is dan wordt de waarde 6 (van positie 9) gekozen als maximale
waarde. Echter in de bovenliggende positie (positie 1) wordt toch als minimale
waarde 4 gekozen. De waarden van posities 10, 21 en 22 hoeven dus helemaal
niet bepaald te worden. Dit deel van de boom hoeft niet "doorzocht" te worden
en we kunnen de zoekboom weer snoeien. Om dit mogelijk te maken moet wel
de tot dan toe gevonden minimale waarde van de bovenliggende positie worden
doorgegeven aan de onderliggende posities. Dit gebreurt in het onderstaande
programma AlphaBeta0.cpp
door middel van de parameter
beta. Deze naam is toevallig zo gekozen door de bedenkers van dit zogenaamde
alpha-beta pruning algoritme en heeft verder geen betekenis. Het alpha-beta
algoritme is in 1958 bedacht door 3 wetenschappers van de Carnegie-Mellon
University (Allen Newell, John Shaw, en Herbert Simon).
#include <iostream>
#include <iomanip>
using namespace std;
int positionValue(int pos);
int valueMoveComputer(int pos, int alpha, int beta);
int valueMoveHuman(int pos, int alpha, int beta);
int positionValue(int pos) {
static const int value[16]={4,5,3,2,6,7,8,9,1,10,2,11,12,13,14,14};
if (pos>=15 && pos<31)
return value[pos-15];
return -1;
}
int valueMoveComputer(int pos, int alpha, int beta) {
cout<<"pos = "<<setw(2)<<pos
<<" alpha = "<<setw(2)<<alpha
<<" beta = "<<setw(2)<<beta<<endl;
int value(positionValue(pos));
if (value==-1) {
value=alpha;
for (int i(1); i<3 && value<beta; ++i) {
int reply(valueMoveHuman(2*pos+i, alpha, beta));
if (reply>value) {
value=reply;
alpha=value;
}
}
}
return value;
}
int valueMoveHuman(int pos, int alpha, int beta) {
cout<<"pos = "<<setw(2)<<pos
<<" alpha = "<<setw(2)<<alpha
<<" beta = "<<setw(2)<<beta<<endl;
int value(positionValue(pos));
if (value==-1) {
value=beta;
for (int i(1); i<3 && value>alpha; ++i) {
int reply(valueMoveComputer(2*pos+i, alpha, beta));
if (reply<value) {
value=reply;
beta=value;
}
}
}
return value;
}
int main() {
cout<<"Pos: 0"
<<" Value: "<<setw(2)<<valueMoveComputer(0, 0, 15)<<endl;
cin.get();
}
De uitvoer van dit programma is als volgt:
pos = 0 alpha = 0 beta = 15 pos = 1 alpha = 0 beta = 15 pos = 3 alpha = 0 beta = 15 pos = 7 alpha = 0 beta = 15 pos = 15 alpha = 0 beta = 15 pos = 16 alpha = 0 beta = 4 pos = 8 alpha = 4 beta = 15 pos = 17 alpha = 4 beta = 15 pos = 4 alpha = 0 beta = 4 pos = 9 alpha = 0 beta = 4 pos = 19 alpha = 0 beta = 4 pos = 20 alpha = 0 beta = 4 pos = 2 alpha = 4 beta = 15 pos = 5 alpha = 4 beta = 15 pos = 11 alpha = 4 beta = 15 pos = 23 alpha = 4 beta = 15 pos = 12 alpha = 4 beta = 15 pos = 25 alpha = 4 beta = 15 Pos: 0 Value: 4
Door de extra output naar cout
aan het begin van de functies
valueMoveComputer
en valueMoveHuman
kunnen we het
gedrag van dit algoritme goed volgen. Doordat de posities 6, 10, 13, 14,
18, 21, 22, 24, 26, 27, 28, 29 en 30 niet in de uitvoer voorkomen blijkt
dat deze posities "gesnoeid" zijn. Zie onderstaand figuur:
Bij de eerste aanroep van de functie valueMoveComputer
moet
voor alpha
het absolute minimum (in dit geval 0) en voor
beta
het absolute maximum (in dit geval 15) worden ingevuld.
Dit is logisch want alpha
is het tot nu toe gevonden maximum
in de bovenliggende posities (we hebben in dit geval geen bovenliggende posities
dus het tot nu toe gevonden maximum is het absolute minimum) en
beta
is het tot nu toe gevonden minimum in de bovenliggende
posities (we hebben in dit geval geen bovenliggende posities dus het tot
nu toe gevonden minimum is het absolute maximum).
Je ziet dat bij het bepalen van de maximale waarde in een positie de waarde
van value
wordt geïnitialiseerd met alpha
en niet met het absolute minimum (0
). Een, in een bovenliggende
positie gevonden, tijdelijk maximum mag namelijk niet in een (2 lagen
dieper) onderliggende positie op een lagere waarde worden gezet! Als
je value initialiseerd met 0
wordt de positie 26 niet
gesnoeid! Probeer maar.
Om dezelfde reden moet bij het bepalen van de minimale waarde in een positie
de waarde van value
worden geïnitialiseerd met
beta
en niet met het absolute maximum (15
). Een,
in een bovenliggende positie gevonden, tijdelijk minimum mag namelijk
niet in een (2 lagen dieper) onderliggende positie op een
hogere waarde worden gezet!
In het bovenstaande programma wordt alleen de waarde van de positie bepaald.
Dit is uiterraard niet voldoende. We moeten ook weten welke zet we moeten
doen om deze waarde te bereiken! Dit kan weer worden bereikt door bij het
zoeken naar de maximale of minimale waarde de beste positie te onthouden
in de lokale variabele bestNextPos
en aan het einde van
de functie terug te geven. De twee returnwaarden (de waarde van de positie
en de beste volgende positie) worden weer ingepakt in een standaard
pair<int, int>
object.
#include <iostream>
#include <iomanip>
using namespace std;
int positionValue(int pos);
pair<int, int> chooseMoveComputer(int pos, int alpha, int beta);
pair<int, int> chooseMoveHuman(int pos, int alpha, int beta);
int positionValue(int pos) {
static const int value[16]={4,5,3,2,6,7,8,9,1,10,2,11,12,13,14,14};
if (pos>=15 && pos<31)
return value[pos-15];
return -1;
}
pair<int, int> chooseMoveComputer(int pos, int alpha, int beta) {
int value(positionValue(pos));
int bestNextPos(pos);
if (value==-1) {
value=alpha;
for (int i(1); i<3 && value<beta; ++i) {
pair<int, int> reply(chooseMoveHuman(2*pos+i, alpha, beta));
if (reply.first>value) {
value=reply.first;
alpha=value;
bestNextPos=2*pos+i;
}
}
}
return make_pair(value, bestNextPos);
}
pair<int, int> chooseMoveHuman(int pos, int alpha, int beta) {
int value(positionValue(pos));
int bestNextPos(pos);
if (value==-1) {
value=beta;
for (int i(1); i<3 && value>alpha; ++i) {
pair<int, int> reply(chooseMoveComputer(2*pos+i, alpha, beta));
if (reply.first<value) {
value=reply.first;
beta=value;
bestNextPos=2*pos+i;
}
}
}
return make_pair(value, bestNextPos);
}
int main() {
pair<int, int> res(chooseMoveComputer(0, 0, 15));
cout<<"Pos: 0"
<<" Value: "<<setw(2)<<res.first
<<" BestPos: "<<setw(2)<<res.second<<endl;
cin.get();
}
Uitvoer van dit programma
AlphaBeta1.cpp
:
Pos: 0 Value: 4 BestPos: 1
Jullie ook? Reacties zijn altijd welkom mailto:bd.hhs.nl