En grådig algoritme bygger løsningen gjennom en serie valg, og tar hvert valg lokalt. Den gjør det som ser best ut her og nå, uten å løse noen delinstanser først. Dynamisk programmering løser delinstansene for alle valgene og bygger på den beste delløsningen. En grådig algoritme velger først og løser bare den ene delinstansen som er igjen. Slike algoritmer er som regel korte og raske. Det kan være vanskelig å vise at de gir riktig svar, og for mange problemer gjør de ikke det.
Grådige valg
Du får punktene på tallinja, og skal dekke dem med færrest mulig lukkede intervaller med lengde 1. Et intervall må dekke , og det er ingen punkter til venstre for . Legg derfor intervallet , som dekker og går lengst til høyre. Fjern punktene det dekker, og gjenta med resten. Punktene er sortert, så algoritmen tar tid.
En algoritme som velger slik, er grådig. Den gjør et valg , her intervallet , og krymper instansen til det som er igjen etter valget, her punktene til høyre for . Så løser den delinstansen og setter og delløsningen sammen. Valget er det beste lovlige valget sett helt lokalt, og det tas før delinstansen er løst, så algoritmen kan ikke se om det lønner seg. Dynamisk programmering som prøver alle valgene, gir også riktig svar på et problem som kan løses grådig, bare langsommere.
For at den grådige algoritmen skal være riktig, må det første valget være trygt.
Intervallproblemet har grådighetsegenskapen. La være intervallet som dekker i en optimal løsning. starter i eller til venstre for det, så det slutter i eller tidligere. Flytt til . Det flyttede intervallet dekker fortsatt og alle punktene dekket til høyre for . Løsningen har like mange intervaller og tar det grådige valget.
Grådighetsegenskapen sier ingenting om valgene etter det første. Til det trengs optimal delstruktur: det grådige valget satt sammen med en optimal løsning på delinstansen som er igjen, gir en optimal løsning. I intervallproblemet tar du den optimale løsningen som har . De andre intervallene i den dekker punktene til høyre for , så de er minst like mange som i en optimal løsning på delinstansen. Bytt dem ut med en optimal løsning på delinstansen. Antallet intervaller øker ikke, så løsningen er fortsatt optimal.
Delinstansen er et problem av samme form, så grådighetsegenskapen gjelder der også, og optimal delstruktur sier at det holder å løse den optimalt. Med induksjon over størrelsen på instansen er alle de grådige valgene trygge.
Læreboka gir en oppskrift i tre trinn.
-
Ett valg, én delinstans
Formuler problemet så hvert valg etterlater nøyaktig én delinstans.
-
Grådighetsegenskapen
Vis at det alltid finnes en optimal løsning som tar det grådige valget.
-
Optimal delstruktur
Vis at det grådige valget sammen med en optimal løsning på delinstansen gir en optimal løsning.
Intervallproblemet følger oppskriften. Valget etterlater bare punktene til høyre for , flyttingen av til viser grådighetsegenskapen, og byttet av de andre intervallene mot en optimal løsning på delinstansen viser optimal delstruktur.
Flyttingen av er et utvekslingsargument, den vanlige måten å vise grådighetsegenskapen på. Start med en vilkårlig optimal løsning. Bytt ut ett av valgene i den med det grådige valget, og vis at løsningen fortsatt er lovlig og ikke blir dårligere. Da finnes en optimal løsning med det grådige valget. Å vise at et grådig valg ikke er trygt, er lettere. Ett moteksempel holder.
Om vi viser at grådighetsegenskapen (the greedy-choice property) holder har vi likevel ikke vist at grådighet er korrekt. Forklar.
Vis løsning Skjul løsning
Grådighetsegenskapen sier bare at det første grådige valget er trygt: det finnes en optimal løsning som tar det. Den sier ingenting om valgene etter det. For å vise at alle valgene er trygge, bruker du induksjon, og da må delinstansen som er igjen etter et grådig valg, være et problem av samme form, der en optimal løsning sammen med valget gir en optimal løsning. Det er optimal delstruktur.
Det kontinuerlige ryggsekkproblemet
I det binære ryggsekkproblemet er hver gjenstand med eller ikke med. I det kontinuerlige ryggsekkproblemet kan du ta med en andel mellom 0 og 1 av hver gjenstand, og får samme andel av verdien. Gjenstand har verdi og vekt , og sekken har kapasitet .
Det grådige valget er gjenstanden med høyest kilopris . Ta med mest mulig av den, så mest mulig av den med nest høyest kilopris, og fortsett til sekken er full. Bare den siste gjenstanden blir delt. Grådighetsegenskapen vises med et utvekslingsargument. Tar en løsning mindre enn mulig av gjenstanden med høyest kilopris, har sekken tom plass eller noe med samme eller lavere kilopris. Fyll den tomme plassen, eller bytt ut like mange kilo av det andre, med den beste gjenstanden, og totalverdien øker eller står stille. Optimal delstruktur: kapasiteten som er igjen, må fylles optimalt med det som er igjen av gjenstandene. Sorteringen etter kilopris tar , og resten er én gjennomgang.
For det binære problemet er det samme valget ikke trygt. Med og gjenstandene under har gjenstand 1 høyest kilopris.
| Gjenstand | |||
|---|---|---|---|
| 1 | 10 | 60 | 6 |
| 2 | 20 | 100 | 5 |
| 3 | 30 | 120 | 4 |
Velger du etter kilopris, tar du 1 og 2, til 160, og da får ikke 3 plass. Det beste er 2 og 3, til 220. I det kontinuerlige problemet tar den grådige algoritmen 1, 2 og to tredjedeler av 3, til , og det er optimalt. Utvekslingsargumentet virker ikke i det binære problemet, fordi en gjenstand bare kan byttes ut hel. Bytter du ut 3 med 1 i den beste løsningen, står 20 kilo tomt, og verdien synker fra 220 til 160. For å avgjøre om en gjenstand skal være med, må du sammenligne den beste løsningen med gjenstanden og den beste uten, og den sammenligningen er dynamisk programmering.
Aktivitetsutvalg
Flere aktiviteter vil bruke det samme rommet, og rommet kan bare brukes av én om gangen. Aktivitet varer i det halvåpne intervallet , så to aktiviteter overlapper ikke når den ene slutter før eller idet den andre starter. Aktivitetsutvalg (activity selection) er å velge flest mulig aktiviteter der ingen to overlapper. Aktivitetene er sortert etter sluttid, .
Velger du en aktivitet , deler den resten i to delinstanser: aktivitetene som slutter ved eller før, og de som starter ved eller senere. Den delingen gir en løsning med dynamisk programmering som prøver hver . Det grådige valget er , aktiviteten som slutter først, og er det valget trygt, trenger du ikke prøve de andre. Aktiviteten lar rommet være ledig for flest mulig av de andre, og siden ingen aktivitet slutter før starter, er bare én delinstans igjen, aktivitetene som starter ved eller senere.
Grådighetsegenskapen vises med et utvekslingsargument. La være en optimal løsning, og la være aktiviteten i som slutter først. Bytt ut med . De andre aktivitetene i overlapper ikke , og siden slutter først, starter de ved eller senere. Siden , slutter før eller idet de starter, så ingen av dem overlapper . Løsningen er like stor og inneholder . Optimal delstruktur: aktivitetene som starter ved eller senere, er en instans av samme problem og må velges optimalt.
Recursive-Activity-Selector får indeksen til den sist valgte aktiviteten, og leter fra etter den første aktiviteten som ikke overlapper . Aktivitetene er sortert, så , og da overlapper ikke nøyaktig når . Aktiviteten tas med, og resten løses rekursivt fra den. Det første kallet har , med en tenkt aktivitet der . Greedy-Activity-Selector gjør det samme med en løkke.
def recursive_activity_selector(s, f, k, n): # plass 0 er a0, med f[0] = 0
m = k + 1
while m <= n and s[m] < f[k]:
m = m + 1
if m <= n:
return [m] + recursive_activity_selector(s, f, m, n)
return []
def greedy_activity_selector(s, f, n):
A = [1]
k = 1
for m in range(2, n + 1):
if s[m] >= f[k]:
A.append(m)
k = m
return A Én rad per aktivitet, sortert etter sluttid, med aktiviteten tegnet fra til . Den stiplede linjen er , sluttiden til den sist valgte aktiviteten. Grønne aktiviteter er valgt, og grå er hoppet over.
Hver aktivitet sjekkes én gang, så begge versjonene tar tid når aktivitetene er sortert, og med sorteringen.
Du skal velge ut videokanaler til forsiden på et videonettsted. Du har et sett med kanaler som hver har en estimert målgruppe, oppgitt som et aldersintervall (f.eks. fra 10 til 25 år). Du skal velge ut flest mulig kanaler, men får ikke velge noen som har overlappende målgrupper. Hvordan vil du gå frem? Forklar kort.
Vis løsning Skjul løsning
Hver kanal er en aktivitet med et aldersintervall i stedet for et tidsintervall, så dette er aktivitetsutvalg. Sorter kanalene etter øvre aldersgrense, velg den første, og gå gjennom resten i rekkefølge. Ta med en kanal når den ikke overlapper den sist valgte, og hopp over den ellers. Regnes begge endene av et aldersintervall med, overlapper en kanal for 25 til 40 år med en for 10 til 25 år, så testen er at den nedre grensen er større enn den øvre grensen til den sist valgte. Utvekslingsargumentet for aktivitetsutvalg viser at kanalen med lavest øvre grense alltid kan være med.
Huffman-koder
En tekst skal lagres med færrest mulig bits. Hvert tegn i alfabetet får et binært kodeord, og den kodede teksten er kodeordene etter hverandre. Tegnet står ganger i teksten. Med like lange kodeord trengs bits per tegn for tegn. Får de hyppige tegnene korte kodeord og de sjeldne lange, blir teksten kortere.
Prefiksfrie koder
I en prefiksfri kode er ingen kodeord et prefiks av et annet. Da er det første kodeordet i en kodet tekst entydig, og teksten kan dekodes ett kodeord om gangen fra starten. Koden tegnes som et binært kodetre med tegnene i løvnodene. Kodeordet til et tegn er stien fra rota til løvnoden, der 0 er venstre barn og 1 er høyre barn, så lengden av kodeordet er dybden . Antall bits i den kodede teksten er kostnaden til treet:
Et optimalt kodetre er fullt: hver indre node har to barn. Har en indre node bare ett barn, kan noden erstattes med barnet, og alle kodeordene under den blir én bit kortere. Et fullt binærtre med løvnoder har indre noder.
Dekodingen følger treet. Start i rota, les én bit om gangen, og gå til venstre barn for 0 og høyre barn for 1. I en løvnode skriver du ut tegnet og starter i rota igjen. Det tar konstant tid per bit.
Huffmans algoritme
Huffman bygger kodetreet nedenfra. Hvert tegn starter som et eget tre med én node, i en min-prioritetskø på frekvens. I hver runde tas de to trærne med lavest frekvens, og , ut av køen. De blir barn av en ny node med frekvens , og settes inn i køen. Etter runder er ett tre igjen, og det er kodetreet.
Valget er grådig. Å slå sammen to trær legger én bit til kodeordet for hvert tegn i dem, så sammenslåingen øker med summen av frekvensene deres, og Huffman velger den billigste. Derfor er også summen av frekvensene i alle de indre nodene. Med to tegn er det én indre node, begge kodeordene har én bit, og er summen av de to frekvensene.
import heapq
from itertools import count
def huffman(C): # C er en ordbok fra tegn til frekvens
tie = count() # løpenummer for like frekvenser, så trær sammenlignes aldri
Q = [(freq, next(tie), c) for c, freq in C.items()]
heapq.heapify(Q)
for i in range(1, len(C)):
fx, _, x = heapq.heappop(Q)
fy, _, y = heapq.heappop(Q)
z = (x, y) # et tre er et tegn eller et par (venstre, høyre)
heapq.heappush(Q, (fx + fy, next(tie), z))
return Q[0][2]
def codes(T, prefix=""):
if isinstance(T, str):
return {T: prefix}
return codes(T[0], prefix + "0") | codes(T[1], prefix + "1")
def decode(T, bits):
text, x = "", T
for b in bits:
x = x[0] if b == "0" else x[1]
if isinstance(x, str):
text += x
x = T
return text Køen tegnet som en skog, sortert etter frekvens med den laveste til venstre. Løvnodene er tegn med frekvens, og en indre node har summen av frekvensene under seg. De to trærne Extract-Min gir, er oransje, den nye noden er fylt, og i det ferdige treet står kodeordene under løvnodene.
Med en binær min-haug bygges køen i tid, og hver av de rundene gjør tre køoperasjoner på . Til sammen er det .
Hvorfor koden er optimal
Står et tegn ett nivå dypere, blir kodeordet én bit lengre, og den kodede teksten blir like mange bits lengre som tegnet har frekvens. De sjeldneste tegnene hører derfor hjemme nederst. Grådighetsegenskapen: la og være de to tegnene med lavest frekvens. Da finnes et optimalt kodetre der og er søsken på største dybde. Ta et optimalt tre , og la og være søsken på største dybde. Bytt plass på og , og kall det nye treet . I står , og i står . Alle andre ledd er like, så kostnaden synker med
fordi har lavest frekvens og står dypest. Byttet gjør ikke treet dyrere, og det samme gjelder for å bytte og . Treet er fortsatt optimalt, og og er søsken nederst, slik Huffman setter dem sammen.
Optimal delstruktur: erstatt og med ett nytt tegn med frekvens , og la være et kodetre for det nye alfabetet. Får barna og , blir kodeordene deres én bit lengre enn kodeordet til , og alle andre kodeord er like:
Tillegget avhenger ikke av , så blant trærne der og er søsken, er optimalt når er det. Grådighetsegenskapen sier at et av disse trærne er optimalt blant alle, så er optimalt. Etter å ha slått sammen og løser Huffman nettopp det mindre problemet der er ett tegn.
Du har oppgitt følgende frekvenser for tegnene a, b, c og d:
Med «huffmantre» menes et tre som kan konstrueres av Huffmans algoritme, hvis det er tilfeldig om en barnenode havner til venstre eller høyre. Det er minst ett korrekt huffmantre i figur 1, men det kan være flere.