Backtracking
Backtracking som løsningsmetode Backtracking løser problemer der løsningene kan beskrives som en sekvens med steg eller valg Kan enten finne én løsning eller alle løsninger Bygger opp løsningen(e) ett steg om gangen: Prøver etter hvert steg alle mulige steg videre Hvis vi ser at et steg ikke kan lede til en løsning stopper vi videre utbygging av løsningen (avskjæring) Tar da et steg tilbake (backtracking) og prøver i stedet neste mulige steg som kan lede til løsning
Backtracking vs. brute force Rå kraft (brute force) algoritmer: Genererer alle mulige sekvenser av steg som kan være en løsning For hver av de mulige løsningene: Sjekk om den er korrekt Ressurskrevende, ubrukelig for større problemer Algoritmer med backtracking: Er ofte basert på en rå kraft algoritme Stopper oppbyggingen av en mulig løsning når vi ser at den ikke kan være korrekt Effektiviteten avhenger av hvor smart/tidlig vi gjør denne avskjæringen av løsninger
Backtracking og rekursjon Backtrackingsalgoritmer er av natur rekursive: Vi tar et og et steg mot en fullstendig løsning I hvert steg løser vi det samme problemet Problemstørrelsen reduseres for hvert steg Bunnen i rekursjonen: Vi har bygget opp en komplett løsning og det er ikke flere steg igjen å ta
Problemeksempler Finne en korrekt rekkefølge: Dronningproblemet Fargelegging av kart Fordeling av ressurser iht. behov eller ønsker Løsning av kryssord, sudoku, brettspill Rutevalg: Finne beste vei gjennom et nettverk Labyrinter
Enkelt eksempel: En labyrint Kvadratisk rutenett der noen ruter er blokkerte Starter i øvre venstre hjørne, prøver å finne vei til nedre venstre hjørne Fire muligheter i hvert steg: Høyre, ned, venstre, opp Avskjæring: Går ikke ut av labyrinten Går ikke til blokkerte ruter Går ikke til ruter som er besøkt tidligere
Datastruktur for labyrinten Setter opp labyrinten som en todimensjonal tabell med heltall, med verdiene 1 (fri) og 0 (stengt) Mens algoritmen utføres: Markerer oppsøkte ruter med verdien 2 i tabellen Tar vare på veien, ved at ruter som ligger på funnet vei markeres med verdien 3 i tabellen
Rekursiv implementasjon Merker av alle besøkte ruter i labyrint-tabellen Bunn i rekursjonen når vi kommer til nedre høyre hjørne Maks. fire rekursive kall, med flytt til hver av naborutene (høyre, ned, venstre opp) som det er lovlig å flytte til Lovlig flytt: Naboruten finnes, er fri og ikke tidligere besøkt Stopper rekursjonen med en gang en vei er funnet Merker rutene på evt. vei når rekursjonen trekker seg tilbake etter at en vei er funnet Java-kode: labyrint.java * *: Inneholder flere tips til løsningen av oblig. 2...
Oblig. 2: Springerproblemet Springer: 2 frem, 1 til siden Starter med en springer i en gitt posisjon på et n x n brett Problem: Finn en måte å flytte springeren rundt på sjakkbrettet, slik at den er innom hver rute en og bare en gang en springertur Finnes ikke alltid løsning, avhenger av startposisjon og størrelsen på brettet
Springerproblemet og backtracking Løses på en tilsvarende måte som labyrinten: Bruk en 2D-tabell til å lagre hvilke ruter som er oppsøkt Må merke en rute som ubrukt igjen når vi backtracker Prøv alle lovlige steg (maks. 8) videre fra hver posisjon Ikke gå utenfor brettet, ikke oppsøk en rute flere ganger Ta vare på antall steg og hvilke flytt som utføres Problemet er løst etter n 2 steg (alle ruter er besøkt) Bruk gjerne labyrintprogrammet som utgangspunkt
Litt kombinatorikk Kombinatorikk: Metoder og formler for å telle opp antall mulige måter som vi kan gjennomføre steg-for-steg prosesser på Eksempler: Hvor mange ulike LOTTO-rekker finnes det? Hvor mange forskjellige flush kan vi få utdelt i poker? Hvor mange ulike oppstillinger av dronninger kan vi lage på et sjakkbrett, slik at ingen står i samme rad eller kolonne? Kjennskap til grunnleggende formler fra kombinatorikken er nødvendig for å kunne forstå kompleksiteten i en del backtrackingproblemer
Multiplikasjonsprinsippet En prosess består av å gjøre n (del)valg i rekkefølge I hvert delvalg har vi ki ulike alternativer, i = 1, 2,, n Hele prosessen kan da utføres på: k 1 k 2 k 3 k n-1 k n ulike måter
Multiplikasjonsprinsippet: Et eksempel Skal velge et antrekk som består av en bukse og en genser n = 2 delvalg k1 = 3 bukser k2 = 2 gensere Totalt: k 1 k 2 = 3 2 = 6 mulige ulike antrekk
Et eksempel til: Svenske bilskilt Vanlige svenske registreringsnumre for biler består av tre bokstaver etterfulgt av tre siffer (0 9) Bokstavene I, Q, V, Å, Ä, Ö brukes ikke pga. at de ligner for mye på andre bokstaver/tall I alt 23 ulike bokstaver kan velges Det er 97 tre-bokstavskombinasjoner som ikke er tillatt å bruke
Antall forskjellige svenske bilskilt n = 6 delvalg Tre valg av bokstaver, 23 ulike muligheter i hvert valg: k 1 = k 2 = k 3 = 23 Tre valg av siffer, 10 ulike muligheter i hvert valg: k 4 = k 5 = k 6 = 10 Antall ulike skilt som inneholder ulovlige bokstavkoder: 97 10 10 10 = 97 000 Totalt antall forskjellige svenske bilskilt som kan lages: 23 23 23 10 10 10 97 000 = 12 070 000
Permutasjoner Har n objekter som alle er ulike Objektene skal stilles opp i en eller annen rekkefølge Det å bestemme en rekkefølge kan sees på som å nummerere objektene fra 1 til n En slik nummerering av n objekter kalles for en permutasjon av objektene Å endre rekkefølger kalles å permutere (eller å stokke om, bytte om) I mange backtrackingsproblemer vil løsningen være en permutasjon som oppfyller visse krav
Permutasjoner, noen eksempler Alle 6 permutasjoner av 123: 123 132 213 231 312 321 Alle 24 permutasjoner av ABCD: ABCD ABDC ACBD ACDB ADBC ADCB BACD BADC BCAD BCDA BDAC BDCA CABD CADB CBAD CBDA CDAB CDBA DABC DACB DBAC DBCA DCAB DCBA
Hvor mange permutasjoner kan lages? Kan bruke multiplikasjonsprinsippet til å regne ut antall mulige ulike permutasjoner av n objekter En permutering er en prosess med n delvalg: Velg nr. 1 : n muligheter Velg nr. 2 : n 1 muligheter Velg nr. n - 1 : 2 muligheter Velg nr. n : 1 mulighet Total antall mulige permutasjoner av n objekter: n (n - 1) (n - 2) 3 2 1 = n! (n-fakultet)
Antall permutasjoner vokser raskt 1! = 1 2! = 2 3! = 6 4! = 24 5! = 120 6! = 720 7! = 5040 8! = 40320 9! = 362880 10! = 3628800 11! = 39916800 12! = 479001600 13! = 6227020800 14! = 87178291200 15! = 1307674368000 16! = 20922789888000 17! = 355687428096000 18! = 6402373705728000 19! = 121645100408832000 20! = 2432902008176640000
Program for å lage og skrive ut alle permutasjoner av tallene 1, 2,, n Permutasjonene bygges opp systematisk og rekursivt, ved at vi etter tur setter alle tallene inn som nummer en, deretter som nummer to osv. Bruker en global heltallsarray p av lengde n til å lagre permutasjonene som genereres Rekursjonen følger indeksene i p Tar vare på hvilke av tallene 1, 2,.., n som allerede er brukt i en permutasjon, i en global boolsk array brukt, (initielt lik false) slik at vi ikke setter inn samme tall to ganger
public static void lagperm(int indeks) { if (indeks == n) skrivperm(); else { for (int i = 0; i < n; i++) { if (!brukt[i]) { p[indeks] = i + 1; brukt[i] = true; lagperm(indeks + 1); brukt[i] = false; } } } } Java-kode med testprogram: permutasjon.java
Dronningproblemet n dronninger skal plasseres på et n x n sjakkbrett, slik at ingen av dronningene kan slå hverandre Vi skal lage et program som finner alle slike utplasseringer av n dronninger En dronning kan beveges fra ruten den står på, i begge retninger horisontalt, vertikalt og langs de to diagonalene
To løsninger for n = 8
Antall løsninger for voksende n n 1 1 2 0 3 0 4 2 5 10 6 4 7 40 8 92 9 352 n 10 724 11 2 680 12 14 200 13 73 712 14 365 596 24 227 514 171 973 736 25 2 207 893 435 808 352 26 22 317 699 616 364 044
Dronningproblemet: n delvalg Kan betraktes som et steg-for-steg problem I hvert steg velger vi en ny posisjon på brettet og setter ut én ny dronning Tidsforbruket for en algoritme som skal løse dronningproblemet avhenger av: Størrelsen på sjakkbrettet, n Hvor tidlig vi avskjærer utbyggingen av en løsning, dvs. tar vekk sist utplasserte dronning og går tilbake (backtracker) til den forrige
Rå kraft -løsning, versjon 0.1 Prøv med alle mulige utplasseringer av n dronninger For hver mulig utplassering: Test om dette er en løsning Det kan relativt enkelt bevises at antall løsninger som må lages og testes for n = 8 er: 64 63 62 61 60 59 58 57 / 8! = 4 426 165 368 For generell n er antall løsninger som må testes: n 2 (n 2-1) (n 2-2) (n 2 - n + 2) (n 2 - n +1 ) / n! Ubrukelig for n > 7 Testprogram: bruteforce1.java
Rå kraft - løsning, versjon 0.2 Avskjæring: Ikke sett dronninger på samme rad For hver rad: Prøv alle rutene Test om hver utplassering av n dronninger er en løsning Antall løsninger som må lages og testes for n = 8 kan beregnes med multiplikasjonsprinsippet: 8 8 8 8 8 8 8 8 = 8 8 = 16 777 216 For generell n: n n Ubrukelig for n > 8 Testprogram: bruteforce2.java
Litt smartere løsning Ikke sett dronninger i samme rad eller samme kolonne En løsning blir da en permutasjon av tallene 1, 2, 3,, n Antall løsninger som må lages og testes for n = 8: 8 7 6 5 4 3 2 1 = 8! = 40 320 For generell n: n! Ubrukelig for n > 11 { 4, 7, 3, 8, 2, 5, 1, 6 } Testprogram: bruteforce3.java
Enda smartere løsning Sett ut dronningene rad for rad For hver ledige kolonne i raden: Sjekk om noen av dronningene som allerede er satt ut ovenfor kan nå dette feltet langs de to diagonalene Hvis ikke, sett ut dronning i denne kolonnen og gå videre til neste rad, ellers prøv neste ledige kolonne Hvis ingen av kolonnene kan brukes, backtrack til forrige rad og prøv en ny plassering der
Animasjon: Backtracking frem til første løsning
Implementasjon Bruker programmet for å lage permutasjoner til å bygge opp løsningene på dronningproblemet rad for rad: Permutasjonene som genereres representerer en løsning som er under utbygging Vil automatisk unngå dronninger i samme rad/kolonne Trenger bare å utvide permutasjonsprogrammet med en funksjon som sjekker de to diagonalene bakover Java-kode: dronning.java