Programmeren A Genetisch Programma voor het Partitie Probleem begeleiding: Inleiding Het Partitie Probleem luidt als volgt: Gegeven een verzameling van n positieve integers, vindt twee disjuncte deelverzamelingen waarvan de vereniging weer de originele verzameling is èn zodat de som van de elementen van de ene deelverzameling gelijk is aan de som van de elementen van de andere deelverzameling. Een voorbeeld van dit probleem is een blokkendoos met daarin een aantal blokken die allemaal een bepaalde hoogte hebben. Zijn er nu van de blokken twee even hoge torens te bouwen zonder dat er een blok overblijft? Dit probleem lijkt niet echt bijzonder, maar tot nu toe heeft nog nooit iemand er een efficiënt algoritme voor bedacht dat altijd het juiste resultaat geeft. In deze opdracht gaan we een speciaal algoritme voor dit probleem ontwerpen. Het algoritme is speciaal, omdat a. het geheel anders werkt dan de algoritmen die je tot nu toe bij programmeren A hebt gehad, en b. het in somige gevallen niet eindigt. Het algoritme werkt globaal als volgt. Er wordt een aantal start-oplossingen gegokt. Vervolgens worden er wat oplossingen veranderd volgens twee hieronder omschreven principes. Daarna worden enkele minder goede oplossingen vervangen door betere oplossingen. Dan wordt er gekeken of er een oplossing bij zit die optimaal is (twee even hoge torens) en als dat niet het geval is herhaalt dit proces zich. Genetische Algoritmen Mensen die bekend zijn met de evolutie-theorie van Darwin herkennen hierboven misschien een soort evolutie. Dit is goed mogelijk, want de bedenkers van dit 1
soort algoritmen hebben Darwins evolutie theorie als uitgangspunt genomen. Men noemt deze algoritmen dan ook genetische algoritmen. Bij genetische algoritmen worden (mogelijke) oplossingen voorgesteld als chromosomen. Een chromosoom is een rijtje genen die allemaal een bepaalde waarde hebben. Een verzameling chromosomen noemt men ook wel een generatie. De evolutie is nu een proces waarin de éne generatie uit de andere ontstaat door kleine veranderingen van de chromosomen en door vervanging van minder goede chromosomen door betere chromosomen (survival of the fittest). Het idee is nu een generatie van chromosomen (= oplossingen) te laten evolueren om zo steeds betere chromosomen te krijgen. Uiteindelijk kan dan een chromosoom gevonden worden die een geschikte oplossing representeert voor het betreffende probleem. Chromosomen, mutaties en cross-overs Een chromosoom kan op twee manieren veranderen. We zullen dit laten zien door middel van voorbeelden. Stel een chromosoom bestaat uit 6 genen en elk gen heeft de waarde 0 of een 1. Dan is 011000 een voorbeeld van een chromosoom. De eerste manier om een chromosoom te veranderen is door middel van eenmutatie. Een mutatie houdt in dat één of meerdere genen van het chromosoom een andere waarde krijgen, ofwel, gemuteerd worden. Stel dat het 1 e en 4 e gen van 011000 een andere waarde krijgen. Het resultaat is dan: 111100. Merk op dat iets anders niet mogelijk is, want elk gen heeft de waarde 0 of 1. Als de genen van een chromosoom meer dan twee mogelijke waarden hebben, dan moet ook de nieuwe waarde van het gen, al dan niet willekeurig, bepaald worden. In deze opgave zullen we alleen met chromosomen werken waarvan de genen òf de éne waarde òf de andere waarde hebben. Genen zijn in deze opgave dus tweewaardig. De tweede manier waarop chromosomen kunnen veranderen is door middel van een cross-over. Als twee chromosomen 110011 en 010101 volgens het crossover principe veranderen kan het resultaat bestaan uit de chromosomen: 110101 en 010011. In dit geval is de eerste helft van 110011 gecombineerd met de tweede helft van 010101 en andersom. Echter, het hoeft niet zo te zijn dat het punt waar de chromosomen gesplitst worden precies op de helft van de chromosomen ligt. Een ander resultaat van een cross-over zou dus kunnen zijn: 110101 en 010011. Nu zijn de chromosomen na het eerste gen gesplitst. Het is mogelijk de chromosomen op meerdere plaatsten te kruisen, maar wij zullen deze mogelijkheid buiten beschouwing laten. De datastructuren Omdat genen tweewaardig zijn zullen we chromosomen gebruiken waarin elk gen 2
een boolean is. De constante aantal_blokken geeft aan hoeveel blokken er in de verzameling zitten. De verzameling blokken wordt gerepresenteerd door een array van integers. Element i van dit array geeft de hoogte van blok i. Elk chromosoom heeft voor elk blok een gen en is dus een array ter lengte aantal_blokken. Voor de duidelijkheid definiëren we een constante chrom_lengte die de lengte van een chromosoom voorstelt, dus chrom_lengte = aantal_blokken. Een generatie bestaat uit pop_grootte (populatie grootte) chromosomen. pop_grootte is ook een constante en een generatie is dus een array ter lengte pop_grootte van chromosomen. We krijgen zo de volgende declaraties: const aantal_blokken = 20; {aatal blokken in de invoer verzameling} chrom_lengte = aantal_blokken; {lengte van een chromosoom } pop_grootte = 10; {aantal chromosomen in een generatie } type blokken_rij = array [1..aantal_blokken] of integer; chromosoom = array [1..chrom_lengte] of boolean; generatie = array [1..pop_grootte] of chromosoom; Random getallen Voordat we aan de implementatie kunnen beginnen, moeten we eerst nog een manier hebben om willekeurige getallen te genereren. Dit is nodig, omdat zowel bij een mutatie als bij een cross-over een willekeurige positie in de chromosomen moet worden bepaald. We gebruiken hiervoor de volgende function: function random(i: integer): integer; { Deze function levert een random integer op uit het interval [0..i- 1]. De globale variabele random_int wordt bij elke aanroep aangepast. } const modulus = 65536; factor = 25173; toename = 13849; begin random := (random_int div (modulus div i)) mod i; random_int := (factor*random_int + toename) mod modulus; end; (* random *) Om deze function te gebruiken in je programma moet je het volgende doen: 3
1. Een globale variabele random_int van het type integer declareren. 2. De function copiëren in je programma. 3. De variabele random_int een initiële waarde geven, voordat je de function random voor het eerst aanroept. De getallen die de function random oplevert, zijn niet echt willekeurig. We spreken dan ook van een pseudo-random generator. De resultaten van random zijn te beïnvloeden door de initiële waarde van random_int. Opgave 1 Schrijf een procedure die de hoogtes van de blokken inleest. Er mag vanuit worden gegaan dat er precies aantal_blokken hoogtes ingevoerd worden. De procedure heeft een blokken_rij als parameter. In deze rij moeten de ingelezen hoogtes komen te staan. Merk op dat een hoogte een positief getal moet zijn! Opgave 2 Schrijf een procedure die een chromosoom een willekeurige waarde geeft. Gebruik hiervoor de gegeven function, random. Deze function geeft een willekeurige integer als resultaat. Bedenk zelf een geschikte manier om van zo n integer een boolean te maken. Opgave 3 Schrijf een procedure die één willekeurig gen van een chromosoom muteert. Gebruik hiervoor weer de random-function. Schrijf ook een procedure die twee chromosomen op een willekeurige plaats kruist en zo een cross-over uitvoert. Een chromosoom representeert dus twee deelverzamelingen blokken. Noem deze verzamelingen T en F. De verzameling T bestaat uit de blokken waarvoor het corresponderende gen true is. De verzameling F bestaat uit de blokken waarvoor het corresponderende gen false is. De hoogte van de toren van verzameling T wordt nu berekend door de hoogtes van de blokken waarvoor het corresponderende gen true is te sommeren. De hoogte van de toren van verzameling F wordt op een analoge manier berekend. Opgave 4 Schrijf de function: function hoogte_toren(ch: chromosoom; b: boolean): integer; 4
{ Deze function geeft de hoogte van de toren van verzameling T van ch als b = true en van F van ch als b = false. } Naast mutaties en cross-overs speelt er nog een ander principe een rol in de evolutie. Dit principe staat wel bekend onder de naam natuurlijke selectie. De bedoeling hiervan is dat sterkere chromosomen meer kans hebben te overleven dan zwakkere chromosomen. Bij ons probleem is een chromosoom sterker dan een ander chromosoom als het hoogteverschil tussen de T- en de F-toren van het chromosoom kleiner is dan het hoogteverschil tussen de T- en F-toren van het andere chromosoom. Opgave 5 Schrijf een function die gegeven een chromosoom het hoogteverschil tussen de T- en F-toren van dit chromosoom berekent. Hiervoor kan de function uit opgave 4 gebruikt worden, maar het is mogelijk een efficiëntere procedure te schrijven. Opgave 6 Als voor een chromosoom het hoogteverschil 0 is, is dat chromosoom een optimale oplossing voor het partitie probleem. Minder goede oplossingen zijn chromosomen met grotere hoogteverschillen. Een sterk chromosoom heeft dus een kleiner hoogteverschil dan een zwak chromosoom. Schrijf nu een function die de sterkte van een chromosoom geeft. We zijn nu al een eind op weg naar het uiteindelijke programma. We moeten nu alleen nog voor het volgende zorgen: Een nieuwe generatie maken van een oude generatie. Een selectie-mechanisme toevoegen, zodat zwakkere chromosomen verwijderd worden en sterkere verdubbeld. De evolutie simuleren. Een manier om het sterkste chromosoom in een generatie te bepalen, zodat gecontroleerd kan worden of er twee even hoge torens gevonden zijn. Een procedure die het uiteindelijke resultaat op een begrijpelijke manier afdrukt. 5
Wat betreft het maken van een nieuwe generatie waarschuwen we er voor dat de nieuwe generatie niet te veel moet afwijken van de oude. Hierdoor kan het namelijk zo zijn dat er naast de zwakke chromosomen ook veel sterke chromosomen verdwijnen. Het aantal cross-overs en mutaties moet dus niet te hoog zijn. Een mogelijke manier om te selecteren is: zoek de twee sterkste chromosomen en de twee zwakste en vervang de twee zwakste chromosomen door de sterkste twee. Een manier die minder gericht is op de aller sterkste is: kies twee paar chromosomen (willekeurig) en verdubbel van elk paar de sterkste ten koste van de zwakste. Deze laatste manier is enigszins te vergelijken met levende organismen. Wanneer twee organismen elkaar tegen komen, zal de sterkste van de twee de ander kunnen doden... De evolutie bestaat uit het afwisselen van de ene generatie chromosomen door de andere generatie. We kunnen het evolutie proces in een aantal stappen weergeven: 1. Maak nieuwe generatie, door middel van mutaties en cross-overs. 2. Voer selectie uit op nieuwe generatie. 3. Controleer of het sterkste chromosoom de gezochte oplossing is. 4. Zo nee, dan stap 1, zo ja, dan klaar. De evolutie stopt dus als er een optimaal chromosoom gevonden is. We kunnen echter een beetje soepeler zijn en de evolutie al laten stoppen als er een chromosoom gevonden is waarvan het hoogteverschil een bepaalde kleine waarde heeft, bijvoorbeeld 3 of minder. Dit is soms nodig, want er zijn verzamelingen blokken waarmee het niet mogelijk is twee even hoge torens te bouwen. Uiteindelijk geeft het evolutie-algoritme een resultaat. Dit is in eerste instantie een chromosoom. We willen echter een partitie van de verzameling blokken. Bedenk dus een manier om gegeven zo n chromosoom de verzamelingen T en F van dit chromosoom netjes af te drukken. Geef bij elke verzameling ook aan wat de hoogte is van de toren van die verzameling. Opgave 7 Schrijf nu een genetisch programma voor het partitie probleem. Test je programma met verschillende verzamelingen blokken. Probeer ook te onderzoeken wat het effect is van het aantal mutaties/cross-overs dat per evolutie-stap uitgevoerd wordt. 6
Beoordeling Het programma wordt beoordeeld op de werking, correctheid en efficiëntie van de algoritmen, duidelijkheid van de algoritmen, I/O en vooral ook op de leesbaarheid van de programmatekst. Naast de programmatekst moet ook voorbeeld invoer- en uitvoer ingeleverd worden. 7