덱 DP (슬라이딩 윈도우 최적화)
정의
덱 DP (Deque DP) 는 점화식 dp[i] = min/max_{j in [i-k, i-1]} (dp[j] + cost(i,j)) 꼴에서 monotonic deque 를 써서 최적 j 후보를 O(N) 에 유지하는 기법. 슬라이딩 윈도우 최댓값/최솟값의 일반화로, 윈도우 내에서 비용까지 포함한 최적 인덱스 를 관리한다.
문제 상황과 동기
dp[i] = max(dp[j] + a[j]) for j < i and i - j <= K. Naive 하면 각 i 에서 K 개 순회 -> O(NK). N=10^6, K=10^5 면 10^11.
핵심 통찰: 윈도우 안의 후보 중 dp[j] + something 이 최대인 j 만 deque 에 유지. 더 이상 최적이 될 수 없는 후보는 pop.
시각화
핵심 아이디어
DP: dp[i] = best(dp[j] + a[i]) for j in [i-K, i-1]
where best = maximal computed value
Deque invariant:
1. Index range: front is always within [i-K, i-1]
2. Value order: dp[deque[0]] >= dp[deque[1]] >= ... (decreasing)
3. Old index from front when out of range
4. New candidate deletes from back if inferior
Deque 에는 인덱스 만 저장. dp[deque.front()] 이 항상 현재 i 에 대한 최적값.
덱 상태 변화
배열 a = [1, -3, 5, -2, 8], K = 3 일 때 i = 4 까지의 덱 변화:
flowchart LR
I0["i=0: dq=[0]\ndp[0]=1"]
I1["i=1: dq=[0,1]\ndp[1]=1+(-3)=-2\n(dp[1]<dp[0], 1 추가)"]
I2["i=2: front=0 유효\ndq=[0,2]\ndp[2]=1+5=6\n(dp[1] 제거: dp[1]<dp[2])"]
I3["i=3: front=0 유효\ndq=[0,3]\ndp[3]=1+(-2)=-1\n(dp[2] 유지)"]
I4["i=4: front=0 만료(4-0>3)\ndq=[2,4]\ndp[4]=6+8=14"]
I0 --> I1 --> I2 --> I3 --> I4
각 스텝에서:
dq.front()가i - K보다 작으면 만료 제거dp[i]는dp[dq.front()] + a[i]로 계산dq.back()이 dp 값이 새 값보다 작으면 제거 (단조 감소 유지)i를dq.back()에 추가
NOTE
덱에는 항상 dp 값이 내림차순 인 인덱스만 남는다. back 에서 제거할 때 dp[dq.back()] <= dp[i] 조건을 쓰는 이유가 여기에 있다. 최솟값을 구할 때는 >= 로 반전.
알고리즘
deque dq
for i = 0..N-1:
# 1. 만료된 front 제거
while dq not empty and dq.front() < i - K:
dq.pop_front()
# 2. 현재 dp[i] 계산
if dq empty:
dp[i] = a[i]
else:
dp[i] = dp[dq.front()] + a[i]
# 3. dq.back() 이 dp[i] 보다 나쁘면 제거
while dq not empty and dp[dq.back()] <= dp[i]:
dq.pop_back()
# 4. 현재 i 를 후보에 추가
dq.push_back(i)
구현
// Maximum sum subsequence: pick elements at distance <= K
// dp[i] = max sum ending at i, each pair distance ≤ K
#include <bits/stdc++.h>
using namespace std;
int main() {
int n, k; cin >> n >> k;
vector<int> a(n);
for (int i = 0; i < n; i++) cin >> a[i];
vector<long long> dp(n);
deque<int> dq;
long long ans = 0;
for (int i = 0; i < n; i++) {
while (!dq.empty() && dq.front() < i - k)
dq.pop_front();
dp[i] = a[i];
if (!dq.empty()) dp[i] += dp[dq.front()];
while (!dq.empty() && dp[dq.back()] <= dp[i])
dq.pop_back();
dq.push_back(i);
ans = max(ans, dp[i]);
}
cout << ans << "\n";
return 0;
}8 3
1 -3 5 -2 8 -1 4 -613복잡도
| 항목 | 값 |
|---|---|
| 시간 (최선/평균/최악) | O(N) |
| 공간 | O(N) (dp 배열) or O(K) (deque + rolling) |
| 각 원소의 deque push/pop | amortized O(1) |
변형 / 활용
| 변형 | 설명 |
|---|---|
| Sliding window minimum | 부등호 반대 (≤ -> >=). dp 없이 순수 배열 최솟값 |
| DP + sliding window cost | dp[i] = min(dp[j] + cost(i,j)) 에서 cost 가 분해 가능할 때 |
| Divide and Conquer DP | monotone queue 와 결합, dp[i][j] 꼴 |
| 1D/1D DP | 점화식 차수가 1차원, 최적화 상수가 K 일 때 |
널리 사용되는 예: BOJ 11003 (최솟값 찾기), BOJ 2096 (내려가기, 슬라이딩 윈도우 DP).
다른 DP 최적화와 비교
| 기법 | 점화식 형태 | 제약 | 복잡도 |
|---|---|---|---|
| 덱 DP | dp[i] = dp[j] + cost for j in [i-K, i-1] | 윈도우 길이 고정 K | O(N) |
| Divide and Conquer DP | dp[i][j] = min(dp[i-1][k] + cost(k,j)) | opt(i,j) 단조 | O(N log N) |
| SMAWK / Knuth | 2D 점화식 | 사각형 부등식 | O(N) 또는 O(N^2) |
| 세그먼트 트리 | dp[i] = min_{j<i} f(j) 일반형 | 제약 없음 | O(N log N) |
덱 DP 는 가장 빠르지만 윈도우 길이가 고정 이라는 엄격한 조건이 필요. 윈도우가 가변이거나 cost 가 분해 불가능하면 세그먼트 트리나 Divide and Conquer 를 사용해야 한다.
선택 기준
j in [i-K, i-1]형태로 윈도우가 명확히 고정: 덱 DPopt(i,j)가 단조 (monotone minima 성질): Divide and Conquer DP- 위 둘 다 아닌 경우: 세그먼트 트리 O(N log N)
함정
1. 부등호 방향
최댓값: dp[dq.back()] <= dp[i] 이면 pop. 최솟값: >= 로 반대. 혼동하지 않도록.
2. 초기 K 값
i < K 일 때는 윈도우가 충분히 크지 않음. i - k 가 음수가 되지 않도록 조건.
3. 0-1 범위 반영
dp[i] 가 기본값 a[i] (단독 선택) 인지, 아니면 이전 dp 값과 합쳐야 하는지. 문제 정의에 따라.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 11003 | 최솟값 찾기 | - | kokoa-lab |
| BOJ 2096 | 내려가기 | - | kokoa-lab |
| BOJ 14865 | 골목길 | - | kokoa-lab |
참고
이 글의 용어 (4개)
- 동적 계획법 (Dynamic Programming)algorithm
- 정의 동적 계획법 (Dynamic Programming, DP) 은 큰 문제를 작은 부분 문제로 나누고, 각 부분 문제의 최적해를 저장하여 중복 계산을 제거하는 최적화 기법. R…
- 두 포인터 (Two Pointer)algorithm
- 정의 두 포인터 (Two Pointer) 는 정렬 또는 단조 구조 에서 두 인덱스 , 을 서로 다른 속도 / 방향 으로 이동시키며 부분 구간 / 페어를 O(N) 에 탐색하는 기법…
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 슬라이딩 윈도우 (Sliding Window)algorithm
- 정의 슬라이딩 윈도우 (Sliding Window) 는 배열/문자열 위에서 고정 크기 또는 가변 크기 윈도우가 한 방향으로 미끄러지며 각 윈도우마다 조건 (합, 최댓값, 중복 여…
💬 댓글