|
|
load program |
|
load complete Delphi-7 project |
De Torens van Hanoi een bekende maar nog steeds leuke logistieke puzzel.
In dit artikel beschrijf ik een Delphi project waarmee je zelf oplossingen kunt zoeken.
Maar ook de computer kan de oplossing leveren.
Daarbij wordt een recursief algoritme gebruikt.
Het spel.
Bekijk eens het onderstaande plaatje:
6 stenen moeten één voor één worden verplaatst van A naar B.
Een steen mag daarbij nooit op een kleinere rusten.
Om dat te vermijden moet een derde (C) stapel worden gebruikt.
Voor slechts twee stenen is de procedure:
1. verplaatst bovenste steen van A naar C
2. verplaats steen van A naar B
3. verplaatst steen van C naar B
zie plaatje:
Twee stenen vereisen 3 verplaatsingen.
Hoeveel zetten (verplaatsingen) zijn nodig bij 3 stenen?
De truuk is om de twee bovenste stenen van A naar C te verplaatsen op eerder genoemde manier.
Verplaats dan de derde steen van A naar B.
Zet dan de twee stenen van C naar B.
We zien, dat dit 3 + 1 + 3 = 7 zetten vergt.
In het algemeen:
n stenen verplaatsen van A naar B vergt (n-1) stenen van A naar C,
één steen van A naar B en weer (n-1) stenen van C naar B.
Als n stenen in m zetten verplaatst kunnen worden dan kosten (n+1) stenen dus 2m+1 zetten.
n stenen verplaatsen kost 2n -1 zetten.
Bewijs:
Neem aan dat bovenstaande formule voor n juist is.
Dan vragen (n+1) stenen dus 2n+1 – 1 zetten (vervang gewoon n door n+1).
Maar er geldt natuurlijk ook: 2. (2n -1) + 1 zetten maar dit is gelijk aan de vorige formule.
Omdat de formule klopt voor n=1 klopt hij ook voor n=2, dus ook voor n=3 enzovoorts.
Opmerking: dit type bewijs heet “inductie”.
Het algoritme
De kracht van computers bevindt zich in de herhaling van code.
Dat kan op verschillende manieren.
We kennen subroutine calls, in Delphi bekend als procedures en functies.
Ook zijn er loops (lussen).
De lokale variabelen van functies en procedures bevinden zich op het stack.
Elke nieuwe aanroep van een procedure heeft zodoende zijn eigen lokale variabelen.
Welnu, binnen functies en procedures zijn er twee methodes om code te herhalen:
1. met loops (iteratie genoemd)
2. met recursie
In het algemeen ziet een loop er zo uit:
Gedurende de initialisatie worden variabelen op een beginwaarde gezet (meestal 0)
De test (van een variabele) bepaalt of de het proces wordt herhaald of de procedure wordt verlaten.
Recursie betekent dat de procedure (of functie) zichzelf opnieuw aanroept.
Dat is verwarrend, dus voor de duidelijkheid dupliceren we even de procedure.
Dat levert op in geval van procedure Proc met parameter a:
Parameter a is verschillend per aanroep!
Uiteraard is een test nodig om te beslissen of de procedure zichzelf moet aanroepen
dan wel de overige code moet uitvoeren.
Nu terug naar de Torens van Hanoi.
We geven waardes 1,2,3 aan de stapels A,B,C.
Als we een steen verplaatsen van (1) naar (2) dan is de reserve stapel gelijk aan 1 xor 2 = 3.
Bij verplaatsing van (2) naar (3) is de reserve stapel 2 xor 3 = 1. Handig!
Stel we moeten n stenen verplaatsen van stapel p1 naar stapel p2.
We bouwen de procedure
procedure solveGame(p1,p2,n : byte);
//move n stones from pile p1 to p2
var p3 : byte;
begin
if resetFlag then exit;
//
if (n = 1) then
begin
reportmove;
movestone(p1,p2); //1 stone to move
end
else
begin
p3 := p1 xor p2; //more than 1 stone to move
solveGame(p1,p3,n-1);
solveGame(p1,p2,1);
solveGame(p3,p2,n-1);
end;
end;
Dat is alles!
Per procedure aanroep verschijnen p1, p2, p3 apart op het stack.
Opmerkingen:
resetFlag wordt op true gezet door een klik op de RESET knop.
De reset aktie wacht tot de kraan tot stilstand is gekomen.
Reportmove schrijft een boodschap in het form om de voortgang weer te geven.
p1,p2,p3 zijn de stapels.
Voor het verplaatsen van 6 stenen van stapel 1 naar stapel 2 is deze simpele aanroep genoeg:
SolveGame(1,2,6);
Blijft over de vraag: wat doet procedure movestone ?
Het programma
Hieronder staat een iets verkleind plaatje van het programma in werking:
Er zijn twee modussen:
play: de speler zoekt naar oplossingen.
Solve : de computer zoekt naar een oplossing.
RESET forceert het spel in de startpositie met alle stenen op stapel A.
Programma structuur
Het project heeft 2 units en een form:
- unit1 : game control, event handling
- game-unit : tekenprocedures voor het spel met de kraan-bewegingen
- form1 : paintbox voor het tonen van het spel en knoppen voor controle
Tekenen geschiedt in een bitmap, die later naar de paintbox wordt gekopiëerd.
De tekenprocedures nemen het merendeel van de code voor hun rekening.
De kraan voor de stenenverplaatsing is puur voor de lol toegevoegd.
Deze kraan hangt aan een rail,
de delen boven en onder de rail worden door afzonderlijke procedures getekend.
Het bovenste deel kan alleen horizontaal bewegen, het onderste deel ook vertikaal.
De teken procedures zijn in drie levels verdeeld.
Level 1 procedures roepen level 2 aan, level 2 procedures level 3 om een klus te klaren.
level 1 paint procedures/functies:
function LoadStone(p : byte) : boolean;
pakt de bovenste steen van stapel p
exit true als OK, false als de stapel leeg is
function unloadStone(p:byte) : boolean;
zet de opgepakte steen op stapel p
exit true als OK, false als stapel p reeds een kleinere steen bevat
procedure movestone(p1,p2 : byte);
roept loadstone( ) en daarna Unloadstone( ) aan
alleen gebruikt door computer search
level 2 paint procedures/functies
procedure erasePile(p : byte);
procedure paintPile(p : byte);
wis/teken stapel p (1,2,3) met alle stenen
procedure moveCraneHor(pos : word);
procedure moveCraneDown(posY,clampPos : word);
procedure moveCraneUp;
beweeg kraan naar pixel positie pos (of posY, vertikaal)
clampPos is de horizontale positie van de grijparmpjes, die moeten zich instellen
op de breedte van de te grijpen steen.
level 3 paint procedures/functies (enkele)
procedure paintCraneRail;
procedure paintcranewheels;
procedure paintWheelJoint;
procedure paintUpperCrane;
procedure paintlowerCrane;
procedure paintCraneLoad;
procedure paintstoneRect(r : Trect; stoneNr : byte);
Er zijn er nog wat meer om bijvoorbeeld pixel posities van stenen of de kraan te berekenen.
Kijk a.u.b. naar de source code voor details.
Variabelen in de game_unit:
var map : Tbitmap;
game : array[1..3,1..maxheight] of byte;
pheight : array[1..3] of byte;//number of stones of pile
stonecount : byte;
cranePosX : word;
cranePosY : word;
craneLoad : byte; //0:no load 1..6:loaded with stone ..
cranePile : byte;
clampBase : word;
De stenen hebben een nummer afhankelijk van hun grootte.
1 is de kleinste, 6 de grootste.
Game(2,1) = 5 wil zeggen dat stapel 2 een steen van grootte 5 bevat op positie 1.
StoneCount is het aantal stenen in het hele spel, te veranderen met een UPDOWN control op form1.
De figuur hieronder toont de stenen op stapel A bij de start:
Hiermee besluit ik dit artikel over de Torens van Hanoi.
Toevoeging: Waarom ik recursie niet zo handig vind
Want op het eerste gezicht is de compacte code verbazingwekkend.
Maar als eerste nadeel blijkt zulke code lastig te begrijpen.
Ten tweede is het proces amper te besturen. Er kan niet tussentijds worden gestopt of hervat.
Om de computer te stoppen moet een truuk worden ingebouwd:
de resetRequest flag forceert exits , wat niet erg elegant is.
Een betere manier zou zijn om een array te maken met verplaatsingscodes
en die codes daarna één voor één uit tevoeren.
Dan kan de speler volledige controle houden, het zoekproces onderbreken,
het zelf overnemen of zetten terugnemen. Nu kan dat allemaal niet.
|
|