2차원 DP 공간 최적화
DP 표에서 현재 행과 이전 행만 유지해 LCS와 편집 거리의 공간을 O(mn)에서 O(min(m,n))으로 줄입니다.
2차원 DP 공간 최적화은(는) CoddyKit의 무료 DSA Interview Prep 강의입니다. 이것은 4개 중 4번째 강의입니다. 아래에서 전체 강의를 무료로 읽을 수 있으며, 내장 코드 에디터와 24/7 AI 튜터와 함께 브라우저에서 직접 실습할 수 있습니다. 이 강의는 DSA Interview Prep 학습 경로의 일부이며, 진행 상황이 웹과 CoddyKit 앱에 동기화됩니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
2D DP에서 공간이 중요한 이유
길이가 1000인 문자열을 위한 2D DP 표에는 1000×1000 = 1,000,000개의 셀이 필요하며, 64비트 정수 기준으로 대략 8 MB입니다. 더 긴 수열(DNA 정렬, 대규모 텍스트 diff)에서는 이 방식이 비실용적이 됩니다. 핵심 관찰은 대부분의 2D DP 점화식이 현재 행과 이전 행만 참조한다는 것입니다. 따라서 전체 표를 하나 또는 두 개의 1차원 배열로 압축할 수 있습니다. 이것이 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')순환 배열 패턴
순환 배열 패턴은 전체 2D 표를 이전 행을 나타내는 1차원 배열로 대체합니다. i번 행을 계산할 때 각 셀은 아직 이전 행의 dp[i-1][j]를 저장하고 있는 현재 값 dp[j]와 방금 갱신된 dp[j-1](즉 dp[i][j-1])를 사용하여 갱신합니다. diagonal 변수는 덮어쓰기 전에 dp[i-1][j-1]을 저장합니다. 이 패턴은 LCS, 편집 거리 및 대부분의 2D DP 문제에 적용됩니다.
# 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]O(min(m,n)) 공간의 LCS
LCS에서는 text1이 더 짧은 문자열이 되도록 하여 n을 작게 유지하십시오. 크기가 n+1인 1차원 배열을 할당합니다. 행 단위로 처리합니다. 각 셀에서 temp = dp[j](이는 dp[i-1][j]입니다)를 저장합니다. 그다음 문자가 일치하면 dp[j] = diag + 1로 설정하고, 그렇지 않으면 dp[j] = max(dp[j], dp[j-1])로 설정합니다. 마지막으로 diag = temp로 설정합니다. 모든 행을 처리한 후 dp[n]에 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')) # 4O(n) 공간의 편집 거리
편집 거리에도 동일한 순환 배열 패턴을 사용합니다. 초기 1차원 배열은 0번 행을 나타냅니다. dp[j] = j(문자 j개 삽입)으로 설정합니다. 각 i번 행에서는 dp[0] = i(문자 i개 삭제)로 설정하고, 갱신하기 전에 diag = dp[0]을 저장합니다. 내부 반복문에서는 temp = dp[j]를 저장하고, 삽입(dp[j-1]+1), 삭제(dp[j]+1), 교체(diag + cost)를 사용하여 새 값을 계산한 다음 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')) # 5O(n) 공간을 사용하는 최소 경로 합
격자에서 최소 경로 합을 구할 때, the 1차원 롤링 배열은 the 첫 번째 행의 누적 합으로 시작합니다(첫 번째 행에서 각 셀로 들어오는 방법은 하나뿐입니다). 이후 각 행에서는 왼쪽에서 오른쪽으로 갱신합니다. 갱신 전 dp[j]는 위 행의 값(dp[i-1][j])이고, 방금 갱신한 dp[j-1]는 왼쪽의 값입니다. 여기에는 대각선이 필요하지 않습니다. 최소 경로 합은 the 대각선 셀을 필요로 하지 않기 때문입니다.
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대각선 접근이 필요한 경우
모든 2차원 DP 문제를 단순한 롤링 배열로 압축할 수 있는 것은 아닙니다. 일부 문제는 the 대각선 원소 dp[i-1][j-1]를 필요로 하며, dp[j]를 덮어쓴 뒤에도 이 원소가 필요합니다. 해결책은 항상 같습니다. 갱신하기 전에 temp = dp[j]를 저장하고, 이를 the 다음 열의 계산에서 diag로 사용합니다. 이 한 셀 미리보기 방식은 세 방향 점화식(LCS, 편집 거리)을 깔끔하게 처리합니다.
# 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')) # 42차원 배낭 공간 최적화
0/1 배낭 문제에도 공간 최적화를 적용할 수 있습니다. 전체 2차원 표의 크기는 (n_items+1) × (capacity+1)입니다. 롤링 배열을 사용하면 이를 O(capacity)로 줄일 수 있습니다. LCS 및 편집 거리와 the 가장 중요한 차이점은 용량 차원을 역순으로 순회한다는 것입니다(큰 값에서 작은 값으로). 이렇게 해야 각 항목이 최대 한 번만 계산됩니다. 정방향으로 순회하면 하나의 항목을 여러 번 선택할 수 있습니다.
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)정방향 순회와 역순 순회
내부 반복문을 어느 방향으로 순회할지 아는 것이 중요합니다. 0/1 배낭에서는 역순으로 순회합니다(각 항목을 최대 한 번만 사용하며, 이전 상태를 참조하면 재사용을 막을 수 있습니다). 무한 배낭에서는 정방향으로 순회합니다(각 항목을 다시 사용할 수 있으므로, 이미 갱신된 상태를 참조하면 여러 번 사용할 수 있습니다). 이 방향을 잘못 선택하면 0/1 배낭이 무한 배낭으로, 또는 그 반대로 조용히 바뀝니다. 방향을 선택하기 전에 항상 제약 조건을 확인하십시오.
# 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+... )O(n) 공간을 사용하는 고유 경로
고유 경로 문제에서는 전체 표를 하나의 행으로 대체할 수 있습니다. 모든 셀을 1로 초기화합니다(첫 번째 행). 이후 각 행에서는 왼쪽에서 오른쪽으로 갱신합니다. dp[j] += dp[j-1]라는 점화식은 위쪽 셀(dp[j], 갱신 전의 현재 값)과 왼쪽 셀(dp[j-1], 이미 갱신된 값)만 사용하므로 대각선이 필요하지 않습니다. 이는 가장 단순한 2차원→1차원 압축입니다.
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복잡한 점화식을 위한 두 행 버퍼
점화식이 두 개 이상의 이전 행에 있는 셀을 필요로 할 때(예를 들어 일부 구간 DP 변형이나 3차원 DP 축소), 두 행 버퍼를 사용합니다. prev와 curr 배열을 유지하고 각 행이 끝날 때 서로 교환합니다. 이렇게 하면 O(2n) = O(n) 공간을 사용합니다. k개 행만큼 거슬러 올라가는 점화식이라면 k개의 배열을 원형 버퍼로 유지합니다. 이는 한 행 롤링 배열 패턴을 일반화한 것입니다.
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공간 최적화가 불가능한 경우
공간 최적화가 항상 가능한 것은 아닙니다. 최적의 solution을 그 값만이 아니라 실제로 복원해야 한다면, 일반적으로 역추적을 위해 the 전체 표가 필요합니다. 우회 방법은 다음과 같습니다. (1) 같은 크기의 별도 결정 표를 저장합니다. (2) 문제를 중간 지점에서 재귀적으로 나누는 히르슈베르크 알고리즘을 사용합니다. 이 알고리즘은 복원을 포함해 O(mn) 시간과 O(min(m,n)) 공간으로 LCS를 계산합니다. (3) 복원이 필요할 때 O(mn) 공간을 받아들입니다.
# 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')간단 확인
이번 학습에서 다룬 자료 구조 및 알고리즘 — 코딩 면접 준비 개념을 제대로 이해했는지 테스트해 보십시오.
학습 내용 요약
이번 학습에서 다음을 배웠습니다. 이전 행만 필요할 때 2차원 DP 표는 롤링 1차원 배열을 사용해 O(n) 공간으로 압축할 수 있습니다. 덮어쓰기 전에 temp를 저장하는 대각선 변수 패턴은 dp[i-1][j-1]이 필요한 점화식을 처리합니다. 또한 0/1 배낭은 용량을 역순으로 순회하고 무한 배낭은 정방향으로 순회합니다. 다음에는 완전 탐색 알고리즘의 기반인 선택, 탐색, 선택 취소 백트래킹 템플릿을 학습합니다.
자주 묻는 질문
“2차원 DP 공간 최적화” 강의는 무료인가요?
네 — “2차원 DP 공간 최적화” 전체 내용을 이 웹사이트에서 무료로 읽을 수 있습니다. 인터랙티브하게 실습하려면(내장 코드 에디터와 24/7 AI 튜터), CoddyKit PRO로 업그레이드하면 DSA Interview Prep 강의 전체를 잠금 해제할 수 있습니다. DSA Interview Prep 강의에는 총 4개의 강의가 포함되어 있습니다.
“2차원 DP 공간 최적화”에서 뭘 배우나요?
DP 표에서 현재 행과 이전 행만 유지해 LCS와 편집 거리의 공간을 O(mn)에서 O(min(m,n))으로 줄입니다. 브라우저에서 직접 실행하는 실습 코드로 DSA Interview Prep을(를) 배우며, 24/7 AI 튜터가 강의를 진행하면서 질문에 답변해줍니다.
DSA Interview Prep을(를) 시작하는 데 경험이 필요한가요?
사전 경험은 필요하지 않습니다. CoddyKit의 DSA Interview Prep은(는) 초급자부터 고급 학습자까지를 위해 구성되어 있으므로, 여기서 시작하거나 처음부터 시작할 수 있으며 자신의 속도대로 진행할 수 있습니다. 이것은 4개 중 4번째 강의입니다.
“2차원 DP 공간 최적화” 강의는 얼마나 걸리나요?
대부분의 CoddyKit 강의는 약 5~10분이 소요됩니다. 각 강의는 간결하고 인터랙티브하여 꾸준한 진행이 가능하며, 웹과 앱에서 중단한 부분부터 바로 시작할 수 있습니다.
이 DSA Interview Prep 강의에서 코드를 작성하고 실행할 수 있나요?
네. 모든 DSA Interview Prep 강의에는 내장 코드 에디터가 포함되어 있으므로, 브라우저에서 바로 실제 코드를 작성하고 실행한 후 즉시 AI 피드백을 받을 수 있습니다 — 로컬 설정이 필요 없습니다.
이 강의의 모든 강의
- 격자의 고유 경로와 최소 경로 합
- 최장 공통 부분 수열
- 편집 거리(Levenshte인)
- 2차원 DP 공간 최적화