DSA Interview Prep · Lektion

Identifiera DP: överlappande delproblem

Identifiera när rekursion med brute force löser samma delproblem flera gånger, rita rekursionsträdet för Fibonacci och se den exponentiella tillväxten.

Lektion 1 av 413 steg

Identifiera DP: överlappande delproblem är en gratis lektion i DSA Interview Prep på CoddyKit. Detta är lektion 1 av 4. Du kan läsa vilka 3 lektioner som helst i den här lärvägen kostnadsfritt i sin helhet – därefter låser CoddyKit PRO upp alla lektioner, plus praktisk övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Den ingår i lärvägen för DSA Interview Prep, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i DSA Interview Prep innehåller totalt 4 lektioner.

Vad är dynamisk programmering?

Dynamisk programmering (DP) löser komplexa problem genom att dela upp dem i enklare överlappande delproblem, lösa varje delproblem en gång och lagra resultatet för att undvika överflödiga beräkningar. DP kan användas när ett problem har två egenskaper: överlappande delproblem (samma delproblem löses flera gånger i en naiv rekursion) och optimal delstruktur (den optimala lösningen kan byggas av optimala lösningar på delproblem). Utan båda dessa egenskaper hjälper DP inte.

# 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: den klassiska introduktionen till DP

Fibonacci-sekvensen (fib(n) = fib(n-1) + fib(n-2)) är det klassiska exemplet på överlappande delproblem. Den naiva rekursionen har exponentiell tidskomplexitet O(2^n) eftersom samma värden beräknas om gång på gång. Rekursionsträdet för fib(6) visar att fib(3) beräknas 3 gånger, fib(2) 5 gånger och så vidare. Denna exponentiella tillväxt är precis det som DP eliminerar genom att lagra redan beräknade resultat.

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 growth

Visualisering av rekursionsträdet

Att rita upp rekursionsträdet för fib(5) visar slöseriet: varje nod skapar två barnnoder och identiska delträd förekommer upprepade gånger. Det totala antalet noder i trädet är O(2^n). När ni ser detta mönster — identiska funktionsanrop med samma argument som upprepas i trädet — tyder det på att DP kan hjälpa genom att cachelagra resultat. Denna förmåga att visualisera är avgörande: om ni kan identifiera de upprepade delträden vet ni att DP kan användas.

# 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)')

Identifiera överlappande delproblem

Så känner ni igen överlappande delproblem: skriv den brute-force-baserade rekursionen och fråga er: ”finns det flera rekursiva anrop med SAMMA argument?” Om svaret är ja kan DP hjälpa. Vanliga signaler i problembeskrivningar är: ”minsta/största antal av X”, ”på hur många sätt kan vi göra Y?” och ”kan vi uppnå Z?”. Dessa formuleringar tyder nästan alltid på ett problem med optimal delstruktur, där svaret vid position i beror på svaren vid tidigare positioner.

# 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 förklarad

Optimal delstruktur innebär att problemets optimala lösning kan konstrueras utifrån optimala lösningar på dess delproblem. Till exempel är den kortaste vägen från A till C via B optimal om och endast om delvägarna A→B och B→C var och en är optimala. Om denna egenskap gäller kan ni bygga den globala optimumlösningen nerifrån och upp utifrån lokala optimumlösningar. Problem som saknar optimal delstruktur (t.ex. den längsta vägen i en allmän graf med cykler) kan inte lösas 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')

Trappklättring: det första DP-problemet

Trappklättring (LeetCode #70): hur många olika sätt finns det att klättra uppför n trappsteg om ni tar 1 eller 2 steg åt gången? Låt dp[i] vara antalet sätt att nå trappsteg i. Ni kan nå trappsteg i från trappsteg i-1 (ett steg) eller trappsteg i-2 (två steg), så dp[i] = dp[i-1] + dp[i-2]. Detta är Fibonacci! Basfall: dp[1] = 1, dp[2] = 2. Att känna igen att problemet ”trappklättring” kan reduceras till Fibonacci är en klassisk insikt från tekniska intervjuer.

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-ramverket: definiera, formulera rekurrensen, bestäm ordningen

Ett tillförlitligt DP-ramverk i tre steg: 1. Definiera tillståndet — vad representerar dp[i] (eller dp[i][j])? Skriv det på engelska. 2. Skriv rekurrensen — uttryck dp[i] med hjälp av mindre delproblem. Ta med alla fall. 3. Bestäm fyllnadsordningen — se till att dp[i-1] (och andra beroenden) har beräknats innan dp[i]. Basfall initierar gränsen. Det här ramverket omvandlar en vag DP-intuition till 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 INTE ska användas

DP är inte alltid svaret. Använd greedy när ett enda lokalt optimalt val alltid leder till den globalt optimala lösningen (activity selection, jump game I). Använd divide and conquer när delproblemen inte överlappar (merge sort, binärsökning). Använd BFS när problemet gäller den kortaste vägen i en oviktad graf. DP är korrekt, men ofta överdrivet när det finns en greedy-algoritm eller en enklare metod. På intervjuer bör Ni diskutera varför Ni valde DP framför alternativen.

# 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.')

Räkna antalet unika delproblem

Antalet unika delproblem avgör DP:ns tids- och minneskomplexitet. För en endimensionell DP på en indata av storleken n finns O(n) delproblem. För en tvådimensionell DP på två indata med storlekarna m och n finns O(mn) delproblem. Om varje delproblem löses på O(k) tid (för k val vid varje steg) blir den totala tiden O(n*k) eller O(mn*k). Räkna alltid antalet unika delproblem först — då får Ni DP:ns tidskomplexitet innan Ni ens har skrivit koden.

# 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: överlappande val

House Robber (LeetCode #198) går ut på att hitta det största beloppet som kan rånas från hus på rad utan att råna två intilliggande hus. Vid varje hus väljer Ni mellan att råna det (lägga till dess värde och hoppa över det föregående) eller att hoppa över det (ta det bästa resultatet från det föregående). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Det här mönstret, där man väljer vid varje steg, är den enklaste rekurrensen för endimensionell DP och förekommer i dussintals intervjuproblem.

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 3

Rimlighetskontroll: brute force kontra DP

Verifiera alltid Er DP mot en brute-force-lösning på små indata. Brute-force-lösningen är Ert facit. När DP matchar brute-force-lösningen för alla testfall vet Ni att rekurrensen är korrekt. Optimera för minnesåtgång först därefter. Det här testdrivna arbetssättet — brute force → top-down-DP → bottom-up-DP → minnesoptimerad DP — är det professionella sättet att utveckla och verifiera DP-lösningar under en 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}')

Snabbkontroll

Testa Er förståelse av begreppen Data Structures & Algorithms — Coding Interview Prep från den här lektionen.

Sammanfattning av lektionen

I den här lektionen har Ni lärt Er: DP:s två beståndsdelar (överlappande delproblem och optimal delstruktur), hur man visualiserar rekursionsträdet för att identifiera upprepade anrop, DP-ramverkets tre steg (definiera tillståndet, rekurrensen och fyllnadsordningen) samt de första exemplen, inklusive Fibonacci, climbing stairs och house robber. Nästa steg är att implementera top-down-DP med memoization.

Gratis att börja

Lär dig Python med en AI-lärare – gratis

Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.

Kurser
30
Lektioner
120

Vanliga frågor

Är lektionen ”Identifiera DP: överlappande delproblem” gratis?

Ja – du kan läsa vilka 3 lektioner som helst i lärvägen DSA Interview Prep, inklusive ”Identifiera DP: överlappande delproblem”, kostnadsfritt i sin helhet här på webben. Därefter låser CoddyKit PRO upp alla lektioner, plus interaktiv övning med en inbyggd kodredigerare och en AI-lärare dygnet runt. Kursen i DSA Interview Prep innehåller totalt 4 lektioner.

Vad lär jag mig i ”Identifiera DP: överlappande delproblem”?

Identifiera när rekursion med brute force löser samma delproblem flera gånger, rita rekursionsträdet för Fibonacci och se den exponentiella tillväxten. Ni övar på DSA Interview Prep med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.

Behöver jag någon erfarenhet för att börja lära mig DSA Interview Prep?

Du behöver inga förkunskaper. Utbildningen i DSA Interview Prep på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 1 av 4.

Hur lång tid tar lektionen ”Identifiera DP: överlappande delproblem”?

De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.

Kan jag skriva och köra kod i den här DSA Interview Prep-lektionen?

Ja. Varje DSA Interview Prep-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.

Alla lektioner i den här kursen

  1. Identifiera DP: överlappande delproblem
  2. Top-down-DP med memoisation
  3. Bottom-up-DP med tabellutfyllnad
  4. Coin change och trappa med minimal kostnad
← Tillbaka till DSA Interview Prep