목표: 합이 k 이상인 가장 짧은 부분 배열의 길이 찾기
핵심 난점:
부분 배열 [i, j]의 합 = prefixSum[j+1] - prefixSum[i]
합 >= k 조건: prefixSum[j+1] - prefixSum[i] >= k → prefixSum[i] <= prefixSum[j+1] - k
문제 변환:
“각 j에 대해, prefixSum[i] <= prefixSum[j] - k를 만족하는 가장 큰 i를 찾아라”
왜 단조 증가?
deque에 prefixSum이 증가하는 순서로 인덱스 저장
예시: prefixSum = [0, 1, -1, 2] indices = [0, 1, 2, 3]
i=2일 때 prefixSum[2]=-1 < prefixSum[1]=1 → 1을 제거!
이유: 2를 선택하면 더 짧은 부분 배열 가능 (2에서 시작 vs 1에서 시작)
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
// 1. Prefix Sum 계산 (long 타입 - overflow 방지)
long[] prefixSum = new long[n + 1];
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
// 2. 단조 증가 Deque
Deque<Integer> deque = new ArrayDeque<>();
int minLength = Integer.MAX_VALUE;
for (int i = 0; i <= n; i++) {
// 3. 조건 만족하는 부분 배열 찾기
// prefixSum[i] - prefixSum[deque.peekFirst()] >= k
while (!deque.isEmpty() &&
prefixSum[i] - prefixSum[deque.peekFirst()] >= k) {
minLength = Math.min(minLength, i - deque.pollFirst());
}
// 4. 단조 증가 유지
// 현재 prefixSum보다 큰 값들 제거
while (!deque.isEmpty() &&
prefixSum[i] <= prefixSum[deque.peekLast()]) {
deque.pollLast();
}
deque.offerLast(i);
}
return minLength == Integer.MAX_VALUE ? -1 : minLength;
}
}
시간복잡도: O(n) 공간복잡도: O(n)
nums = [2, -1, 2], k = 3Step 1: Prefix Sum 계산
nums = [2, -1, 2]
indices = [0, 1, 2]
prefixSum = [0, 2, 1, 3]
↑ ↑ ↑ ↑
i=0 i=1 i=2 i=3
Step 2: Deque로 처리
i=0: prefixSum[0]=0
deque: [0]
minLength: ∞
i=1: prefixSum[1]=2
// 조건 확인: 2 - 0 = 2 < 3 → 불만족
// 단조성 확인: 2 > 0 → OK
deque: [0, 1]
minLength: ∞
i=2: prefixSum[2]=1
// 조건 확인: 1 - 0 = 1 < 3 → 불만족
// 단조성 확인: 1 <= 2 → 1 제거!
deque: [0]
deque: [0, 2]
minLength: ∞
i=3: prefixSum[3]=3
// 조건 확인: 3 - 0 = 3 >= 3 → 만족! ✓
minLength = min(∞, 3-0) = 3
deque에서 0 제거
// 다시 확인: deque = [2]
// 3 - 1 = 2 < 3 → 불만족
// 단조성: 3 > 1 → OK
deque: [2, 3]
답: 3
nums = [84, -37, 32, 40, 95], k = 167Prefix Sum:
nums = [84, -37, 32, 40, 95]
prefixSum = [0, 84, 47, 79, 119, 214]
↑ ↑ ↑ ↑ ↑ ↑
i=0 i=1 i=2 i=3 i=4 i=5
처리 과정:
i=0: prefixSum[0]=0
deque: [0]
i=1: prefixSum[1]=84
// 84 - 0 = 84 < 167
// 84 > 0 → OK
deque: [0, 1]
i=2: prefixSum[2]=47
// 47 - 0 = 47 < 167
// 47 <= 84 → 1 제거!
deque: [0, 2]
i=3: prefixSum[3]=79
// 79 - 0 = 79 < 167
// 79 > 47 → OK
deque: [0, 2, 3]
i=4: prefixSum[4]=119
// 119 - 0 = 119 < 167
// 119 > 79 → OK
deque: [0, 2, 3, 4]
i=5: prefixSum[5]=214
// 214 - 0 = 214 >= 167 ✓
minLength = 5-0 = 5
deque: [2, 3, 4]
// 214 - 47 = 167 >= 167 ✓
minLength = min(5, 5-2) = 3
deque: [3, 4]
// 214 - 79 = 135 < 167
// 214 > 119 → OK
deque: [3, 4, 5]
답: 3 (부분 배열 [32, 40, 95])
Case 1: prefixSum이 감소하는 경우
prefixSum: [0, 5, 3, 8]
↑ ↑ ↑ ↑
i=0 1 2 3
i=2일 때:
- prefixSum[2]=3 < prefixSum[1]=5
- 만약 나중에 합이 k 이상이 되면:
- [1,?]보다 [2,?]가 항상 더 짧음!
- 왜? 2가 1보다 오른쪽에 있으니까
- 따라서 1은 절대 답이 될 수 없음 → 제거!
Case 2: 조건 만족 시 제거
prefixSum: [0, 2, 5, 10]
k = 8
i=3일 때:
- 10 - 0 = 10 >= 8 ✓ → [0,3] 길이 3
- 10 - 2 = 8 >= 8 ✓ → [1,3] 길이 2
- 0 제거! (이미 사용했으니 더 이상 필요 없음)
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
int minLength = Integer.MAX_VALUE;
for (int i = 0; i < n; i++) {
long sum = 0;
for (int j = i; j < n; j++) {
sum += nums[j];
if (sum >= k) {
minLength = Math.min(minLength, j - i + 1);
break;
}
}
}
return minLength == Integer.MAX_VALUE ? -1 : minLength;
}
}
시간복잡도: O(n²) 결과: TLE (Time Limit Exceeded)
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
long[] prefixSum = new long[n + 1];
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
TreeMap<Long, Integer> map = new TreeMap<>();
int minLength = Integer.MAX_VALUE;
for (int i = 0; i <= n; i++) {
// prefixSum[i] - x >= k
// x <= prefixSum[i] - k
Long target = prefixSum[i] - k;
// headMap: key <= target인 모든 항목
for (Map.Entry<Long, Integer> entry : map.headMap(target, true).entrySet()) {
minLength = Math.min(minLength, i - entry.getValue());
}
map.put(prefixSum[i], i);
}
return minLength == Integer.MAX_VALUE ? -1 : minLength;
}
}
시간복잡도: O(n²) (headMap 순회) 결과: TLE
| 방법 | 시간복잡도 | 공간복잡도 | 결과 |
|---|---|---|---|
| Brute Force | O(n²) | O(1) | TLE |
| TreeMap | O(n²) or O(n log n) | O(n) | TLE or Slow |
| 단조 Deque | O(n) | O(n) | AC ✅ |
부분 배열 합을 O(1)에 계산
→ 모든 가능한 부분 배열을 빠르게 검사 가능
역할 1: 불필요한 후보 제거
// prefixSum이 감소하면 제거
while (!deque.isEmpty() &&
prefixSum[i] <= prefixSum[deque.peekLast()]) {
deque.pollLast();
}
역할 2: 조건 만족 시 즉시 제거
// 이미 답을 찾았으면 제거 (더 긴 부분 배열은 불필요)
while (!deque.isEmpty() &&
prefixSum[i] - prefixSum[deque.peekFirst()] >= k) {
minLength = Math.min(minLength, i - deque.pollFirst());
}
양수만 있으면: Two Pointer로 O(n) 가능
음수가 있으면: 합이 증가했다가 감소 → Two Pointer 불가
예: [5, -3, 8]
누적: 5 → 2 → 10
↑ ↓ ↑
증가 감소 증가
class Solution {
public int shortestSubarray(int[] nums, int k) {
int n = nums.length;
// Prefix Sum 배열 (long 타입으로 overflow 방지)
long[] prefixSum = new long[n + 1];
for (int i = 0; i < n; i++) {
prefixSum[i + 1] = prefixSum[i] + nums[i];
}
// 단조 증가 Deque (인덱스 저장)
Deque<Integer> deque = new ArrayDeque<>();
int minLength = Integer.MAX_VALUE;
for (int i = 0; i <= n; i++) {
// 1. 조건을 만족하는 부분 배열 찾기
// prefixSum[i] - prefixSum[j] >= k
while (!deque.isEmpty() &&
prefixSum[i] - prefixSum[deque.peekFirst()] >= k) {
// 최소 길이 갱신 후 제거
// (이미 사용했으므로 더 이상 필요 없음)
minLength = Math.min(minLength, i - deque.pollFirst());
}
// 2. 단조 증가 유지
// 현재 prefixSum이 더 작으면 이전 것들 제거
// 이유: 현재 위치가 더 오른쪽이면서 값도 작으므로
// 나중에 더 짧은 부분 배열을 만들 수 있음
while (!deque.isEmpty() &&
prefixSum[i] <= prefixSum[deque.peekLast()]) {
deque.pollLast();
}
// 3. 현재 인덱스 추가
deque.offerLast(i);
}
return minLength == Integer.MAX_VALUE ? -1 : minLength;
}
}
/**
* 시간복잡도: O(n) - 각 인덱스는 최대 1번 추가, 1번 제거
* 공간복잡도: O(n) - prefixSum 배열과 deque
*
* Runtime: 20-30ms
* Memory: 55-60MB
*/
long[] prefixSum = new long[n + 1];
// nums[i]가 최대 10^5, n이 최대 10^5
// 최악의 경우: 10^5 * 10^5 = 10^10 > Integer.MAX_VALUE
deque의 prefixSum 값들:
[작은 값] → [중간 값] → [큰 값]
왜? 작으면서 오른쪽에 있는 게 유리
→ 더 짧은 부분 배열 가능
// 첫 번째: pollFirst() - 답 찾기
while (...) {
minLength = min(minLength, i - deque.pollFirst());
}
// 두 번째: pollLast() - 불필요한 후보 제거
while (...) {
deque.pollLast();
}
LeetCode 209. Minimum Size Subarray Sum ⭐⭐
LeetCode 1425. Constrained Subsequence Sum ⭐⭐⭐⭐
LeetCode 1499. Max Value of Equation ⭐⭐⭐⭐
#MonotonicDeque #PrefixSum #ShortestSubarray #단조큐 #누적합 #Deque #SubarraySum
#Hard #슬라이딩윈도우 #음수처리 #LeetCodeHard #최적화 #O(n)알고리즘