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,
-
argumentet
-
rekursjonsdybde
-
returnert resultat.
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.]
-
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
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.
-
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.
-
Hva er kjøringstiden til programmet over? Kjør programmet
for n=8. Går det fort?
-
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
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.
-
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.
-
Lag nå en annen implementasjon med en sterkere datainvariant: nevner
skal alltid være positiv (nev>0).
Diskuter forskjeller og evt. fordeler/ulemper med de to implementasjonene.
Kan du komme på enda en annen datainvariant som kunne brukes?