LIS (Longest Increasing Subsequence)
수열 안에 있는 증가 하는 부분 수열 중 가장 긴 것을 찾는 문제
Idea1
완전탐색입니다.
1) 수열을 처음부터 하나씩 증가하면서 (a[]의 모든 원소마다), 각 경우마다는
첫번째 수 a[i]를 정하고, 인덱스를 1씩 늘려가면서(a[i+1] ... ) 첫번째 수보다 큰 숫자들을 컨테이너(sub_a[])에 담습니다.
2) sub_a를 1)번 과정을 되풀이하는 재귀함수를 구현합니다.
a[] = {3, 2, 1, 7, 5, 4, 2, 6}
첫번째 원소 3을 기준으로, 그 다음 가능한 7 5 4 6 중 다시 재귀적으로 LIS를 뽑습니다.
#include <vector>
#include <algorithm>
using namespace std;
/*
LIS의 길이를 반환
*/
int lis(const vector<int>& A) {
if (A.empty()) return 0;
int ret = 0;
for (int i = 0; i < A.size(); ++i) {
vector<int> B;
for (int j = i + 1; j < A.size(); ++j) {
if (A[i] < A[j]) B.emplace_back(A[j]);
}
ret = max(ret, 1 + lis(B));
}
return ret;
}
Idea2 (입력 바꾸기) O(N2)
메모이제이션을 적용한 동적계획법으로 중복되는 문제를 해결해봅시다.
하지만, lis()는 입력을 벡터로 받기 때문에, 이미 한번 계산한 부분 수열을 메모이제이션 하기 어렵습니다.
map 연관 컨테이너를 이용해서 할 수 있겠지만, 시간이 많이 걸리겠습니다.
이 상황을 피하기 위해 다른 아이디어를 내봅시다. (이것이 DP의 핵심인 것 같습니다. 최적 부분 구조를 찾고, 이전과 다음 구조의 연관성을 파악하고 정의하는 것입니다.)
lis()의 입력을 보면 다음 둘 중하나가 됩니다.
1. 원래 주어진 수열 S (1개)
2. 원래 주어진 수열의 원소 S[i]에 대해, S[i+1 ... ]에서 S[i]보다 큰 수만 포함하는 수열 (N개)
여기서 대응관계를 보면,
lis()의 입력 A와 S의 인덱스는 1:1 대응한다는 것입니다.
즉 A(vector)를 S의 인덱스(int)로 바꾸어, 좀 더 효율적인 메모이제이션이 가능합니다.
lis(start) : a[start]에서 시작하는 LIS(부분 증가수열) 중 최대 길이
라고 문제를 재정의하고 풀어봅시다.
int N;
int dp[100], a[100];
/*
총 N개의 부분 문제를 갖고, 한 문제 마다 반복문 O(N)
따라서 O(N^2)
*/
int lis2(int start) {
int& ret = dp[start];
if (ret) return ret;
// a[start]는 포함하므로 길이는 적어도 1이상이다.
ret = 1;
for (int next = start + 1; next < N; ++next) {
if (a[start] < a[next])
ret = max(ret, lis2(next) + 1);
}
return ret;
}
lis2(i)는 a[start]의 LIS의 길이를 반환해주므로,
최종해를 구하기 위해서는 start를 0부터 N-1까지 lis2()에 넣어 계산하고, 최댓값을 구해주어야 하겠습니다.
Idea3 O(NlogN)
생각해내기 쉽지 않은 아이디어 입니다만, 이렇게 생각하면 구현은 쉽습니다.
dp[i] : 길이가 i인 LIS을 만들 수 있는 마지막 원소 중 가장 작은 값
따라서, dp의 크기가 곧 LIS의 길이 이고, 처음에는 빈 배열로 시작합니다.
a[] = {3, 2, 5, 2, 3, 1, 4}로 확인해 봅시다.
-
(1) (dp[]의 마지막 원소)와 (2)(a[]의 원소)를 비교하여
(1) < (2) 이면 dp[]에 (2)를 추가합니다.
그렇지 않으면 dp[]에서 (2)가 들어갈 장소(lower_bound)를 찾아서 (2)를 대입합니다.
순서대로 dp[]를 확인해보면,
3 << 처음에는 그냥 넣어줍니다.
2 << "1자리"에 만들 수 있는 가장 마지막 원소는 3보다는 2가더 유리합니다.
2 5 << 2<5이므로 뒤에 넣어줍니다.
2 5 << lower_bound(2)가 인덱스 0을 찾았고, 2가 2를 대체했습니다.
2 3 << lower_bound(3)가 인덱스 1을 찾았고, 3이 5를 대체했습니다. "2자리"에서는 (2, 5)보다 (2, 3)이 LIS를 만들기 유리합니다.
1 3 << lower_bound(1)가 인덱스 0을 찾았고, 1이 2를 대체했습니다.
1 3 4 << 3<4 이므로 뒤에 넣어줍니다.
참고해야할 사항은 이렇게 만들어진 dp[]는 LIS 수열이 아니고,
단지 그 길이가 LIS와 같다는 점을 알 수 있습니다.
dp[] 역할을 vector v가 해주고 있습니다.
#11053 백준
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
int a[1000000];
vector<int> v {-2076543210};
int main() {
int n;
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
/*
O(NlogN)
*/
for (int i = 0; i < n; ++i) {
int num = a[i];
if (v.back() < num) {
v.emplace_back(num);
}
else {
auto it = lower_bound(v.begin(), v.end(), num);
*it = num;
}
}
printf("%d\n", v.size() - 1);
return 0;
}
+ LIS 수열 출력 (Backtrace)
새로운 배열을 만듭니다.
P[i] : 수열의 i번째 원소가 dp[]에서 위치하는 인덱스
위의 예제에서 p[] 는 다음과 같이 나옵니다.
1 1 2 1 2 1 3
각각 배열 뒤에서 부터 봤을 때 3이 처음으로 나온 인덱스는 6(0부터시작)이기 때문에, a[6]을
2가 처음으로 나온 인덱스는 4이기 때문에, a[4]를
1이 처음으로 나온 인덱스는 3이기 때문에, a[3]을
역순으로 출력해 주시면 됩니다.
이 과정에서 메모리와 시간을 아끼기 위해 backtrace 알고리즘이 추가됩니다.
#백준 14003번
#include <cstdio>
#include <algorithm>
#include <vector>
using namespace std;
int a[1000000];
int p[1000000];
int n;
vector<int> v {-2076543210};
/*
배열의 크기가 커서 메모리가 늘어나고,
다시 출력하는데 O(N)의 시간이 걸리므로,
재귀함수를 이용한 백트래킹을 합니다.
*/
void backtrace(int idx, int last) {
if (idx < 0) return;
if (p[idx] == last) {
backtrace(idx - 1, last - 1);
printf("%d ", a[idx]);
}
else {
backtrace(idx - 1, last);
}
}
int main() {
scanf("%d", &n);
for (int i = 0; i < n; ++i) {
scanf("%d", &a[i]);
}
/*
O(NlogN)
*/
for (int i = 0; i < n; ++i) {
int num = a[i];
if (v.back() < num) {
p[i] = v.size();
v.emplace_back(num);
}
else {
auto it = lower_bound(v.begin(), v.end(), num);
*it = num;
p[i] = distance(v.begin(), it);
}
}
printf("%d\n", v.size() - 1);
backtrace(n - 1, v.size() - 1);
return 0;
}
'알고리즘, 자료구조 > 알고리즘 개념' 카테고리의 다른 글
[BFS] 너비 우선 탐색 (0) | 2019.09.09 |
---|---|
[DFS] 깊이 우선 탐색 (0) | 2019.09.08 |
[Sort] 대표적인 정렬의 모든 것 (0) | 2019.08.31 |
[KMP] 문자열 알고리즘 (0) | 2019.08.30 |
16. 비트마스크 (bitmask) (0) | 2019.08.12 |
댓글