Algoritmer och datastrukturer Backtracking Dynamisk programmering Föreläsning 12 (Weiss kap. 7.6-7) Snåla algoritmer Backtracking Exempel: Kappsäcksproblemet, labyrintsökning Dynamisk programmering © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Exempel: Kappsäcksproblemet Kan man ur en grupp föremål F1,…,FN med vikterna V1,…,VN välja ut en delgrupp som väger exakt M kilo? Exempel: Man kan packa till alla totalvikter upp till 21 kilo utom till vikterna 1, 4, 17 och 20 föremål F1 F2 F3 F4 F5 vikt 3 7 2 3 6 Analogt problem: Kila under en byrå med träbitar av olika tjocklek. © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
löser ibland inte problemet alls Snåla algoritmer En snål (eng. greedy) algoritm väljer den för tillfället optimala lösningen utan hänsyn till framtida konsekvenser. Exempel: En snål strategi för kappsäcksproblemet vore att prova vikterna i avtagande följd. Metoden löser inte problemet givet uppsättningen på föregående bild för t.ex. totalvikten 11kg. Varför inte? Problemet kan dock lösas med en snål algoritm om vikterna är tvåpotenser 1, 2, 4, 8, 16, … Om Vikterna är 1,2,4,8,...,$2^n$ kan vi packa upp till totalvikterna 1,...,2n+1-1. Dessutom kan man använda en snål algoritm (detta har med optimalitet att göra) analogt: Ta klossarna i avtagande ordning Tag 7 (skippa sen 6) + 3 = 10 och sen är det kört … skulle hoppat över 7 och tagit 6 + 3 + 2 i stället Snåla algoritmer löser ibland inte problemet alls löser inte alltid problemet optimalt (Weiss coin) © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Rekursiv algoritmidé Kappsäcksproblemet kan lösas rekursivt: Antag att vi skall packa upp till M kilo med föremål ur samlingen F1,…,FN Fallanalys: (1-3 är basfallen) 1. Om kappsäcken väger M kilo har vi löst problemet. 2. Om kappsäcken väger mer än M kilo har vi misslyckats. 3. Om det finns mer plats men inte fler föremål har vi misslyckats. återstår två fall: (rekursionssteget) 4. Vi tar med FN och försöker packa resterande utrymme med F1,…,FN-1 5. Vi slänger FN och försöker packa resterande utrymme med F1,…,FN-1 Systematiskt steg för steg uttömmande sökning kombinatorisk explosion N! fall vad krymper mot noll? Hitta måttet! Exakt M kilo är målet Vad gör vi rekursion över? (samtidigt över kvarvarande utrymme och antal föremål) basfallen kanske inte uppenbara? säcken står på en våg - vet inte vad föremålen väger © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Förenkla problemet genom att bara betrakta vikterna V1,…,VN Låt S beteckna packningens vikt Generalisering: Definiera predikatet packa(S, M, V1,…,VN ) som är sant om det finns en delvektor i V1,…,VN vars summa är M - S och falskt annars Problemet är att avgöra om det finns en delvektor i V1,…,VN vars summa är M. ej konsekutiv delvektor M = Mål S = Summa (readn gjort) M – S = kvar att göra Spec S = 0 vid starten så packa(0,M,V1…VN) är ett specialfall av ovanst. © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Definiera packa(S, M, V1,…,VN ) rekursivt: Basfallen: 1. Returnera sant om S = M 2. Returnera falskt om S > M 3. Returnera falskt om S < M och N = 0 Rekursionssteg: S < M och N > 0. Lös problemet för utrymmet M - S och vikterna V1,…,VN Rekursionsantagande: Antag att packa(X, M, V1,…,VN-1 ) löser problemet för ett godtyckligt X S samt vikterna V1,…,VN-1 Svårt rekursionsantagande M - X <= M - S det är denna differens som krymper eller # vikter Tavlan först! OBS, det finns material på papper här… rita roten i beslutsträdet och visa vilka parametrar som krymper rita upp trädet för Packa(0,9,(2,7,3)) © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Med stöd av rekursionsantagandet kan vi anta att: packa(S + VN, M, V1,…,VN-1 ) löser problemet för fallet då VN tas med och att packa(S, M, V1,…,VN-1 ) löser problemet för fallet då VN slängs Alltså: returnera SANT för S och V1,…,VN om packa(S + VN, M, V1,…,VN-1 ) eller packa(S, M, V1,…,VN-1 ) returnerar SANT. X är här S + Vn >= S ok enl. ant. © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Implementering i Java Vad händer om vi vänder på anropen? boolean packa( int S, int M, int[] V, int N ) { if ( S == M ) return true; else if ( S > M || N == 0 ) return false; else return packa( S + V[ N - 1 ], M, V, N - 1 ) || packa( S, M, V, N - 1 ); } Vad händer om vi vänder på anropen? Samma resultat, olika ber.ordning © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Vilka vikter löste problemet? En vikt får bara skrivas ut när vi vet att den bidrar till lösningen. boolean packa( int S, int M, int[] V, int N ) { if ( S == M ) return true; else if ( S > M || N == 0 ) return false; else if( packa( S + V[ N - 1 ], M, V, N - 1 ) ) { System.out.print( V[ N - 1 ] ); } else return packa( S, M, V, N - 1 ); } Rek. funktion med sidoeffekt. Det som skrivits kan ej göras ogjort © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Backtracking Att lösa ett problem genom att rekursivt prova alla möjligheter. I varje rekursionssteg väljs den bästa dellösningen. Exempel Labyrint Tic-Tac-Toe se Weiss 7.7 © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Exempel: Labyrintsökning Givet: En rektangulär labyrint L med ickecykliska gångar En position (cell) i labyrinten är antingen ”golv” eller ”vägg” (svart) Gångarna har bredden 1 Förflyttning sker stegvis en cell i taget vågrätt eller lodrätt 1 2 3 4 5 6 7 8 9 10 Förväxla ej med formuleringen I inl. uppg. 6 © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Tre olika sökproblem Resultat 1. Finns det en utväg från (x,y)? Ja/Nej 2. Skriv ut en utväg från (x,y) en väg 3. Skriv ut den kortaste utvägen från (x,y) optimal väg Jfr vägnät + bärbuske © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Genom att ta ett steg kommer vi in i en dellabyrint. (Man backar bara till en cell för att prova en annan riktning.) Ett steg uppåt från (5,6) tar oss till dellabyrinten (L1) Ett steg nedåt från (5,6) tar oss till dellabyrinten (L2) 1 2 3 4 5 6 7 8 9 10 5 6 7 8 9 4 1 2 3 10 © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Divide & Conquer Problemet att hitta ut från (5,6) i L kan delas upp rekursivt i delproblemen att hitta ut via (4,6) i L1 eller via (6,6) i L2, eftersom vi kan nå L1 och L2 i ett steg 1. Det finns en väg ur L om det finns en väg ur L1, eller en väg ur L2 2. Skriv ut en väg via L1, eller en via L2 3. Skriv ut den kortaste av vägarna via L1 resp. L2 Basfall: a) Återvändsgränd b) Ute Rekursionsantagande: Sökalgoritmen kan avgöra problemet för dellabyrinterna De tre problemen Om labyrinten innehåller cykler leder ett steg inte alltid tillen mindre dellabyrint. Man får en dellabyrint som innehåller cykeln och som inte krymper vidare. Ickevälgrundat. Nix rekursion! © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Pseudoalgoritm Datatyp för labyrinter boolean finnsUtväg( pos, labyrint ) { om pos är utanför labyrinten returnera SANT annars för varje riktning r { p' = steg( pos, r ) om ärTillåtetSteg från pos till p' och finnsUtväg( p', labyrint ) } returnera FALSKT Datatyp för labyrinter Hjälpfunktioner ges av pseudoalg. © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Klassen Labyrint Behövs datatyper för Labyrinten Positioner Riktningar public class labyrint { public Labyrint( String filnamn ); public void hittaUt (Position startPos ) { ... } private boolean ärUtanför( Position p ) { ... } private Position steg( Position p, Riktning r ) { ... } private boolean ärTillåtetSteg( Position gPos,Position pos, Riktning rikt ) { ... } private boolean sök ( Position gPos, Position pos ) { ... } private boolean ärGolv( Position p ) { ... } private char[][] labyrinten; private int bredd, höjd; } Behövs datatyper för Labyrinten Positioner Riktningar Inför typnamnen först - vänta med def. Ops från pseudoalg. Jag använder ÅÄÖ på bilderna bara © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Datatyper för positioner och riktningar public class Position { public Position( int rad, int kolumn ) { this.rad = rad; this.kolumn = kolumn; } public boolean equals( Position rhs ) { return ( rad == rhs.rad && kolumn == rhs.kolumn ); } public int rad, kolumn; }; public enum Riktning { VÄNSTER, UPP, HÖGER, NER } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Tre användbara operationer boolean ärUtanför( Position pos ); // avgör om pos är utanför labyrinten eller ej Position steg( Position pos, Riktning riktning ); // returnerar positionen man kommer till // genom ett steg från pos i angiven riktning bool tillåtetSteg( Position gPos, Position pos, Riktning riktning ); // avgör om ett steg från pos i angiven riktning // leder utanför labyrinten, eller till en annan // golvposition än gPos gPos x<--/--x Pos © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Ett steg i rätt riktning…? gPos vi gick härifrån ... Nix backa! sök rikt pos ...och hit steg(pos,rikt) kan vi gå dit? © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden ärUtanför boolean ärUtanför( Position p ) { return p.rad < 0 || p.rad >= höjd || p.kolumn < 0 || p.kolumn >= bredd; } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden steg Position steg( Position p, Riktning r ) { switch ( r ) { case VÄNSTER: return new Position( p.rad, p.kolumn - 1 ); case HÖGER: return new Position( p.rad, p.kolumn + 1 ); case NER: return new Position( p.rad + 1, p.kolumn ); case UPP: return new Position( p.rad - 1, p.kolumn ); } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden ärTillåtetSteg boolean ärTillåtetSteg( Position gPos, Position pos, Riktning rikt ) { Position nyPos = steg( pos, rikt ); return ärUtanför( nyPos ) || ( labyrinten[nyPos.rad][nypos.kolumn] == golv && ! nyPos.equals( gPos ) ); } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden hittaUt Varför Startpos två gånger? void hittaUt( Position startPos ) { if ( sök( startPos, startPos ) ) System.out.print( "det finns en utväg” ); else System.out.print( ”det finns ingen utväg” ); } Varför Startpos två gånger? © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden Sök boolean sök( Position gPos, Position pos ) { if ( ärUtanför( pos ) ) return true; else { for ( Riktning riktning : Riktning.values() ) { if ( ärTillåtetSteg( gPos, pos, riktning ) && sök( pos, steg( pos, riktning ) ) ) } return false; // inlåst!? © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Hur skriver man ut vägen? När kan man tidigast skriva ut information om vägen? Hur räknar man ut den kortaste vägen? © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden hittaUtväg Varför Startpos två gånger? void hittaUtväg( Position startPos ) { if ( ! sökUtväg( startPos, startPos ) ) System.out.print( ”det finns ingen utväg” ); } Varför Startpos två gånger? ! sök… - annars skrivs den ut © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden sökUtväg Konsekvens: vägen skrivs ut baklänges SLUTA HÄR boolean sökUtväg( Position gPos, Position pos ) { if ( ärUtanför( pos ) ) return true; else { for ( Riktning riktning : Riktning.values() ) { if ( ärTillåtetSteg( gPos, pos, riktning ) && sökUtväg( pos, steg( pos, riktning ) ) ) { } return false; // inlåst!? System.out.print( riktning ); Konsekvens: vägen skrivs ut baklänges SLUTA HÄR © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Hur skriver man ut vägen rättvänd? Lägg riktningarna på en stack istället för att skriva ut dem under sökningen Skriv ut innehållet i stacken när sökningen är klar Alt. Lägg dem i en kö på väg ner i rekursionen. Skriv ut kön i basfallet. OBS Denna metod fungerar inte för att hitta den kortaste vägen © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden hittaRättUtväg void hittaRättUtväg( Position startPos ) { Stack<Riktning> vägen; if ( sökRättVäg( startPos, startPos, vägen ) ) while ( ! vägen.isEmpty() ) { skrivRiktning( vägen.peek() ); vägen.pop(); } else System.out.print( ”det finns ingen utväg” ); © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden sökRättVäg boolean sökRättVäg( Position gPos, Position pos, Stack<Riktning> vägen ) { if ( ärUtanför( pos ) ) return true; else { for ( Riktning riktning : Riktning.values() ) { if ( ärTillåtetSteg( gPos, pos, rikt ) && sökRättVäg( pos, steg( pos, rikt ), vägen ) ) vägen.push( rikt ); } return false; © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Hur finner man den kortaste vägen? Inför en speciell datatyp för vägar Operationer: returnera vägens längd: längd() lägg till en riktning: add skriv ut vägen: print() Man kan inte skriva förrän man vet vilken som är kortast Vägar måste kunna hanteras som värden Flera vägar kan behöva jämföras © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Klassen Väg Man måste inte använda en kö, men det är praktiskt class Väg { public Väg() { längden = Integer.MAX_VALUE; // Antag ”oändligt” lång vägen = new LinkedList(); } public int längd() { return längden; } public Väg add( Riktning r ) { ... } // Ger en ny väg inkl. r public void print() { ... } private LinkedList<Riktning> vägen; // FIFO-kö private int längden; }; Man måste inte använda en kö, men det är praktiskt © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden hittaKortasteVägen void hittaKortasteVägen( Position startPos ) { Väg kortasteVägen = sökKortasteVägen( startPos, startPos, new Väg() ); if ( kortasteVägen.längd() < Integer.MAX_VALUE ) kortasteVägen.print(); else System.out.print( ”det finns ingen utväg” ); } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Metoden sökKortasteVägen Väg sökKortasteVägen( Position gPos, Position pos, Väg vägen ) { if ( ärUtanför( pos ) ) return vägen; else { Väg kortast, aktuell; for ( Riktning riktning : Riktning.values() ) { if ( ärTillåtetSteg( gPos, pos, rikt ) ) { aktuell = sökKortasteVägen( pos, steg( pos, rikt ), vägen.add( rikt ) ); if ( aktuell.längd() < kortast.längd() ) kortast = aktuell; } return kortast; Alla riktningar analyseras Här är det riktig backtracking! © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Dynamisk programmering Problemet En rekursiv D&C-algoritm som löser många överlappande delproblem blir orimligt ineffektiv Dynamisk programmering Lösningarna på delproblemen sätts successivt in i en tabell. Inget delproblem löses flera gånger. Metoden kräver extra minne men kan i gengäld bli avsevärt effektivare än en naiv D&C-algoritm. Exempel: se Weiss 7.6 Det finns ofta ett minimeringsvillkor med i bilden t.ex. kappsäck med bivillkoret att så få vikter som möjligt skall användas Se Coin change © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Ex. fibonaccital med dynamisk programmering Hämta värdet från tabellen om n 7 Komplettera tabellen om n > 7 1 2 3 5 4 8 13 6 21 7 fibTable Det finns ofta ett minimeringsvillkor med i bilden t.ex. kappsäck med bivillkoret att så få vikter som möjligt skall användas Se Coin change Tekniken kallas memoization © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Implementering i Java public class MemoizingFib { private long[] fibTable; int highFibIndex; public MemoizingFib(int size) { fibTable = new long[size]; fibTable[0] = fibTable[1] = 1; highFibIndex = 1; } public long fib(int n) { if ( n >= fibTable.length ) // allocate a bigger table and copy elements if ( n > highFibIndex ) { fibTable[n] = fib(n-1) + fib(n-2); highFibIndex = n; } return fibTable[n]; } I lata funktionella språk kan man använda en lat oändlig lista som definieras ömsesidigt rekursivt med funktionen public static void main(String[] args) { MemoizingFib mf = new MemoizingFib(100); for ( int n = 0; n < 100; n++ ) System.out.println(mf.fib(n)); } © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/
Komplexitet trade off: tid - minne T(N) = tidsåtgång M(N) = minnesbehov för beräkning av det N:te fibonaccitalet Iterativ fib Naiv rekursiv fib Memoiserande fib WC T(N) = O(N) M(N) = O(1) T(N) = O(2N) M(N) = O(N) AC T(N) = O(1) M(N) är O(N) för rekursiv fib eftersom stacken når djupet max N antalet anrop är exponentiellt, men rekursionsdjupet är aldrig större än N. © Uno Holmer, Chalmers, 2019-01-01 www.cse.chalmers.se/~holmer/