I120 - Algoritmer, Datastrukturer og Programmering

Oppgavesett 3

   Fredag 15.9.00

Oppgave 1: Fibonacci-utskrift

Bruk programmet for beregning av Fibonacci-tall (uten bruk av memoisering) fra forelesningen, og utvid det med utskriftkommandoer slik at du får en utskrift som markerer, for hvert rekursivt kall, Dybden kan passelig markeres med indentering av utskriften. For eksempel, kallet fib(4) (dvs. >javac fib med påfølgende argument 4) burde produsere noe som følger:
f(4) ...
  f(3) ...
    f(2) ...
      f(1) ...
      = 1
      f(0) ...
      = 1
    = 2
    f(1) ...
    = 1
  = 3
  f(2) ...
    f(1) ...
    = 1
    f(0) ...
    = 1 
  = 2
= 5

Gi en enkel regel som bestemmer, for en slik utskrift, hvilken returverdilinje '=x' som tilhører en gitt kallinje 'f(y)...'
 

     

Oppgave 2: Rekursjon og avskjæring

Dette er en "oppgavifisering" av endel av notatet ["Kombinatorisk søking, rekursjon, avskjæring" av Stein Krogdahl, Universitetet i Oslo.]
  1. I den første delen ønsker vi å generere/skrive ut alle mulige kombinasjoner av lengde n av de n første tallene. For n=3 vil vi skrive ut sekvensene
  2. 111  112  113  121  122  ...  ...  332  333
    Vi har derfor en global array int[] p=new int[n], og ønsker å lage metoder som genererer alle mulige kombinasjoner. For n=3 kunne en iterativ variant være
    void gen1() {
      for (int i=1;i<=3;i++) {p[0]=i; gen2();}
    }
    
    void gen2() {
      for (int i=1;i<=3;i++) {p[1]=i; gen3();}
    }
    
    void gen3() {
      for (int i=1;i<=3;i++) {p[2]=i; "skriv ut p";}
    }
    
    int[] p=new int[3];
    For n=3 gjør gen1() jobben med å generere (i p) alle kombinasjoner fra 111 til 333. Vi merker at det første sifferet (p[0]) går "sakte" opp, mens det siste (p[2]) går fort. Det er imidlertid  kjedelig/meningsløst/unødvendig å skrive 3 neste like metoder gen1, gen2 og gen3. Dessuten er det umulig å få til dette for vilkårlige n. Derfor skal vi nå la
    void gen(int i)
    være en rekursiv metode der gen(k) genererer alle mulige kombinasjoner av 1...n i segmentet p[k-1] til p[n-1] (dvs. den genererer fra det k'te tallet i sekvensen). Eksempelvis svarer gen(1) til gen1, gen(2) til gen2 osv. Merk at gen(n) må foreta selve utskriften. Programmer en slik rekursiv metode og test den ut for n=4.
  3. I stedet for å generere alle sekvenser av lengde n skal vi i fortsettelsen generere alle permutasjoner av tallene 1 til n. (En permutasjon av 1,2,...,n er en "omordning" av 1,2,...,n. Eksempelvis er 321 en permutasjon av 123, mens 211 ikke er det.) Vi kan først benytte oss av løsningen til det forrige punkt ved at vi like før selve utskrivningen sjekker at p faktisk er en permutasjon av 1...n. Gjør dette, og test ved kjøring at programmet ditt er riktig.
  4. Hva er kjøringstiden til programmet over? Kjør programmet for n=8. Går det fort?
  5. Avskjæring: Det finnes n! permutasjoner av n elementer, og dette er et stort tall. Dog er antallet sekvenser totalt enda større (hvor stort?), slik at programmet over er enda mindre effektivt enn det kunne vært. Vi skal derfor gjøre en avskjæring ved at vi så tidlig som mulig "oppdager" at en påbegynt sekvens aldrig kan bli en permutasjon, og unngår å bygge videre på en slik løsning. Det er for eksempel ingen vits å bygge videre på en sekvens der det første og tredje tallet er det samme. Denne avskjæringen kan gjøres på flere måter. Før man bygger videre på en sekvens kan man se "bakover" på den hittil genererte sekvensen og oppdage håpløse tilfeller. Vi velger en litt annen variant, nemlig vi bruker en global array
  6. boolean[] brukt=new boolean[n+1]
    der brukt[k]==true hviss tallet k allerede er brukt i sekvensen under bygging. (Husk å "blanke ut" deler av denne arrayen når vi prøver et nytt tall!.) Glem brukt[0]. Bruk en slik global array til å foreta avskjæring slik at vi aldrig bygger videre på en påbegynt sekvens som ikke kan bli en lovlig permutasjon. (Ekstraoppgave: Hva er kjøringstiden til generering av alle permutasjoner av 1...n med dette programmet?) Test ved kjøring for n=8. Bruk UNIX-kommandoen /bin/time deresProgram og se på user-time.

Oppgave 3: Data Invarianter -- Rasjonale tall

 Data-invarianter spesifiserer betingelser som gjelder for datastrukturen som representerer en instans av en abstrakt datatype. Slike data-invarianter skal gjelde etter initialisering og før en metode blir kalt, og etter at metoden returnerer. Dvs. data-invarianten er sann hele tiden unntatt muligens mens en av datatypens metoder kjører.
Angi en data-invariant for din implementasjon av Conways life, og vis at invarianten vil gjelde i alle tilfeller unntatt muligens mens vi er inne i en av klassens metoder.

Rasjonale tall, eller brøker, er slike tall som 1/2, 2/6, -3600/840. De består av to heltall: teller (det som er over brøkstreken) og nevner (det som er under). Det kreves at nevner er ulik 0, for eksempel 8/0 eller 0/0 er ikke rasjonale tall.

Brøker kan adderes 2/6 + 1/2 = 5/6, multipliseres 2/6*1/2=2/12, substraheres 2/6-1/2=-1/6 og divideres (2/6) / (1/2)= 4/6. For addisjon, subtraksjon, divisjon og multiplikasjon har vi at dersom Y=a/b og X=c/d, så er:

Y+X = (a*d+c*b)/b*d
Y-X = (a*d-c*b)/b*d
Y*X = (a*c)/(b*d)
Y/X = (a*d)/(b*c)
To brøker kan sammenlignes -- dersom begge nevnere er positive, vil a/b < c/d dersom a*d<c*b. Vi sier at a/b=c/d dersom a*d=c*b, f.eks., 2/6=5/15. Denne definisjon av brøklikhet fører til en ``normal form'' -- en hver brøk kan skrives entydig på en form som ikke lenger kan forkortes, f.eks. 5/15 = 2/6 =1/3, og den siste er
normal form for de to første (og for seg selv). Ved forkorting dividerer vi teller og nevner med det største tallet som går opp i begge -- et slikt tall kalles største felles divisor (greatest common divisor -- gcd). Tilsvarende definisjoner for addisjon, subtraksjon, divisjon og multiplikasjon er:

Oppgaven går ut på å lage to implementasjoner av et interface Rasjonal med to forskjellige data invarianter.

public interface Rasjonal {
    int teller(); int nevner();
    void add(Rasjonal x);
    void sub(Rasjonal x);
    void mul(Rasjonal x);
    void div(Rasjonal x);
    boolean eq(Rasjonal x);
    boolean lt(Rasjonal x);
}
De to første metodene returnerer teller, resp., nevner av brøken; de fire neste utfører aritmetiske operasjoner: adderer, substraherer,multipliserer og dividerer den aktuelle brøken med argument brøk x. De to siste sammenligner den aktuelle brøken med argument brøk x og returnerer true hvis de er like, resp. hvis dette tallet er mindre enn x.
  1. Implementer interface Rasjonal med en klasse med to heltallsattributter nev, tel, tilsvarende nevner og teller, der hele data invarianter sier at nevner må være ulik 0 (nev!=0). Implementer også en metode boolean DI() som skal brukes inne i andre metoder for å verifisere at datainvarianten holder.
  2. Lag nå en annen implementasjon med en sterkere datainvariant: nevner skal alltid være positiv (nev>0).

  3. Diskuter forskjeller og evt. fordeler/ulemper med de to implementasjonene. Kan du komme på enda en annen datainvariant som kunne brukes?