Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver pseudokod. 1 Kommer det här på tentan? Först skall vi repetera och kanske förtydliga problemlösningsprocessen. Sedan skall vi titta på en samling frågor man kan använda som bas för sitt problemlösande. Kom ihåg att problemlösning egentligen inte är bundet till datorer på något sätt men här är naturligtvis framställningen präglad av att vi söker en datoriserad lösning. Sen skall vi se hur man kan beskriva algoritmer. 1.1 PROBLEM OCH MODELLER Problem Vi har alltså ett problem vi vill lösa. Problemet måste vara lösbart (beräkningsbart) med dator eftersom det är sådana problem vi intresserar oss för här. Hurvida problem är lösbara studerar man i ämnet beräkningsbarhet (computability theory). Hurvida något är beräkningsbart har egentligen inget med datorer att göra utan handlar om hurvida det är beräkningsbart överhuvudtaget, oavsett metod. Vi måste börja med att försöka förstå problemet och kanske formulera om/förtydliga det så vi har en tydlig specifikation av vad lösningen skall prestera. Modeller Sedan måste vi hitta/skapa en (vanligen matematisk) modell så vi kan resonera om problemet, tex bevisa egenskaper hos det, och kan välja lösningsmetoder, i vårt fall algoritmer. En modell är ett försök att beskriva verkligheten (inte förklara den) på ett formaliserat sätt. En karta är tex en modell av geografin, Maxwells ekvationer beskriver elektriska och magnetiska fenomen osv. Man kan vanligen beskriva ett fenomen med flera modeller tex kan ljuset beskrivas både som elektromagnetisk vågrörelse och som partikelström. När modellerna överlappar varandra måste dom leda till samma resultat men en modell kan sällan beskriva verkligheten exakt. Man skall också vara medveten om att valet av modell delvis styr hur vi ser på världen och att vi blir styrda av modellvalet när den tex implementerats i en dator. 112 Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver Inom modellen representerar man vanligen problemet med en datastruktur tex en graf om man vill representera relationer mellan objekt. I många fall är datastrukturen modellen. Det är inte ovanligt så modellvalet består ofta i att välja datastruktur, sen ger sig resten. Modellen ger oss grunden för olika verktyg för att konstruera algoritmer. Den är alltså en grund för vad vi kommer att kunna göra när vi försöker lösa problemet. Algoritmdesign För att konstruera algoritmer har vi de olika vektyg som datastrukturer, färdiga algoritmer och algoritmdesignmetoder som modellen ger oss. Verktygslådan känner du förhoppningsvis till vid det här laget, den har presenterat i de föregående kapitlen. Analys av Problem och Algoritm När algoritmen är klar skall den analyseras ur olika aspekter. Att den är korrekt (och begriplig) bör redan vara avklarat, helst i form av ett formellt bevis. Sen är det algoritmens resurskrav (vanligen tid, minne) som är intressanta. Detta ger en övre gräns för hur lång tid det tar att exekvera algoritmen respektive minneskravet i värsta fall. För att avgöra om algoritmen är optimal analyserar vi sedan problemet och skapar ett bevis för hur lång tid det måste ta att lösa problemet i värsta fall Det ger oss en undre gräns. Om det finns ett “gap” mellan algoritmens övre gräns och problemets undre gräns så kan vi antingen försöka hitta en ny algoritm eller hitta ett nytt bevis inom den modell vi valt. Fungerar inte det måste vi byta modell och fungerar inte det så måste du försöka förändra problemet (tex generalisera, specialisera osv) för att försöka hitta en bättre algoritm. 1.2 ATT STÄLLA RÄTT FRÅGOR Mina uppgiftsformuleringar får ibland kritik för att dom är oklara eller ofullständiga. Det är ibland befogad kritik men ofta(st?) tycks elever ha inställningen att man efter att ha läst igenom ett problem omedelbart skall ha klart för sig vad man skall göra och hur det skall gå till. I formuleringen skall dessutom allt man behöver veta för att lösa problemet vara tydligt angivet. † Det tror jag är en ovanlig situation för en civilingenjör som ju ofta sysslar men olika former av produktutveckling‡. På en högskola blir det naturligtvis många givna “färdiga” problem som skall lösas, och även de problem jag ger är ju till stor del givna, men man behöver †. Det tycks gälla såväl er som eleverna i 2an att döma av en del kommentarerna på Il uppgifterna. Viss utveckling sker dock, ni klagar mycket mindre och ni löser oftast problemet rätt bra även om ni “knorrar” medan tvåorna ofta blir handfallna :-) ‡. Om man konstruerar tex batterieliminatorer och enda skillnaden är utspänningen så stämmer det naturligtvis men är det du vill göra? 1.2 Att ställa rätt frågor 113 också träna att utifrån en oklar och diffus fråga/uppgift försöka komma fram till vad som är problemet och hur det skall lösas vilket jag tror är mer verklighetsanpassat. Att göra det tar ofta tid. (Och det är kanske där problemet ligger för er del :-)) Skiena skriver: “Problemlösning är inte en vetenskap utan delvis konst och delvis skicklighet”. “Konst”-delen har väl med kreativitet och anlag att göra. Nyckeln till skickligheten är nog, förutom ren erfarenhet, att kunna formulera och ställa de rätta frågorna. När man sedan löser uppgiften så är det viktigt att man redovisar sina frågor och svaren samt motiverar sina slutsatser dvs alla frågor-svar har också en motivering till svaret. Det är tex vanligt att man sorterar först i en algoritm som löser binpackproblemet, inte alla förklarar varför det är bra. Det går inte att precist ange vilka frågor man skall ställa, det beror ju på problemets art och hur mycket som är givet, men man kan ge exempel på frågor, vissa generella och självklara, andra mer problemspecifika. För varje problem får man försöka svara på de frågor som “passar” problemet (dvs de som går att svara på) . Här är en liten lista. Många frågor är fritt översatta från boken The Algorithm Design Manual av Steven Skiena. Först något om distinktionen mellan strategi och taktik Strategi handlar om den “stora bilden”, om hur man skall angripa problemet. Vilken modell skall användas och hur skall man modellera problemet om man valt en graf som modell? Taktik handlar om hur man löser de små problemen. Skall vi använda en efterföljarlista eller en efterföljarmatris för att representera grafen? Bägge måste man hantera men man kan inte använda taktiklösningar för strategiproblem och vice versa. 114 Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver 1.3 FRÅGOR 1. Förstår du problemet? Vad skall du egentligen göra dvs vad går uppgiften ut på? a) Vilken information har vi och vilken behöver vi? b) Vilken av den givna informationen är bakgrundsinformation som egentligen inte behövs för att lösa uppgiften? c) Vad består indata av? Vad skall utdata vara? Hur skall utdata levereras? d) Varför är det ett problem att lösa detta? e) Kan du konstruera ett litet mini exempel som går att lösa för hand? Vad händer när du försöker göra det? f) Hur stor är en typisk probleminstans? Är det 10, 1000, 106 indata? g) Hur viktigt är det att lösningen är exakt? Kan det räcka med en approximativ lösning? h) Vad är de teoretiskt minsta respektive största lösningarna? (tex antalet paket i binpackproblemet, i TSP är summan av alla bågar en övre begränsning på vägen) i) Hur viktig är hastigheten? Minnesåtgången? Måste problemet lösas på 1 sekund eller 1 timme? (Tex kan ett problem som skall beräknas en gång ju få ta lite tid på sig medan ett problem som skall lösas upprepade gånger kanske måste gå fortare. Är det en realtidslösning så kanske det måste gå riktigt fort. Är det en raket så kanske det inte finns plats för något minne osv.) j) Hur mycket tid kan du lägga på att hitta en effektiv algoritm? Måste du fixa en lösning snabbt eller kan du pröva lite olika angreppsvinklar och ta den bästa du hittar? (Vem betalar och hur mycket) k) Vilken typ av problem är det? (Exempel på olika problemtyper: graf problem, numeriska problem, sträng problem, geometriska problem osv.) Kan problemet formuleras på flera sätt dvs som olika typer av problem? Vilket sätt verkar bäst? l) Hur skall du mäta kvaliteten på en lösning? (Ibland är det ju enkelt: skall man tex sortera så skall det vara sorterat) m) Finns det något vi kan göra för att göra problemet mer hanterbart tex förenkla det eller lösa ett generellare problem? 2. Vad är känt om problemet? a) Finns det information om det på internet? Prövade du med flera sökmotorer? Är informationen trovärdig? Finns det i någon lärobok? Finns det i någon av de kataloger över problem som finns? Finns det i någon av de kataloger över “svåra” problem (NPC problem) som finns? Vad sägs om problemet och dess lösningar där? 1.3 Frågor 115 (Letade du på rätt ställe? Kollade du i index under alla tänkbara nyckelord? b) Är problemet ett känt problem med polynomisk lösning? (Tex ett sorteringsproblem eller att hitta kortaste väg i en graf) c) Är problemet ett känt problem utan känd polynomisk lösning? - Se a). Ännu viktigare här. - Se “Kan du hitta en enkel heuristik för problemet?” - Fungerar intelligent uttömmande sökning? - Är probleminstanserna så små att en exponentiell algoritm skulle fungera? d) Finns det en implementation man kan använda? 3. Kan du hitta en enkel heuristik för problemet? a) Kan man lösa problemet genom att använda någon enkel regel tex tag största elementet / ett slumpmässigt element / noden längst bort först? b) Om det går: på vilka data fungerar det bra/mindre bra? Hur stämmer det med problemets data? c) På vilken typ av indata fungerar det dåligt? Om du inte hittar några, kan du motivera att den fungerar bra för alla indata? d) Hur snabb är din heuristik? Finns det en enkel implementation? 4. Finns det specialfall av problem som kan lösas enkelt? a) Kan man lösa problemet effektivt om man ignorerar några indata? b) Vad händer när man sätter några indata till triviala värden som 0 eller 1? Blir det lättare att lösa? c) Kan problemet förenklas så mycket att det kan lösas enkelt? Är problemet fortfarande intressant eller trivialt nu? (tex så är cylinderuppgiften en enkel variant av TSP) d) Varför kan inte algoritmen för det förenklade problemet användas till det ursprungliga problemet? e) Är ditt problem ett specialfall av ett mer generellt problem? (SOS är tex en enklare variant av kappsäcksproblemet som i sin tur är en enklare variant av ett flerdimensionellt kappsäcksproblem.) 5. Vilka standardmetoder verkar kunna fungera? a) Förenklas uppgiften om man gör någon form av “pre-computation” tex först sorterar indata? b) Kan man dela problemet i två (eller flera) mindre delar? Kan man då använda binärsökning? Kan man partitionera indata i på något sätt, stora - små, vänster - höger osv? Kan mer generell divide and conquer användas? c) Har indata en naturlig vänster till höger ordning som tex en sträng, elementen i en permutation eller löven i ett träd? Beräknas samma 116 Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver delproblem flera gånger? Kan man då använda dynamisk programmering? d) Gör man vissa operationer upprepade gånger tex söker efter något? Kan man snabba upp det genom att välja en bra datastruktur? (tex hashtabell, binärt sökträd, prioritetskö osv) e) Kan en väl vald datastruktur rent av “lösa” problemet? f) Kan problemet formuleras som ett linjärt program? Som ett heltalsproblem? 6. Frågor i samband med speciella lösningmetoder. a) Tex i samband med backtrack: Fast eller variabel tuppelstorlek? Vad skall vara “grenar” och vad skall vara “nivåer” i det abstrakta sökträdet? Vad kan man tänka sig för villkor för att klippa i trädet? Vad är den förväntade komplexiteten? 7. Fortfarande ingen lösning? a) Gå tillbaka och börja om med frågorna igen. Kanske blir svaren annorlunda andra gången. b) Är du villig att hyra en expert? 1.4 Att beskriva Algoritmer. 117 1.4 ATT BESKRIVA ALGORITMER. En fråga som är intressant är hur man skall beskriva algoritmer för andra (och sig själv). Man vill ju att det skall vara lätt att förstå algoritmen. En algoritmbeskrivning bör bestå av en beskrivning av vilken ide algoritmen bygger på, en abstrakt algoritm i pseudokod, huvudsakligen läsbar för människor, datastrukturbeskrivningar och implementationsdetaljer, korrekthetsbevis samt komplexitetsanalys. Punkterna 1-4 nedan utgör en beskrivning av hur man kan beskriva en algoritm. Ibland måste även (delar av) punkt 5 hänföras hit, nämligen i de fall då det inte är uppenbart att det går att hitta en implementering av någon ADT eller hjälpfunktion eller då man behöver mer information för att kunna analysera algoritmens komplexitet. Punkterna 1-6 utgör en implementation av algoritmen. I en fullständig beskrivning bör man även ge en introduktion till problemet, referenser och ha ett avsnitt med egna slutsatser och reflektioner. 1. Förklara hur algoritmen fungerar. För att göra det kan du använda både text, pseudotext och figurer, det viktiga är att Idén framgår tydligt och att man får hjälp att förstå den abstrakta algoritmen i punkt 2. Ett sätt att göra detta är att göra en ”torrsimning” på ett exempel. (Se tex. föreläsningsanteckningarna för Prims eller Kruskals algoritmer ) 2. Ge en (beskriv en) abstrakt algoritm. En abstrakt algoritm är en programspråksliknande beskrivning där du kan använda ADT’s för tex grafer och listor samt satser av typen ”for varje nod w på EL(v) loop”. Om du använder resultatet från en känd algoritm, tex. kostnadsmatrisen som fås av Dijkstras algoritm, så måste du beskriva hur det resultatet ser ut och, om det inte står annorlunda i uppgiften, även beskriva den kända algoritmen. Detta gäller i synnerlighet om du anpassat algoritmen lite för att passa ditt problem. Men var inte för abstrakt! Algoritmer måste formuleras noggrant och precist. Om du skall arbeta med ett binärt träd och tex. skriva ut ”kanten” så är inte ”genomlöp trädet” en bra abstraktion. Självklart skall de satser och underprogram du använder också vara implementerbara! Om det inte är uppenbart att de är det, så måste du visa hur du har tänkt dig att implementera dem. Gör i så fall det efter att du gett den abstrakta algoritmen, inte i den. De måste specificeras så väl att det är möjligt att kontrollera att algoritmen terminerar och att den är korrekt. 3. Bevisa att algoritmen fungerar. 4. Analysera algoritmens komplexitet. Observera att det inte räcker att konstatera vad komplexiteten är utan du måste motivera varför den är det du påstår att den är. Det är också viktigt att du analyserar din algoritm och inte utgår från kända fakta om problemet. 5. Beskriv de olika delarna i den abstrakta algoritmen. Tex. hur du implementerar de ADT’s som du använder, hur ”for varje nod på EL(v) loop” görs eller hur en ”hjälp” algoritm du använt fungerar. Om det är en standard algoritm räcker det vanligen om du gör punkt 2 eller hänvisar till en källa. Naturligtvis behöver du inte beskri- 118 Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver va hur man implementerar tex en lista eller hur en efterföljarmatris mm. ser ut. (om du inte använt en egen ny uppfinning förstås!) Främsta anledningen till detta arbete är att kunna avgöra om den abstrakta algoritmen kan implementeras och för att kunna beräkna komplexiteten för algoritmen samt som förberedelse till nästa punkt. 6. Implementera fler detaljer. Detta sker oftast i form av stegvisa förfiningar av algoritmen. Eventuellt skall alla detaljer implementeras. (Se tex. Kruskals eller Prims algoritmer. Observera att implementationen av Prims algoritm skiljer sig markant från den abstrakta algoritmen. Det beror naturligtvis på att vi skall förstå den abstrakta algoritmen och datorn skall förstå implementationen.) Under kursen/tentan är det nästan alltid steg 1+2+3+4(+5) som skall göras där (+5) endast görs när det är lämpligt tex för att göra en komplexitetsberäkning. Undantag från detta påpekas alltid speciellt i uppgiften. För mer noggran beskrivning av steg 1 se förel.anteckningar+bok. För mer noggran beskrivning av steg 2 se nedan. För mer noggran beskrivning av steg 3 se bok och hemsidan. För mer noggran beskrivning av steg 5 se förel.anteckningar+bok. För mer noggran beskrivning av steg 6 se grundkurserna pt+ptfk. 1.5 Pseudokod 119 1.5 PSEUDOKOD Låt oss exemplifiera hur det kan se ut under steg 2 i vår beskrivning ovan. Syftet med pseudokod är att ge en lättläst beskrivning, utan för mycket detaljer, av algoritmen och dess struktur. Samtidigt måste den vara tillräckligt precis för att man skall kunna analysera den och översätta den till körbar kod. Man skall alltså eftersträva en balans mellan begriplig Svenska (Engelska) och kodens precision, ofta måste man beskriva algoritmen i flera steg, på olika nivåer, så det är viktigt att abstrahera och dela upp algoritmen i delar som beskrivs var för sig. Man får inte vara för abstrakt. Exempel på för abstrakt pseudokod: • Om problemet är att sortera: “Sortera så att det blir sorterat”. • Om problemet är att beskriva mergesort: Dela problemet i två lika delar. Sätt ihop delarna så de blir sorterade. Upprepa rekursivt. • Oavsett problem: “Lös problemet” Exempel på bra nivå: Detta är en beskrivning av Kruskals algoritm som beräknar minimala uppspännande träd från Horowitz, Sahni, Rajasekaran: Computer Algorithms 1998, sid 224 (radummer kan man ha men det är ovanligt men här vill jag kunna hänvisa till speciella rader) 1 // E är mängden av alla bågar i grafen, 2 // t är det minimala uppspännade trädet 3 t = Ø 4 while ( t has < n-1 edges and E≠Ø ) do 5 Chose an edge (v,w) from E of lowest cost 6 Delete (v,w) from E 7 if (v,w) does not create a cycle in t then 8 add (v,w) to t 9 else 10 discard (v,w) 11 } Här framkommer algoritmens kärna utmärkt men det är massor av saker som inte enkelt kan göras om till kod. Om vi antar att grafhantering och mängdhantering är kända (dvs tex hur man tar bort en båge ur grafen (rad 6) och hur man kollar att E≠Ø (rad 4)) så måste man även precisera hur man tar fram den billigaste bågen (rad 5) och hur man kollar att ingen cykel uppstår (rad 7). Lämpligast är förmodligen att behålla koden ovan och göra separata beskrivningar av det. Algoritmens kärna skall ju framgå utan för mycket detaljer. Kanske måste man också utveckla algoritmen dvs i det här fallet göra den mer kodlik, se nedan. Men beskrivningen ovan skall naturligtvis vara med i algoritmbeskrivningen även i fortsättningen. Man måste också beskriva tex hur grafen är implementerad för att kunna analysera komplexiteten. Låt oss se hur Kruskals algoritm ser ut efter att man preciserat en del av dessa saker (från samma källa): 120 Problemlösning, att ställa rätt frågor, hur man kan beskriva algoritmer och hur man skriver 12 algorithm Kruskal (E, cost, n, t) 13 // E is the set of edges in G, G has n vertices, 14 // cost(u,v) is the cost of edge (u,v), 15 // t is the set of edges in the minimum-cost span. tree, 16 // the final cost is returned 17 18 Construct a heap out of the edge cost using Heapify; 19 for i:= 1 to n do parent[i]:=-1; 20 // Each vertex is in a different set 21 i := 0, mincost := 0.0; 22 while ((i<n-1) and (heap not empty)) do 23 Delete a minimum cost edge (u,v) from 24 the heap and reheapify using Adjust; 25 j := Find(u); k := Find(v); 26 if (j ≠ k) then 27 i := i+1; 28 t[i,1] := u; t[i,2] := v; 29 mincost := mincost + cost[u,v]; 30 Union(j,k); 31 } 32 } 33 if (i≠n-1) then write (“No span. tree”); 34 else return mincost; 35 } Nu är flera detaljer klara. Vissa rader ser ofärdiga ut (tex 18, 23, 24, 25) och det stämmer. Där sker olika anrop till en datastruktur som kallas för en “heap”. Även rad 19 är kryptisk för man ser inte ut att använda vektorn “parent” till något mer än att fylla den med -1 (men den hör till heapen). Allt detta måste förklaras mer noggrant om man inte anser att kunskap om en heap är allmängods, då räcker det att nämna dess komplexitet (och naturligtvis hur/ varför man använder den redan i samband med förra kodsnutten). Kvar är nu bara rad 30 vars komplexitet måste nämnas under punkt 4. Men det är fortfarande en del att göra innan det blir kod (steg 6) av detta. Fler exempel på pseudokod finns i de flesta algoritmböcker. Se tex en algoritm för att konstruera stabila matchningar från Kleinberg, Tardos: Algorithm design, 2004, sid 5