수열 $\{a_n\}$이 주어졌을 때, 부분 수열 중 증가하는 것의 개수를 빠르게 세는 방법을 알아봅시다.
LIS(Longest Increasing Subsquence)라고도 불리는 이 알고리즘은 DP와 이분탐색 두 가지 방법이 있습니다.
DP: $O(N^2)$
길이 찾기
$dp[i]$를 $a_i$로 끝나는 LIS의 길이라고 합시다.
예를 들어 $\{a_n\} = \{1, 2, 1, 3, 2, 5\}$이라고 하면 $dp[3]$은 $3$입니다. $(\because \{1, 2, 3\})$
이때 $j < i, a_j < a_i$라면 $dp[i] = max(dp[j] + 1, dp[i])$임을 알 수 있습니다.
$\underset{i = 1}{\overset{n - 1}{FOR}}\ \underset{j = 0}{\overset{i-1}{FOR}}$ 로 반복문을 돌려 $dp$배열을 완성할 수 있습니다.
아래는 11053번: 가장 긴 증가하는 부분 수열의 정답 코드입니다.
1 |
|
부분 수열 찾기
완성된 $dp$배열을 통해 LIS를 찾아봅시다.
먼저 부분 수열의 끝에 오는 원소부터 역추적합니다.
가장 긴 증가하는 부분 수열의 길이가 $n$이라고 하면 $dp[i] = n, n-1, n-2, n-3$인 $i$를 뒤에서부터 순서대로 찾으면 됩니다.
찾은 순서의 역순이 LIS가 됩니다.
1 |
|
이분 탐색: $O(NlogN)$
길이 찾기
이분 탐색을 이용해 LIS를 찾아가면 길이를 빠르게 구할 수 있습니다.
이 알고리즘은 풀이를 먼저 보고 이해하는 것이 편합니다.
아래는 12015번: 가장 긴 증가하는 부분 수열 2의 정답 코드입니다.
1 |
|
$LIS$ 배열은 항상 오름차순 정렬을 유지합니다.
이분탐색을 통해 구한 $index$로 $LIS_{index}$를 $a_i$로 대체합니다.
이렇게 하면 $LIS$는 항상 LIS의 형태를 띄므로 답을 구할 수 있습니다.
하지만 $LIS$는 실제로 LIS가 아닙니다. 그럼에도 불구하고 $LIS$의 길이가 LIS의 길이가 되는 이유를 살펴보겠습니다.
수열 $\{a_n\} = \{1, 3, 7, 4, 5, 2, 8\}$를 예로 들어 설명해 보겠습니다.
$a_0 = 1$, $LIS = [1]$
$a_1 = 3$, $LIS = [1, 3]$
$a_2 = 7$, $LIS = [1, 3, 7]$
$a_3 = 4$, $LIS = [1, 3, 4]$
$LIS_2$를 $a_3$으로 대체했습니다. $\{1, 3, 4\}$로 시작하는 증가 부분 수열이 $\{1, 3, 7\}$로 시작하는 것보다 길거나 같기 때문입니다.
7보다 큰 수가 온다면 4 뒤에 올 수 있을 테고, 7보다 작은 수가 오더라고 4 뒤에 올 수 있기 때문입니다.
$a_4 = 5$, $LIS = [1, 3, 4, 5]$
$a_5 = 2$, $LIS = [1, 2, 4, 5]$
여기서는 $\{1, 3, 4, 5\}$와 $\{1, 2\}$를 동시에 보고 있는 것으로 이해할 수 있습니다.
아까와 같은 이유로 $\{1, 3, \dots\}$ 대신 $\{1, 2, \dots\}$를 구하는 것입니다.
앞의 3은 이때까지 이러한 수가 등장했다는 것을 의미하지, 그 이상의 의미를 가지지 않습니다.
5보다 큰 수가 등장해서 $LIS$가 길어질 때 3은 아무런 영향도 미치지 않습니다. 대신 $\{1, 3\}$으로 시작하는 수열을 빠르게 찾을 수 있도록 해 주는 것이죠.
그러니까 위에서 봤던 $dp[i]$를 빠르게 구하도록 도와주는 역할을 하는 것입니다.
부분 수열 찾기
각 반복문에서 찾은 index가 $dp[i]$와 같다는 것을 알 수 있습니다. 그래서 이를 따로 저장하는 배열을 만들어서 DP와 같은 방법으로 구하면 됩니다.