0Pricing
DSA Interview Prep · Lektion

DP erkennen: Überlappende Teilprobleme

Erkennen Sie, wann brute-force-Rekursion dasselbe Teilproblem erneut löst, zeichnen Sie den Rekursionsbaum für Fibonacci und beobachten Sie das exponentielle Wachstum.

DP erkennen: Überlappende Teilprobleme ist eine kostenlose DSA Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 1 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 DSA Interview Prep-Lernpfads, und dein Fortschritt wird über Web und CoddyKit-App synchronisiert. Der DSA Interview Prep-Kurs umfasst insgesamt 4 Lektionen.

Was ist dynamische Programmierung?

Dynamische Programmierung (DP) löst komplexe Probleme, indem sie diese in einfachere, überlappende Teilprobleme zerlegt, jedes Teilproblem einmal löst und das Ergebnis speichert, um redundante Berechnungen zu vermeiden. DP ist anwendbar, wenn ein Problem zwei Eigenschaften besitzt: überlappende Teilprobleme (dasselbe Teilproblem wird bei einer naiven Rekursion mehrfach gelöst) und eine optimale Teilstruktur (die optimale Lösung lässt sich aus optimalen Lösungen für Teilprobleme aufbauen). Ohne beide Eigenschaften hilft DP nicht.

# 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: Der klassische Einstieg in DP

Die Fibonacci-Folge (fib(n) = fib(n-1) + fib(n-2)) ist das klassische Beispiel für überlappende Teilprobleme. Die naive Rekursion hat die exponentielle Laufzeit O(2^n), weil sie dieselben Werte wiederholt berechnet. Der Rekursionsbaum für fib(6) zeigt, dass fib(3) dreimal, fib(2) fünfmal und so weiter berechnet wird. Genau diese exponentielle Zunahme beseitigt DP, indem die berechneten Ergebnisse gespeichert werden.

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

Den Rekursionsbaum visualisieren

Das Zeichnen des Rekursionsbaums für fib(5) macht die Verschwendung sichtbar: Jeder Knoten erzeugt zwei Kindknoten, und identische Teilbäume erscheinen wiederholt. Die Gesamtzahl der Knoten im Baum beträgt O(2^n). Wenn Sie dieses Muster sehen – identische Funktionsaufrufe mit denselben Argumenten, die im Baum wiederholt vorkommen –, deutet dies darauf hin, dass DP durch das Zwischenspeichern von Ergebnissen helfen kann. Diese Fähigkeit zur Visualisierung ist entscheidend: Wenn Sie die wiederholten Teilbäume erkennen können, wissen Sie, dass DP anwendbar ist.

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

Überlappende Teilprobleme erkennen

Um überlappende Teilprobleme zu erkennen, schreiben Sie zunächst die Brute-Force-Rekursion und fragen Sie sich: „Gibt es mehrere rekursive Aufrufe mit DENSELBEN Argumenten?“ Wenn ja, kann DP helfen. Häufige Hinweise in Aufgabenstellungen sind Formulierungen wie „minimale/maximale Anzahl von X“, „auf wie viele Arten lässt sich Y erreichen?“ oder „kann Z erreicht werden?“. Solche Formulierungen weisen fast immer auf ein Problem mit optimaler Teilstruktur hin, bei dem die Antwort an Position i von Antworten an früheren Positionen abhängt.

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

Optimale Teilstruktur erklärt

Optimale Teilstruktur bedeutet, dass sich die optimale Lösung eines Problems aus optimalen Lösungen seiner Teilprobleme aufbauen lässt. Der kürzeste Pfad von A nach C über B ist beispielsweise genau dann optimal, wenn die Teilpfade A→B und B→C jeweils einzeln optimal sind. Wenn diese Eigenschaft gilt, können Sie das globale Optimum aus lokalen Optima von unten nach oben aufbauen. Probleme ohne optimale Teilstruktur (z. B. der längste Pfad in einem allgemeinen Graphen mit Zyklen) können nicht mit DP gelöst werden.

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

Treppensteigen: Ihre erste DP

Climbing Stairs (LeetCode #70): Auf wie viele verschiedene Arten können Sie n Treppenstufen erklimmen, wenn Sie jeweils 1 oder 2 Stufen nehmen? Sei dp[i] = Anzahl der Möglichkeiten, die Stufe i zu erreichen. Sie können Stufe i von Stufe i-1 (ein Schritt) oder von Stufe i-2 (zwei Schritte) aus erreichen, daher gilt dp[i] = dp[i-1] + dp[i-2]. Das ist die Fibonacci-Folge! Basisfälle: dp[1] = 1, dp[2] = 2. Zu erkennen, dass sich „Treppensteigen“ auf Fibonacci reduzieren lässt, ist eine klassische Einsicht in Interviews.

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!

Das DP-Framework: Definieren, Rekurrenz formulieren, Reihenfolge festlegen

Ein zuverlässiges dreistufiges DP-Framework: 1. Definieren Sie den Zustand — was stellt dp[i] (oder dp[i][j]) dar? Schreiben Sie dies zunächst auf Englisch auf. 2. Formulieren Sie die Rekurrenz — drücken Sie dp[i] durch kleinere Teilprobleme aus. Berücksichtigen Sie alle Fälle. 3. Bestimmen Sie die Füllreihenfolge — stellen Sie sicher, dass dp[i-1] (und andere Abhängigkeiten) vor dp[i] berechnet werden. Basisfälle initialisieren die Randbereiche. Dieses Framework verwandelt eine vage DP-Intuition in einen konkreten Implementierungsplan.

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

Wann Sie DP NICHT verwenden sollten

DP ist nicht immer die richtige Lösung. Verwenden Sie Greedy, wenn eine einzelne lokal optimale Entscheidung stets zur global optimalen Lösung führt (Aktivitätsauswahl, Jump Game I). Verwenden Sie Divide and Conquer, wenn sich die Teilprobleme nicht überschneiden (Merge Sort, binäre Suche). Verwenden Sie BFS, wenn es sich um die Suche nach dem kürzesten Pfad in einem ungewichteten Graphen handelt. DP ist zwar korrekt, aber oft überdimensioniert, wenn ein Greedy- oder einfacherer Ansatz existiert. Besprechen Sie in Vorstellungsgesprächen, warum Sie sich für DP und gegen die Alternativen entschieden haben.

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

Anzahl der verschiedenen Teilprobleme

Die Anzahl der verschiedenen Teilprobleme bestimmt die Zeit- und Speicherkomplexität von DP. Bei einer eindimensionalen DP für eine Eingabe der Größe n gibt es O(n) Teilprobleme. Bei einer zweidimensionalen DP für zwei Eingaben der Größen m und n gibt es O(mn) Teilprobleme. Wird jedes Teilproblem in O(k) Zeit gelöst (für k Auswahlmöglichkeiten in jedem Schritt), ergibt sich insgesamt O(n*k) beziehungsweise O(mn*k). Zählen Sie immer zuerst die verschiedenen Teilprobleme — damit erhalten Sie die Zeitkomplexität der DP, noch bevor Sie den Code schreiben.

# 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: Überlappende Entscheidungen

House Robber (LeetCode #198) fragt nach dem maximalen Betrag, den Sie aus einer Reihe von Häusern stehlen können, ohne benachbarte Häuser auszurauben. Bei jedem Haus haben Sie die Wahl: Sie rauben es aus (addieren seinen Wert und überspringen das vorherige Haus) oder Sie überspringen es (übernehmen das beste Ergebnis des vorherigen Schritts). dp[i] = max(dp[i-1], dp[i-2] + nums[i]). Dieses Muster von Entscheidungen in jedem Schritt ist die einfachste eindimensionale DP-Rekurrenz und tritt in Dutzenden von Aufgaben aus Vorstellungsgesprächen auf.

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

Plausibilitätsprüfung: Brute Force vs. DP

Überprüfen Sie Ihre DP immer anhand einer Brute-Force-Lösung mit kleinen Eingaben. Brute Force dient dabei als Referenzlösung. Sobald die DP für alle Testfälle mit Brute Force übereinstimmt, wissen Sie, dass die Rekurrenz korrekt ist. Erst danach sollten Sie den Speicherbedarf optimieren. Dieser testgetriebene Ansatz — Brute Force → Top-down-DP → Bottom-up-DP → speicheroptimierte DP — ist die professionelle Methode, DP-Lösungen während eines Vorstellungsgesprächs zu entwickeln und zu überprüfen.

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

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 gelernt: die zwei Bestandteile von DP (überlappende Teilprobleme und optimale Teilstruktur), wie Sie den Rekursionsbaum visualisieren, um wiederholte Aufrufe zu erkennen, das dreistufige DP-Framework (Zustand definieren, Rekurrenz formulieren, Füllreihenfolge festlegen) sowie erste Beispiele, darunter Fibonacci, Climbing Stairs und House Robber. Als Nächstes implementieren Sie Top-down-DP mit Memoisierung.

Häufig gestellte Fragen

Ist die Lektion „DP erkennen: Überlappende Teilprobleme“ kostenlos?

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

Was lerne ich in „DP erkennen: Überlappende Teilprobleme“?

Erkennen Sie, wann brute-force-Rekursion dasselbe Teilproblem erneut löst, zeichnen Sie den Rekursionsbaum für Fibonacci und beobachten Sie das exponentielle Wachstum. Du übst DSA 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 DSA Interview Prep zu starten?

Keine Vorkenntnisse erforderlich. DSA 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 1 von 4.

Wie lange dauert die Lektion „DP erkennen: Überlappende Teilprobleme“?

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 DSA Interview Prep-Lektion Code schreiben und ausführen?

Ja. Jede DSA 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 DSA Interview Prep