Utrymmesoptimering för 2D-DP
Minska utrymmet för LCS och edit-avstånd från O(mn) till O(min(m,n)) genom att bara behålla den aktuella och föregående raden i DP-tabellen.
Utrymmesoptimering för 2D-DP är en gratis lektion i Förberedelse inför kodningsintervjuer på CoddyKit. Detta är lektion 4 av 4. Ni kan läsa hela lektionen gratis nedan och sedan öva praktiskt i webbläsaren med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt. Den ingår i lärvägen för Förberedelse inför kodningsintervjuer, och Era framsteg synkroniseras mellan webben och CoddyKit-appen. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.
Varför minnesanvändningen spelar roll i 2D-DP
En 2D-DP-tabell för strängar med längden 1000 kräver 1000×1000 = 1 000 000 celler — ungefär 8 MB för 64-bitarsheltal. För längre sekvenser (DNA-justering, stora textdiffar) blir detta opraktiskt. Den viktiga observationen är att de flesta 2D-DP-rekurrenser bara använder den aktuella raden och raden före, så hela tabellen kan komprimeras till en eller två endimensionella arrayer. Detta är grunden för minnesoptimering i 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')Mönstret med rullande arrayer
Mönstret med en rullande array ersätter hela 2D-tabellen med en endimensionell array som representerar den föregående raden. När rad i beräknas uppdaterar Ni varje cell j med det aktuella värdet dp[j] (som fortfarande innehåller föregående rads dp[i-1][j]) och det just uppdaterade dp[j-1] (som är dp[i][j-1]). En variabel diagonal fångar upp dp[i-1][j-1] innan det skrivs över. Mönstret kan användas för LCS, redigeringsavstånd och de flesta 2D-DP-problem.
# 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 med O(min m,n)-minne
För LCS ska Ni se till att text1 är den kortare strängen (så att n är litet). Allokera en endimensionell array med storleken n+1. Bearbeta raderna en i taget. I varje cell sparar Ni först temp = dp[j] (detta är dp[i-1][j]). Sedan gäller följande: om tecknen matchar, dp[j] = diag + 1; annars dp[j] = max(dp[j], dp[j-1]). Sätt slutligen diag = temp. Efter alla rader innehåller dp[n] LCS-längden.
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')) # 4Redigeringsavstånd med O(n)-minne
Redigeringsavstånd använder samma rullande mönster. Den inledande endimensionella arrayen representerar rad 0: dp[j] = j (infoga j tecken). För varje rad i sätter Ni dp[0] = i (ta bort i tecken) och sparar diag = dp[0] före uppdateringen. I den inre loopen sparar Ni temp = dp[j], beräknar det nya värdet från insättning (dp[j-1]+1), borttagning (dp[j]+1) och ersättning (diag + cost), och sätter sedan 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')) # 5Minsta vägsumma med O(n)-minne
För Minsta vägsumma på ett rutnät börjar den 1D-rullande arrayen som prefixsummorna för den första raden (det finns bara en väg till varje cell på den första raden). För varje efterföljande rad uppdateras den från vänster till höger: dp[j] före uppdateringen är värdet från raden ovanför (dp[i-1][j]), och det nyligen uppdaterade dp[j-1] kommer från vänster. Någon diagonal behövs inte här, eftersom minsta vägsumma inte kräver cellen diagonalt ovanför.
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)) # 7När åtkomst till diagonalen behövs
Alla 2D-DP-problem kan inte komprimeras med en enkel rullande array, eftersom vissa behöver diagonalelementet dp[i-1][j-1] efter att dp[j] har skrivits över. Lösningen är alltid densamma: spara temp = dp[j] innan uppdateringen och använd det som diag vid beräkningen av nästa kolumn. Denna förskjutning med en cell hanterar rekurrenser i tre riktningar (LCS, edit distance) på ett tydligt sätt.
# 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')) # 4Minnesoptimering för 2D-knapsack
Problemet 0/1 Knapsack kan också dra nytta av minnesoptimering. Den fullständiga 2D-tabellen har dimensionerna (n_items+1) × (capacity+1). Den rullande arrayen reducerar detta till O(capacity). Den avgörande skillnaden jämfört med LCS/edit distance är att kapacitetsdimensionen itereras i omvänd ordning (från högt till lågt). Då räknas varje objekt högst en gång — en iteration framåt skulle göra det möjligt att välja samma objekt flera gånger.
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)Framåt- kontra bakåtriktad iteration
Det är avgörande att veta i vilken riktning den inre loopen ska iterera: bakåt för 0/1-knapsack (varje objekt används högst en gång — genom att läsa tidigare tillstånd förhindras återanvändning). Framåt för unbounded knapsack (varje objekt kan återanvändas — genom att läsa redan uppdaterade tillstånd tillåts flera användningar). Om detta blir fel ändras 0/1-knapsack obemärkt till unbounded knapsack eller tvärtom. Bekräfta alltid begränsningen innan riktningen väljs.
# 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+... )Unika vägar med O(n)-minne
För unika vägar kan hela tabellen ersättas med en enda rad. Initiera alla celler till 1 (den första raden). För varje efterföljande rad uppdateras den från vänster till höger: dp[j] += dp[j-1]. Någon diagonal behövs inte, eftersom rekurrensen endast använder cellen ovanför (dp[j], det aktuella värdet före uppdateringen) och cellen till vänster (dp[j-1], som redan har uppdaterats). Detta är den enklaste komprimeringen från 2D till 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]])) # 2Tvåradersbuffert för komplexa rekurrenser
När rekurrensen behöver celler från två eller fler föregående rader (t.ex. vissa varianter av intervall-DP eller reduktioner av 3D-DP) används en tvåradersbuffert: underhåll arrayerna prev och curr och byt plats på dem efter varje rad. Detta ger O(2n) = O(n) minne. För rekurrenser som går tillbaka k rader underhålls k arrayer som en cirkulär buffert. Detta generaliserar mönstret med en rullande rad.
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')) # 4När minnesoptimering inte är möjlig
Minnesoptimering är inte alltid möjlig. Om den optimala lösningen måste rekonstrueras (inte bara dess värde), behöver ni i allmänhet hela tabellen för bakåtspårning. Alternativ är: (1) Lagra en separat beslutstabell av samma storlek. (2) Använd Hirschbergs algoritm, som beräknar LCS på O(mn)-tid och O(min(m,n))-minne, inklusive rekonstruktion, genom att rekursivt dela problemet vid mittpunkten. (3) Acceptera O(mn)-minne när rekonstruktion krävs.
# 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')Snabbkontroll
Testa er förståelse av begreppen Data Structures & Algorithms — Coding Interview Prep från den här lektionen.
Lektionssammanfattning
I den här lektionen lärde ni er att: 2D-DP-tabeller kan komprimeras till O(n)-minne med en rullande 1D-array när endast den föregående raden behövs, mönstret med en diagonalvariabel (spara temp innan värdet skrivs över) hanterar rekurrenser som behöver dp[i-1][j-1], och 0/1-knapsack itererar kapaciteten bakåt medan unbounded knapsack itererar framåt. Härnäst studerar vi mallen för Backtracking: Choose, Explore, Unchoose — grunden för algoritmer för uttömmande sökning.
Lär dig Förberedelse inför kodningsintervjuer med en AI-lärare – gratis
Skriv och kör riktig kod i webbläsaren, få omedelbar hjälp av en AI-lärare dygnet runt och fortsätt där du slutade – på webben eller i appen.
- Kurser
- 90
- Lektioner
- 360
Vanliga frågor
Är lektionen ”Utrymmesoptimering för 2D-DP” gratis?
Ja – hela texten till ”Utrymmesoptimering för 2D-DP” kan läsas gratis här på webben. Om Ni vill öva interaktivt med en inbyggd kodredigerare och en AI-handledare som är tillgänglig dygnet runt och låsa upp resten av kursen i Förberedelse inför kodningsintervjuer, kan Ni uppgradera till CoddyKit PRO. Kursen i Förberedelse inför kodningsintervjuer innehåller totalt 4 lektioner.
Vad lär jag mig i ”Utrymmesoptimering för 2D-DP”?
Minska utrymmet för LCS och edit-avstånd från O(mn) till O(min(m,n)) genom att bara behålla den aktuella och föregående raden i DP-tabellen. Ni övar på Förberedelse inför kodningsintervjuer med praktisk kod som körs direkt i webbläsaren, medan en AI-handledare som är tillgänglig dygnet runt svarar på Era frågor under lektionen.
Behöver jag någon erfarenhet för att börja lära mig Förberedelse inför kodningsintervjuer?
Du behöver inga förkunskaper. Utbildningen i Förberedelse inför kodningsintervjuer på CoddyKit är upplagd för allt från nybörjare till avancerade elever, så att du kan börja här eller från början och gå fram i din egen takt. Detta är lektion 4 av 4.
Hur lång tid tar lektionen ”Utrymmesoptimering för 2D-DP”?
De flesta CoddyKit-lektioner tar cirka 5–10 minuter. Varje lektion är kort och interaktiv, så att du gör stadiga framsteg och kan fortsätta precis där du slutade – på webben eller i appen.
Kan jag skriva och köra kod i den här Förberedelse inför kodningsintervjuer-lektionen?
Ja. Varje Förberedelse inför kodningsintervjuer-lektion innehåller en inbyggd kodredigerare, så att du kan skriva och köra riktig kod direkt i webbläsaren och få omedelbar AI-feedback – utan lokal installation.
Alla lektioner i den här kursen
- Unika vägar och minsta vägsumma i rutnät
- Längsta gemensamma delsekvens
- Edit-avstånd (Levenshtein)
- Utrymmesoptimering för 2D-DP