Problemlösning, att ställa rätt frågor, hur man kan beskriva

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