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 DSA 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 DSA Interview Prep, a Twój postęp synchronizuje się między webem a aplikacją CoddyKit. Kurs DSA 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')) # 4Odległ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')) # 5Min 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)) # 7Kiedy 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')) # 4Optymalizacja 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]])) # 2Bufor 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')) # 4Kiedy 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 DSA Interview Prep, przejdź na CoddyKit PRO. Kurs DSA 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 DSA 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ąć DSA Interview Prep?
Nie wymagamy żadnego doświadczenia. DSA 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 DSA Interview Prep?
Tak. Każda lekcja DSA 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
- Unikalne ścieżki i minimalna suma ścieżki na siatkach
- Najdłuższy wspólny podciąg
- Odległość edycyjna (Levenshteina)
- Optymalizacja pamięci dla dwuwymiarowego DP