0Pricing
DSA Interview Prep · Lektion

Speicheroptimierung für 2D-DP

Reduzieren Sie den Speicherbedarf von LCS und Editierdistanz von O(mn) auf O(min(m,n)), indem Sie nur die aktuelle und die vorherige Zeile der DP-Tabelle behalten.

Speicheroptimierung für 2D-DP ist eine kostenlose DSA Interview Prep-Lektion auf CoddyKit. Dies ist Lektion 4 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.

Warum der Speicherbedarf bei 2D-DP wichtig ist

Eine 2D-DP-Tabelle für Zeichenketten der Länge 1000 benötigt 1000×1000 = 1,000,000 Zellen – ungefähr 8 MB bei 64-Bit-Ganzzahlen. Für längere Sequenzen (DNA-Alignment, große Text-Diffs) wird dies unpraktisch. Die zentrale Beobachtung ist, dass die meisten 2D-DP-Rekurrenzen nur die aktuelle und vorherige Zeile betrachten. Daher kann die gesamte Tabelle auf ein oder zwei 1D-Arrays komprimiert werden. Das ist der Kern der Optimierung des Speicherbedarfs bei 2D-DP.

# Full 2D DP: O(mn) space
# LCS for 1000-char strings
m, n = 1000, 1000
dp_2d_size = m * n * 8  # bytes (64-bit ints)
print(f'2D table: {dp_2d_size:,} bytes = {dp_2d_size//1024} KB')

# 1D rolling array: O(n) space
dp_1d_size = n * 8
print(f'1D array: {dp_1d_size:,} bytes = {dp_1d_size} bytes')
print(f'Space saving: {dp_2d_size // dp_1d_size}x')

Muster des rollierenden Arrays

Das Muster des rollierenden Arrays ersetzt die vollständige 2D-Tabelle durch ein 1D-Array, das die vorherige Zeile darstellt. Beim Berechnen der Zeile i aktualisieren Sie jede Zelle j mithilfe des aktuellen Werts dp[j] (der weiterhin dp[i-1][j] aus der vorherigen Zeile enthält) und des gerade aktualisierten dp[j-1] (also dp[i][j-1]). Eine Variable diagonal erfasst dp[i-1][j-1], bevor dieser Wert überschrieben wird. Dieses Muster gilt für LCS, Edit-Distanz und die meisten 2D-DP-Probleme.

# Rolling array template for 2D DP
# Before update: dp[j] holds dp[i-1][j] (previous row)
# After update: dp[j] holds dp[i][j] (current row)

def rolling_array_template(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * (n + 1)  # represents one row
    for i in range(1, m + 1):
        diag = 0  # stores dp[i-1][j-1] before overwrite
        for j in range(1, n + 1):
            temp = dp[j]  # save dp[i-1][j] before overwriting
            # compute dp[i][j] using dp[j] (above) and dp[j-1] (left) and diag
            dp[j] = diag + dp[j] + dp[j-1]  # placeholder logic
            diag = temp
    return dp[n]

LCS mit O(min m,n) Speicher

Stellen Sie bei der LCS sicher, dass text1 die kürzere Zeichenkette ist, damit n klein ist. Reservieren Sie ein 1D-Array der Größe n+1. Verarbeiten Sie die Zeilen nacheinander. Speichern Sie an jeder Zelle zunächst temp = dp[j] (dies ist dp[i-1][j]). Anschließend gilt: Wenn die Zeichen übereinstimmen, dp[j] = diag + 1; andernfalls dp[j] = max(dp[j], dp[j-1]). Setzen Sie schließlich diag = temp. Nach allen Zeilen enthält dp[n] die Länge der LCS.

def lcs_space_opt(text1, text2):
    # Ensure text2 is the shorter one
    if len(text1) < len(text2):
        text1, text2 = text2, text1
    m, n = len(text1), len(text2)
    dp = [0] * (n + 1)
    for i in range(1, m + 1):
        diag = 0
        for j in range(1, n + 1):
            temp = dp[j]  # dp[i-1][j]
            if text1[i-1] == text2[j-1]:
                dp[j] = diag + 1
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp
    return dp[n]

print(lcs_space_opt('ABCBDAB', 'BDCABA'))  # 4
print(lcs_space_opt('AGGTAB', 'GXTXAYB')) # 4

Edit-Distanz mit O(n) Speicher

Die Edit-Distanz verwendet dasselbe Muster des rollierenden Arrays. Das initiale 1D-Array stellt Zeile 0 dar: dp[j] = j (Einfügen von j Zeichen). Setzen Sie für jede Zeile i dp[0] = i (Löschen von i Zeichen) und speichern Sie diag = dp[0] vor der Aktualisierung. Speichern Sie in der inneren Schleife temp = dp[j], berechnen Sie den neuen Wert aus Einfügen (dp[j-1]+1), Löschen (dp[j]+1) und Ersetzen (diag + cost) und setzen Sie anschließend diag = temp.

def edit_dist_opt(s, t):
    m, n = len(s), len(t)
    dp = list(range(n + 1))   # row 0: dp[0][j] = j
    for i in range(1, m + 1):
        diag = dp[0]           # dp[i-1][0] before dp[0] update
        dp[0] = i              # dp[i][0] = i
        for j in range(1, n + 1):
            temp = dp[j]       # dp[i-1][j]
            cost = 0 if s[i-1] == t[j-1] else 1
            dp[j] = min(
                dp[j-1] + 1,  # insert
                dp[j] + 1,    # delete
                diag + cost   # replace or match
            )
            diag = temp
    return dp[n]

print(edit_dist_opt('horse', 'ros'))  # 3
print(edit_dist_opt('intention', 'execution'))  # 5

Min Path Sum mit O(n)-Speicher

Bei Min Path Sum auf einem Gitter beginnt das eindimensionale rollende Array mit den Präfixsummen der ersten Zeile (es gibt nur einen Weg zu jeder Zelle der ersten Zeile). Aktualisieren Sie jede weitere Zeile von links nach rechts: dp[j] enthält vor der Aktualisierung den Wert aus der darüberliegenden Zeile (dp[i-1][j]), und dp[j-1] wurde gerade aktualisiert und stammt von links. Eine Diagonale wird hier nicht benötigt, da Min Path Sum die diagonale Zelle nicht verwendet.

def min_path_sum_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [float('inf')] * n
    dp[0] = 0
    for i in range(m):
        # Update first column (only from above)
        dp[0] += grid[i][0]
        for j in range(1, n):
            # min of above (dp[j] = old) and left (dp[j-1] = updated)
            dp[j] = grid[i][j] + min(dp[j], dp[j-1])
    return dp[n-1]

grid = [[1,3,1],[1,5,1],[4,2,1]]
print(min_path_sum_opt(grid))  # 7

Wann diagonaler Zugriff erforderlich ist

Nicht alle 2D-DP-Probleme lassen sich mit einem einfachen rollenden Array komprimieren, da manche nach dem Überschreiben von dp[j] noch das diagonale Element dp[i-1][j-1] benötigen. Die Lösung ist immer dieselbe: Speichern Sie temp = dp[j] vor der Aktualisierung und verwenden Sie den Wert als diag für die Berechnung der nächsten Spalte. Dieser Blick auf eine Zelle im Voraus verarbeitet alle Rekurrenzen mit drei Richtungen (LCS, Editierdistanz) übersichtlich.

# Recap: the diagonal save pattern
# Without it: dp[j-1] updated (left) and dp[j] about to be overwritten
# With it:

def show_diagonal_pattern(s1, s2):
    n = len(s2)
    dp = [0] * (n + 1)
    for ch1 in s1:
        diag = 0  # was dp[i-1][0] = 0 for LCS
        for j, ch2 in enumerate(s2, 1):
            temp = dp[j]  # SAVE before overwrite
            if ch1 == ch2:
                dp[j] = diag + 1  # use saved diagonal
            else:
                dp[j] = max(dp[j], dp[j-1])
            diag = temp  # advance diagonal
    return dp[n]

print(show_diagonal_pattern('ABCBDAB', 'BDCABA'))  # 4

Speicheroptimierung beim 2D-Rucksackproblem

Auch das 0/1-Rucksackproblem profitiert von einer Speicheroptimierung. Die vollständige 2D-Tabelle hat die Abmessungen (n_items+1) × (capacity+1). Das rollende Array reduziert den Speicherbedarf auf O(capacity). Der entscheidende Unterschied zu LCS und Editierdistanz: Durchlaufen Sie die Kapazitätsdimension rückwärts (von hoch nach niedrig). So wird sichergestellt, dass jedes Element höchstens einmal gezählt wird — ein Vorwärtsdurchlauf würde ermöglichen, ein Element mehrfach auszuwählen.

def knapsack_01(weights, values, capacity):
    dp = [0] * (capacity + 1)
    for w, v in zip(weights, values):
        # Reverse order: prevents using the same item twice
        for c in range(capacity, w - 1, -1):
            dp[c] = max(dp[c], dp[c - w] + v)
    return dp[capacity]

weights = [1, 3, 4, 5]
values  = [1, 4, 5, 7]
cap = 7
print(knapsack_01(weights, values, cap))  # 9 (items 3+4: weight 3+4=7, value 4+5=9)

Vorwärts- und Rückwärtsiteration

Die richtige Richtung für die innere Schleife ist entscheidend: rückwärts beim 0/1-Rucksackproblem (jedes Element wird höchstens einmal verwendet — der Zugriff auf vorherige Zustände verhindert eine erneute Verwendung) und vorwärts beim unbeschränkten Rucksackproblem (jedes Element kann erneut verwendet werden — der Zugriff auf bereits aktualisierte Zustände erlaubt mehrere Verwendungen). Eine falsche Richtung ändert 0/1 unbemerkt in unbeschränkt oder umgekehrt. Überprüfen Sie daher immer zuerst die Einschränkung, bevor Sie die Richtung wählen.

# 0/1 Knapsack: each item used AT MOST ONCE → iterate reverse
def knapsack_01_demo(weights, values, cap):
    dp = [0] * (cap + 1)
    for w, v in zip(weights, values):
        for c in range(cap, w-1, -1):  # REVERSE
            dp[c] = max(dp[c], dp[c-w] + v)
    return dp[cap]

# Unbounded Knapsack: items can be reused → iterate forward
def knapsack_unbounded(weights, values, cap):
    dp = [0] * (cap + 1)
    for c in range(1, cap + 1):
        for w, v in zip(weights, values):
            if c >= w:
                dp[c] = max(dp[c], dp[c-w] + v)  # FORWARD
    return dp[cap]

print(knapsack_01_demo([2,3],[3,4],5))     # 7
print(knapsack_unbounded([2,3],[3,4],5))   # 8 (use weight-2 twice: 3+3=6? or 4+... )

Unique Paths mit O(n)-Speicher

Bei Unique Paths kann die gesamte Tabelle durch eine einzige Zeile ersetzt werden. Initialisieren Sie alle Zellen mit 1 (der ersten Zeile). Aktualisieren Sie jede weitere Zeile von links nach rechts: dp[j] += dp[j-1]. Eine Diagonale wird nicht benötigt, da die Rekurrenz nur die darüberliegende Zelle (dp[j], der aktuelle Wert vor der Aktualisierung) und die linke Zelle (dp[j-1], bereits aktualisiert) verwendet. Dies ist die einfachste 2D→1D-Komprimierung.

def unique_paths_opt(m, n):
    dp = [1] * n  # first row: all 1s
    for i in range(1, m):
        for j in range(1, n):
            dp[j] += dp[j-1]  # above (dp[j]) + left (dp[j-1])
    return dp[n-1]

# With obstacles
def unique_paths_obstacles_opt(grid):
    m, n = len(grid), len(grid[0])
    dp = [0] * n
    dp[0] = 1
    for i in range(m):
        if grid[i][0] == 1: dp[0] = 0  # blocked column
        for j in range(1, n):
            if grid[i][j] == 1: dp[j] = 0  # blocked
            else: dp[j] += dp[j-1]
    return dp[n-1]

print(unique_paths_opt(3, 7))  # 28
print(unique_paths_obstacles_opt([[0,0,0],[0,1,0],[0,0,0]]))  # 2

Zweizeilenpuffer für komplexe Rekurrenzen

Wenn die Rekurrenz Zellen aus zwei oder mehr vorherigen Zeilen benötigt (z. B. bei einigen Varianten der Intervall-DP oder bei Reduktionen von 3D-DP), wird ein Zweizeilenpuffer verwendet: Verwalten Sie die Arrays prev und curr und tauschen Sie sie nach jeder Zeile. Dadurch ergibt sich ein Speicherbedarf von O(2n) = O(n). Bei Rekurrenzen, die auf k Zeilen zurückgreifen, verwalten Sie k Arrays als Ringpuffer. Das verallgemeinert das Muster des einzeiligen rollenden Arrays.

def lcs_two_row_buffer(s1, s2):
    m, n = len(s1), len(s2)
    prev = [0] * (n + 1)  # dp[i-1]
    curr = [0] * (n + 1)  # dp[i]
    for i in range(1, m + 1):
        curr[0] = 0
        for j in range(1, n + 1):
            if s1[i-1] == s2[j-1]:
                curr[j] = prev[j-1] + 1
            else:
                curr[j] = max(prev[j], curr[j-1])
        prev, curr = curr, prev  # swap (curr becomes prev)
    return prev[n]  # after swap, prev holds the last computed row

print(lcs_two_row_buffer('ABCBDAB', 'BDCABA'))  # 4

Wann Speicheroptimierung nicht möglich ist

Eine Speicheroptimierung ist nicht immer möglich. Wenn Sie die optimale Lösung rekonstruieren müssen (nicht nur ihren Wert), benötigen Sie im Allgemeinen die vollständige Tabelle für die Rückverfolgung. Mögliche Alternativen sind: (1) Speichern einer separaten Entscheidungstabelle gleicher Größe. (2) Verwendung des Hirschberg-Algorithmus, der LCS in O(mn) Zeit und O(min(m,n)) Speicher einschließlich Rekonstruktion berechnet, indem er das Problem am Mittelpunkt rekursiv aufteilt. (3) Akzeptieren eines Speicherbedarfs von O(mn), wenn eine Rekonstruktion erforderlich ist.

# When reconstruction needed: must keep full table or use Hirschberg
# Hirschberg's idea: compute LCS length in O(n) space at midpoint of s1,
# recurse on left and right halves. O(mn) time, O(n) space + reconstruction.

# For interview: mention the trade-off
# 'I can reduce to O(n) space if only the value is needed.
#  To also reconstruct the sequence, I need the full O(mn) table
#  or a more complex divide-and-conquer approach.'

print('Space opt: O(n) for length only')
print('Full table: O(mn) needed for reconstruction')

Kurzer Test

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

Zusammenfassung der Lektion

In dieser Lektion haben Sie gelernt: 2D-DP-Tabellen lassen sich auf O(n) Speicher komprimieren, wenn nur die vorherige Zeile benötigt wird, das Muster mit einer Diagonalvariablen (temp vor dem Überschreiben speichern) verarbeitet Rekurrenzen, die dp[i-1][j-1] benötigen, und beim 0/1-Rucksackproblem wird die Kapazität rückwärts durchlaufen, beim unbeschränkten Rucksackproblem dagegen vorwärts. Als Nächstes behandeln wir die Backtracking-Vorlage: Auswählen, Erkunden, Rückgängigmachen — die Grundlage von Algorithmen zur vollständigen Suche.

Häufig gestellte Fragen

Ist die Lektion „Speicheroptimierung für 2D-DP“ kostenlos?

Ja — der vollständige Text von „Speicheroptimierung für 2D-DP“ 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 „Speicheroptimierung für 2D-DP“?

Reduzieren Sie den Speicherbedarf von LCS und Editierdistanz von O(mn) auf O(min(m,n)), indem Sie nur die aktuelle und die vorherige Zeile der DP-Tabelle behalten. 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 4 von 4.

Wie lange dauert die Lektion „Speicheroptimierung für 2D-DP“?

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. Eindeutige Pfade und minimale Pfadsumme in Rastern
  2. Längste gemeinsame Teilsequenz
  3. Editierdistanz (Levenshtein)
  4. Speicheroptimierung für 2D-DP
← Zurück zu DSA Interview Prep