Förberedelse inför kodningsintervjuer · Lektion

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.

Lektion 4 av 413 steg

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

Redigeringsavstå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'))  # 5

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

Nä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'))  # 4

Minnesoptimering 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]]))  # 2

Två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'))  # 4

Nä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.

Gratis att börja

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

  1. Unika vägar och minsta vägsumma i rutnät
  2. Längsta gemensamma delsekvens
  3. Edit-avstånd (Levenshtein)
  4. Utrymmesoptimering för 2D-DP
← Tillbaka till Förberedelse inför kodningsintervjuer