DP 가이드 (9/11)

이전 편: [코딩테스트] 8. 격자 DP

다음 편: [코딩테스트] 10. 배낭 문제

DP 유형별 패턴 시리즈

선형 DP · 격자 DP · LIS · 배낭 문제 · 실전 패턴과 면접 대비

DP 가이드 : 기본 개념설계 전략유형별 패턴

LIS(Longest Increasing Subsequence)는 수열에서 값이 오름차순을 유지하는 가장 긴 부분 수열의 길이를 구하는 문제다. DP 문제에서 자주 출제되고, 응용 범위도 넓다.

문제 이해

수열 [3, 1, 4, 1, 5, 9, 2, 6]에서 LIS를 찾아보자.

오름차순을 유지하는 부분 수열 중 가장 긴 것을 찾으면 된다. 연속일 필요는 없다. 순서만 지키면 된다.

[3, 1, 4, 1, 5, 9, 2, 6]
         ↑     ↑  ↑  ↑
         4     5  9  6  → 길이 4이지만 9 다음에 6은 감소 → 안 됨

         4     5     6  → [1, 4, 5, 6] 길이 4 ✓

LIS는 여러 개일 수 있다. [1, 4, 5, 9]도 길이 4인 LIS다.

상태 정의

dp[i] = arr[i]를 마지막 원소로 하는 LIS의 길이.

핵심은 "i번 원소를 반드시 포함"하는 가장 긴 증가 수열이라는 점이다.

수열: [3, 1, 4, 1, 5]
       ↑  ↑  ↑  ↑  ↑
dp:   [1, 1, 2, 1, 3]

dp[0]=1: [3]
dp[1]=1: [1]
dp[2]=2: [1,4] 또는 [3,4]
dp[3]=1: [1] (앞의 1과 같지만 인덱스가 다름, 앞에 4>1이라 [1,4,1]은 불가)
dp[4]=3: [1,4,5] 또는 [3,4,5]

최종 답은 dp 배열의 최댓값이다.

점화식

dp[i]를 구하려면 arr[i]보다 앞에 있고 값이 작은 원소들을 전부 확인한다.

dp[i] = max(dp[j] + 1)   for all j < i where arr[j] < arr[i]

arr[j] < arr[i]인 j들 중 dp[j]가 가장 큰 것을 선택해서 +1 한다.

int[] dp = new int[n];
Arrays.fill(dp, 1);

for (int i = 1; i < n; i++) {
    for (int j = 0; j < i; j++) {
        if (arr[j] < arr[i]) {
            dp[i] = Math.max(dp[i], dp[j] + 1);
        }
    }
}

int answer = 0;
for (int val : dp) answer = Math.max(answer, val);
  • Arrays.fill(dp, 1) : 모든 원소는 자기 혼자만으로 길이 1의 LIS를 이룬다
  • 내부 루프에서 j < i이고 값이 작은 경우를 전부 확인

시간복잡도는 O(N²)이다.

O(N log N) 풀이

N이 10만 이상이면 O(N²)은 시간 초과가 난다. 이분 탐색을 활용하면 O(N log N)으로 줄일 수 있다.

배열 tail을 유지한다. tail[k]는 길이 k+1인 증가 수열의 가장 작은 마지막 원소다.

List<Integer> tail = new ArrayList<>();

for (int x : arr) {
    int pos = Collections.binarySearch(tail, x);
    if (pos < 0) pos = -(pos + 1);

    if (pos == tail.size()) tail.add(x);
    else tail.set(pos, x);
}

int answer = tail.size();

tail의 길이가 LIS의 길이가 된다.

O(N log N) 풀이의 주의점

tail 배열 자체는 실제 LIS가 아닐 수 있다. 길이만 올바르다. 실제 수열을 출력해야 한다면 별도의 역추적이 필요하다.

다음 : 배낭 문제