최장 증가 부분 수열 Longest Increasing Subsequence (LIS)
이 글은 제 개인적인 공부를 위해 작성한 글입니다.
틀린 내용이 있을 수 있고, 피드백은 환영합니다.
개요
어떠한 수열이 주어질 때, 그 수열에서 일부 원소를 뽑아내어 새로 만든 수열을 부분 수열이라고 한다.
그리고 그 부분 수열 중에서 요소들이 오름차순으로 정렬된 가장 긴 수열을 최장 증가 부분 수열이라고 한다.
크게 두 가지 $O(N^2)$ 방식과 $O(NlogN)$ 방식이 존재한다.
방법 1 : 다이나믹 프로그래밍 (DP)
가장 직관적인 방법이다. 반복문을 사용하여 각 위치에서의 LIS 길이를 계산한다.
구현이 간단하고 LIS 수열을 역추적하여 구해내기 쉽다.
- dp[i] : i번째 인덱스에서 끝나는 최장 증가 부분 수열의 길이
- i보다 앞에 있는 모든 j (0 ≤ j < i)를 검사한다.
- 만약 arr[j] < arr[i]라면, arr[i]를 arr[j] 뒤에 붙일 수 있다.
- 점화식 : dp[i] = max(dp[i], dp[j] + 1)
백준 2631번 : 줄세우기
https://www.acmicpc.net/problem/2631
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int N;
std::cin >> N;
std::vector<int> arr(N);
std::vector<int> dp(N, 1);
for (int i = 0; i < N; i++)
{
std::cin >> arr[i];
}
for (int i = 0; i < N; i++)
{
for (int j = 0; j < i; j++)
{
if (arr[j] < arr[i])
{
dp[i] = std::max(dp[i], dp[j] + 1);
}
}
}
std::cout << N - *std::max_element(dp.begin(), dp.end());
return 0;
}
그렇다면 정답이 되는 LIS 배열까지 구해보자.
역추적을 위해 부모(이전) 인덱스를 저장할 별도의 배열을 하나 더 사용한다.
- dp[i]가 갱신될 때(즉, 더 긴 수열을 발견했을 때), 그 수열을 이어준 직전 인덱스 j를 parent[i]에 저장한다.
- DP 계산 완료 후, LIS의 끝점인 최대 값을 가진 인덱스를 찾는다.
- parent 배열을 타고 시작점까지 거슬러 올라가며 LIS 배열을 복원한다.
- 복원된 LIS 배열은 역순이므로, 최종적으로 뒤집어서 출력한다.
백준 14002번 : 가장 긴 증가하는 부분 수열 4
https://www.acmicpc.net/problem/14002
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int n;
std::cin >> n;
std::vector arr(n, 0);
std::vector dp(n, 1);
std::vector parent(n, -1);
for (int i = 0; i < n; ++i)
{
std::cin >> arr[i];
}
for (int i = 1; i < n; i++)
{
for (int j = 0; j < i; j++)
{
if (arr[j] < arr[i] && dp[j] + 1 > dp[i])
{
dp[i] = std::max(dp[i], dp[j] + 1);
parent[i] = j;
}
}
}
auto iter = std::max_element(dp.begin(), dp.end());
int last_idx = std::distance(dp.begin(), iter);
std::vector<int> path;
for (int i = last_idx; i != -1; i = parent[i])
{
path.push_back(arr[i]);
}
std::cout << *iter << "\n";
for (int i = path.size() - 1; i >= 0; i--)
{
std::cout << path[i] << " ";
}
return 0;
}
방법 2 : 이분 탐색 (Binary Search)
시간 복잡도가 $O(NlogN)$으로 DP 방법보다 더 효율적인 방법이다.
LIS의 길이만 구할 때 사용하기 쉽다. 실제 LIS 배열을 구하는 것은 다소 복잡하다.
핵심 아이디어는 LIS의 마지막 원소가 가능한 작을수록 더 긴 LIS를 생성할 수 있다이다.
- 빈 배열 L을 만든다
- 주어진 수열을 하나씩 순회하며 만난 원소를 E라고 해보자.
- E가 L의 마지막 원소보다 크다면, L의 끝에 E를 추가한다.
- E가 L의 마지막 원소보다 작거나 같다면, L에서 E보다 크거나 같은 원소 중 가장 왼쪽에 있는 원소를 E로 교체한다.
- 최종적으로 L의 길이가 LIS의 길이가 된다.
백준 12015번 : 가장 긴 증가하는 부분 수열 2
https://www.acmicpc.net/problem/12015
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> arr(n);
std::vector<int> dp;
for (auto& elem : arr)
{
std::cin >> elem;
}
for (int i = 0; i < n; i++)
{
auto iter = std::lower_bound(dp.begin(), dp.end(), arr[i]);
if (iter == dp.end())
{
dp.push_back(arr[i]);
}
else
{
*iter = arr[i];
}
}
std::cout << dp.size() << "\n";
return 0;
}
하지만 위 방법은 길이만 구할 수 있고, DP 배열의 각 원소는 실제 LIS와 다를 수 있다.
3, 5, 2, 6, 1 배열로 해보면 DP 배열은 아래와 같이 변한다.
- [3]
- [3, 5]
- [2, 5]
- [2, 5, 6]
- [1, 5, 6]
실제 LIS는 [3, 5, 6]이지만 DP 배열은 [1, 5, 6]이 된다.
인덱스를 저장하는 추가적인 배열을 사용해서 해결할 수 있다.
dp 배열의 맨 뒤에 추가되거나 교체될 때마다 해당 인덱스를 기록하고, 역순으로 따가라며 답을 구할 수 있다.
백준 14003번 : 가장 긴 증가하는 부분 수열 5
https://www.acmicpc.net/problem/14003
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
#include <algorithm>
#include <iostream>
#include <vector>
int main()
{
std::ios_base::sync_with_stdio(false);
std::cin.tie(nullptr);
std::cout.tie(nullptr);
int n;
std::cin >> n;
std::vector<int> arr(n);
std::vector<int> dp;
std::vector idx(n, 0);
for (auto& elem : arr)
{
std::cin >> elem;
}
for (int i = 0; i < n; i++)
{
auto iter = std::lower_bound(dp.begin(), dp.end(), arr[i]);
if (iter == dp.end())
{
idx[i] = dp.size();
dp.push_back(arr[i]);
}
else
{
idx[i] = iter - dp.begin();
*iter = arr[i];
}
}
std::vector<int> ans;
int target = dp.size();
for (int i = n - 1; i >= 0; i--)
{
if (idx[i] == target - 1)
{
ans.push_back(arr[i]);
target--;
}
}
std::cout << dp.size() << "\n";
for (int i = ans.size() - 1; i >= 0; i--)
{
std::cout << ans[i] << " ";
}
return 0;
}