we hadden Backtracking verbetering i i Datastructuren college 0 0: : : 0: : : P r r r r r b r b r P r r r b r b r backtracking we hoeven vaak de kandidaat niet helemaal af te maken om hem te kunnen verwerpen hier verwerpen we een kandidaat permutatie zodra we keer hetzelfde element tegenkomen. je verwerpt dan in een klap alle voortzettingen scheelt veel werk backtracking: depth-first zoeken algemeen backtrack algoritme niet alle kandidaten hoeven even lang te zijn. rr r br b r b r r br b r We hoeven de boom niet echt te bouwen: doorloop boom depth-first: eerst kinderen, daarna broers en zussen kinderen van links naar rechts bijhouden van huidige pad + te onderzoek voortzettingen is voldoende begin met lege deeloplossing voegtoe ( i ) if (klaar) meld_succes; else for ( alle_voortzettingen ) voeg_toe_aan_deeloplossing; if ( past ) voegtoe (i+); haal_uit_deeloplossing; kan ook met extra functie het eigenlijke backtracken kan ook impliciet zijn 4
alternatief backtrack algoritme kortste pad van muis naar kaas begin met lege deeloplossing voegtoe ( i ) if (klaar) meld_succes; else for ( alle_voortzettingen ) deze staat nu if ( past ) binnen de conditie voeg_toe_aan_deeloplossing; voegtoe (i+); haal_uit_deeloplossing; 5 we willen niet alle oplossingen, maar de lengte van het kortste pad muis van niks, weet pas of er kaas is als hij er op staat zoek oplossingen met backtracken ommuur doolhof om testen op binnen doolhof blijven te voorkomen kortste pad kan geen cycle bevatten! plaats muizen op het pad om cycles te voorkomen 6 kortste pad representatie kortste pad algoritme normaal: pad = <V 0, V, V k-, V k- > met V 0 is beginpunt (muis) V k- is eindpunt (kaas) Voor alle i: V i en V i- grenzen aan elkaar we zoeken het kortste van die paden kortste pad ieder veld hoogstens keer voor hier bouwen we het pad in de doolhof vinden van buren is eenvoudig voorkomen van cycles door merken van velden geen extra geheugen nodig backtrack-functie levert afstand tot kaas op 7 maak met behulp van backtracking alle mogelijke paden (begin bij muis, geen cycles) functieresultaat is afstand tot de kaas int afstand ( veld ) if ( op eindpunt ) return 0; else if ( veld is vrij ) // de test! bezet veld; // voorkomt cycles in pad bepaal minimum afstand vanaf alle buren; maak veld vrij; // backtrack stap return minimum + ; else return oneindig; 8
kortste pad: C++ representatie enum Veld Vrij, Muis, Kaas, Muur ; const int Lengte = 8, Breedte = 7; const int Oneindig = Lengte * Breedte; Veld doolhof [ Breedte ] [ Lengte ] = Muur, Muur, Muur, Muur, Muur, Muur, Muur, Muur, Vrij, Vrij, Vrij, Muur, Vrij, Muur, Muur, Vrij, Muur, Vrij, Muur, Vrij, Muur, Muur, Vrij, Vrij, Vrij, Vrij, Vrij, Muur, Muur, Vrij, Vrij, Vrij, Muur, Vrij, Muur, Muur, Kaas, Muur, Vrij, Vrij, Vrij, Muur, Muur, Vrij, Muur, Vrij, Vrij, Vrij, Muur, Muur, Muur, Muur, Muur, Muur, Muur, Muur ; 9 kortste pad algoritme in C++ int kortstepad ( int x, int y ); // voor mutual recursion int stuurmuizenvanaf ( int x, int y ) int min_weg = kortstepad ( x+, y ); min_weg = min (min_weg, kortstepad ( x, y+ )); min_weg = min (min_weg, kortstepad ( x-, y )); return min (min_weg, kortstepad ( x, y- )); int kortstepad ( int x, int y ) switch ( doolhof [ x ] [ y ]) case Kaas: return 0; case Vrij: // de test! doolhof [ x ] [ y ] = Muis; int min_weg = stuurmuizenvanaf ( x, y ); doolhof [ x ] [ y ] = Vrij; // backtrack stap return min_weg + ; default: return Oneindig; i.p.v. for-loop 0 kortste pad programma int main ( ) cout << kortstepad ( 6, 4 ) << endl; system("pause"); return EXIT_SUCCESS; resultaat is 6 correct, maar niet erg informatief we willen graag het pad zelf kennen probleem: op het moment dat we het kortste pad vinden weten we niet dat dit zo is. oplossingen: alle paden bewaren: duur en onnodig kortste pad afdrukken met normaal backtrack algoritme vinden we alle oplossingen hoe vinden we het kortste? bewaar kortste tot nu toe ( ktnt ) i.p.v. meldsucces nieuwe oplossing korter dan ktnt? zo ja: ktnt = nieuwe oplossing als je alles geprobeerd hebt is ktnt de kortste! kortste pad tot nu toe bewaren
kortste pad afdrukken globale object kortste pad lengte kortste tot nu toe als reference argument geef huidige lengte mee zodat je weet of je iets beters gevonden hebt. void kortstepad ( veld, lengte, int& kortste ) if ( op eindpunt ) if ( lengte < kortste ) kortste = lengte; // beter pad gevonden kopieerpad ( ); else if ( veld is vrij ) // de test! bezet veld; // voorkomt cycles in pad kortstepad vanaf alle buren ( lengte+ ); maak veld vrij; // backtrack stap kortste pad printen in C++ void kortsteweg ( int x,int y, int lengte, int& kortste ) switch ( doolhof [ x ][ y ]) case Kaas: if ( lengte < kortste ) // koter pad? kortste = lengte; copyweg ( ); return; i.p.v. case Vrij: // de test! for-loop doolhof [ x ][ y ] = Muis; muizenvanaf ( x, y, lengte+, kortste ); doolhof [ x ][ y ] = Vrij; // backtrack stap return; default: return; 4 muizen vanaf de buren void muizenvanaf ( int x, int y, int lengte, int& kortste ) kortsteweg ( x+, y, lengte, kortste ); kortsteweg ( x, y+, lengte, kortste ); kortsteweg ( x-, y, lengte, kortste ); kortsteweg ( x, y-, lengte, kortste ); by reference argument: zelfde effect als globale variabele, maar mooier int main ( ) int kortste = Oneindig; kortsteweg ( 4, 6, 0, kortste ); if ( kortste < Oneindig ) cout << "De lengte van de korste weg is " << kortste << endl; toonweg ( weg ); else cout << "Geen weg gevonden\n"; toonweg ( doolhof ); system("pause"); return EXIT_SUCCESS; 5 korste pad: oplossing ##################### ### ### ### ### ### ### ### ### ### ### @ @ @ ### ### ### K ### @ @ ### ### ### @ ### ##################### wat iedereen binnen 0 seconden al gezien had 6 4
op de zelfde manier magische vierkanten in zo min mogelijk zetten met een paard op een schaakbord van een positie naar een andere vind een rondgang van het paard op het schaakbord zodat alle posities bezocht worden. 7 vierkant van N bij N bevat getallen van 0N -, of N som van alle rijen en kolommen is gelijk soms ook de som van de diagonaal gelijk oplossing 0 5 7 4 6 8 oplossing 0 7 5 4 6 8. 8 magische vierkanten met backtracking N bij N vierkant is matrix int[n][n] met een beetje moeite kunnen we de som van de rijen en kolommen uitrekenen +++(m-)+m = m*(m+)/ hier m = N - er bestaan betere algoritmen dan backtracking! dus totale som (N -)*(N -+)/ voor iedere rij/kolom is de som dus (N -)*N/ vul kolom voor kolom kijk of som nog steeds kan passen gebruik bool vrij[nn] om te kijken of getallen vrij zijn 9 magische vierkanten in C++ const int N = ; const int NN = N*N; const int SOM = (NN-)*N/; int main ( ) int m [ N ] [ N ]; bool vrij [ NN ]; for ( int i=0; i<nn; i+= ) vrij [ i ] = true; vul ( 0, 0, m, vrij, 0 ); system("pause"); return EXIT_SUCCESS; 0 5
vullen in C++ void vul( int i, int j, int m [N][N], bool vrij [NN], int s ) if ( j==n ) // vierkant vol? som in print ( m ); kolom else if ( i==n && s==som ) // kolom vol en som klopt? vul ( 0, j+, m, vrij, 0 ); else if ( i<n ) // nog wat toevoegen aan kolom? for ( int n=0; n<nn; n+= ) if ( vrij[n] && s+n<=som ) // n vrij en past hier? waar kijken we vrij [ n ] = false; naar de rijen? m [ i ][ j ] = n; vul ( i+, j, m, vrij, s+n ); // recursie vrij [ n ] = true; // backtrack stap vullen in C++ void vul ( int i, int j, int m [N][N], bool vrij [NN], int s ) if ( j==n ) print ( m ); bool klopt ( int m [N][N], int i ) else if ( i==n && s==som ) vul ( 0, j+, m, vrij, 0 ); int s=0; else if ( i<n ) for ( int j=0; j<n; j+= ) for ( int n=0; n<nn; n+= ) s += m [ i ][ j ]; if ( vrij[n] && s+n<=som ) return s==som; vrij [ n ] = false; m [ i ][ j ] = n; if ( j<n- klopt ( m, i )) vul ( i+, j, m, vrij, s+n ); vrij[n] = true; kan dit slimmer? representatie deeloplossing kortste pad analyse globale rij waar we oplossing in bouwen permutaties, n-queens één globaal bord/wereld waar we oplossing in bouwen muis in doolhof, kortste pad paard op schaakbord soms rij van borden nodig schaakspel, 5 puzzel Algemeen voor backtrack algoritmen: O(V L ) V aantal mogelijkheden L langst mogelijke pad niet beter dan brute force; effect afsnijden sub-bomen is onzeker Voor muis in doolhof: V = Lengte * Breedte n L = Lengte * Breedte n complexiteit = O(V L ) door slim te genereren: O( L ) = O( Lengte*Breedte ) dergelijke heuristieken helpen dus echt door afsnijden sub-bomen met predikaat valt hoeveelheid werk vaak nog wel mee 4 6
kanoën waar gebeurd: soldaten moeten een brede rivier oversteken kleine jongentjes willen wel helpen met hun kano maar, in de kano kunnen of kinderen, of soldaat dus niet soldaten, of soldaat + kind de oplossing is niet moeilijk, maar we gaan met backtracking een oplossing zoeken backtrack kanoën oplossing is een rijtje van die situaties klasse kant met: aantal jongens hier aantal soldaten hier kant van de kano methoden voor: is klaar? kan over varen? vaar over constructor 5 6 const int Begin = ; enum InBoot J, JJ, S ; enum Kano Hier, Daar ; class Kant public: int jongens; int soldaten; Kano kano; klasse kant Kant ( ): jongens ( Begin ), soldaten ( Begin ), kano ( Hier ) ; bool klaar ( ) return soldaten==0 && jongens==begin && kano==hier; ; bool kan ( InBoot i ); void vaar ( Kant& naar, InBoot i ); ; 7 de methode kan bool Kant :: kan ( InBoot i ) if ( kano==hier ) switch ( i ) case J: return jongens >0; case JJ: return jongens >; case S: return soldaten>0; else // kano==daar switch ( i ) case J: return Begin-jongens >0; case JJ: return Begin-jongens >; case S: return Begin-soldaten>0; 8 7
methode vaar void Kant :: vaar ( Kant& naar, InBoot i ) if ( kano==hier ) naar.kano = Daar; &! switch ( i ) case J: naar.jongens = jongens-; naar.soldaten = soldaten ; return; case JJ:naar.jongens = jongens-; naar.soldaten = soldaten ; return; case S: naar.jongens = jongens ; naar.soldaten = soldaten-; return; else. 9 uitvoer operator << ostream& operator << (ostream& os, Kant k) return os << "Kant: Jongens " << k.jongens << " soldaten " << k.soldaten << ", daar: Jongens " << begin-k.jongens << " soldaten " << begin-k.soldaten << (k.kano==hier?" kano is hier\n":" kano is daar\n"); 0 backtracken voor kortste oplossing nodig: huidig pad en beste tot nu toe + lengte const int MaxPad = 0*Begin; // kies iets dat zeker lang genoeg is Kant pad [ MaxPad ]; Kant kortste [ MaxPad ]; int lengte = MaxPad; // ktnt: pad // ktnt: lengte bewaren van huidige pad in beste tot nu toe: void bewaar ( int l ) lengte = l; for ( int i=0; i<=l; i+= ) kortste [ i ] = pad [ i ]; toevoegen void voegtoe ( int i ) assert ( i<maxpad- ); if ( pad [ i ]. klaar ( ) ) if ( i < lengte ) bewaar ( i ); type conversie else for ( InBoot ib=j; ib<=s; ib = InBoot ( ib+ ) ) if ( pad [ i ]. kan ( ib ) ) pad [ i ]. vaar ( pad [ i+ ], ib ); voegtoe ( i+ ); // backtrack stap is impliciet 8
resultaat: programma uitvoeren Assertion failed: i<maxpad- hoe kan dit?? cycle jongentje blijft heen en weer roeien wat doen we eraan? testen op nieuwe situatie kortste pad kan nooit keer dezelfde toestand bevatten andere mogelijkeheid: bovengrens op lengte pad void voegtoe ( int i ) assert ( i<maxpad- ); if ( pad[i].klaar() ) if ( i<lengte ) bewaar ( i ); else for ( InBoot ib=j; ib<=s; ib = InBoot ( ib+ ) ) if ( pad[i].kan( ib ) ) pad[i].vaar ( pad[i+], ib ); if ( isnieuw ( i ) ) voegtoe( i+ ); // backtrack toevoegen e poging 4 resultaat 0: Kant: Jongens soldaten, daar: Jongens 0 soldaten 0, kano is hier : Kant: Jongens 0 soldaten, daar: Jongens soldaten 0, kano is daar : Kant: Jongens soldaten, daar: Jongens soldaten 0, kano is hier : Kant: Jongens soldaten, daar: Jongens soldaten, kano is daar 4: Kant: Jongens soldaten, daar: Jongens 0 soldaten, kano is hier 5: Kant: Jongens 0 soldaten, daar: Jongens soldaten, kano is daar 6: Kant: Jongens soldaten, daar: Jongens soldaten, kano is hier 7: Kant: Jongens soldaten 0, daar: Jongens soldaten, kano is daar 8: Kant: Jongens soldaten 0, daar: Jongens 0 soldaten, kano is hier ga na dat dit een correcte oplossing is programma werkt ook voor andere Begin waarden resultaat Begin = 0: Kant: Jongens soldaten, daar: Jongens 0 soldaten 0 kano is hier : Kant: Jongens soldaten, daar: Jongens soldaten 0 kano is daar : Kant: Jongens soldaten, daar: Jongens soldaten 0 kano is hier : Kant: Jongens 0 soldaten, daar: Jongens soldaten 0 kano is daar 4: Kant: Jongens soldaten, daar: Jongens soldaten 0 kano is hier 5: Kant: Jongens soldaten, daar: Jongens soldaten kano is daar 6: Kant: Jongens soldaten, daar: Jongens soldaten kano is hier 7: Kant: Jongens 0 soldaten, daar: Jongens soldaten kano is daar 8: Kant: Jongens soldaten, daar: Jongens soldaten kano is hier 9: Kant: Jongens soldaten, daar: Jongens soldaten kano is daar 0: Kant: Jongens soldaten, daar: Jongens soldaten kano is hier : Kant: Jongens soldaten 0, daar: Jongens soldaten kano is daar : Kant: Jongens soldaten 0, daar: Jongens 0 soldaten kano is hier kan het net zo snel met jongens en soldaten? 5 6 9
reflectie void voegtoe ( int i ) assert ( i<maxpad- ); if ( pad[i].klaar() ) is het wel nodig om hier if ( i<lengte ) een abort te doen? bewaar(i); else for ( InBoot ib=j; ib<=s; ib = InBoot (ib+) ) if ( pad[i].kan(ib) ) pad[i].vaar ( pad[i+], ib ); if ( isnieuw ( i ) ) voegtoe( i+ ); // backtrack 7 void voegtoe ( int i ) if ( pad[i].klaar() ) if ( i<lengte ) bewaar(i); else if ( i<maxpad- ) for ( InBoot ib=j; ib<=s; ib = InBoot (ib+) ) if ( pad[i].kan(ib) ) zonder abort pad[i].vaar ( pad[i+], ib ); if ( isnieuw ( i ) ) voegtoe( i+ ); // backtrack is die nog echt nodig? Nee, maar wel verstandig om takken snel af te snijden 8 op de zelfde manier boer, geit, kool en wolf kannibalen en missionarissen. 9 heuristieken Archimedes: Eureka! slimme vondsten representatie van de toestand n-queens: rij van kolomen i.p.v. heel bord volgorde van nieuwe elementen in voegtoe eerste koningin op eerste helft van de velden afbreken van het zoeken er zit een cycle in het pad herkennen dat we hier al zijn geweest we hebben de gespiegelde toestand al gezien herkennen dat het niets meer zal worden we hebben al een kortere oplossing gezien. maken backtracking vaak net bruikbaar 40 0
moeten we backtracken? alleen als je geen beter algoritme weet: O(n L ) breadth-first zoeken is vaak sneller, maar kan veel administratie vergen breadth-first: doorzoek heel niveau voordat je naar kinderen kijkt. muis in doolhof breadth-first idee: merk alle velden met afstand tot de kaas op veld van de kaas is afstand 0 op alle buren is afstand op buren van veld met n is afstand hoogstens n+ Analyse: buren van veld met afstand n vinden en merken: O(Lengte*Breedte) = O(n ) = O(V) Alle L velden merken: O(L * V) = O(n 4 ) Onthouden wat te merken veld vinden in O() totaal L * O() = O(L) = O(n )!! 4 4 breadth-first doolhof breadth-first doolhof const int Muur=-, Kaas=0, Vrij=Oneindig; void vuldoolhof ( ) bool gevonden = true; // er is een veld met afstand n void merkveld ( int i, int j, int n ) if ( doolhof[i][j] > n ) doolhof[i][j] = n; in het begin alles Oneindig (groot) for ( int n=0; gevonden ;n++ ) gevonden = false; for ( int i = 0; i < Lengte; i++ ) iets slimmer dan n<lengte*breedte for ( int j = 0; j < Breedte; j++ ) if ( doolhof[i][j]==n ) // er is een veld op afstand n void merkvelden ( int i, int j, int n ) merkveld ( i+, j, n ); merkveld ( i-, j, n ); merkveld ( i, j+, n ); merkveld ( i, j-, n ); merkvelden ( i, j, n+ ); gevonden = true; 4 44
breadth-first doolhof void toonweg ( ) for ( int i = 0; i < Lengte; i++ ) for ( int j = 0; j < Breedte; j++ ) switch ( doolhof [i][j] ) case Vrij: cout << " "; break; case Muur: cout << "###"; break; default: cout << setw() << doolhof[i][j] << ' ';break; cout << endl; 45 breadth-first doolhof resultaat ##################### ### 4 5 6 ### 8 ### ### ### 5 ### 7 ### ### 4 5 6 ### ### ### 7 ### ### 0 ### 4 5 6 ### ### ### 5 6 7 ### ##################### kortste weg van ieder punt direct af te lezen door aanpassen doolhof is administratie klein met bijhouden velden (queue) gaat het nog beter denk twee keer na voor je gaat programmeren!! reflectie is belangrijk!!! 46 Wat hebben we gedaan dictaat: H boek: backtracking komt niet voor Zoeken van oplossingen: in principe boom van oplossingen depth-first: bouw steeds één tak; rij i.p.v. boom brute force: bouw complete kandidaat-oplossing test of kandidaat-oplossing voldoet backtrack: test bij iedere toevoeging of het nog wat kan worden Opgave: weer backtracking 47