Forberedelse til kodeinterviews · Lektion

Top-down-DP med memoization

Tilføj et memo-dict til en rekursiv løsning for at beskære gentagne kald, og brug @lru_cache til memoization med minimal kode.

Lektion 2 af 413 trin

Top-down-DP med memoization er en gratis Forberedelse til kodeinterviews-lektion på CoddyKit. Dette er lektion 2 af 4. Du kan læse hele lektionen gratis nedenfor — og derefter øve dig praktisk i browseren med en indbygget kodeeditor og en AI-vejleder, der er tilgængelig døgnet rundt. Den er en del af læringsforløbet i Forberedelse til kodeinterviews, og dine fremskridt synkroniseres på tværs af nettet og CoddyKit-appen. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Top-down-DP: Idéen med memoisering

Top-down-DP starter med den oprindelige rekursive løsning og tilføjer memoisering: en cache, der gemmer resultatet af hvert delproblem, første gang det beregnes. Ved efterfølgende kald med de samme argumenter returneres det cachelagrede resultat med det samme uden rekursion. Det omdanner en naiv rekursion på O(2^n) til O(n) med minimale kodeændringer — ofte blot ved at tilføje 2-3 linjer til en eksisterende rekursiv løsning.

# Top-down approach:
# 1. Write the recursive solution (natural but slow)
# 2. Add a memo dict to cache results
# 3. Before recursing, check if the result is cached
# 4. Before returning, store the result in the cache

# This is also called 'memoization' (US spelling)
# 'memoize' means 'to remember', not 'memorize'

# The cache key is the function arguments
# For fib: key is n
# For 2D DP: key is (i, j)
# For 3D DP: key is (i, j, k)
print('Top-down = recursion + memo cache')

Memoiseret Fibonacci

Hvis du tilføjer en memo-ordbog til den naive Fibonacci-rekursion, reduceres tiden fra O(2^n) til O(n). Det første kald til fib(k) beregner og gemmer resultatet. Alle efterfølgende kald for samme k returnerer straks den cachelagrede værdi. Pladskompleksiteten er O(n) for memo-ordbogen plus O(n) for kaldestakken. Sammenlign antallet af kald: uden memoisering foretager fib(30) cirka 2 millioner kald; med memoisering præcis 30 kald.

def fib_memo(n, memo=None):
    if memo is None:
        memo = {}
    if n in memo:
        return memo[n]  # return cached result
    if n <= 1:
        return n
    memo[n] = fib_memo(n-1, memo) + fib_memo(n-2, memo)
    return memo[n]

# Verify speed improvement:
print(fib_memo(30))   # fast!
print(fib_memo(50))   # still fast
print(fib_memo(100))  # no problem

# Without memo, fib_naive(50) would take minutes
# With memo: each of the 50 sub-problems computed once

Brug af @functools.lru_cache

Pythons @functools.lru_cache(maxsize=None)-dekorator (eller aliaset @cache i Python 3.9+) memoisérer automatisk en funktion ud fra dens argumenter. Det er den enkleste måde at tilføje top-down-DP på i en teknisk jobsamtale — skriv den rekursive løsning, sæt dekoratoren på, og så er du færdig. Dekoratoren cacher alle resultater i en ordbog med nøgler baseret på funktionens argumenter, som skal være hashbare (ingen lister — brug tupler i stedet).

import functools

@functools.lru_cache(maxsize=None)
def fib(n):
    if n <= 1:
        return n
    return fib(n-1) + fib(n-2)

print(fib(50))   # 12586269025
print(fib(100))  # works instantly

# Clear cache between tests if needed:
fib.cache_clear()

# Python 3.9+ shorthand:
# from functools import cache
# @cache
# def fib(n): ...

print(fib.cache_info())  # shows hits, misses, maxsize, currsize

Top-down-møntveksling

Møntveksling (LeetCode #322): Givet møntværdier og et målbeløb skal du finde det mindste antal mønter, der er nødvendigt. Den rekursive formulering er: For hver mønt tager du den og løser problemet for det resterende beløb, hvorefter du vælger minimumsværdien. Memoisér på beløbet for at undgå genberegning. Basistilfælde: amount=0 kræver 0 mønter; et umuligt beløb returnerer uendelighed (eller -1 efter rekursionen).

import functools

def coin_change_top_down(coins, amount):
    @functools.lru_cache(maxsize=None)
    def dp(remaining):
        if remaining == 0:
            return 0  # no coins needed
        if remaining < 0:
            return float('inf')  # impossible
        # Try each coin and take the minimum
        return 1 + min(dp(remaining - c) for c in coins)

    result = dp(amount)
    return result if result != float('inf') else -1

print(coin_change_top_down([1, 5, 6, 9], 11))  # 2: (5+6) or (2*5+1?no: 9+2?no) 5+6=11 YES
print(coin_change_top_down([2], 3))             # -1: impossible
print(coin_change_top_down([1, 2, 5], 11))      # 3: 5+5+1

Top-down-trappegang med K trin

Generalisér trappegang, så du må tage fra 1 til k trin ad gangen. Tilstanden er det aktuelle trin, og fra trin i kan du nå trin i+1, i+2, ..., i+k. Rekurrensrelationen er: dp(i) = sum of dp(i-j) for j in 1..k if i-j >= 0. Memoisering gør dette til O(n*k) i stedet for O(k^n). Denne generalisering optræder i problemer som 'minimumomkostning ved at nå det sidste trin' og 'optælling af måder at udfylde et gitter på'.

import functools

def climb_k_steps(n, k):
    @functools.lru_cache(maxsize=None)
    def dp(i):
        if i == 0:
            return 1  # base: one way to stay at ground
        if i < 0:
            return 0  # impossible
        # From stair i, you could have come from i-1, i-2, ..., i-k
        return sum(dp(i - j) for j in range(1, k+1) if i - j >= 0)

    return dp(n)

# k=2 (original): should match fib-like sequence
print([climb_k_steps(n, 2) for n in range(7)])  # [1,1,2,3,5,8,13]
# k=3: more options
print([climb_k_steps(n, 3) for n in range(7)])  # [1,1,2,4,7,13,24]

Top-down-LCS: 2D-memoisering

Den længste fælles delsekvens (LCS) kræver en 2D-tilstand: dp(i, j) = LCS-længden af s1[:i] og s2[:j]. Hvis s1[i-1] == s2[j-1], er tegnene ens: dp(i,j) = 1 + dp(i-1, j-1). Ellers: dp(i,j) = max(dp(i-1,j), dp(i,j-1)) — spring ét tegn over fra en af strengene. Memoisering på (i, j) giver O(mn) i stedet for O(2^(m+n)).

import functools

def lcs_top_down(s1, s2):
    m, n = len(s1), len(s2)

    @functools.lru_cache(maxsize=None)
    def dp(i, j):
        if i == 0 or j == 0:
            return 0  # empty prefix has LCS of 0
        if s1[i-1] == s2[j-1]:
            return 1 + dp(i-1, j-1)  # characters match
        return max(dp(i-1, j), dp(i, j-1))  # skip one

    return dp(m, n)

print(lcs_top_down('abcde', 'ace'))   # 3: 'ace'
print(lcs_top_down('abc', 'abc'))     # 3: 'abc'
print(lcs_top_down('abc', 'def'))     # 0: no common chars

Memo-ordbog eller lru_cache: Hvornår skal du vælge hvad

Brug @lru_cache, når dine funktionsargumenter er hashbare primitive typer (int, str, tuple). Brug en manuel memo-ordbog, når du skal videregive en muterbar tilstand (lister, ordbøger) ved at konvertere den til tupler, når du skal holde styr på, hvilke nøgler der er beregnet, eller når du arbejder i en klassemetode, hvor self ikke bør caches. Den manuelle memo-ordbog er mere eksplicit og undgår subtile problemer med lukninger i rekursive hjælpefunktioner.

# @lru_cache: clean, automatic, O(1) overhead
# Use when: arguments are simple (int, str, tuple)
import functools
@functools.lru_cache(maxsize=None)
def simple_dp(n):
    if n <= 1: return n
    return simple_dp(n-1) + simple_dp(n-2)

# Manual memo dict: explicit, flexible
# Use when: complex state, need to inspect memo, class methods
def manual_memo_dp(s1, s2):
    memo = {}
    def dp(i, j):
        if (i,j) in memo: return memo[(i,j)]
        if i == 0 or j == 0:
            return 0
        if s1[i-1] == s2[j-1]:
            memo[(i,j)] = 1 + dp(i-1, j-1)
        else:
            memo[(i,j)] = max(dp(i-1,j), dp(i,j-1))
        return memo[(i,j)]
    return dp(len(s1), len(s2))

print(manual_memo_dp('abcde', 'ace'))  # 3

Top-down-målsum

Målsum (LeetCode #494): tildel + eller - til hvert tal, og optæl de tildelinger, der giver en bestemt målsum. Tilstanden er: dp(index, current_sum). Ved hvert indeks prøver du både at lægge (+) det aktuelle tal til og trække (-) det fra. Memoisering på (index, current_sum) omdanner den udtømmende søgning på O(2^n) til O(n * sum_range). Sumintervallet er begrænset af totalsummen af alle tal, hvilket giver O(n * S) mulige tilstande i alt.

import functools

def find_target_sum_ways(nums, target):
    @functools.lru_cache(maxsize=None)
    def dp(index, current_sum):
        if index == len(nums):
            return 1 if current_sum == target else 0
        # Try adding the number
        add = dp(index + 1, current_sum + nums[index])
        # Try subtracting the number
        subtract = dp(index + 1, current_sum - nums[index])
        return add + subtract

    return dp(0, 0)

print(find_target_sum_ways([1,1,1,1,1], 3))  # 5
print(find_target_sum_ways([1], 1))            # 1
print(find_target_sum_ways([1], -1))           # 1

Top-down og bottom-up: Fordele og ulemper

Fordele ved top-down (memoisering): naturlig at skrive (tager udgangspunkt i den rekursive løsning), beregner kun de delproblemer, der faktisk er nødvendige (doven beregning), og gør det nemt at tilføje en cache trinvist. Fordele ved bottom-up (tabulering): ingen overhead fra kaldestakken (ingen Python-grænse for rekursion), mere cachevenlig hukommelsesadgang og nemmere pladsoptimering. Begge har den samme asymptotiske kompleksitet. Start under tekniske jobsamtaler med top-down for at verificere korrektheden, og konvertér derefter til bottom-up, hvis der bliver bedt om bedre pladsforbrug.

# Top-down advantages:
# + Natural: write recursive, add @cache
# + Lazy: only computes needed sub-problems
# + Easy to reason about correctness
# - Uses call stack (recursion limit in Python)
# - Higher constant factor (function call overhead)

# Bottom-up advantages:
# + No recursion limit
# + Better cache performance (sequential memory)
# + Easier to space-optimise (rolling array)
# - Must compute all sub-problems in order
# - Less intuitive for complex 2D/3D problems

# Interview strategy:
# Start with top-down to verify recurrence,
# convert to bottom-up only if asked.
print('Top-down: easy to write | Bottom-up: efficient for large n')

Orddeling med top-down-DP

Ordopdeling (LeetCode #139) spørger, om en streng s kan opdeles i ord fra en ordbog. Tilstanden er: dp(i) = om s[i:] kan opdeles. Fra indeks i prøver du alle ord: Hvis s[i:i+len(w)] == w, fortsætter du rekursivt med det resterende suffiks. Memoisering på startindekset omdanner den udtømmende søgning på O(2^n) til O(n^2) (eller O(n * max_word_len)) med opslag i et sæt.

import functools

def word_break(s, word_dict):
    word_set = set(word_dict)

    @functools.lru_cache(maxsize=None)
    def dp(start):
        if start == len(s):
            return True  # successfully segmented entire string
        for end in range(start + 1, len(s) + 1):
            if s[start:end] in word_set and dp(end):
                return True
        return False

    return dp(0)

print(word_break('leetcode', ['leet', 'code']))       # True
print(word_break('applepenapple', ['apple', 'pen'])) # True
print(word_break('catsandog', ['cats', 'dog', 'and', 'cat', 'san', 'andog'])) # False

Rekursionsgrænse og Itertools

Pythons standardrekursionsgrænse er 1000 (fastsat af sys.getrecursionlimit()). For DP-problemer med store input (n = 10.000+) vil top-down-memoisering ramme denne grænse. Du kan enten hæve grænsen med sys.setrecursionlimit(100000) eller konvertere til bottom-up-DP. I konkurrenceprogrammering er det almindeligt at hæve grænsen; i produktionskode bør du altid foretrække bottom-up- eller iterative løsninger af hensyn til pålideligheden.

import sys

print('Default recursion limit:', sys.getrecursionlimit())  # 1000

# For large DP problems, increase if needed:
# sys.setrecursionlimit(100000)

# Better: convert to bottom-up DP for large n
def fib_bottom_up(n):
    if n <= 1: return n
    a, b = 0, 1
    for _ in range(2, n+1):
        a, b = b, a + b
    return b

# No recursion limit issue:
print(fib_bottom_up(10000))  # works fine, no recursion

Hurtigt tjek

Afprøv din forståelse af begreberne Data Structures & Algorithms — Coding Interview Prep fra denne lektion.

Opsummering af lektionen

I denne lektion lærte du: top-down-DP med en memo-ordbog og @lru_cache-dekoratoren, memoiserede løsninger på Fibonacci, møntveksling, LCS, målsum og ordopdeling samt hvornår du skal vælge top-down frem for bottom-up. Næste gang implementerer vi bottom-up-DP med tabulering og pladsoptimering.

Gratis at komme i gang

Lær Forberedelse til kodeinterviews med en AI-underviser — gratis

Skriv og kør rigtig kode i din browser, få øjeblikkelig hjælp fra en AI-underviser døgnet rundt, og fortsæt, hvor du slap, på web eller i appen.

Kurser
90
Lektioner
360

Ofte stillede spørgsmål

Er lektionen “Top-down-DP med memoization” gratis?

Ja — hele teksten til “Top-down-DP med memoization” kan læses gratis her på nettet. Hvis du vil øve dig interaktivt med en indbygget kodeeditor og en AI-vejleder døgnet rundt og få adgang til resten af Forberedelse til kodeinterviews-kurset, skal du opgradere til CoddyKit PRO. Forberedelse til kodeinterviews-kurset indeholder 4 lektioner i alt.

Hvad lærer jeg i “Top-down-DP med memoization”?

Tilføj et memo-dict til en rekursiv løsning for at beskære gentagne kald, og brug @lru_cache til memoization med minimal kode. Du øver dig i Forberedelse til kodeinterviews med praktisk kode, som du kører direkte i browseren, og en AI-vejleder døgnet rundt besvarer dine spørgsmål, mens du arbejder dig gennem lektionen.

Skal jeg have erfaring for at begynde på Forberedelse til kodeinterviews?

Der kræves ingen tidligere erfaring. Forberedelse til kodeinterviews på CoddyKit er tilrettelagt for både begyndere og øvede, så du kan starte her eller fra begyndelsen og lære i dit eget tempo. Dette er lektion 2 af 4.

Hvor lang tid tager lektionen “Top-down-DP med memoization”?

De fleste CoddyKit-lektioner tager cirka 5–10 minutter. Hver lektion er kort og interaktiv, så du gør løbende fremskridt og kan fortsætte, hvor du slap – på både web og app.

Kan jeg skrive og køre kode i denne Forberedelse til kodeinterviews-lektion?

Ja. Alle Forberedelse til kodeinterviews-lektioner har en indbygget kodeeditor, så du kan skrive og køre rigtig kode direkte i din browser og få øjeblikkelig feedback fra AI – uden lokal opsætning.

Alle lektioner i dette kursus

  1. Genkendelse af DP: overlappende delproblemer
  2. Top-down-DP med memoization
  3. Bottom-up-DP med tabulering
  4. Coin change og trappe med minimale omkostninger
← Tilbage til Forberedelse til kodeinterviews