Innledning La oss tenke oss at vi har en grunnskole-klasse på 25 elever der enkelte av elever er uvenner med hverandre. Hvis uvenner sitter nær hverandre blir det bråk og slåssing. Er det mulig å plassere elevene slik at uvenner ikke kommer i nærheten av hverandre? En tenkelig løsning vil være å prøve forskjellige måter å plassere de 25 elevene til man finner en plassering som kan godtas. Dessverre kan 25 elever plasseres på 25!=1,55*10 25 måter. Det vil si at en svært rask datamaskin som genererer og tester en milliard permutasjoner per sekund vil bruke ca 500 millioner år på å teste alle permutasjonene. Og hvis det er mye uvennskap i klassen er det ikke en gang sikkert at man da har løst problemet. (Hvilket kanskje ikke er så interessant etter 500 millioner år) Mange problemer vi prøver å løse kan tilnærmes ved en eller annen form for testing av alle mulige permutasjoner, men vi støter dessverre stadig på samme begrensning når antallet komponenter man skal generere permutasjoner av stiger blir problemet fort uhåndterlig. I det følgende skal vi vise forskjellige algoritmer for å generere permutasjoner. Vi vil også diskutere avskjæring, en metode for å redusere tidsforbruket, slik at det blir mulig å håndtere litt større problemer. En algoritme for permutasjonsgenerering For enkelhets skyld vil vi generere permutasjoner av tallene fra 0 til n-1 i en array. Det er enkelt å endre algoritmen slik at den permuterer andre typer objekter. Ideen er at vi bytter om på rekkefølgen av tallene på en systematisk måte slik at alle mulige permutasjoner blir generert. Algoritmen er vist på neste side. Prinsippet er at lagperm(int j) kalles rekursivt. Lagperm lager alle permutasjoner fra og med siffer j, til og med siffer n-1. Dette gjøres ved at siffer j byttes mot siffer j+1 og lagperm(j+1) kalles. Deretter byttes siffer j mot j+2 og lagperm (j+1) kalles. Dette fortsetter til alle sifre til høyre for siffer j har byttet plass med j og alle permutasjoner av disse sifrene er blitt dannet. For å rydde alt tilbake på plass før lagperm(j) returnerer, blir alle siffer til venstre for j flyttet en plass til venstre og siffer j flyttes til slutt. Denne operasjonen kalles rotervenstre(j) La oss si at vi skal generere permutasjoner av fire tall slik at vi starter med disse tallene i arrayen numbers: 0 1 2 3 lagperm(0) kaller først lagperm(1) som kaller lagperm(2) som kaller lagperm(3), som skriver ut 0 1 2 3 og returnerer til lagperm(2). Lagperm(2) kjører så bytt(2,3) og vi får 0 1 3 2. Deretter kjøres lagperm(3) som skriver ut 0 1 3 2 deretter retur til lagperm(2) som kjører rotervenstre(2) og vi er tilbake til 0 1 2 3 Deretter returneres til lagperm(1) som kjører bytt(1,2) og vi har 0 2 1 3. lagperm(1) kaller da lagperm(2), som kaller lagperm(3), som skriver ut 0 2 1 3 og returnerer. lagperm(2) kaller bytt(2,3), og vi har 0 2 3 1, lagperm(3) kalles og skriver ut 0 2 3 1
Deretter retur til lagperm (2) som kjører rotervenstre(2) og vi er tilbake til 0 2 1 3 Retur til lagperm(1) som nå kaller bytt(1,3) og vi får 0 3 1 2. Kall til lagperm(2) som kaller lagperm(3) som skriver ut 0 3 1 2 og retur til lagperm(2) som kjører bytt(2,3) og vi får 0 3 2 1, som skrives ut via lagperm(3) som 0 3 2 1 og ryddes med rotervenstre(2) til 0 3 1 2. Retur til lagperm(1) som kjører rotervenstre(1) og rydder opp til 0 1 2 3, Deretter retur til lagperm(0) som kjører bytt(0,1) og vi har 1023 osv. public class SimplePermGenerator { int n; //antall tall som skal permuteres int[] numbers; //permutasjonene lages her public SimplePermGenerator(int n) { numbers=new int[n]; for (int i=0; i<n;i++){ numbers[i]=i; //lager den første permutasjonen 0, 1, 2,.. void lagperm(int j){ if (j==n-1){ printperm(); else{ //basistilfelle vi er kommet til siste tall //en ferdig permutasjon kan skrives ut. //lag alle permutasjoner av de neste tallene for (int i=j+1;i<n;i++){ bytt(j,i);//bytt tall j med hver av de neste tallene //lag alle perms. av de neste tallene rotervenstre(j);//rydd opp etter alle byttene return; void bytt(int i, int j){ int temp=numbers[i]; numbers[i]=numbers[j]; numbers[j]=temp; return; void rotervenstre(int j){ //flytter tallene til høyre for j ett trinn mot venstre og legger //tall j helt bakerst. int temp=numbers[j]; for (int i=j;i<n-1;i++) numbers[i]=numbers[i+1]; numbers[n-1]=temp; return; void printperm(){ for(int i=0;i<n;i++)system.out.print(numbers[i]+" "); System.out.println(""); public static void main(string[] args){ SimplePermGenerator permgen=new SimplePermGenerator(4); permgen.lagperm(0);
En enklere algoritme Hvis en studerer permutasjonene som ble generert i SimpleGenerator : 0 1 2 3, 0 1 3 2, 0 2 1 3, 0 2 3 1, 0 3 1 2, 0 3 2 1, 1 0 2 3, 1 0 3 2, osv. ser en at en enklere måte å generere dem på kan oppnås ved å betrakte hvert tall som et siffer i et n tallssystem og så bruke en telleteknikk der en går fra 0000 og oppover. Dersom en så dropper alle kombinasjoner der noen sifre er like har man laget permutasjonene. En algoritme som bruker denne teknikken er gjengitt nedenfor: public class SimplerGenerator { int n; int[] numbers; boolean [] brukt; void lagperm(int j){ for (int i=0;i<n;i++){ if (!brukt[i]){ numbers[j]=i; brukt[i]=true; if (j==n-1)//ferdig permutasjon printperm(); else brukt[i]=false; public SimplerGenerator(int n) { numbers=new int[n]; brukt=new boolean[n]; Den boolske arrayen brukt[ ] viser om et gitt tall allerede er i bruk. Hvis så er tilfelle hopper vi over å bruke tallet og prøver neste. Merk at når vi slutter å bruke et gitt tall i setter vi brukt[i] til false. Som man ser slipper vi her hjelperutinene bytt og rotervenstre. Vi har fått en enklere algoritme. Denne algoritmen kan virke mindre effektiv ettersom vi må gjennomgå alle "tall" som kan lages ved hjelp av de aktuelle "sifrene" og ikke bare permutasjoner av sifrene slik som i den forrige algoritmen, men den enkle algoritmen sparer tid på å være enklere slik at man ved lave n ikke vil merke særlig forskjell. Den enklere rutina har også den fordelen at den holder greie på hvilke tall som er brukt. Dette kan i enkelte tilfelle være nyttig når vi skal gjøre avskjæringer..
Avskjæringer For å redusere antall permutasjoner å generere og dermed redusere kjøretida når vi skal løse et problem, kan vi kutte videre beregninger av permutasjoner hvis de tallene vi hittil har generert ikke kan godtas. Ved søk etter konfliktfri plassering av elever, som vi innledet med, kan vi avbryte videre generering av en rekke permutasjoner hvis vi har konflikt mellom elever vi allerede har plassert. Hvis elev 0 og elev 1 er uvenner er det bortkastet arbeid å lage permutasjoner der disse elevene blir plassert ved siden av hverandre, det vil si at vi kan kutte ut (skjære av) alle permutasjoner som begynner med 0 1 og alle permutasjoner som begynner med 1 0. Å kutte videre beregning når vi vet at det ikke er vits i å fortsette kalles avskjæring (pruning på engelsk). Hvis avskjæringen er så sterk at alle permutasjoner som blir ferdig generert er brukbare, sier vi at avskjæringen er fullstendig. Eksempel: Dronningoppgaven: Dronning er en sjakkbrikke som kan bevege seg både vannrett, loddrett og diagonalt. Dronningoppgaven går ut på å plassere n dronninger på et nxn sjakkbrett slik at de ikke kan slå hverandre. Da kan ingen dronninger stå i samme kolonne og ingen dronninger kan stå på samme linje. To dronninger kan heller ikke stå på samme diagonal. Her er en løsning på et 4 x 4 brett. 0 1 2 3 0 D 1 D 2 D 3 D Denne løsningen kan representeres ved permutasjonen: 1 3 0 2 Oppgave: Forklar hvordan representasjonen av dronningposisjonene som permutasjon sikrer at ingen dronninger står på samme linje eller i samme kolonne. En løsning er da gyldig dersom ingen dronning står på samme diagonal. To dronninger i og j er på samme fallende (\) diagonal dersom: numbers[i] i = numbers[j] - j To dronninger i og j er på samme stigende (/) diagonal dersom: numbers[i] + i = numbers[j] + j På neste side er SimplerGenerator utstyrt med en avskjærende testrutine testdiag som sjekker at den dronninga som ble plassert sist ikke er på samme diagonal som noen tidligere plassert dronning. Dersom den er på samme diagonal blir videre beregning på permutasjonen avskåret. Oppgave: Er avskjæringa på neste side fullstendig?
public class Dronning { int n; int[] numbers; boolean [] brukt; public Dronning(int n) { numbers=new int[n]; brukt=new boolean[n]; for (int i=0; i<n;i++){ numbers[i]=i; void lagperm(int j){ for (int i=0;i<n;i++){ if (!brukt[i]){ numbers[j]=i; brukt[i]=true; if ((j==n-1)&&testdiag(j))//ferdig permutasjon printperm(); else if (testdiag(j)) brukt[i]=false; boolean testdiag(int j){ boolean ok=true; for (int i=0;i<j;i++){ ok = ok && ((numbers[i]-i)!=(numbers[j]-j)); ok = ok && ((numbers[i]+i)!=(numbers[j]+j)); return ok; Oppgave: Skriv om den andre permutasjonsgeneratoren slik at du får plassert kall til test for avskjæring. (Tips se liten skrift nedenfor) Du trenger testkall på tre steder
Generalisering av permutasjonsgenerator Som vi ser av det siste eksempelet, kan permutasjonsgeneratoren spesialtilpasses til problemet som skal løses, men for å spare utviklingsarbeid er det bedre å lage en generell permutasjonsgenerator som kan brukes til ulike problemer uten å endres. Vi trenger da et grensesnitt som spesifiserer hvordan permutasjonene kan avskjæres og hvordan de kan overleveres til klassen som skal bruke dem. Forslag til et slikt grensesnitt følger nedenfor: public interface PermInterface { // implementeres av klasser som skal bruke en permutasjonsgenerator. public void useperm(); //melding om at ny permutasjon er klar til bruk public boolean cutoff(int j);//angir om beregning skal avskjæres Da må permutasjonsgeneratoren tilpasses slik at den bruker dette interfacet for å kommunisere med klassen som skal bruke den: public class PermGenerator { int n; //antall tall som skal permuteres int[] numbers; //permutasjonene lages her PermInterface pinterface; public PermGenerator(int n,perminterface p) { pinterface=p; numbers=new int[n]; for (int i=0; i<n;i++){ numbers[i]=i; void lagperm(int j){ if (j==n-1){ if (!pinterface.cutoff(j)) pinterface.useperm(); else{ if (!pinterface.cutoff(j)) for (int i=j+1;i<n;i++){ bytt(j,i); if (!pinterface.cutoff(j)) rotervenstre(j); return; //rotervenstre og bytt er som vist tidligere
Nedefor ser du en klasse som løser dronningoppgaven ved hjelp av den generelle permutasjonsgeneratoren fra forrige side. public class Dronning2 implements PermInterface{ int n; PermGenerator pg; Obs! public boolean cutoff(int j){ boolean cut=false; for (int i=0;i<j;i++){ //skjær av hvis dronningen er på samme diagonal som en annen cut = cut ((pg.numbers[i]-i)==(pg.numbers[j]-j)); cut = cut ((pg.numbers[i]+i)==(pg.numbers[j]+j)); return cut; public void useperm(){ for (int kol=0;kol<n;kol++)system.out.print("+---"); System.out.println("+"); for (int linje=0;linje<n;linje++){ for (int kol=0;kol<n;kol++) if (pg.numbers[kol]==linje) System.out.print(" D "); else System.out.print(" "); System.out.println(" "); for (int kol=0;kol<n;kol++)system.out.print("+---"); System.out.println("+"); System.out.println(""); public Dronning2(int n) { pg=new PermGenerator(n, this); pg.lagperm(0); public static void main(string[] args){ Dronning2 dronningene=new Dronning2(8); Permutasjonsgeneratoren kjøres med lagperm(0) Det er sikrest å instansiere en ny permutasjonsgenerator når du skal starte med ny permutasjonsgenerering, men det er ikke sikkert det er nødvendig Hvis du ikke vil generere alle mulige permuteringer, men greier deg med den første løsningen, må du bygge om permutasjonsgeneratoren slik at den kan stoppes, f.eksempel ved å avskjære ytterligere permutasjonsgenerering når et flagg boolean ferdig er satt. Det kan finnes bedre alternativer enn permutasjonsgenerering for å finne løsninger på et gitt problem. Husk at permutasjonsgenerering kan få svært lang kjøretid når det er mange elementer som skal permuteres. Det er dårlig design at permutasjonsgeneratoren eksponerer numbers-arrayen som public. Oppgave: Forbedre dette!