문제 개요
정수 배열 nums와 정수 limit가 주어졌을 때, 부분 배열 내 임의의 두 원소 간 절대 차이가 주어진 제한(limit)보다 작거나 같은 조건을 만족하는 가장 긴 비어 있지 않은 연속 부분 배열의 길이를 구하는 문제입니다.
예를 들어 입력이 다음과 같다고 가정해 보겠습니다.
nums = [8, 2, 4, 7], limit = 4
이 경우 정답은 2입니다. 모든 가능한 부분 배열을 확인해 보면 그 이유를 알 수 있습니다.
- [8] → |8 - 8| = 0 ≤ 4 ✅
- [8, 2] → |8 - 2| = 6 > 4 ❌
- [8, 2, 4] → |8 - 2| = 6 > 4 ❌
- [8, 2, 4, 7] → |8 - 2| = 6 > 4 ❌
- [2] → |2 - 2| = 0 ≤ 4 ✅
- [2, 4] → |2 - 4| = 2 ≤ 4 ✅
- [2, 4, 7] → |2 - 7| = 5 > 4 ❌
- [4] → |4 - 4| = 0 ≤ 4 ✅
- [4, 7] → |4 - 7| = 3 ≤ 4 ✅
- [7] → |7 - 7| = 0 ≤ 4 ✅
조건을 만족하는 가장 긴 부분 배열의 길이는 [2, 4] 또는 [4, 7]처럼 2입니다.
접근 방법: 슬라이딩 윈도우 + 단조 데크(Monotonic Deque)
모든 부분 배열을 일일이 검사하면 시간 복잡도가 O(n²) 이상으로 늘어나 비효율적입니다. 대신 슬라이딩 윈도우 기법과 두 개의 단조 데크(deque)를 활용하면 O(n) 시간에 문제를 해결할 수 있습니다.
maxD: 현재 윈도우 구간에서 최댓값 후보들을 내림차순으로 유지하는 데크minD: 현재 윈도우 구간에서 최솟값 후보들을 오름차순으로 유지하는 데크
핵심 아이디어는 다음과 같습니다. 어떤 부분 배열의 최댓값과 최솟값의 차이가 limit 이하라면, 그 배열 내 임의의 두 원소의 차이도 반드시 limit 이하입니다. 따라서 maxD.front() - minD.front() <= limit를 만족하는 한 윈도우를 계속 확장하고, 조건이 깨지면 왼쪽 끝을 줄여 나갑니다.
알고리즘 단계
- 결괏값
ret = 0, 윈도우 포인터i = 0(오른쪽 끝),j = 0(왼쪽 끝)으로 초기화합니다. - 두 개의 빈 데크
maxD,minD를 선언합니다. - 배열을 순회하며 각 원소
nums[i]에 대해:maxD의 뒤쪽 값이nums[i]보다 작으면 pop_back()으로 제거합니다(내림차순 유지).minD의 뒤쪽 값이nums[i]보다 크면 pop_back()으로 제거합니다(오름차순 유지).nums[i]를 두 데크 뒤에 삽입합니다.
maxD.front() - minD.front() > k인 동안:nums[j]가maxD.front()와 같으면 maxD에서 앞 원소를 제거합니다.nums[j]가minD.front()와 같으면 minD에서 앞 원소를 제거합니다.j를 증가시켜 윈도우 왼쪽 끝을 축소합니다.
- 현재 윈도우 길이
i - j + 1과ret중 큰 값을 저장합니다. - 순회가 끝나면
ret을 반환합니다.
C++ 구현 예제
아래 코드를 통해 실제 동작을 더 잘 이해할 수 있습니다.
#include <bits/stdc++.h>
using namespace std;
class Solution {
public:
int longestSubarray(vector<int>& nums, int k) {
int ret = 0;
int i = 0;
int j = 0;
deque<int> maxD;
deque<int> minD;
int n = nums.size();
for (int i = 0; i < n; i++) {
while (!maxD.empty() && maxD.back() < nums[i])
maxD.pop_back();
while (!minD.empty() && minD.back() > nums[i])
minD.pop_back();
maxD.push_back(nums[i]);
minD.push_back(nums[i]);
while (maxD.front() - minD.front() > k) {
if (nums[j] == maxD.front())
maxD.pop_front();
if (nums[j] == minD.front())
minD.pop_front();
j++;
}
ret = max(ret, i - j + 1);
}
return ret;
}
};
main(){
Solution ob;
vector<int> v = {7,8,2,4};
cout << (ob.longestSubarray(v, 4));
}입력
{7,8,2,4}, 4출력
2
복잡도 분석
- 시간 복잡도: O(n) — 각 원소는 데크에 최대 한 번 삽입되고 한 번 제거되므로 전체 연산 횟수는 배열 길이에 비례합니다.
- 공간 복잡도: O(n) — 최악의 경우 두 데크가 배열 전체 크기만큼 공간을 사용할 수 있습니다.
이처럼 단조 데크를 활용한 슬라이딩 윈도우는 '구간 최댓값/최솟값'을 상수 시간에 추적할 수 있게 해주는 강력한 패턴으로, 유사한 문제(슬라이딩 윈도우 최댓값 등)에도 폭넓게 응용됩니다.