|

DP 테이블을 2차원에서 1차원으로 줄여도 되는 기준

DP 테이블 글 대표 이미지
DP 테이블을 1차원으로 줄여도 되는 기준을 순회 방향과 knapsack 예제로 설명합니다.

1차원 DP는 2차원 DP 테이블을 무작정 줄이는 기술이 아닙니다. 현재 상태를 계산할 때 어떤 이전 값이 필요한지 확인한 뒤, 덮어써도 안전할 때만 쓸 수 있는 메모리 최적화입니다.

가장 중요한 기준은 현재 칸을 계산할 때 아직 써야 하는 값을 덮어쓰지 않는가입니다. 이 기준을 놓치면 코드는 짧아지지만 답은 조용히 틀립니다.

1차원 DP 판단 기준 카드
1차원 DP는 참조 방향과 덮어쓰기 위험을 먼저 확인해야 한다.

2차원 DP를 먼저 생각해야 하는 이유

처음부터 1차원 DP로 시작하면 전이 관계가 잘 보이지 않습니다. 예를 들어 물건 i까지 고려했을 때 무게 w에서의 최댓값은 보통 dp[i][w]처럼 생각하는 편이 자연스럽습니다.

dp[i][w] =
  i번째 물건까지 고려했고
  가방 용량이 w일 때 만들 수 있는 최대 가치

이렇게 2차원으로 정의하면 행은 선택한 물건 범위, 열은 용량을 의미합니다. 그다음 현재 행이 이전 행만 참조하는지 확인하면 1차원으로 줄일 수 있는지 판단할 수 있습니다.

줄여도 되는 핵심 조건

2차원 DP에서 dp[i][w]가 dp[i-1][…]만 참조한다면, i-1행 전체를 따로 보관하지 않고 같은 배열을 갱신할 수 있습니다. 다만 같은 배열을 쓰는 순간 dp[w]는 이전 행 값일 수도 있고, 이미 현재 행으로 갱신된 값일 수도 있습니다.

  • 이전 행 값만 필요하면 1차원 배열 후보가 된다
  • 현재 행에서 갱신된 값을 다시 쓰면 문제가 되는지 확인한다
  • 문제가 된다면 순회 방향을 바꿔 덮어쓰기를 피한다
  • 전이식보다 순회 순서가 더 중요해지는 순간이 있다

0/1 knapsack은 왜 역순으로 도는가

0/1 knapsack에서는 각 물건을 한 번만 쓸 수 있습니다. 그런데 용량을 작은 값에서 큰 값으로 정순 순회하면, 방금 넣은 물건을 같은 i단계에서 다시 쓰는 효과가 생길 수 있습니다.

# 0/1 knapsack: 같은 물건을 한 번만 써야 하므로 역순
for weight, value in items:
    for w in range(capacity, weight - 1, -1):
        dp[w] = max(dp[w], dp[w - weight] + value)

역순으로 돌면 dp[w – weight]는 아직 현재 물건으로 갱신되기 전 값입니다. 그래서 같은 물건을 한 번만 쓰는 조건이 유지됩니다.

정순으로 돌면 어떤 버그가 생길까

물건 하나의 무게가 3, 가치가 5라고 해 보겠습니다. 정순으로 w=3을 갱신한 뒤 w=6을 계산하면 dp[3]이 이미 5가 되어 있습니다. 그러면 같은 물건을 두 번 넣은 것처럼 dp[6]=10이 될 수 있습니다.

capacity = 6
item = (weight=3, value=5)

정순 갱신:
dp[3] = 5
dp[6] = dp[3] + 5 = 10  # 같은 물건을 두 번 쓴 효과

unbounded knapsack은 왜 정순이 자연스러운가

unbounded knapsack은 같은 물건을 여러 번 쓸 수 있습니다. 이 경우에는 현재 단계에서 갱신된 dp[w – weight]를 다시 사용하는 것이 오히려 의도와 맞습니다.

# unbounded knapsack: 같은 물건을 여러 번 쓸 수 있으므로 정순
for weight, value in items:
    for w in range(weight, capacity + 1):
        dp[w] = max(dp[w], dp[w - weight] + value)

같은 전이식처럼 보여도 문제의 선택 조건이 달라지면 순회 방향이 달라집니다. 그래서 1차원 DP에서는 코드를 외우기보다 덮어쓰기 의미를 읽어야 합니다.

순회 방향을 고르는 질문

  1. 현재 선택을 같은 단계에서 다시 써도 되는가
  2. dp[w – x]가 이전 행 값이어야 하는가
  3. 현재 행에서 갱신된 값이어도 되는가
  4. 작은 인덱스에서 큰 인덱스로 가면 중복 사용이 생기는가
  5. 큰 인덱스에서 작은 인덱스로 가면 필요한 값이 사라지는가

1차원 DP로 줄이면 안 되는 경우

모든 2차원 DP가 1차원으로 쉽게 줄어드는 것은 아닙니다. 현재 값이 이전 행뿐 아니라 같은 행의 여러 방향, 대각선, 혹은 복잡한 범위를 동시에 참조한다면 단순한 1차원 배열 하나로는 상태를 보존하기 어렵습니다.

  • 경로 복원이 필요해 전체 선택 기록을 남겨야 하는 경우
  • 현재 행의 왼쪽과 이전 행을 섞어 참조하는 경우
  • 상태 차원이 의미적으로 독립적이라 압축하면 설명이 무너지는 경우
  • 메모리는 줄지만 버그 가능성이 크게 올라가는 경우

실전 체크리스트

  • 먼저 2차원 dp 정의를 문장으로 쓴다
  • 전이식이 어떤 행과 열을 참조하는지 표시한다
  • 같은 배열로 갱신했을 때 덮어쓰기 위험을 찾는다
  • 순회 방향을 바꾸면 위험이 사라지는지 확인한다
  • 작은 예제로 중복 사용 여부를 직접 추적한다

DP 테이블 최적화 정리

1차원 DP의 핵심은 배열 크기를 줄이는 데 있지 않습니다. 현재 상태를 계산할 때 필요한 이전 값이 무엇인지, 그 값이 갱신 과정에서 덮어써지는지 이해하는 데 있습니다.

0/1 knapsack은 역순, unbounded knapsack은 정순이라는 암기는 출발점일 뿐입니다. 진짜 기준은 같은 선택을 다시 써도 되는지입니다. 관련 배경은 CP-Algorithms knapsack 정리knapsack이 DP의 대표 문제인 이유를 함께 보면 좋습니다.

함께보면 좋은 글