0Pricing
Coding Interview Prep · Lekcja

Optymalizacja pamięci dla dwuwymiarowego DP

Zmniejszą Państwo zużycie pamięci LCS i odległości edycyjnej z O(mn) do O(min(m,n)), zachowując tylko bieżący i poprzedni wiersz tablicy DP.

Optymalizacja pamięci dla dwuwymiarowego DP to bezpłatna lekcja Coding Interview Prep na CoddyKit. To lekcja 4 z 4. Możesz przeczytać całą lekcję poniżej za darmo — a potem ćwiczyć ją interaktywnie w przeglądarce z wbudowanym edytorem kodu i tutorem AI dostępnym 24/7. To część ścieżki edukacyjnej Coding Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Dlaczego pamięć ma znaczenie w DP 2D

Tabela DP 2D dla napisów o długości 1000 wymaga 1000×1000 = 1,000,000 komórek — około 8 MB dla 64-bitowych liczb całkowitych. W przypadku dłuższych sekwencji (wyrównywanie DNA, porównywanie dużych plików tekstowych) staje się to niepraktyczne. Kluczowa obserwacja jest taka, że większość rekurencji DP 2D odwołuje się tylko do bieżącego i poprzedniego wiersza, więc całą tabelę można skompresować do jednej lub dwóch tablic 1D. To podstawa optymalizacji pamięci w DP 2D.

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

Wzorzec tablicy kroczącej

Wzorzec tablicy kroczącej zastępuje pełną tabelę 2D tablicą 1D reprezentującą poprzedni wiersz. Podczas obliczania wiersza i aktualizujesz każdą komórkę j, korzystając z bieżącej wartości dp[j] (która nadal przechowuje wartość dp[i-1][j] z poprzedniego wiersza) oraz właśnie zaktualizowanej wartości dp[j-1] (czyli dp[i][j-1]). Zmienna diagonal przechowuje dp[i-1][j-1] przed jego nadpisaniem. Ten wzorzec ma zastosowanie w LCS, odległości edycyjnej i większości problemów DP 2D.

# 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 z pamięcią O(min(m,n))

W przypadku LCS dopilnuj, aby text1 był krótszym napisem (dzięki temu n będzie małe). Przydziel tablicę 1D o rozmiarze n+1. Przetwarzaj wiersze po kolei. W każdej komórce: zapisz temp = dp[j] (jest to dp[i-1][j]). Następnie: jeśli znaki są zgodne, dp[j] = diag + 1; w przeciwnym razie dp[j] = max(dp[j], dp[j-1]). Na końcu ustaw diag = temp. Po przetworzeniu wszystkich wierszy dp[n] zawiera długość 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

Odległość edycyjna z pamięcią O(n)

Odległość edycyjna korzysta z tego samego wzorca kroczącego. Początkowa tablica 1D reprezentuje wiersz 0: dp[j] = j (wstawienie j znaków). Dla każdego wiersza i ustaw dp[0] = i (usunięcie i znaków) i zapisz diag = dp[0] przed aktualizacją. W pętli wewnętrznej zapisz temp = dp[j], oblicz nową wartość na podstawie wstawienia (dp[j-1]+1), usunięcia (dp[j]+1) i zastąpienia (diag + cost), a następnie ustaw 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 przy użyciu O(n) pamięci

W przypadku problemu Min Path Sum na siatce jednowymiarowa tablica krocząca zaczyna się od sum prefiksowych pierwszego wiersza (do każdej komórki pierwszego wiersza można dotrzeć tylko na jeden sposób). W każdym kolejnym wierszu należy wykonywać aktualizację od lewej do prawej: przed aktualizacją dp[j] jest wartością z wiersza powyżej (dp[i-1][j]), a właśnie zaktualizowane dp[j-1] pochodzi z lewej strony. Element po przekątnej nie jest tu potrzebny, ponieważ suma minimalnej ścieżki nie wymaga komórki po przekątnej.

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

Kiedy potrzebny jest dostęp do przekątnej

Nie każdy problem DP 2D można skompresować za pomocą prostej tablicy kroczącej, ponieważ niektóre wymagają elementu po przekątnej dp[i-1][j-1] po nadpisaniu dp[j]. Rozwiązanie jest zawsze takie samo: należy zapisać temp = dp[j] przed jego aktualizacją i użyć tej wartości jako diag podczas obliczania następnej kolumny. To przechowanie wartości z jednej komórki pozwala przejrzyście obsłużyć wszystkie rekurencje wykorzystujące trzy kierunki (LCS, odległość edycyjna).

# 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

Optymalizacja pamięci w problemie plecaka 2D

Problem plecaka 0/1 również korzysta z optymalizacji pamięci. Pełna tablica 2D ma wymiary (n_items+1) × (capacity+1). Tablica krocząca zmniejsza wymagane miejsce do O(capacity). Kluczowa różnica w porównaniu z LCS i odległością edycyjną polega na tym, że po wymiarze pojemności należy iterować w odwrotnej kolejności (od dużych wartości do małych). Dzięki temu każdy przedmiot jest uwzględniany co najwyżej raz — iterowanie do przodu pozwoliłoby wybierać ten sam przedmiot wielokrotnie.

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)

Iteracja w przód a iteracja wstecz

Kluczowe znaczenie ma wybór kierunku iteracji wewnętrznej pętli: wstecz w problemie plecaka 0/1 (każdy przedmiot można wykorzystać najwyżej raz — odwoływanie się do poprzednich stanów zapobiega ponownemu użyciu) oraz w przód w problemie plecaka bez ograniczeń (każdy przedmiot można wykorzystać ponownie — odwoływanie się do już zaktualizowanych stanów umożliwia wielokrotne użycie). Błędny wybór kierunku po cichu zmienia problem plecaka 0/1 w problem plecaka bez ograniczeń lub odwrotnie. Przed wyborem kierunku należy zawsze potwierdzić ograniczenie.

# 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 przy użyciu O(n) pamięci

W przypadku problemu Unique Paths całą tablicę można zastąpić jednym wierszem. Należy zainicjalizować wszystkie komórki wartością 1 (pierwszy wiersz). W każdym kolejnym wierszu należy wykonywać aktualizację od lewej do prawej: dp[j] += dp[j-1]. Element po przekątnej nie jest potrzebny, ponieważ rekurencja korzysta tylko z komórki powyżej (dp[j], czyli bieżącej wartości przed aktualizacją) oraz z komórki po lewej (dp[j-1], już zaktualizowanej). Jest to najprostszy przykład kompresji 2D→1D.

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

Bufor dwóch wierszy dla złożonych rekurencji

Gdy rekurencja wymaga komórek z co najmniej dwóch poprzednich wierszy (np. w niektórych wariantach programowania dynamicznego na przedziałach lub redukcjach DP 3D), stosuje się bufor dwóch wierszy: utrzymuje się tablice prev i curr, a po każdym wierszu zamienia je miejscami. Daje to O(2n) = O(n) pamięci. W przypadku rekurencji odwołujących się do k poprzednich wierszy należy utrzymywać k tablic w buforze cyklicznym. Jest to uogólnienie wzorca jednowierszowej tablicy kroczącej.

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

Kiedy optymalizacja pamięci nie jest możliwa

Optymalizacja pamięci nie zawsze jest możliwa. Jeśli trzeba odtworzyć optymalne rozwiązanie (a nie tylko jego wartość), do odtworzenia ścieżki na ogół potrzebna jest pełna tablica. Możliwe rozwiązania zastępcze to: (1) przechowywanie osobnej tablicy decyzji o takim samym rozmiarze; (2) użycie algorytmu Hirschberga, który oblicza LCS w czasie O(mn) i przy użyciu O(min(m,n)) pamięci, włącznie z odtwarzaniem rozwiązania przez rekurencyjny podział problemu w punkcie środkowym; (3) zaakceptowanie O(mn) pamięci, gdy wymagane jest odtwarzanie rozwiązania.

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

Szybkie sprawdzenie

Proszę sprawdzić swoją wiedzę na temat zagadnień Data Structures & Algorithms — Coding Interview Prep z tej lekcji.

Podsumowanie lekcji

W tej lekcji poznano: tabele DP 2D można kompresować do O(n) pamięci za pomocą jednowymiarowej tablicy kroczącej, gdy potrzebny jest tylko poprzedni wiersz, wzorzec zmiennej diagonalnej (zapisanie temp przed nadpisaniem) obsługuje rekurencje wymagające dp[i-1][j-1] oraz w problemie plecaka 0/1 pojemność przetwarza się w odwrotnej kolejności, a w problemie plecaka bez ograniczeń — w kolejności rosnącej. Następnie zajmiemy się szablonem backtrackingu: wybór, eksploracja i cofnięcie wyboru — podstawą algorytmów przeszukiwania wyczerpującego.

Często zadawane pytania

Czy lekcja „Optymalizacja pamięci dla dwuwymiarowego DP” jest bezpłatna?

Tak — pełny tekst „Optymalizacja pamięci dla dwuwymiarowego DP” jest dostępny za darmo tutaj w sieci. Aby ćwiczyć ją interaktywnie (wbudowany edytor kodu i tutor AI dostępny 24/7) i odblokować resztę kursu Coding Interview Prep, przejdź na CoddyKit PRO. Kurs Coding Interview Prep zawiera 4 lekcji w sumie.

Co nauczysz się w „Optymalizacja pamięci dla dwuwymiarowego DP”?

Zmniejszą Państwo zużycie pamięci LCS i odległości edycyjnej z O(mn) do O(min(m,n)), zachowując tylko bieżący i poprzedni wiersz tablicy DP. Ćwiczysz Coding Interview Prep z praktycznym kodem, który uruchamiasz bezpośrednio w przeglądarce, a tutor AI dostępny 24/7 odpowiada na Twoje pytania podczas pracy nad lekcją.

Czy potrzebuję doświadczenia, aby zacząć Coding Interview Prep?

Nie wymagamy żadnego doświadczenia. Coding Interview Prep w CoddyKit jest strukturyzowany dla początkujących i zaawansowanych użytkowników, więc możesz zacząć tutaj lub od początku i uczyć się w swoim tempie. To lekcja 4 z 4.

Ile czasu zajmuje lekcja „Optymalizacja pamięci dla dwuwymiarowego DP”?

Większość lekcji CoddyKit trwa około 5–10 minut. Każda lekcja to mały, interaktywny krok, dzięki czemu robisz systematyczne postępy i zawsze wracasz dokładnie do tego samego miejsca — na webie i w aplikacji.

Czy mogę pisać i uruchamiać kod w tej lekcji Coding Interview Prep?

Tak. Każda lekcja Coding Interview Prep zawiera wbudowany edytor kodu, więc piszesz i uruchamiasz prawdziwy kod bezpośrednio w przeglądarce i od razu otrzymujesz sprzężenie zwrotne od AI — bez konfiguracji na komputerze.

Wszystkie lekcje w tym kursie

  1. Unikalne ścieżki i minimalna suma ścieżki na siatkach
  2. Najdłuższy wspólny podciąg
  3. Odległość edycyjna (Levenshteina)
  4. Optymalizacja pamięci dla dwuwymiarowego DP
← Powrót do Coding Interview Prep