LIS에 대해 알아보자
LIS
LIS(Longest Increasing Subsequence) : 최장 증가 부분 수열
수열 하나가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구할 때 3가지 방법이 있다.
- 완전 탐색을 이용한 방법(O(2ⁿ))
- DP를 이용한 방법 (O(n²))
- 이진 탐색을 이용한 방법 (O(n logn))
완전탐색을 이용한 방법
완전 탐색의 경우에는 하나하나 다 비교해보면 되므로 모든 증가 부분수열을 고려한다. 배열을 받아서 원 배열에서 증가 부분 수열의 첫번째 수를 선택한 뒤, 다음 수의 조건(첫 수보다 원 배열에서 뒤에 있고 큰 후보)에 해당하는 값들의 배열을 꾸려 재귀를 해나가면 될 것 같다.
어떤 수가 나올지는 상관없이 자신보다 작은 숫자인지 확인하고 이전 값에서 1을 계속 더해가면 해당 LCS의 길이를 구할 수 있다.
DP를 이용한 방법
DP를 이용한 방법은 아래와 같이 알고리즘을 짜볼 수 있다.
수열의 길이와 같은 dp배열을 하나 선언한다.
수열을 처음부터 끝까지 순서대로 1개씩 탐색한다. ( 현재 위치 = i )
dp[i]에 넣을 값을 초기화해준다. (val)
현재 위치(i)보다 이전에 있는 원소(j) 중에서 현재 원소보다 작은지 체크한다. (크거나 같으면 LIS 불가능)
현재 원소보다 작다면, dp[j]가 val 보다 큰지 체크한다. 이 때 val보다 크다면 j번째 원소를 포함했을 때가, 지금까지 확인한 최장 증가 부분 수열보다 더 길다는 의미이므로 val에 dp[j]를 할당해준다.
현재 원소도 포함해주어야 하므로 최종적으로 dp[i]에 val + 1을 할당해준다.
dp배열의 원소 중에서 가장 큰 값을 출력한다.
1for(int i=1;i<N;i++) {
2 for(int j=0;j<i;j++) {
3 if (array[i] > array[j] && dp[j] + 1 > dp[i])
4 dp[i] = dp[j] + 1; // 증가 수열
5 }
6 if (max < dp[i])
7 max = dp[i];
8
9}
처음에 dp 배열의 값들을 1로 초기화 해주어야하며, if문의 조건에 dp[j] + 1 > dp[i]
가 들어간 이유는 예를 들어서 10 20 30 20 인 경우 값이 20을 만나게 되면 값이 줄어드는 것을 볼 수 있다. 이 부분을 보장해주기 위해 해당 조건을 추가한 것이다.
이진 탐색을 이용한 방법
해당 부분은 잘 정리된 블로그 포스팅을 참고해서 썼다.
- 배열 마지막 요소보다 새로운 수가 크다면, 배열에 넣는다.
- 그렇지 않다면, 그 수가 들어갈 자리에 넣는다. (이분 탐색을 통해 들어갈 자리를 찾는다)
LIS의 갯수만 구할 때의 가장 이해하기 쉬운 코드를 찾아보았다.
1dp[0] = array[0];
2int idx = 0;
3for (int i = 1; i < n; i++) {
4 if (dp[idx] < array[i]) {
5 dp[++idx] = array[i];
6 }
7 else {
8 int ii = lower_bound(array.begin(), array.end(), idx);
9 dp[ii] = array[i];
10 }
11}
12// ans => idx + 1
혹은 이런 방식으로 짧게 구현하는 방법도 있다
1for (int i=0; i<n; i++) {
2 vector<int>::iterator iter = lower_bound(dp.begin(), dp.end(), arr[i]);
3 if(iter == dp.end()) dp.push_back(arr[i]);
4 else *iter = arr[i];
5}
6cout << dp.size();
lower_bound
을 통해서 못 찾으면 push_back, 찾으면 해당 값을 배열의 값으로 치환해준다.
LIS 경로 추적
위 그림을 통해서 소스의 전체적인 느낌을 파악한 뒤 다른 문제에서 요하는 경로 추적 또한 코드로 보았다.
1dp[0] = array[0];
2tracking[0] = new Pair(0, array[0]);
3int idx = 0;
4for (int i = 1; i < n; i++) {
5 if (dp[idx] < array[i]) {
6 dp[++idx] = array[i];
7 tracking[i] = new Pair(idx, array[i]);
8 }
9 else {
10 int ii = lower_bound(idx, array[i]);
11 dp[ii] = array[i];
12 tracking[i] = new Pair(ii, array[i]);
13 }
14}
이후에는 스택에서 인덱스와 pair의 first을 비교하여 스택에 넣어준 다음 꺼내주게 되면 순서대로 출력할 수 있다.
1lis.push_back(v[0]);
2p.push_back({0, v[0]});
3
4for (int i=1; i<n; i++) {
5 int idx = lower_bound(lis.begin(), lis.end(), v[i]) - lis.begin();
6 if(idx >= lis.size()) {
7 lis.push_back(v[i]);
8 }
9 else {
10 lis[idx] = v[i];
11 }
12 p.push_back({idx, v[i]});
13}
14stack<int> st;
15int len = lis.size();
16cout << len << '\n';
17for (int i=n-1; i>=0; i--) {
18 if (p[i].first == len-1) {
19 st.push(p[i].second);
20 len--;
21 }
22}
23while(!st.empty()) {
24 cout << st.top() << ' ';
25 st.pop();
26}
나는 이런 식으로 만들어서 pair로 추적하는 아이디어를 집어 넣어서 구현하였다.