티스토리 뷰
LIS(Longest Increasing Subsequence) 알고리즘은 '가장 긴 증가하는 부분 수열' 알고리즘으로도 불린다.
주로 그리디 알고리즘이나 다이나믹 프로그래밍의 예시로 등장한다.
LIS는 수열 내 증가하는 부분 수열 중 가장 긴 수열을 말한다.
예를들어 아래와 같은 수열이 존재할때
3 | 1 | 5 | 2 | 4 | 2 |
이때 가장 긴 증가하는 부분 수열은 {1,2,4}이며 길이는 3이다.
3 | 1 | 5 | 2 | 4 | 2 |
아래의 예시에서는 가장 긴 증가하는 부분 수열은 {10,20,30,50} 이며 길이는 4가 된다.
10 | 20 | 10 | 30 | 20 | 50 |
이제 이 알고리즘을 이용하는 문제를 해결해 보자.
이 문제를 어떻게 풀어야할까? 이 문제는 동적계획법을 이용하여 O(N²)에 풀 수 있다.
{10,20,10,30,20,50}의 수열에서 각각의 자리마다 현재까지의 가장 긴 증가하는 부분수열을 기록하여 가장 큰 값을 출력하면 그것이 가장 긴 증가하는 부분수열의 길이가 된다. 원리를 살펴보자.
총 6개의 값이 있으니 i=1부터 i=6까지 살펴보자.
먼저 기본적으로 길이를 계산할때 자기 자신을 포함하므로 각각의 DP값을 1로 설정하고 시작한다.
i=1, 10일때 10보다 먼저 나온 수가 없으므로 DP[1]=1을 기록하고 넘어간다.
i=2, 20일때 앞서 나온 수 중에 20보다 작은 10이라는 숫자가 있다. 따라서 DP[2]=DP[1]+1이 된다.
i=3, 10일때 10보다 작은 수가 없으므로 DP[3]=1을 기록한다.
i=4, 30일때 30보다 작은 수가 여러개 존재한다. 이 중에 가장 큰 값을 기준으로 +1 해주면 된다.
현재까지의 DP값중 가장 큰값은 DP[2]인 2이다. DP[2]역시 그 전까지 증가하는 수열의 길이만을 기록했으므로 20보다 큰 30은 그동안의 값중 가장 큰값인 DP[4]=DP[2]+1을 해주면 된다.
i=5, 20일땐 20보다 작은 숫자인 DP[1]또는 DP[3]의 값에 +1을 해주면 된다.
i=6, 50일땐 앞에 i=4의 경우와 같이 자신보다 작은 DP값중 가장 큰값에 +1을 해주면 된다. DP[6]=DP[4]+1이다.
이 후 DP에 저장된 값중 가장 큰 값이 주어진 수열내에 가장 긴 증가하는 부분수열의 길이가 될것이다.
C++로 구현한 코드는 다음과 같다.
#include <bits/stdc++.h>
using namespace std;
int dp[1001];
int arr[1001];
int main(void)
{
int N;
cin >> N;
for (int i = 0; i < N; i++)
{
cin >> arr[i];
dp[i] = 1;
}
int ans = 1;
for (int i = 0; i < N; i++)
{
for (int j = 0; j < i; j++)
{
if (arr[i] > arr[j])
{
dp[i] = max(dp[i], dp[j] + 1);
}
}
ans = max(ans, dp[i]);
}
cout << ans;
}
그렇다면 위 문제도 살펴보자. 이 문제는 입력값으로 최대 1,000,000의 데이터가 주어진다. 따라서 위 알고리즘 처럼 동적계획법을 통해 O(N²)으로 푼다면 당연히 시간초과가 날것이다. 이 문제는 최소 O(NlogN)으로 풀어야만 시간초과를 피할 수 있다. O(NlogN)으로 풀기 위해서 우리는 lower bound에 대해 알아야 한다.
lower bound는 정렬된 배열중 임의의 값에 대해 값이 삽입될 수 있는 위치 중 가장 작은 인덱스를 가르킨다.
예를들어 [1,3,4,6]이라는 배열이 존재하고 이 배열에 3이라는 값을 삽입하고자 할때 lower bound는 3이 처음 등장하는 두번째 위치를 가르킨다. 이 작업은 O(logN)의 시간동안 수행되며 C++ STL에 내장되어 있다.
그렇다면 lower bound를 이용하여 위 문제를 해결해보자.
우리는 임의의 리스트를 하나 만들고 이 리스트에 lower bound를 이용하여 데이터를 추가할것이다.
과정은 다음과 같다. 리스트가 비어있다면 데이터를 하나 추가하고, 그 후부터 등장하는 수에 대해 리스트의 마지막 값보다 크면 리스트의 맨 뒤에 추가하고 리스트의 마지막 값보다 작다면 해당 리스트의 lower bound를 구하여 값을 덮어쓴다. 아래 과정을 통해 이해 해보자.
{10 20 10 30 20 50} 라는 수열이 있고 현재 리스트는 아무것도 들어있지 않다.
i=1, 10일때 리스트가 비어있으므로 값을 추가한다. List={10}이 된다.
i=2, 20일때 List의 가장 큰 값인 10보다 더 크므로 리스트의 마지막에 추가한다. List={10,20}이 된다.
i=3, 10일때 List의 가장 큰 값인 20과 비교한다. 더 작으므로 해당 값은 증가하는 부분 수열이 될 수 없다. 따라서 10의 lower bound를 구한다. lower bound는 1이므로(index는 1부터 시작한다 가정) 1의 자리에 10을 덮어쓴다.
따라서 List={10,20}이 된다. (덮어 쓰는 이유는 아래에 설명)
i=4, 30일때 List의 가장 큰 값인 20보다 크므로 뒤에 추가한다. List={10,20,30}이 된다.
i=5, 20일때 List의 가장 큰 값인 30보다 작으므로 lower bound를 구하고 덮어쓴다.
List={10,20,30}이 된다.
i=6, 50일때 List의 가장 큰 값인 30보다 크므로 마지막에 추가한다.
최종적으로 List에 남은 값은 {10,20,30,50}이 된다. 이 때 이 List의 크기인 4가 가장 긴 증가하는 부분수열의 길이가 된다.
(※주의. 해당 배열이 가장 긴 증가하는 부분수열은 아니다. 우리는 size를 통해 길이만을 알 수 있다.)
위 과정만을 본다면 우리가 lower bound를 통해 값을 덮어쓰는 과정이 의아할 수 있다. 아래 예시를 보자
{30,10,20,10,50}
우리는 먼저 30을 리스트에 추가할 것이다. List={30}
그리고 다음 숫자인 10을 볼때 10은 리스트에서 가장 큰 값인 30보다 작으므로 뒤에 추가할 수 없다.
그러나 lower bound를 통해 1번 index에 덮어써야한다. 만약 덮어쓰지 않는다면 우리는 10부터 시작하는 가장 긴 증가하는 부분수열을 확인할 수가 없다. 30뒤에 10이 나온순간 부터 30부터 시작하는 가장 긴 부분수열의 길이는 증가 할 수 없다. 따라서 값을 덮어쓰고 다음과정을 진행해야 한다. 또 위와 같이 덮어쓰는 과정을 거치기에 List에 남은 값이 가장 긴 증가하는 부분수열이 아닐 가능성이 존재하게 된다.
C++로 구현한 코드는 다음과 같다.
#include <bits/stdc++.h>
using namespace std;
int main(void)
{
int N;
cin >> N;
vector <int> list;
for (int i = 0; i < N; i++)
{
int val;
cin >> val;
if (list.empty() || list.back() < val) //리스트가 비어있고, 리스트의 가장 큰값보다 현재값이 크다면
{
list.push_back(val); //리스트의 뒤에 현재 값을 추가
}
else
{
auto pos = lower_bound(list.begin(), list.end(), val); //해당 값의 lower bound를 구함
*pos = val; //해당 위치에 값을 덮어쓰기
}
}
cout << list.size();
}
O(N²)와 O(NlogN) 의 LIS알고리즘을 살펴보았다.
위 알고리즘을 이용하여 BOJ : 14002 가장 긴 증가하는 부분 수열4 , BOJ : 14003 가장 긴 증가하는 부분 수열5 문제를 풀어보기 바란다. 힌트를 주자면 별도의 배열을 만들어 각 데이터의 lower bound를 기록하자.
이후 뒤에서부터 길이를 만족하는 인덱스의 값을 역순으로 출력하면 된다.
추가로 BOJ : 14002 가장 큰 증가 부분 수열 문제와 BOJ : 11054 가장 긴 바이토닉 부분 수열 문제도 풀어보자.
LIS알고리즘을 응용하여 해결할 수 있다.
'알고리즘' 카테고리의 다른 글
[알고리즘] Inversion Count (Merge Sort) (0) | 2021.01.13 |
---|
- Total
- Today
- Yesterday
- 컴퓨터 통신
- ReactNative
- 중앙대학교
- 스레드
- 투포인터
- 자바
- java
- 시뮬레이션
- 자바스크립트
- 백트래킹
- 벨만포드
- BFS
- node.js
- 예외처리
- 동적계획법
- 백준
- nest.js
- dfs
- boj
- 알고리즘
- 세그먼트 트리
- 컴퓨터 구조
- 재귀
- Computer Architecture
- typeORM
- nodeJS
- nestjs
- 그래프
- 구현
- 그리디
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |