어떤 문제를 푸는가
수열에서 순서를 유지하며 골라낸 증가하는 부분 수열 중 가장 긴 것의 길이를
구합니다. 예: [10, 9, 2, 5, 3, 7, 101, 18]의 LIS는 [2, 3, 7, 101]로 길이
4입니다. 부분 수열이므로 연속일 필요는 없고, 순서만 지키면 됩니다.
1단계 — \(O(N^2)\) DP
dp[i]를 "\(i\)번째 원소로 끝나는 가장 긴 증가 수열의 길이"로 둡니다.
$$ dp[i] = 1 + \max_{j < i,\ a[j] < a[i]} dp[j] $$
자기 앞의 모든 작은 원소를 보고, 그중 가장 긴 것에 자신을 이어 붙입니다.
답은 \(\max_i dp[i]\). 직관적이지만 \(O(N^2)\)이라 \(N\)이 수만을 넘으면 느립니다.
2단계 — \(O(N \log N)\) 풀이
핵심 발상: 길이가 \(\ell\)인 증가 수열들 중 마지막 원소가 가장 작은 값 을
tails[ℓ]에 저장합니다. 마지막 값이 작을수록 뒤에 더 많은 원소를 이을 수
있으니 유리합니다. 이 tails 배열은 항상 증가 상태를 유지합니다.
각 원소 \(x\)를 처리할 때:
tails에서 \(x\) 이상인 첫 위치 를 이분 탐색으로 찾는다.- 그 자리를 \(x\)로 교체한다. (그 자리가 끝이면 길이가 1 늘어남)
처리가 끝나면 tails의 길이가 곧 LIS의 길이입니다.
왜 옳은가
tails는 "각 길이별 최선의 끝값"을 유지합니다. \(x\)로 어떤 자리를 교체하면,
그 길이의 증가 수열을 더 작은 값으로 끝낼 수 있게 되어 미래의 선택지가 넓어
집니다. tails가 증가하므로 이분 탐색이 가능하고, 교체는 다른 자리의 불변식을
깨지 않습니다. 다만 tails 자체는 실제 LIS가 아닙니다 — 길이만 정확합니다.
복잡도
| 방법 | 시간 |
|---|---|
| DP | \(O(N^2)\) |
| 이분 탐색 | \(O(N \log N)\) |
\(N \le 10^5\) 이상이면 \(O(N \log N)\)이 사실상 필수입니다.
엄격히 증가 vs 비감소
"같은 값 허용(비감소)"이면 lower_bound(같은 값 이상) 대신 upper_bound
(같은 값보다 큰 첫 위치)를 써 같은 값을 이어 붙입니다. 문제 조건을 정확히
읽어 둘을 구분하는 것이 핵심입니다. 다음 강의에서 구현과 경로 복원을 봅니다.