INLEIDING Dynamisch Programmeren 1 Dynamisch Programmeren Section Page Inleiding................................................. 1 1 Oplossing................................................ 2 2 Subprobleem............................................. 3 3 Datastruktuur............................................ 4 4 Uitvoer.................................................. 5 4 Testdata................................................ 10 6 Rekenvoorbeeld......................................... 11 6 Programma............................................. 20 10 Index................................................... 22 11 Het Rugzakprobleem Piter Dykstra 12 januari 2004 1. Inleiding. Het probleem is als volgt. Een inbreker breekt in bij mensen die je rustig tot de categorie der zinloos rijken kunt rekenen. In de juwelenkamer vindt onze kleine zelfstandige een aantal items van verschillende omvang en verschillende waarde. Nu is de rugzak van onze handwerker beperkt van omvang, zodat het er op aan komt om alleen de meest waardevolle zaken (per volume) mee te nemen. De vraag is nu: Hoe kan de zakenman het best te werk gaan? Naïef beredeneerd zal bijna iedereen als volgt te werk gaan: 1. Je neemt de dingen met de hoogste prijs-per-volume-verhouding. Net zolang tot die er niet meer zijn, of totdat er geen plaats meer is in de rugzak. 2. Als er nog plek over is in de rugzak neem je daarna van het item dat dan de meest optimale prijs-volume-verhouding heeft zoveel als mogelijk. 3. En daarmee ga je door totdat je alle items hebt geprobeerd.
2 Dynamisch Programmeren OPLOSSING 1 Dit werkt in de praktijk uitstekend, zo wijst de ervaring uit. Maar de methode is niet helemaal waterdicht. En omdat het om echt groot geld gaat, is het wel de moeite om daar even god naar te kijken. Het is mogelijk dat door van een groot en duur ding er één te veel te nemen, er een redelijk grote ruimte in de rugzak overbijft, die niet meer optimaal gevuld kan worden. Door nou iets minder gretig te zijn op de allerduurste dingen, kan het zijn dat de rest efficiënter kan worden gevuld, zodat uiteindelijk toch meer waarde in de zak verdwijnt. Ver gezocht? Jazeker! Maar het aardige is dat de waterdichte oplossing een door zijn eenvoud schitterend voorbeeld van dynamisch programmeren geeft, waardoor het een stuk eenvoudiger wordt om alignment en clustering te begrijpen. 2. Oplossing. Om niet de gouden oplossing mis te lopen passen we dynamisch programmeren toe. Daarvoor zijn in het probleem 3 componenten nodig: 1. Een eenvoudig subprobleem, waarin een deel van het probleem wordt opgelost. Er moet uit een aantal item s worden gekozen totdat er niet meer in de zak past. Een lege zak is snel gevuld en geen items is ook snel opgelost. Hoe meer items des te moeilijker; hoe groter de zak, eveneens, des te moeilijker. Een subprobleem kan dus zijn: probleem(i, j ), waarin i het aantal items is, j de grootte van de zak en probleem is de oplossing van het hele rugzakprobleem voor i items en j-grootte; d.w.z. de manier om een rugzak ter grootte van j met i mogelijke items te vullen, zodat de waarde van die items samen maximaal is. We beginnen zonder items (i = 0) en met een zak waar niks in kan (j = 0). En van hieruit breiden we als een olievlek het aantal opgeloste subproblemen uit. De resterende twee componenten zijn eigenlijk eigenschappen waaraan het subprobleem moet voldoen. 2. Subprobleem-overlap. Bij de oplossing van een subprobleem moet er gebruik worden gemaakt van oplossingen van andere (kleinere) subproblemen. Dat heet de subprobleem overlap. We hebben twee mogelijkheden: als we probleem(i, j ) willen oplossen, kunnen we gebruik maken van probleem(i - x, j ) of probleem(i, j - y ). De vraag is nu of we uitgaande van de optimale deeloplossingen wel een optimale oplossing voor probleem(i, j ) kunnen krijgen?
2 SUBPROBLEEM Dynamisch Programmeren 3 We zullen in 3 zien dat dat inderdaad zo is. 3. Subprobleem-optimalisatie Wanneer we steeds meer subproblemen hebben opgelost, door i en j steeds op te hogen, komen we vanzelf terecht in een situatie waarin probleem(aantalitems, maxgrootte ) is opgelost. De vraag is dan of ook het hele probleem is opgelost? Dat is triviaal in ons geval omdat het subprobleem op twee parameters na gelijk is aan het hoofdprobleem, maar dat is in het algemeen niet zo; een subprobleem kan dan ook net iets anders geformuleerd zijn. 3. Subprobleem. Stel we willen probleem(i, j ) oplossen en we hebben optimale oplossingen voor probleem(i, j ) met i < i. Welke van die oplossingen kunnen we gebruiken om een optimale oplossing voor probleem(i, j ) te krijgen? Eigenlijk is de enige keus die we hebben: We gebruiken item i it wel (*), of niet (**). In dat laatste geval is: probleem(i, j ) = probleem(i - 1, j ).(**) Maar als het wel verstandig is om item i te gebruiken (*), dan is de meest optimale oplossing gelijk aan de meest optimale oplossing voor een rugzak die de grootte van item i kleiner is dan wat we nu hebben (!) plus de waarde van item i. Dus: probleem(i, j ) = probleem(i - 1, j - grootte(item(i)) ) + waarde(item(i)) (*) Ons subprobleem heeft dus de subprobleem-overlap-eigenschap! We hebben alleen een alternatief wanneer er een deelprobleem-oplossing bestaat, d.w.z. : j - grootte(item(i)) 0 Maar wanneer is (*) de aangewezen keus boven (**)? Wel, wanneer (*) meer oplevert dan (**), d.w.z. : probleem(i - 1, j - grootte(item(i)) ) + waarde(item(i)) > probleem(i - 1, j ) In ons geval zou dat als volgt kunnen: Stel we willen probleem(i, j ) oplossen en we hebben al optimale oplossingen voor probleem(i - 1, j ). Dan beschouwen we item i en we gaan na of de oplossing voor probleem(i,
4 Dynamisch Programmeren UITVOER 3 j - grootte(item(i)) ) plus de waarde(item(i)) meer is dan probleem(i - 1, j ). Zo ja, dan nemen we die nieuwe waarde, anders houden we gewoon probleem(i - 1, j ) als oplossing voor probleem(i, j ). De rest is implementatie. 4. Datastruktuur. We hebben in dit probleem niet te maken met een tweedimensionaal array, zoals bij paarsgewijze alignment, omdat we niet geïnteresseerd zijn in oplossingen met minder dan i items, wanneer we ze al voor i items hebben uitgerekend. Daarom houden we een ééndimensionaal array van oplossingen bij. Of eigenlijk twee arrays, want we gebruiken van de oplossing van een subprobleem twee zaken: 1. De waarde (ofwel rugzakwaarde ); de hoogste waarde van de items, die totnutoe in de rugzak van grootte i is verzameld. 2. Het item dat als laatste is toegevoegd aan de rugzak van grootte i. Meer is niet nodig! Of in Java: define Volume int define leeg 1 define Item int define Waarde int define maxvolume 23 De data 4 static Waarde [ ] rugzakwaarde new Waarde[maxVolume]; static Item [ ] rugzakitem new Item [maxvolume ]; See also section 10. 5. Uitvoer. Wanneer we voor de rugzakken van elke grootte weten wat er als laatste aan toe is gevoegd om een optimale inhoud te krijgen, dan kunnen we door gebruik te maken van de subprobleem overlap-eigenschap, bij de oplossing voor rugzak van grootte i volume(laatsteitem ), zien wat het voorlaatste item moet zijn geweest. Dat is namelijk van die rugzak het laatste item. Als we van rugzak met volume maxvolume 1 = 22 (meest rechtse in figuur 1) willen weten wat er allemaal in zit, dan zien we in rugzakitem [22] dat het item 5 als laatste is toegevoegd. Item 5 is 6 eenheden groot. Rugzak
5 UITVOER Dynamisch Programmeren 5 1 1 0 1 2 4 5 4 5 4 4 5 5 5 5 4 5 5 5 5 5 5 5 Fig 1. Het bepalen van de inhoud van de grootste rugzak (meest rechtse) aan de hand van de subproblemen. 22 is optimaal gevuld, omdat gebruik is gemaakt van de optimaal gevulde rugzak 22-6, dus rugzak 16 plus het waardevolste item in deze situatie. Bij rugzak 16 zien we dat item 5 daar ook als laatste is toegevoegd. En de optimale oplossing die daarvoor is gebruikt, was rugzak 16-6 = 10. Voor rugzak 10 was item 4 de beste keus in combinatie met de oplossing van rugzak 10-5 = 5. En rugzak 5 kan op zijn best één item 4 bevatten. De beste oplossing voor rugzak 22 is de lijst met items: 5, 5, 4, 4 met een totale waarde van 13 + 13 + 10 + 10 = 46! define writeln System.out.println Geef uitvoer. 5 static void printoplossing () { Item huidigeitem ; Neem de grootste zak. 6 while (rugzakitem [huidigezak ] leeg ) {... Tot we de bodem zien doen we het volgende: We nemen het item dat het laatst is toegevoegd. 7 En drukken het af. 8 En ga verder met volgende deeloplossing. 9 6. Neem de grootste zak. 6 Volume huidigezak maxvolume 1; This code is used in section 5. 7. We nemen het item dat het laatst is toegevoegd. 7 huidigeitem rugzakitem [huidigezak ]; This code is used in section 5.
6 Dynamisch Programmeren REKENVOORBEELD 8 8. En drukken het af. 8 writeln ("De rugzak bevat een item: " + huidigeitem + " met waarde: " + waarde [huidigeitem ]); This code is used in section 5. 9. En ga verder met volgende deeloplossing. 9 huidigezak huidigezak volume[huidigeitem ]; This code is used in section 5. 10. Testdata. Van de items moeten natuurlijk een paar eigenschappen worden bijgehouden: de Waarde en het Volume. Vergeef me de non-objectgeoriënteerde aanpak. Het volgende tabelletje, waarmee we het principe programmeren, wordt uitgevoerd als twee integer-arrays: Item Waarde Volume 0 3 2 1 4 3 2 7 4 3 8 6 4 10 5 5 13 6 De data 4 + static Waarde [ ] waarde { 3, 4, 7, 8, 10, 13 ; static Volume [ ] volume { 2, 3, 4, 6, 5, 6 ; 11. Rekenvoorbeeld. Allereerst lossen we het eenvoudigste geval op: geen items te stelen. We initialiseren de arrays rugzakwaarde en rugzakitem : Voor als er helemaal geen items zijn 11 for (int i 0; i < rugzakwaarde.length ; i++) rugzakwaarde [i] 0; for (int i 0; i < rugzakitem.length ; i++) rugzakitem [i] leeg ;
12 REKENVOORBEELD Dynamisch Programmeren 7 12. De arrays zien er dan zo uit: Grootte 0 1 2 3 3 4 5 6 7 8 9 - Item - Waarde 0 0 0 0 0 0 0 0 0 0 0 - Aan deze lege set van items voegen we steeds ééntje toe en gaan voor alle rugzakken na wat de dan de beste samenstelling wordt: Voor alle items i doe: 12 for (int i 0; i < waarde.length ; i++) 13. en rugzakken... Voor alle rugzakken j doe: 13 for (int j 0; j < maxvolume; j++) 14. Beginnend bij item 0 kijken we voor elke rugzak of we het resultaat kunnen verbeteren. Item 0 is 2 eenheden groot; dus bij elke even rugzak kan er weer een item bij. Nadat twee items 0 zijn toegevoegd, ziet de score er zo uit: Grootte 0 1 2 3 3 4 5 6 7 8 9 - Item 0 0 0 0 - Waarde 0 0 3 3 6 6 0 0 0 0 0 - Nadat alle rugzakken maximaal met alleen items 0 zijn gevuld: Grootte 0 1 2 3 4 5 6 7 8 9 10 - Item 0 0 0 0 0 0 0 0 0 - Waarde 0 0 3 3 6 6 9 9 12 12 15 - Daarna gaan we verder met item 1. Dat is 3 eenheden groot en kan dus pas voor het eerst in rugzak 3. En het levert ook een prestatieverbetering van 1 op! Item 1 heeft een lagere waarde/volume-verhouding dan item 0. Toch is er in de rugzakken met een oneven volume plaats voor zo n item, omdat de totale waarde van de rugzak net een puntje hoger uitkomt.
8 Dynamisch Programmeren REKENVOORBEELD 14 Een item 0 gaat er dus uit daarvoor in de plaats komt een item 1 en de zak (van het subprobleem) is helemaal gevuld. Grootte 0 1 2 3 4 5 6 7 8 9 10 - Item 0 1 0 0 0 0 0 0 0 - Waarde 0 0 3 4 6 6 9 9 12 12 15 - Als we aan rugzak 4 een item 1 (met waarde 4 en volume 3) willen toevoegen, moeten we bij rugzak 1 kijken, hoe die verder optimaal gevuld kan worden. Rugzak 1 is leeg; de score is dus 4 en dat is lager dan we hadden (6). Niks doen dus Bij rugzak 5 hebben we wel succes. Toevoegen van item 1 brengt ons bij rugzak 2 met score 3 en 3+5 = 7 is 1 meer dan we hadden. In rugzak 5 voegen we dus item 1 toe. De stand ziet er dan zo uit: Grootte 0 1 2 3 4 5 6 7 8 9 10 - Item 0 1 0 1 0 0 0 0 0 - Waarde 0 0 3 4 6 7 9 9 12 12 15-15. Wat we in elke stap doen is het oplossen van een subprobleem en in code ziet dat er zo uit: probleem(i, j). 15 Bepaal zoekindex subprobleem 16 if (zoekindex 0) { Bepaal zoekwaarde van dat subprobleem 17 if (rugzakwaarde [j] < zoekwaarde ) { Neem item j 18 16. Het subprobleem voor item i heeft deze index: Bepaal zoekindex subprobleem 16 int zoekindex j volume[i]; This code is used in section 15. 17. De waarde van alternatief (**) wordt:
17 REKENVOORBEELD Dynamisch Programmeren 9 Bepaal zoekwaarde van dat subprobleem 17 int zoekwaarde rugzakwaarde [j volume [i]] + waarde [i]; This code is used in section 15. 18. Werk de resultaten bij: Neem item j 18 rugzakwaarde [j] zoekwaarde ; rugzakitem [j] i; This code is used in section 15. 19. Nog twee tabellen waarin per array per item de stand nog eens is weergegeven.. rugzakitem voor i=0 tot maxvolume - 1: 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 ----------------------------------------------- -1-1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0-1 -1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0-1 -1 0 1 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2-1 -1 0 1 2 1 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2 2-1 -1 0 1 2 4 2 4 2 4 4 4 4 4 4 4 4 4 4 4 4 4 4-1 -1 0 1 2 4 5 4 5 4 4 5 5 5 5 4 5 5 5 5 5 5 5. rugzakwaarde voor i=0 tot maxvolume - 1: 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 --------------------------------------------------------------- 0 0 3 3 6 6 9 9 12 12 15 15 18 18 21 21 24 24 27 27 30 30 33 0 0 3 4 6 7 9 10 12 13 15 16 18 19 21 22 24 25 27 28 30 31 33 0 0 3 4 7 7 10 11 14 14 17 18 21 21 24 25 28 28 31 32 35 35 38 0 0 3 4 7 7 10 11 14 14 17 18 21 21 24 25 28 28 31 32 35 35 38 0 0 3 4 7 10 10 13 14 17 20 20 23 24 27 30 30 33 34 37 40 40 43 0 0 3 4 7 10 13 13 16 17 20 23 26 26 29 30 33 36 39 39 42 43 46
10 Dynamisch Programmeren PROGRAMMA 19 20. Programma. (Rugzak.java 20) public class Rugzak { De data 4 public static void main (String [ ] args ) { Voor als er helemaal geen items zijn 11 Voor alle items i doe: 12 { Voor alle rugzakken j doe: 13 { probleem(i, j). 15 printoplossing (); Geef uitvoer. 5 21. En wanneer we dit allemaal hebben uitgevoerd is de volgende tekst op het scherm onze beloning: piter@gandalf: /javablast/rugzak> make run javatangle Rugzak.w Output file(s): (Rugzak.java) javac *.java java Rugzak De rugzak bevat een item: 5 met waarde: 13 De rugzak bevat een item: 5 met waarde: 13 De rugzak bevat een item: 4 met waarde: 10 De rugzak bevat een item: 4 met waarde: 10 piter@gandalf: /javablast/rugzak>
22 NAMES OF THE SECTIONS Dynamisch Programmeren 11 22. Index. args 20. dynamisch 1. huidigeitem 5, 7, 8, 9. huidigezak 5, 6, 7, 9. Item [4], 5. item 2, 4. laatsteitem 5. leeg [4], 5, 11. length 11, 12. main 20. maxvolume [4], 5, 6, 13. omvang 1. out 5. overlap 2, 5. println 5. printoplossing 5, 20. programmeren 1. Rugzak 20. rugzakitem 4, 5, 7, 11, 18. rugzakwaarde 4, 11, 15, 17, 18. String 20. subprobleem 2, 5. System 5. Volume [4], 6, 10. volume 5, 9, 10, 16, 17. Waarde [4], 10. waarde 1, 4, 8, 10, 12, 17. writeln [5], 8. zoekindex 15, 16. zoekwaarde 15, 17, 18. Names of the Sections. (Rugzak.java 20) Bepaal zoekindex subprobleem 16 Used in section 15. Bepaal zoekwaarde van dat subprobleem 17 Used in section 15. De data 4, 10 Used in section 20. En drukken het af. 8 Used in section 5. En ga verder met volgende deeloplossing. 9 Used in section 5. Geef uitvoer. 5 Used in section 20. Neem de grootste zak. 6 Used in section 5. Neem item j 18 Used in section 15. Voor alle items i doe: 12 Used in section 20. Voor alle rugzakken j doe: 13 Used in section 20. Voor als er helemaal geen items zijn 11 Used in section 20. We nemen het item dat het laatst is toegevoegd. 7 Used in section 5. probleem(i, j). 15 Used in section 20.