LIS

한 수열에서 순서를 유지하며 값이 계속 증가하는 가장 긴 부분 수열(Longest Increasing Subsequence)을 찾는 문제입니다.

동작 과정

O(N²) DP

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

A = [10, 20, 10, 30, 20, 50]

i=0 (10): 앞에 없음                      dp[0]=1
i=1 (20): 10 < 20 → dp[0]+1=2            dp[1]=2
i=2 (10): 10 < 10 아님, 20 < 10 아님      dp[2]=1
i=3 (30): 10<30(2), 20<30(3), 10<30(2)    dp[3]=3
i=4 (20): 10<20(2), 10<20(2)              dp[4]=2
i=5 (50): 10(2),20(3),10(2),30(4),20(3)   dp[5]=4

dp = [1, 2, 1, 3, 2, 4]  → 답 = max(dp) = 4
LIS 중 하나: 10 → 20 → 30 → 50

O(N log N) — 이진 탐색

"길이 k인 증가 부분 수열의 마지막 값 최솟값"을 배열 tails로 관리합니다.

A = [10, 20, 10, 30, 20, 50]

10 → tails=[10]                    (새 길이 1)
20 → 20 > 10 이므로 뒤에 붙임
     tails=[10, 20]                (새 길이 2)
10 → 10 이상인 첫 위치(index 0)를 교체
     tails=[10, 20]                (길이 변화 없음, 10으로 갱신)
30 → 30 > 20 이므로 뒤에 붙임
     tails=[10, 20, 30]            (새 길이 3)
20 → 20 이상인 첫 위치(index 1)를 교체
     tails=[10, 20, 30]            (20 → 20, 변화 없음)
50 → 50 > 30 이므로 뒤에 붙임
     tails=[10, 20, 30, 50]        (새 길이 4)

답 = tails.length = 4

tails는 항상 정렬 상태를 유지하므로 삽입 위치를 이진 탐색으로 찾을 수 있습니다. tails 자체는 실제 LIS가 아니라 길이만 보장합니다.

구현

// O(N²) — 어떤 LIS인지 복원하기 쉽다
int lisDp(int[] a) {
    int n = a.length;
    int[] dp = new int[n];
    Arrays.fill(dp, 1);

    for (int i = 1; i < n; i++) {
        for (int j = 0; j < i; j++) {
            if (a[j] < a[i]) {
                dp[i] = Math.max(dp[i], dp[j] + 1);
            }
        }
    }
    return Arrays.stream(dp).max().orElse(0);
}
// O(N log N) — 길이만 필요할 때
int lisBinarySearch(int[] a) {
    int[] tails = new int[a.length];
    int size = 0;

    for (int x : a) {
        // tails[0..size)에서 x가 들어갈 위치 탐색
        int lo = 0, hi = size;
        while (lo < hi) {
            int mid = (lo + hi) >>> 1;
            if (tails[mid] < x) lo = mid + 1;   // 엄격히 증가
            else hi = mid;
        }
        tails[lo] = x;
        if (lo == size) size++;
    }
    return size;
}

Arrays.binarySearch는 값이 없을 때 -(삽입위치)-1을 주므로 그대로 쓰기 번거롭습니다. 위처럼 lower bound를 직접 구현하는 편이 명확합니다 — Arrays 참고.

복잡도

구현시간공간
DPO(N²)O(N)
이진 탐색O(N log N)O(N)

N이 1,000 이하면 DP로 충분하고, 100,000 수준이면 이진 탐색이 필요합니다.

주의사항

핵심 정리

→ 동적 계획법

CSLOG에서 보기