Ruimteoptimalisatie voor 2D-DP
Reduceer de ruimte voor LCS en edit distance van O(mn) naar O(min(m,n)) door alleen de huidige en vorige rijen van de DP-tabel te bewaren.
Ruimteoptimalisatie voor 2D-DP is een gratis Voorbereiding op programmeerinterviews-les op CoddyKit. Dit is les 4 van 4. Je kunt de volledige les hieronder gratis lezen en daarna in de browser praktisch oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is. Deze les maakt deel uit van het leertraject Voorbereiding op programmeerinterviews. Je voortgang wordt gesynchroniseerd op het web en in de CoddyKit-app. De cursus Voorbereiding op programmeerinterviews bevat in totaal 4 lessen.
Waarom ruimtegebruik belangrijk is bij 2D-DP
Een 2D-DP-tabel voor tekenreeksen met lengte 1000 vereist 1000×1000 = 1.000.000 cellen — ongeveer 8 MB voor gehele getallen van 64 bits. Voor langere sequenties (DNA-uitlijning, grote tekstvergelijkingen) wordt dit onpraktisch. De belangrijkste observatie is dat de meeste 2D-DP-recurrenties alleen naar de huidige en vorige rij kijken, zodat de volledige tabel kan worden samengeperst tot één of twee 1D-arrays. Dit vormt de kern van ruimteoptimalisatie voor 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')Patroon voor een rollende array
Het patroon van de rollende array vervangt de volledige 2D-tabel door een 1D-array die de vorige rij voorstelt. Bij het berekenen van rij i werk je elke cel j bij met de huidige waarde dp[j] (die nog steeds de waarde dp[i-1][j] van de vorige rij bevat) en de zojuist bijgewerkte dp[j-1] (dat is dp[i][j-1]). Een variabele diagonal bewaart dp[i-1][j-1] voordat die wordt overschreven. Dit patroon geldt voor LCS, bewerkingsafstand en de meeste 2D-DP-problemen.
# 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 met O(min m,n)-ruimte
Zorg er voor LCS voor dat text1 de kortere tekenreeks is, zodat n klein is. Reserveer een 1D-array met grootte n+1. Verwerk de rijen één voor één. Sla bij elke cel temp = dp[j] op (dit is dp[i-1][j]). Doe daarna het volgende: als de tekens overeenkomen, dp[j] = diag + 1; anders dp[j] = max(dp[j], dp[j-1]). Stel ten slotte diag = temp in. Na alle rijen bevat dp[n] de lengte van de 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')) # 4Bewerkingsafstand met O(n)-ruimte
Voor bewerkingsafstand gebruik je hetzelfde rollende patroon. De oorspronkelijke 1D-array stelt rij 0 voor: dp[j] = j (j tekens invoegen). Stel voor elke rij i dp[0] = i in (i tekens verwijderen) en sla diag = dp[0] op voordat je de waarde bijwerkt. Sla in de binnenste lus temp = dp[j] op, bereken de nieuwe waarde uit invoegen (dp[j-1]+1), verwijderen (dp[j]+1) en vervangen (diag + cost), en stel daarna diag = temp in.
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')) # 5Minimale padsom met O(n)-ruimte
Voor Min Path Sum op een raster begint de 1D-rolarray met de prefixsommen van de eerste rij (er is maar één manier om elke cel in de eerste rij te bereiken). Werk voor elke volgende rij van links naar rechts bij: dp[j] vóór de bijwerking is de waarde uit de rij erboven (dp[i-1][j]), en het zojuist bijgewerkte dp[j-1] komt van links. Een diagonaal element is hier niet nodig, omdat de minimale padsom de diagonale cel niet vereist.
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)) # 7Wanneer toegang tot de diagonaal nodig is
Niet alle 2D-DP-problemen kunnen met een eenvoudige rolarray worden gecomprimeerd, omdat sommige het diagonale element dp[i-1][j-1] nodig hebben nadat dp[j] is overschreven. De oplossing is altijd hetzelfde: sla temp = dp[j] vóór de bijwerking op en gebruik deze waarde als diag voor de berekening van de volgende kolom. Met deze vooruitblik van één cel kunnen alle recursies met drie richtingen (LCS, bewerkingsafstand) netjes worden verwerkt.
# 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')) # 4Ruimte-optimalisatie voor 2D-rugzakproblemen
Ook het 0/1-rugzakprobleem profiteert van ruimte-optimalisatie. De volledige 2D-tabel heeft de afmetingen (n_items+1) × (capacity+1). De rolarray verkleint dit tot O(capacity). Het belangrijkste verschil met LCS en bewerkingsafstand is dat je de capaciteitsdimensie achterwaarts doorloopt (van hoog naar laag). Zo wordt elk item hoogstens één keer geteld — voorwaarts doorlopen zou toestaan dat een item meerdere keren wordt geselecteerd.
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)Voorwaarts tegenover achterwaarts doorlopen
Weten in welke richting je de binnenste lus moet doorlopen is cruciaal: achterwaarts voor 0/1-rugzakproblemen (elk item wordt hoogstens één keer gebruikt — terugkijken naar eerdere toestanden voorkomt hergebruik). Voorwaarts voor onbegrensde rugzakproblemen (elk item kan opnieuw worden gebruikt — kijken naar al bijgewerkte toestanden maakt meerdere gebruiken mogelijk). Als je dit verkeerd doet, verander je ongemerkt een 0/1-probleem in een onbegrensd probleem of andersom. Controleer altijd eerst de beperking voordat je de richting kiest.
# 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+... )Unieke paden met O(n)-ruimte
Voor unieke paden kan de volledige tabel worden vervangen door één rij. Initialiseer alle cellen op 1 (de eerste rij). Werk voor elke volgende rij van links naar rechts bij: dp[j] += dp[j-1]. Een diagonaal element is niet nodig, omdat de recursie alleen de cel erboven gebruikt (dp[j], de huidige waarde vóór de bijwerking) en de cel links ervan (dp[j-1], die al is bijgewerkt). Dit is de eenvoudigste compressie van 2D naar 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]])) # 2Buffer met twee rijen voor complexe recursies
Wanneer de recursie cellen uit twee of meer vorige rijen nodig heeft (bijvoorbeeld bij sommige varianten van interval-DP of reducties van 3D-DP), gebruik je een buffer met twee rijen: houd de arrays prev en curr bij en wissel ze na elke rij om. Dit geeft O(2n) = O(n) ruimte. Houd voor recursies die k rijen terugkijken k arrays bij als een circulaire buffer. Dit is een generalisatie van het patroon met één rolrij.
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')) # 4Wanneer ruimte-optimalisatie niet mogelijk is
Ruimte-optimalisatie is niet altijd mogelijk. Als je de optimale oplossing moet reconstrueren (en niet alleen de waarde ervan nodig hebt), heb je over het algemeen de volledige tabel nodig om terug te kunnen zoeken. Mogelijke oplossingen zijn: (1) Een aparte beslissingstabel van dezelfde grootte opslaan. (2) Het algoritme van Hirschberg gebruiken, dat LCS in O(mn)-tijd en O(min(m,n))-ruimte berekent en daarbij de oplossing reconstrueert door het probleem recursief op het middelpunt te splitsen. (3) O(mn)-ruimte accepteren wanneer reconstructie vereist is.
# 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')Korte controle
Test je begrip van de concepten uit Data Structures & Algorithms — Coding Interview Prep in deze les.
Samenvatting van de les
In deze les heb je geleerd: 2D-DP-tabellen kunnen worden gecomprimeerd tot O(n)-ruimte met een 1D-rolarray wanneer alleen de vorige rij nodig is, het patroon met een diagonale variabele (temp opslaan vóór het overschrijven) verwerkt recursies die dp[i-1][j-1] nodig hebben, en 0/1-rugzakproblemen doorlopen de capaciteit achterwaarts, terwijl onbegrensde rugzakproblemen deze voorwaarts doorlopen. Hierna bestuderen we het sjabloon voor Backtracking: Kiezen, Verkennen, Ongedaan maken — de basis van algoritmen voor uitputtend zoeken.
Leer Voorbereiding op programmeerinterviews met een AI-tutor — gratis
Schrijf echte code en voer die uit in je browser, krijg direct hulp van een AI-tutor die 24/7 beschikbaar is en ga verder waar je gebleven bent op het web of in de app.
- Cursussen
- 90
- Lessen
- 360
Veelgestelde vragen
Is de les “Ruimteoptimalisatie voor 2D-DP” gratis?
Ja — de volledige tekst van “Ruimteoptimalisatie voor 2D-DP” kun je hier gratis op het web lezen. Als je interactief wilt oefenen met een ingebouwde code-editor en een AI-begeleider die 24/7 beschikbaar is, en de rest van de cursus Voorbereiding op programmeerinterviews wilt ontgrendelen, kun je upgraden naar CoddyKit PRO. De cursus Voorbereiding op programmeerinterviews bevat in totaal 4 lessen.
Wat leer ik in “Ruimteoptimalisatie voor 2D-DP”?
Reduceer de ruimte voor LCS en edit distance van O(mn) naar O(min(m,n)) door alleen de huidige en vorige rijen van de DP-tabel te bewaren. Je oefent met Voorbereiding op programmeerinterviews door code rechtstreeks in de browser uit te voeren. Een AI-begeleider die 24/7 beschikbaar is beantwoordt je vragen terwijl je de les doorwerkt.
Heb ik ervaring nodig om met Voorbereiding op programmeerinterviews te beginnen?
Ervaring vooraf is niet nodig. Voorbereiding op programmeerinterviews op CoddyKit is opgebouwd voor beginners tot gevorderden, zodat je hier of bij het begin kunt starten en in je eigen tempo kunt leren. Dit is les 4 van 4.
Hoe lang duurt de les “Ruimteoptimalisatie voor 2D-DP”?
De meeste lessen van CoddyKit duren ongeveer 5–10 minuten. Elke les is kort en interactief, zodat je gestaag vooruitgaat en op het web en in de app precies verdergaat waar je was gebleven.
Kan ik code schrijven en uitvoeren in deze les over Voorbereiding op programmeerinterviews?
Ja. Elke les over Voorbereiding op programmeerinterviews bevat een ingebouwde code-editor, zodat je rechtstreeks in je browser echte code kunt schrijven en uitvoeren en direct feedback van AI krijgt — lokale installatie is niet nodig.
Alle lessen in deze cursus
- Unieke paden en minimale padsom op grids
- Langste gemeenschappelijke subsequence
- Edit distance (Levenshtein)
- Ruimteoptimalisatie voor 2D-DP