0Pricing
Coding Interview Prep · Lektion

Top-Down-DP mit Memoisation

Ergänzen Sie eine rekursive Lösung um ein Memo-Dictionary, um doppelte Aufrufe zu vermeiden, und verwenden Sie @lru_cache für Memoisation mit minimalem Code.

Top-Down-DP mit Memoisation ist eine kostenlose Coding Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 2 von 4. Du kannst die komplette Lektion unten kostenlos lesen – dann übst du sie direkt im Browser mit einem integrierten Code-Editor und einem KI-Tutor rund um die Uhr. Sie ist Teil des Coding Interview Prep-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Top-down-DP: Die Idee der Memoisierung

Top-down-DP beginnt mit der ursprünglichen rekursiven Lösung und ergänzt sie um Memoisierung: einen Cache, der das Ergebnis jedes Teilproblems beim ersten Berechnen speichert. Bei nachfolgenden Aufrufen mit denselben Argumenten wird das gespeicherte Ergebnis sofort zurückgegeben, ohne erneut zu rekursieren. Dadurch wird aus einer naiven Rekursion mit O(2^n) eine Lösung mit O(n) — bei minimalen Codeänderungen, oft genügt es, einer bestehenden rekursiven Lösung 2–3 Zeilen hinzuzufügen.

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

Memoisiertes Fibonacci

Das Hinzufügen eines Memo-Dictionarys zur naiven Fibonacci-Rekursion reduziert die Laufzeit von O(2^n) auf O(n). Der erste Aufruf von fib(k) berechnet und speichert das Ergebnis. Alle nachfolgenden Aufrufe für dasselbe k geben den gespeicherten Wert sofort zurück. Die Speicherkomplexität beträgt O(n) für das Memo-Dictionary plus O(n) für den Aufruf-Stack. Vergleichen Sie die Anzahl der Aufrufe: Ohne Memoisierung führt fib(30) etwa 2 Millionen Aufrufe aus, mit Memoisierung genau 30.

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

@functools.lru_cache verwenden

Pythons Decorator @functools.lru_cache(maxsize=None) (oder das Alias @cache ab Python 3.9) memoisiert eine Funktion automatisch anhand ihrer Argumente. Dies ist die sauberste Möglichkeit, in Vorstellungsgesprächen Top-down-DP hinzuzufügen: Schreiben Sie die rekursive Lösung, versehen Sie sie mit dem Decorator, und fertig. Der Decorator speichert alle Ergebnisse in einem Dictionary, dessen Schlüssel aus den Funktionsargumenten bestehen. Diese müssen hashbar sein (keine Listen — verwenden Sie stattdessen Tupel).

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-DP für Coin Change

Coin Change (LeetCode #322): Bei gegebenen Münzwerten und einem Zielbetrag soll die minimale Anzahl benötigter Münzen ermittelt werden. Die rekursive Formulierung lautet: Wählen Sie für jede Münze diese Münze aus und lösen Sie das Problem für den verbleibenden Betrag; anschließend nehmen Sie das Minimum. Memoisieren Sie anhand des Betrags, um Neuberechnungen zu vermeiden. Basisfall: Für amount=0 werden 0 Münzen benötigt; für einen nicht erreichbaren Betrag wird unendlich zurückgegeben (oder nach der Rekursion -1).

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-Climbing-Stairs mit K Schritten

Verallgemeinern Sie Climbing Stairs so, dass 1 bis k Schritte erlaubt sind. Der Zustand ist die aktuelle Treppenstufe. Von Stufe i aus können Sie die Stufen i+1, i+2, ..., i+k erreichen. Die Rekurrenz lautet: dp(i) = sum of dp(i-j) for j in 1..k if i-j >= 0. Durch Memoisierung wird die Komplexität von O(k^n) auf O(n*k) reduziert. Diese Verallgemeinerung tritt in Aufgaben wie „minimale Kosten bis zur letzten Stufe“ und „Anzahl der Möglichkeiten, ein Gitter zu füllen“ auf.

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: Zweidimensionale Memoisierung

Die Longest Common Subsequence (LCS) benötigt einen zweidimensionalen Zustand: dp(i, j) = Länge der LCS von s1[:i] und s2[:j]. Falls s1[i-1] == s2[j-1], stimmen die Zeichen überein: dp(i,j) = 1 + dp(i-1, j-1). Andernfalls gilt: dp(i,j) = max(dp(i-1,j), dp(i,j-1)) — überspringen Sie ein Zeichen aus einer der beiden Zeichenketten. Memoisierung anhand von (i, j) ergibt O(mn) statt 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-Dictionary vs. lru_cache: Wann Sie was wählen sollten

Verwenden Sie @lru_cache, wenn die Argumente Ihrer Funktion aus hashbaren primitiven Typen bestehen (int, str, tuple). Verwenden Sie ein manuelles Memo-Dictionary, wenn Sie veränderlichen Zustand übergeben müssen (Listen oder Dictionaries, die Sie in Tupel umwandeln), verfolgen müssen, welche Schlüssel bereits berechnet wurden, oder sich in einer Klassenmethode befinden, in der self nicht mitgespeichert werden sollte. Das manuelle Memo-Dictionary ist expliziter und vermeidet subtile Probleme mit Closures in rekursiven Hilfsfunktionen.

# @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-Target-Sum

Target Sum (LeetCode #494): Weisen Sie jeder Zahl ein + oder - zu und zählen Sie die Zuweisungen, die eine vorgegebene Zielsumme ergeben. Zustand: dp(index, current_sum). Probieren Sie an jedem Index sowohl das Addieren (+) als auch das Subtrahieren (-) der aktuellen Zahl aus. Memoisierung anhand von (index, current_sum) reduziert die Brute-Force-Komplexität von O(2^n) auf O(n * sum_range). Der Summenbereich ist durch die Gesamtsumme aller Zahlen begrenzt, sodass es insgesamt O(n * S) Zustände gibt.

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 vs. Bottom-up: Vor- und Nachteile

Top-down (Memoisierung) hat folgende Vorteile: Der Ansatz lässt sich natürlich formulieren, da er mit der rekursiven Lösung beginnt, berechnet nur tatsächlich benötigte Teilprobleme (lazy) und ermöglicht das schrittweise Hinzufügen eines Caches. Bottom-up (Tabellierung) bietet folgende Vorteile: keinen Aufwand für den Aufruf-Stack (und kein Python-Rekursionslimit), besseren cachefreundlichen Speicherzugriff und einfachere Speicheroptimierung. Beide Ansätze haben dieselbe asymptotische Komplexität. Beginnen Sie in Vorstellungsgesprächen mit Top-down, um die Korrektheit zu überprüfen, und wandeln Sie die Lösung anschließend in Bottom-up um, wenn ein geringerer Speicherbedarf verlangt wird.

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

Word Break mit Top-down-DP

Word Break (LeetCode #139) fragt, ob sich ein String s in Wörter aus einem Dictionary aufteilen lässt. Zustand: dp(i) = ob sich s[i:] aufteilen lässt. Probieren Sie ab Index i alle Wörter aus: Falls s[i:i+len(w)] == w, führen Sie die Rekursion für das verbleibende Suffix fort. Memoisierung anhand des Startindex reduziert die Brute-Force-Komplexität von O(2^n) auf O(n^2) (oder O(n * max_word_len), wenn die Set-Mitgliedschaftsprüfung einbezogen wird).

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

Rekursionslimit und Itertools

Pythons standardmäßiges Rekursionslimit beträgt 1000 (festgelegt durch sys.getrecursionlimit()). Bei DP-Aufgaben mit großen Eingaben (n = 10.000+) stößt Top-down-Memoisierung an dieses Limit. Möglichkeiten sind, das Limit mit sys.setrecursionlimit(100000) zu erhöhen oder die Lösung in Bottom-up-DP umzuwandeln. In Competitive Programming ist das Erhöhen des Limits üblich; in Produktionscode sollten Sie aus Gründen der Zuverlässigkeit immer Bottom-up- oder iterative Lösungen bevorzugen.

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

Schnelltest

Testen Sie Ihr Verständnis der Konzepte aus Data Structures & Algorithms — Coding Interview Prep in dieser Lektion.

Zusammenfassung der Lektion

In dieser Lektion haben Sie Top-down-DP mit einem Memo-Dictionary und dem Decorator @lru_cache gelernt, memoiserte Lösungen für Fibonacci, Coin Change, LCS, Target Sum und Word Break kennengelernt und erfahren, wann Sie Top-down gegenüber Bottom-up bevorzugen sollten. Als Nächstes implementieren Sie Bottom-up-DP mit Tabellierung und Speicheroptimierung.

Häufig gestellte Fragen

Ist die Lektion „Top-Down-DP mit Memoisation“ kostenlos?

Ja — der vollständige Text von „Top-Down-DP mit Memoisation“ ist hier im Web kostenlos zu lesen. Um sie interaktiv zu üben (integrierter Code-Editor und 24/7 KI-Tutor) und den Rest des Coding Interview Prep-Kurses freizuschalten, upgrade auf CoddyKit PRO. Der Coding Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Was lerne ich in „Top-Down-DP mit Memoisation“?

Ergänzen Sie eine rekursive Lösung um ein Memo-Dictionary, um doppelte Aufrufe zu vermeiden, und verwenden Sie @lru_cache für Memoisation mit minimalem Code. Du übst Coding Interview Prep mit praktischem Code, den du direkt im Browser ausführst, und ein 24/7 KI-Tutor beantwortet deine Fragen während du die Lektion bearbeitest.

Brauche ich Erfahrung, um Coding Interview Prep zu starten?

Keine Vorkenntnisse erforderlich. Coding Interview Prep auf CoddyKit ist für Anfänger bis fortgeschrittene Lernende strukturiert, sodass du hier starten oder von Anfang an beginnen und in deinem eigenen Tempo voranschreiten kannst. Dies ist Lektion 2 von 4.

Wie lange dauert die Lektion „Top-Down-DP mit Memoisation“?

Die meisten CoddyKit-Lektionen dauern etwa 5–10 Minuten. Jede ist kompakt und interaktiv, sodass du stetig Fortschritte machst und genau dort weitermachst, wo du aufgehört hast – im Web und in der App.

Kann ich in dieser Coding Interview Prep-Lektion Code schreiben und ausführen?

Ja. Jede Coding Interview Prep-Lektion enthält einen integrierten Code-Editor, sodass du echten Code direkt in deinem Browser schreibst und ausführst und sofort KI-Feedback erhältst — ohne lokale Einrichtung erforderlich.

Alle Lektionen in diesem Kurs

  1. DP erkennen: Überlappende Teilprobleme
  2. Top-Down-DP mit Memoisation
  3. Bottom-Up-DP mit Tabellierung
  4. Coin Change und Treppe mit minimalen Kosten
← Zurück zu Coding Interview Prep