Gjenkjenne DP: overlappende delproblemer
Identifiser når rekursjon med bruteforce løser det samme delproblemet på nytt, tegn rekursjonstreet for Fibonacci og se den eksponentielle veksten.
Gjenkjenne DP: overlappende delproblemer er en gratis leksjon i Forberedelse til kodeintervjuer på CoddyKit. Dette er leksjon 1 av 4. Du kan lese hele leksjonen gratis nedenfor – og deretter øve praktisk i nettleseren med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt. Den er en del av læringsløpet i Forberedelse til kodeintervjuer, og fremdriften din synkroniseres mellom nettet og CoddyKit-appen. Kurset i Forberedelse til kodeintervjuer inneholder totalt 4 leksjoner.
Hva er dynamisk programmering?
Dynamisk programmering (DP) løser komplekse problemer ved å dele dem opp i enklere, overlappende delproblemer, løse hvert delproblem én gang og lagre resultatet for å unngå overflødige beregninger. DP kan brukes når et problem har to egenskaper: overlappende delproblemer (det samme delproblemet løses flere ganger i en naiv rekursjon) og optimal delstruktur (den optimale løsningen kan bygges fra optimale løsninger på delproblemer). Uten begge disse egenskapene hjelper ikke DP.
# Two ingredients of DP:
# 1. Overlapping sub-problems:
# fib(5) -> fib(4) + fib(3)
# fib(4) -> fib(3) + fib(2) <- fib(3) computed twice!
# Without caching: O(2^n) calls for Fibonacci
# 2. Optimal substructure:
# Shortest path from A to C through B:
# shortest(A,C) = shortest(A,B) + shortest(B,C)
# The sub-path A->B must itself be the shortest
# Contrast with greedy: greedy makes one locally optimal
# choice; DP tries all choices and picks the best.
print('DP = overlapping sub-problems + optimal substructure')Fibonacci: Det klassiske utgangspunktet for DP
Fibonacci-følgen (fib(n) = fib(n-1) + fib(n-2)) er det kanoniske eksempelet på overlappende delproblemer. Den naive rekursjonen har eksponentiell tidskompleksitet O(2^n) fordi den beregner de samme verdiene på nytt flere ganger. Rekursjonstreet for fib(6) viser at fib(3) beregnes 3 ganger, fib(2) 5 ganger og så videre. Denne eksponentielle veksten er nettopp det DP eliminerer ved å lagre beregnede resultater.
import time
def fib_naive(n):
if n <= 1:
return n
return fib_naive(n-1) + fib_naive(n-2)
# Count the calls:
call_count = [0]
def fib_count(n):
call_count[0] += 1
if n <= 1: return n
return fib_count(n-1) + fib_count(n-2)
fib_count(10)
print(f'Calls for fib(10): {call_count[0]}') # 177 calls for n=10!
call_count[0] = 0
fib_count(20)
print(f'Calls for fib(20): {call_count[0]}') # 21891 calls
# n=30 -> ~2.7 million calls: exponential growthVisualisering av rekursjonstreet
Hvis du tegner rekursjonstreet for fib(5), blir sløsingen tydelig: Hver node oppretter to barn, og identiske deltrær dukker opp flere ganger. Det totale antallet noder i treet er O(2^n). Når du ser dette mønsteret – identiske funksjonskall med de samme argumentene som gjentas i treet – er det et tegn på at DP kan hjelpe ved å mellomlagre resultater. Denne visualiseringsferdigheten er avgjørende: Hvis du kan identifisere de gjentatte deltrærne, vet du at DP kan brukes.
# fib(5) recursion tree (simplified):
# fib(5)
# / \
# fib(4) fib(3)
# / \ / \
# fib(3) fib(2) fib(2) fib(1)
# / \ \
# fib(2) fib(1) fib(1)
# / \
# fib(1) fib(0)
# fib(3) appears TWICE
# fib(2) appears THREE TIMES
# Each redundant call wastes exponential time
# Key insight: fib(n) only has O(n) DISTINCT sub-problems
# (fib(0), fib(1), ..., fib(n))
# DP computes each ONCE -> O(n) total
print('Distinct sub-problems: O(n) but naive calls: O(2^n)')Identifisere overlappende delproblemer
For å gjenkjenne overlappende delproblemer skriver du den brute-force-baserte rekursjonen og spør deretter: «Finnes det flere rekursive kall med de SAMME argumentene?» Hvis svaret er ja, kan DP hjelpe. Vanlige signaler i oppgavebeskrivelser er: «minimums-/maksimumsantall av X», «hvor mange måter finnes det å gjøre Y på?» og «kan vi oppnå Z?». Disse formuleringene tyder nesten alltid på et problem med optimal delstruktur, der svaret i posisjon i avhenger av svar på tidligere posisjoner.
# DP signal phrases in problem statements:
# 'minimum number of coins to make amount X'
# 'maximum profit from stock trades'
# 'number of ways to climb n stairs'
# 'can you reach the last index?'
# 'longest common subsequence'
# 'edit distance between two strings'
# All have this shape:
# solve(input) = f(solve(smaller_input_1), solve(smaller_input_2), ...)
# And multiple branches end up calling solve with the same argument.
# If the recursion tree has repeated nodes: DP
# If subproblems are all independent: divide-and-conquer (no DP needed)
print('Repeated arguments in recursion tree -> DP')Optimal delstruktur forklart
Optimal delstruktur betyr at den optimale løsningen på problemet kan bygges fra optimale løsninger på delproblemene. For eksempel er den korteste stien fra A til C gjennom B optimal hvis og bare hvis delstiene A→B og B→C begge er optimale hver for seg. Hvis denne egenskapen gjelder, kan du bygge det globale optimumet nedenfra og opp fra lokale optima. Problemer som mangler optimal delstruktur (for eksempel lengste sti i en generell graf med sykluser), kan ikke løses med DP.
# Optimal substructure examples:
# SHORTEST PATH: shortest(A,C) = min over all B: shortest(A,B) + w(B,C)
# -> Sub-paths must be optimal: YES, has optimal substructure
# LONGEST PATH (no cycles, DAG): can also use DP
# -> Longer path through node B means sub-path A->B must be longest
# LONGEST PATH (with cycles): NO optimal substructure
# -> Best path from A to C might reuse nodes: sub-problems not independent
# COIN CHANGE: min coins for amount n = 1 + min(min coins for n-coin_i)
# -> YES: optimal for n-coin_i is needed for optimal n
print('Optimal substructure: build global optimum from local optima')Trappeklatring: Din første DP
Trappeklatring (LeetCode #70): Hvor mange ulike måter kan du klatre n trinn på hvis du tar 1 eller 2 trinn om gangen? La dp[i] være antallet måter å nå trinn i på. Du kan nå trinn i fra trinn i-1 (ett trinn) eller trinn i-2 (to trinn), så dp[i] = dp[i-1] + dp[i-2]. Dette er Fibonacci! Basistilfeller: dp[1] = 1 og dp[2] = 2. Å gjenkjenne at «trappeklatring» kan reduseres til Fibonacci, er en klassisk innsikt i jobbintervjuer.
def climb_stairs(n):
if n <= 2:
return n
dp = [0] * (n + 1)
dp[1] = 1 # 1 way to reach step 1
dp[2] = 2 # 2 ways to reach step 2: (1+1) or (2)
for i in range(3, n + 1):
dp[i] = dp[i-1] + dp[i-2] # come from i-1 or i-2
return dp[n]
for n in range(1, 8):
print(f'climb_stairs({n}) = {climb_stairs(n)}')
# 1, 2, 3, 5, 8, 13, 21 -- Fibonacci sequence!DP-rammeverket: Definer, rekurrer, ordne
Et pålitelig DP-rammeverk i tre trinn: 1. Definer tilstanden – hva representerer dp[i] (eller dp[i][j])? Skriv det på engelsk. 2. Skriv rekurrensen – uttrykk dp[i] ved hjelp av mindre delproblemer. Ta med alle tilfeller. 3. Bestem utfyllingsrekkefølgen – sørg for at dp[i-1] (og andre avhengigheter) er beregnet før dp[i]. Basistilfellene initialiserer grensen. Dette rammeverket gjør diffus DP-intuisjon om til en konkret implementeringsplan.
# Framework applied to climbing stairs:
# Step 1 - Define state:
# dp[i] = number of distinct ways to reach step i
# Step 2 - Recurrence:
# dp[i] = dp[i-1] + dp[i-2] (come from step i-1 or i-2)
# Step 3 - Fill order:
# Compute dp[1], dp[2], dp[3], ..., dp[n] in order
# Because dp[i] depends on dp[i-1] and dp[i-2] (smaller)
# Base cases: dp[1]=1, dp[2]=2
# Framework applied to coin change:
# Step 1: dp[amount] = minimum coins to make that amount
# Step 2: dp[i] = 1 + min(dp[i-coin] for coin in coins if i >= coin)
# Step 3: Fill i from 1 to amount
# Base: dp[0] = 0 (zero coins for zero amount)
print('DP framework: define state -> recurrence -> fill order')Når DP IKKE bør brukes
DP er ikke alltid svaret. Bruk greedy når ett lokalt optimalt valg alltid fører til den globalt optimale løsningen (aktivitetsutvelging, jump game I). Bruk divide and conquer når delproblemene ikke overlapper (merge sort, binary search). Bruk BFS når problemet gjelder korteste vei i en uvektet graf. DP er korrekt, men ofte unødvendig omfattende når en greedy- eller enklere løsning finnes. I intervjuer bør du forklare hvorfor du valgte DP fremfor alternativene.
# DP vs alternatives:
# Problem: can you jump to the end of the array?
# Greedy: track max reachable index -> O(n) O(1) BETTER than DP
# Problem: shortest path unweighted graph?
# BFS: O(V+E) BETTER than DP on general graph
# Problem: sort an array?
# Comparison sort: O(n log n), no DP needed
# DP IS the right choice when:
# - Greedy fails (choices interact)
# - Need to count/enumerate all possibilities
# - Problem has 'how many ways' or 'minimum/maximum' flavor
# - Recursion tree clearly shows overlapping sub-problems
print('Ask: does greedy fail? If yes, consider DP.')Antall distinkte delproblemer
Antallet distinkte delproblemer bestemmer DP-ens tids- og plasskompleksitet. For en 1D-DP på et input med størrelse n finnes det O(n) delproblemer. For en 2D-DP på to input med størrelsene m og n finnes det O(mn) delproblemer. Hvis hvert delproblem løses på O(k)-tid (for k valg i hvert trinn), blir den totale tiden O(n*k) eller O(mn*k). Tell alltid antallet distinkte delproblemer først – dette gir DP-ens tidskompleksitet før du har skrevet så mye som én kodelinje.
# Sub-problem count examples:
# Problem | Sub-problems | Each costs | Total
# Fibonacci | O(n) | O(1) | O(n)
# Coin change | O(amount) | O(coins) | O(amount * coins)
# LCS (m,n chars) | O(m*n) | O(1) | O(m*n)
# Edit distance | O(m*n) | O(1) | O(m*n)
# 0/1 Knapsack | O(n*W) | O(1) | O(n*W)
# Matrix chain | O(n^2) | O(n) | O(n^3)
# Rule: DP time = (# distinct sub-problems) * (time per sub-problem)
print('Time = subproblems * work-per-subproblem')House Robber: Overlappende valg
House Robber (LeetCode #198) spør hvor stort beløp du maksimalt kan rane fra hus på rad uten å rane nabohus. Ved hvert hus velger du om du skal rane det (legge til verdien og hoppe over det forrige) eller hoppe over det (ta det beste resultatet fra det forrige). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Dette mønsteret med et valg i hvert trinn er den enkleste 1D-DP-rekurrensen og dukker opp i dusinvis av intervjuproblemer.
def rob(nums):
if not nums: return 0
if len(nums) == 1: return nums[0]
dp = [0] * len(nums)
dp[0] = nums[0]
dp[1] = max(nums[0], nums[1])
for i in range(2, len(nums)):
dp[i] = max(dp[i-1], # skip house i
dp[i-2] + nums[i]) # rob house i
return dp[-1]
print(rob([1, 2, 3, 1])) # 4: rob house 0 and 2 (1+3)
print(rob([2, 7, 9, 3, 1]))# 12: rob house 0, 2, 4 (2+9+1)
print(rob([2, 1, 1, 2])) # 4: rob house 0 and 3Rimelighetssjekk: Brute force mot DP
Kontroller alltid DP-løsningen mot en brute-force-løsning på små input. Brute-force-løsningen er fasiten din. Når DP-løsningen samsvarer med brute-force-løsningen i alle testtilfeller, vet du at rekurrensen er korrekt. Optimaliser først deretter plassbruken. Denne testdrevne fremgangsmåten – brute-force → top-down DP → bottom-up DP → plassoptimalisert DP – er den profesjonelle måten å utvikle og verifisere DP-løsninger på under et intervju.
# Brute-force for house robber (exponential)
def rob_brute(nums, i=0):
if i >= len(nums):
return 0
# Option 1: rob house i
rob_it = nums[i] + rob_brute(nums, i + 2)
# Option 2: skip house i
skip_it = rob_brute(nums, i + 1)
return max(rob_it, skip_it)
# Verify on small inputs:
test_cases = [[1,2,3,1], [2,7,9,3,1], [2,1,1,2]]
for tc in test_cases:
bf = rob_brute(tc)
dp = rob(tc)
print(f'{tc}: brute={bf}, dp={dp}, match={bf==dp}')Kort sjekk
Test forståelsen din av konseptene i Data Structures & Algorithms — Coding Interview Prep fra denne leksjonen.
Oppsummering av leksjonen
I denne leksjonen lærte du om: to ingredienser i DP (overlappende delproblemer og optimal delstruktur), hvordan du kan visualisere rekursjonstreet for å finne gjentatte kall, det tretrinns DP-rammeverket (definer tilstanden, rekurrens, utfyllingsrekkefølge) og de første eksemplene, blant annet Fibonacci, climbing stairs og House Robber. Neste del handler om å implementere top-down-DP med memoisering.
Lær deg Forberedelse til kodeintervjuer med en AI-veileder – gratis
Skriv og kjør ekte kode i nettleseren, få umiddelbar hjelp fra en AI-veileder som er tilgjengelig døgnet rundt, og fortsett der du slapp – på nettet eller i appen.
- Kurs
- 90
- Leksjoner
- 360
Ofte stilte spørsmål
Er leksjonen «Gjenkjenne DP: overlappende delproblemer» gratis?
Ja – hele teksten i «Gjenkjenne DP: overlappende delproblemer» er gratis å lese her på nettet. For å øve interaktivt med en innebygd kodeeditor og en AI-veileder som er tilgjengelig døgnet rundt, og for å låse opp resten av Forberedelse til kodeintervjuer-kurset, kan du oppgradere til CoddyKit PRO. Kurset i Forberedelse til kodeintervjuer inneholder totalt 4 leksjoner.
Hva lærer jeg i «Gjenkjenne DP: overlappende delproblemer»?
Identifiser når rekursjon med bruteforce løser det samme delproblemet på nytt, tegn rekursjonstreet for Fibonacci og se den eksponentielle veksten. Du øver på Forberedelse til kodeintervjuer med praktisk kode som du kjører direkte i nettleseren, mens en AI-veileder som er tilgjengelig døgnet rundt, svarer på spørsmålene dine mens du jobber deg gjennom leksjonen.
Trenger jeg erfaring for å begynne med Forberedelse til kodeintervjuer?
Ingen tidligere erfaring er nødvendig. Forberedelse til kodeintervjuer på CoddyKit er lagt opp for både nybegynnere og viderekomne, så De kan begynne her eller helt fra start og lære i Deres eget tempo. Dette er leksjon 1 av 4.
Hvor lang tid tar leksjonen «Gjenkjenne DP: overlappende delproblemer»?
De fleste CoddyKit-leksjoner tar omtrent 5–10 minutter. Hver leksjon er kort og interaktiv, slik at De gjør jevne fremskritt og kan fortsette akkurat der De slapp – både på nettet og i appen.
Kan jeg skrive og kjøre kode i denne Forberedelse til kodeintervjuer-leksjonen?
Ja. Alle Forberedelse til kodeintervjuer-leksjoner har en innebygd kodeeditor, slik at De kan skrive og kjøre ekte kode direkte i nettleseren og få umiddelbar tilbakemelding fra AI – uten lokal konfigurering.
Alle leksjonene i dette kurset
- Gjenkjenne DP: overlappende delproblemer
- Top-down-DP med memoisation
- Bottom-up-DP med tabulering
- Coin Change og trapp med minimale kostnader