3.2 Rekker og summer: Taylor- og potensrekker
Implementere en endelig sum som tilnærmer en funksjon — det kanoniske `s = 0; løkke; return s`-mønsteret, inkludert rekurrente rekker med løkke.
I IN1900 er det to eksamener. Midtveiseksamen i oktober er en heldigital flervalgseksamen i Inspera, UiOs eksamenssystem, og teller 25 prosent. Den avsluttende eksamenen i desember varer fire timer, teller 75 prosent, og består for det meste av fritekst-koding: du skriver Python-kode rett inn i et tekstfelt, uten hjelpemidler og uten å kunne kjøre den.
Der møter du sjanger F. Sjanger F er bokas kode for oppgaven «skriv en funksjon som summerer en rekke, og skriv en testfunksjon til den». Tallene:
- Sjanger F står i hvert eneste sett (100 prosent) av de åtte nyere avsluttende settene.
- Den er verdt 4–6 poeng, altså rundt en tolvdel av hele eksamenen.
- Testfunksjonen, som er del-b, er et selvstendig delkrav i 7 av 8 sett (88 prosent). Den tok vi i kap. 3.1; her tar vi selve summen.
Prioriteten er den høyeste boka opererer med. Boka bruker tre nivåer: må sitte (nivå 1), kunne (nivå 2) og kjenne til (nivå 3). Rekker er nivå 1.
Det beste med sjangeren er at den nesten ikke forandrer seg. Innpakningen roterer år for år — eksponentialfunksjonen, logaritmen, invers tangens hyperbolicus, sinus hyperbolicus, arcus sinus, cosinus, Chebyshev-polynomer — men programmeringsøvelsen er identisk hver gang: en variabel som starter på null, en løkke, en addisjon, og en return. Lærer du mønsteret her, gjenkjenner du oppgaven uansett hvilken funksjon de har valgt.
Om delpoeng: sensor gir uttelling for deler av en løsning. Riktig løkkestruktur belønnes selv om du bommer på ett ledd eller på en indeks. De offisielle løsningsforslagene sier det rett ut: å få alle indekser 100 prosent riktig under tidspress er nesten umulig.
Forkunnskaper
Fire ting fra tidligere i boka:
- Løkker og range fra kap. 1.3, særlig at stoppverdien i range er eksklusiv. Det er den enkeltdetaljen som avgjør flest poeng i dette kapitlet.
- Funksjoner fra kap. 1.6: def, argumenter og return.
- Potenser og heltallsdivisjon fra kap. 1.1. Du trenger å vite at / alltid gir flyttall, og at ** er potens.
- Testfunksjoner fra kap. 3.1 — hver oppgave i dette kapitlet ender med en test.
Matematikken bak er ikke pensum her; du skal implementere formelen, ikke utlede den. Vil du likevel vite hvorfor en endelig sum kan tilnærme en funksjon, står utledningen i Taylorpolynomer med restledd og Uendelige rekker og konvergenstester.
Kapitlet er delt i fire løkker med tidsanslag og tar knapt en time.
Løkke 1 — fra summetegn til løkke (~12 min)
En kalkulator har ingen «eksponentialknapp» inni seg. Den regner ut ved å legge sammen ledd fra en uendelig rekke og stoppe når leddene blir små nok. Formelen er
Vi kan selvsagt ikke legge sammen uendelig mange ledd. Det vi kan, er å stoppe etter ledd nummer :
Les summetegnet som en oppskrift på en løkke. Under tegnet står k = 0: det er der løkka begynner. Over står : det er der den slutter, og skal være med. Til høyre står leddet: det er uttrykket som legges til hver runde.
Det er hele oversettelsen. Alt annet i dette kapitlet er varianter av den samme fire linjene.
Bokas faste mal for sjanger F. Fire steg, alltid i denne rekkefølgen:
1. Nullstill akkumulatoren før løkka: s = 0.
2. Løkke over leddnumrene, med range(N+1) når summen går til og med .
3. Legg til leddet: s += <uttrykk med k>.
4. Returnér summen etter løkka: return s.
from math import factorial
def exp_rekke(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return s
print(exp_rekke(1.0, 5))Utskrift:
2.7166666666666663Tre detaljer som er poenggivende hver gang:
- range(N+1), ikke range(N). Summen går til og med , og stoppverdien i range er eksklusiv. Med range(N) mister du det siste leddet.
- return s står utenfor løkka. Havner return inne i løkka, avbrytes funksjonen etter første runde og du får bare det første leddet.
- Løkkevariabelen heter det samme som i formelen. Står det under summetegnet, skriv k i koden. Sensor leser koden mot formelen, og like navn gjør sammenligningen umiddelbar.
En akkumulator er en variabel som samler opp et resultat gjennom en løkke. Den settes til null før løkka og økes med ett ledd per runde.
s += ledd betyr nøyaktig det samme som s = s + ledd: hent den gamle verdien, legg til, og lagre tilbake i samme variabel. Skrivemåten kalles en augmentert tilordning.
Startverdien må stå før løkka. Står s = 0 inne i løkka, nullstilles summen hver runde, og funksjonen returnerer bare siste ledd.
s = 0
for k in range(1, 5):
s += k*(k+1)
print(s)Utskrift:
40Går summen fra til og med , har den ledd, og løkka skrives for k in range(N+1):. Stoppverdien i range er eksklusiv, så range(N+1) gir verdiene .
Begynner summen på i stedet, skrives løkka for k in range(1, N+1): — første argument er startverdien, som er inklusiv.
N = 3
print(list(range(N+1)), list(range(1, N+1)))Utskrift:
[0, 1, 2, 3] [1, 2, 3]Regelen i én setning: les grensene rett av summetegnet, og legg alltid én til den øvre.
Fakultet: produktet av alle heltall fra 1 til og med , og per definisjon. Funksjonen hentes med from math import factorial — modulen står først i importsetningen.
Den tar et ikke-negativt heltall og returnerer et heltall. Sender du et flyttall inn, får du ValueError.
from math import factorial
for k in range(5):
print(k, factorial(k))Utskrift:
0 1
1 1
2 2
3 6
4 24Du skal ikke implementere fakultet selv. Det er bortkastet tid på eksamen, og de offisielle løsningsforslagene bruker factorial direkte.
Hvor mange ledd trengs før rekka for treffer verdien fra math.exp på ti desimaler? Vis summen for og .
Vi bruker malen uendret og kaller funksjonen med fire ulike verdier av .
from math import factorial, exp
def exp_rekke(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return s
for N in [1, 3, 6, 15]:
print(N, exp_rekke(0.5, N))
print('math.exp(0.5) =', exp(0.5))Utskrift:
1 1.5
3 1.6458333333333333
6 1.6487196180555554
15 1.6487212707001278
math.exp(0.5) = 1.6487212707001282Med ett ledd () er svaret bare . Med seks ledd stemmer fem desimaler, og med seksten ledd stemmer alle sifrene bortsett fra det siste — avviket er om lag , altså ren avrundingsstøy.
Det er nettopp derfor testfunksjonen fra kap. 3.1 bruker abs(expected - computed) < tol og aldri ==.
Legg merke til hvor raskt fakultetet i nevneren vokser. Ledd nummer 15 er , altså rundt . Det er grunnen til at få ledd holder når er liten — og et varsel om at mange flere ledd trengs når er stor.
(Innstegsoppgave, sjanger A — kodesporing, altså «hva skriver programmet ut?».) Hva skriver programmet ut?
s = 0
for k in range(4):
s += 2*k
print(s, end=' ')(Midtveisnivå, sjanger A — kodesporing av en rekkesum.) Hva skriver programmet ut? Pass på formateringen: {ledd:6.3f} betyr totalbredde 6 med 3 desimaler.
from math import factorial
s = 0
for k in range(3):
ledd = 2**k / factorial(k)
s += ledd
print(f'{k} {ledd:6.3f} {s:6.3f}')Feilkatalogen er bokas nummererte register over feil som går igjen; hver feil har et nummer du kan slå opp. Feil #1 — av-én i range eller i antall punkter — er den hyppigste i hele emnet, og i rekkeoppgaver ser den slik ut:
from math import factorial, exp
def galt(x, N):
s = 0
for k in range(N):
s += x**k / factorial(k)
return s
def riktig(x, N):
s = 0
for k in range(N+1):
s += x**k / factorial(k)
return s
print(galt(1.0, 5), riktig(1.0, 5))
print(exp(1.0))Utskrift:
2.708333333333333 2.7166666666666663
2.718281828459045Forskjellen ser liten ut, men den er systematisk: galt summerer bare til og mister alltid det siste leddet. På en testfunksjon med stram toleranse gir det AssertionError uansett hvor mange ledd du tar med.
Sjekklista er kort. Sier oppgaven «fra 0 til og med », skriv range(N+1). Sier den «de første leddene», er range(N) riktig. Sier den ingenting presist, velg én tolkning og skriv én kommentarlinje om valget — det er den ene legitime bruken av kommentarer i dette faget, og den gir uttelling.
a) Skriv exp_rekke(x, N) som summerer leddene fra til og med .
b) Skriv en testfunksjon som sammenligner med math.exp for med .
c) Hva skjer med testen hvis du bytter range(N+1) til range(N)?
Løkke 2 — ledd som skifter fortegn (~12 min)
— naturlig pausepunkt —
Mange av de nyttigste rekkene har ledd som veksler mellom pluss og minus. Den naturlige logaritmen er et eksempel:
I koden løses dette med én faktor. Du trenger ingen if-setning og ingen hjelpevariabel som skifter fortegn — bare et fortegnsledd som er en potens av .
Legg samtidig merke til at denne summen starter på , ikke på . Det er en av de tre tingene du leser rett av summetegnet: startverdi, sluttverdi og leddets uttrykk.
Uttrykket (-1)**k er 1 når er et partall og når er et oddetall. Det er standardmåten å få et ledd til å skifte fortegn på.
Parentesen rundt er nødvendig: -1**k betyr -(1**k), som alltid er , fordi ** binder sterkere enn det unære minustegnet.
Skal det første leddet være positivt når summen starter på , bruker du (-1)**(k+1).
for k in range(4):
print(k, (-1)**k, end=' ')
print()Utskrift:
0 1 1 -1 2 1 3 -1 Skriv ln1p_rekke(x, N) som tilnærmer med leddene fra til og med , og sammenlign med math.log for .
To ting skiller denne fra eksponentialrekka: løkka starter på 1, og fortegnet veksler. Begge deler leses rett av formelen.
from math import log
def ln1p_rekke(x, N):
s = 0
for k in range(1, N+1):
s += (-1)**(k+1) * x**k / k
return s
print(ln1p_rekke(0.5, 5))
print(ln1p_rekke(0.5, 30))
print(log(1.5))Utskrift:
0.40729166666666666
0.4054651080980436
0.4054651081081644Med fem ledd stemmer to desimaler. Med tretti ledd stemmer ni. Sammenlign med eksponentialrekka, der seks ledd ga fem riktige desimaler: denne rekka konvergerer langt saktere, fordi nevneren bare er og ikke .
Konsekvensen for en testfunksjon er praktisk. Toleransen må velges etter metoden, ikke etter ønske: med er avviket rundt , så tol = 1e-10 går akkurat. Med måtte du hatt tol = 1e-2.
Margnotat om uttelling: de to detaljene sensor ser etter her er range(1, N+1) — altså at løkka starter der summetegnet sier — og at fortegnet er (-1)**(k+1) slik at det første leddet blir positivt. Bommer du på eksponenten i fortegnet, får du hele summen med motsatt fortegn; det er ett poeng, ikke oppgaven.
a) Skriv cos_rekke(x, N) som summerer leddene til og med .
b) Skriv en testfunksjon mot math.cos for med og toleranse 1e-10.
c) Hvorfor kan du ikke bruke tol = 1e-10 med ?
Løkke 3 — rekker med bare oddetallsledd (~15 min)
Sinus hyperbolicus beskriver blant annet formen på en hengende kjede, og har rekka
Her er hvert eneste ledd en oddetallspotens. Fristelsen er å løkke over alle heltall og hoppe over partallene med en if. Ikke gjør det. Løkk over leddnummeret, og la uttrykket lage oddetallet.
Det er nettopp det gjør: med blir lik . Løkkevariabelen teller ledd; uttrykket regner ut hvilken potens leddet har.
Når rekka bare har oddetallspotenser, løkker du over leddnummeret n og bruker 2*n+1 der eksponenten og fakultetet skal stå. Løkkevariabelen er da antall ledd du har tatt med, ikke potensen.
Med n = 0, 1, 2, 3 gir 2*n+1 verdiene 1, 3, 5 og 7. Skal du ha partallspotenser i stedet, er uttrykket 2*n.
for n in range(4):
print(n, 2*n+1)Utskrift:
0 1
1 3
2 5
3 7Regelen som sparer deg for feil: det samme uttrykket skal stå både i eksponenten og i fakultetet. Står det x**(2*n+1) i telleren, skal nevneren være factorial(2*n+1) — ikke factorial(n).
Skriv sinh_rekke(x, N) som summerer de første oddetallsleddene, og sjekk mot math.sinh for .
Malen er uendret. Det eneste nye er at
2*n+1 erstatter k to steder.from math import factorial, sinh
def sinh_rekke(x, N):
s = 0
for n in range(N+1):
s += x**(2*n+1) / factorial(2*n+1)
return s
print(sinh_rekke(0.8, 3))
print(sinh_rekke(0.8, 8))
print(sinh(0.8))Utskrift:
0.8881056101587302
0.8881059821876232
0.888105982187623Fire ledd () gir seks riktige desimaler, og ni ledd treffer alle sifrene bortsett fra det siste. Rekka konvergerer nesten like raskt som eksponentialrekka, og av samme grunn: fakultetet i nevneren.
En liten kontroll du kan gjøre i hodet på eksamen: det første leddet er selv, siden og . Gir funksjonen din noe annet enn 0,8 når du kaller den med , er indekseringen gal.
Margnotat om uttelling: her er det to poenggivende ting og én felle. Poengene ligger i range(N+1) og i at 2*n+1 står begge steder. Fellen er å skrive factorial(n) i nevneren; da får du en helt annen funksjon, og testen mot math.sinh avslører det med en gang.
Legg merke til at nevneren er — ikke .
a) Skriv atanh_rekke(x, N).
b) Skriv en testfunksjon mot math.atanh for . Velg og toleranse som passer sammen, og begrunn valget i en kommentar.
c) Kontroller i hodet hva funksjonen gir for , og forklar hvorfor.
Løkke 4 — rekurrente rekker, løst med løkke (~16 min)
Noen rekker er ikke gitt ved en formel for leddet, men ved en rekurrens: hvert ledd defineres ut fra de forrige. Chebyshev-polynomene er det klassiske eksempelet:
De brukes i numerisk analyse fordi de fordeler tilnærmingsfeilen jevnt over et intervall. For oss er de interessante av en helt annen grunn: de er den ene rekketypen der studenter systematisk velger feil verktøy.
Løsningen er en løkke som husker de to siste verdiene. Du trenger to variabler, og du oppdaterer dem i takt.
En rekurrensformel ser ut som en invitasjon til å la en funksjon kalle seg selv. Det er utenfor pensum i dette emnet, og en slik løsning markeres som uønsket i de offisielle løsningsforslagene. Dette er felle #9 — rekursjon der løkke er forventet.
Begrunnelsen er både faglig og praktisk. Emnet underviser aldri teknikken, så den forventes ikke; og en funksjon som kaller seg selv to ganger per nivå gjør eksponentielt mange kall, slik at tar minutter i stedet for mikrosekunder.
Alt løses med løkke. Oppskriften er den samme hver gang: lagre de verdiene formelen trenger, gå ett steg av gangen, og oppdater lagrene i riktig rekkefølge.
Malen for en rekurrens som trenger de to foregående verdiene:
1. Ta unna basistilfellene med en if øverst: den minste verdien returneres direkte.
2. Sett de to lagrene til de kjente startverdiene: T_forrige og T_denne.
3. Løkke fra 1 til — én runde per steg du må ta deg fram.
4. Regn den nye verdien først, i en egen variabel, og flytt deretter lagrene ett hakk.
5. Returnér den siste verdien.
def chebyshev(n, x):
if n == 0:
return 1.0
T_forrige = 1.0
T_denne = x
for k in range(1, n):
T_neste = 2*x*T_denne - T_forrige
T_forrige = T_denne
T_denne = T_neste
return T_denne
for n in range(6):
print(n, chebyshev(n, 0.5))Utskrift:
0 1.0
1 0.5
2 -0.5
3 -1.0
4 -0.5
5 0.5Punkt 4 er hele oppgaven. Skriver du T_forrige = T_denne før du har regnet ut den nye verdien, er den gamle T_forrige allerede overskrevet, og formelen bruker feil tall. Hjelpevariabelen T_neste finnes utelukkende for å hindre det.
Merk også at løkka går til range(1, n), ikke range(1, n+1): når allerede er satt, trengs bare steg for å komme fram til . Sjekk det med : løkka går én runde og regner ut .
Test chebyshev(n, x) mot den kjente formelen for .
Fasiten kommer her ikke fra
math, men fra en formel du kan skrive opp uavhengig av koden. Det er en helt gyldig — og ofte bedre — kilde til expected.def chebyshev(n, x):
if n == 0:
return 1.0
T_forrige = 1.0
T_denne = x
for k in range(1, n):
T_neste = 2*x*T_denne - T_forrige
T_forrige = T_denne
T_denne = T_neste
return T_denne
def test_chebyshev():
x = 0.3
expected = 4*x**3 - 3*x
computed = chebyshev(3, x)
tol = 1e-10
msg = f'ventet {expected}, fikk {computed}'
assert abs(expected - computed) < tol, msg
test_chebyshev()
print('Testen gikk gjennom')
print(chebyshev(3, 0.3))Utskrift:
Testen gikk gjennom
-0.792Regn gjerne etter for hånd: , og . Formelen gir . De to veiene møtes.
Margnotat om uttelling. Sensor krysser av for fire ting: at basistilfellet er behandlet, at begge startverdiene er satt, at oppdateringen bruker en hjelpevariabel slik at ingen verdi går tapt, og at løsningen bruker løkke. Bommer du på løkkegrensen — range(1, n+1) i stedet for range(1, n) — får du i stedet for . Det er ett poeng, og strukturen står.
Fibonacci-tallene er definert ved , og , og dukker opp i modeller for bestandsvekst.
a) Skriv fib(n) med en løkke, etter malen for rekurrente rekker.
b) Skriv en testfunksjon som sjekker tre verdier du kan slå opp i hodet.
c) Forklar i én setning hvorfor T_neste-triksen fra malen ikke er strengt nødvendig her, men hvorfor du likevel bør skrive det slik.
Programmet skal summere rekka og teste den. Det har tre feil.
import factorial from math
def exp_rekke(x, N):
for k in range(N):
s = 0
s += x**k / factorial(k)
return s
def test_exp_rekke(x, N):
assert exp_rekke(x, N) == 2.718281828a) Nevn de tre feilene og si hva hver av dem fører til.
b) Skriv den rettede koden.
Fem feil, i den rekkefølgen de oftest dukker opp i besvarelser:
1. #1 — av-én i range. «Til og med » betyr range(N+1). Dette er den hyppigste feilen i hele emnet.
2. Akkumulatoren nullstilt inne i løkka, eller return inne i løkka. Begge gir ett ledd i stedet for en sum. s = 0 står før løkka, return s etter.
3. Feil ledd-indeksering i oddetallsrekker. Løkk over leddnummeret og bruk 2*n+1 — og bruk samme uttrykk i eksponenten og i fakultetet.
4. #9 — å velge en teknikk som ligger utenfor pensum for rekurrente rekker. Rekurrens løses med løkke og to lagervariabler, ikke ved at en funksjon kaller seg selv; det siste er utenfor pensum og markeres uønsket i fasit.
5. #10 — importsetningen i feil rekkefølge. from math import factorial, aldri motsatt.
Og én som ikke har eget nummer, men som koster like mye: å reimplementere factorial selv. Det tar tid du ikke har, og gir ingen ekstra poeng.
Begrepsbank
Begrepsbanken er flashcard-/repetisjonsstoff — den gjentar det du nettopp har lest. Hopp trygt over ved førstegangslesing; tidsanslaget for kapitlet gjelder kjernestoffet.
En sum av ledd som følger et mønster. I dette emnet er rekkene alltid endelige: du summerer fra ledd 0 (eller 1) til og med ledd , og bruker summen som en tilnærming til en funksjonsverdi. Jo flere ledd, jo bedre tilnærming — hvor mange som trengs, avhenger av hvor raskt leddene minker.
Bokas kode for oppgaven «skriv en funksjon som summerer en rekke, og skriv en testfunksjon til den». Sjangeren står i hvert eneste av de åtte nyere avsluttende settene og er verdt 4–6 poeng. Innpakningen roterer mellom eksponential, logaritme, cosinus, sinus hyperbolicus, invers tangens hyperbolicus, arcus sinus og Chebyshev — øvelsen er identisk.
Tre ting leses rett av tegnet: startverdien under tegnet blir første argument i range, sluttverdien over tegnet blir stoppverdien pluss én, og uttrykket til høyre blir det du legger til hver runde. Alt annet er den samme malen hver gang.
Variabelen som samler opp summen. Den settes til 0 før løkka og økes med += inne i løkka. Nullstilles den inne i løkka, mister du hele summen og sitter igjen med siste ledd.
Å stoppe en uendelig sum etter ledd . Feilen du gjør, kalles trunkeringsfeilen, og den er omtrent lik det første leddet du utelot. Det er derfor toleransen i testfunksjonen må velges etter hvor mange ledd du tar med.
Hvor raskt leddene minker. Rekker med fakultet i nevneren — eksponential, cosinus, sinus hyperbolicus — treffer mange desimaler med få ledd. Rekker med bare eller i nevneren — logaritme, invers tangens hyperbolicus — trenger titalls ledd for samme nøyaktighet.
En rekke der leddene skifter fortegn. Løses med faktoren (-1)**k, aldri med en if-setning. Parentesen rundt minus én er nødvendig, fordi ** binder sterkere enn det unære minustegnet.
En rekke der bare oddetallspotensene er med, som sinus hyperbolicus og invers tangens hyperbolicus. Du løkker over leddnummeret og lar 2*n+1 lage potensen. Samme uttrykk skal stå i eksponenten og i nevneren.
Samme idé med 2*k i stedet: cosinusrekka har leddene . Kontrollen er den samme — det samme uttrykket begge steder.
En definisjon der hvert ledd bygger på de foregående, som . Løses alltid med løkke og to lagervariabler i dette emnet.
To variabler som holder de to siste verdiene, pluss en hjelpevariabel for den nye. Rekkefølgen er: regn ut den nye verdien først, flytt deretter lagrene ett hakk. Flytter du først, er verdien du trengte allerede overskrevet.
Den minste verdien i en rekurrens, som returneres direkte med en if øverst i funksjonen. Chebyshev trenger ett (), Fibonacci trenger at både og er dekket av startverdiene.
En funksjon som kaller seg selv er utenfor pensum i IN1900 og markeres uønsket i de offisielle løsningsforslagene (#9). I tillegg blir den svært treg for rekurrenser med to ledd, fordi antall kall dobles for hvert nivå.
Uttrykket til høyre for summetegnet, oversatt direkte til Python. Teller og nevner skrives som de står: x**k / factorial(k), (-1)**k * x**(2*k) / factorial(2*k). Ingen forenkling er nødvendig, og forenkling er en vanlig kilde til feil.
Toleransen skal ligge like over avviket rekka faktisk har med det antall ledd du valgte. Med fakultet i nevneren og 15 ledd er 1e-10 trygt; med en langsom rekke og 5 ledd må du opp i 1e-2. Skriv én kommentarlinje om valget.
expected trenger ikke komme fra math. En eksakt formel du kan skrive opp uavhengig — som — er en like god, og ofte bedre, kilde: den er tydelig uavhengig av koden du tester.Regelen om toleranse gjelder flyttall. Er alle verdiene heltall, som i Fibonacci, er assert expected == computed helt riktig, og en toleranse ville vært unødvendig pynt.
En gratis fornuftssjekk på eksamen: første ledd i eksponentialrekka er 1, i oddetallsrekkene er det selv, og i logaritmerekka er det . Stemmer ikke det, er indekseringen gal — og du har oppdaget det uten å kunne kjøre koden.
Dette kapitlet er skrevet av Anthropics toppmodeller (Claude Opus og Claude Fable) og er foreløpig ikke manuelt gjennomgått — kvalitetskontrollen gjøres av uavhengige KI-agenter, og innmeldte feil rettes fortløpende. Funnet en feil? Meld fra, så retter vi den. Les mer om hvordan innholdet lages.
Skolesaga er en uavhengig læringsressurs og er ikke tilknyttet eller godkjent av Universitetet i Oslo. Dette er ikke offisielt studiemateriell. Les mer.