https://www.acmicpc.net/problem/12015
12015번: 가장 긴 증가하는 부분 수열 2
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000,000)
www.acmicpc.net
1. 문제의 유형 및 이해
이분탐색
유명한 LIS(Longest Increasing Subsequence) 알고리즘 입니다.
주어진 수열에서 가장 긴 증가하는 부분 수열을 찾으면 됩니다.
알려진 방법으로 LIS알고리즘을 푸는 방법은 크게 3가지
dp, 이분탐색, segment tree가 있습니다.
dp의 경우 O($N^2$) 으로 N의 범위가 커진다면 해결하는데 어려움이 있습니다.
https://www.acmicpc.net/problem/11053
11053번: 가장 긴 증가하는 부분 수열
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오. 예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이
www.acmicpc.net
의 경우 N의 범위가 1~1000으로 O($N^2$)으로 가능하지만 이번 문제의 경우 불가능합니다.
우리는 이분탐색을 활용하여 NlogN 시간복잡도로 풀어낼 수 있습니다.
2. 문제 접근 방법
문제에서 원하는 정답은 '가장 긴 증가하는 부분 수열의 길이' 입니다.
결국 1 2 5 10 9 와 같은 수열이 있을 때 LIS는 1 2 5 10 또는 1 2 5 9 이지만, 우리가 원하는 정답은 4로 동일합니다.
실제로 증가하는 부분 수열을 출력하는 문제는 ' 가장 긴 증가하는 부분수열 시리즈'를 통해 경험할 수 있습니다.
돌아와서, 우리는 부분 수열이 더 늘어날 수 있는지에 집중하면 됩니다.
어떤 배열 하나를 두고 여기에다가 부분 수열을 기록하는 겁니다.
예시에서의 11 20 10 30 21 50를 보도록 하면
1 : [11] length 1
2 : [11, 20] length 2 // 가장 증가했을 때 수가 1이였기 때문에 그 값보다 크다면 부분 수열을 증가 시켜 줍니다.
3 : [10,20] length 2 // 뒤에서 설명
4 : [10, 20, 30] length 3
5 : [10, 20, 21] length 3
6 : [10, 20, 21, 50] legnth 4
3번째와 5번째를 통해 원리를 이해할 수 있습니다.
3번째 경우에서 11 20 10 이 들어왔을 때 가장 긴 증가하는 부분 수열은 11 20이 됩니다. 이미 길이는 측정이 된 상태이죠. 이후에 변하는 [10, 20]은 실제 가장 긴 증가하는 부분 수열이 아닌, 10이 만약에 부분수열로써 참여하게 된다면, 어디서 시작할 수 있는가를 의미하는 것이 조금 더 가깝습니다.
'만약에 부분 수열로써 참여하게 된다면'이라는 말이 조금 헷갈릴 수 있는데, 다음과 같이 생각해봅시다.
이후에 만약 11 20 10 11 12 13이 들어온다면 이를 처리하기 위해 10의 위치는 어디로 가야할지를 정하는 것과 같습니다. 가장 긴 증가하는 부분 수열에 영향을 끼치지도 않았고, 이후에 들어올 값들에 대해서 고려할 수 있습니다.
즉, 이분탐색을 이용하는 방법은 어떠한 x가 실제 부분 수열에 참여하게 될 경우 어디에 위치할 수 있는가를 찾기위해서 이분탐색으로 빠르게 자신의 자리를 찾는데 의의가 있습니다.
이어서 5번 경우를 보게되면 21이 들어왔을 때 20보다는 크고 30보다는 작은 위치에 있습니다.
이전에 들어온 값들이 항상 자신보다 이전에 위치하고 있기 때문에 자신보다 큰 친구를 갈아끼워주면 자신의 위치로써 활동할 수 있는 것입니다.
정리하자면, '새로운 부분수열로써 참여할 가능성이 있는 위치에 기록한다' 이라는 말에서 조금 바꾸어 보면
매 탐색마다 마지막 값중 최솟값 을 통해 새로운 부분수열을 확인할 수 있다. 라는 것입니다.
새로운 부분 수열의 [min ~~~ max]값을 갱신해주는 것이죠.
9 5 10 이라면 9 10이나 5 10 이나 동일하기 때문에 5와 9의 자리가 동일하고, 이후에 7 , 8 이 들어왔을 때
그 전 최소 값인 5로 시작하는 것이 더 길고 새로운 LIS를 보장합니다.
또한 9 5 10 9 라고할 때
[min 5 ~ max 10]에서 [min 5 ~ max 9]로 갱신해주면서 길이는 변하지 않고 새로운 부분수열에서 더 많은 값들을 확인할 수 있습니다. 실제로 그 값이 수열이 아닐지라도 그 전 수열 값들은 내포하고 있기 때문에 길이를 구한다는 면에서는 정답이 변하지 않습니다.
예시)
15 9 11 8 10 12 7 6
15
9
9 11
8 11 (8은 9를 내포하고 있다고 생각해도 무방)
8 10 (10은 11을 내포하고 있다고 생각해도 무방)
8 10 12
7 10 12
6 10 12 (실제 증가하는 부분 수열은 아니지만 마지막 min값이 6인 부분수열과 지금까지 갱신한 모든 길이가 3인 부분 수열도 내포하고 있습니다)
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.StringTokenizer;
/**
* @문제번호 : Q12015
* @문제이름 : 가장 긴 증가하는 부분 수열 2
* @난이도 : Gold II
* @date : 2022-02-15 오후 1:36
* @author : pcrmcw0486
* @문제이해
* 수열 A가 주어질 때 가장 긴 증가하는 부분 수열을 구하자.
* @알고리즘
* 이분탐색
* @접근방법
* 우리가 집중해야되는건 '가장 긴 증가하는 수열'이 된다.
* 즉, 증가하는 수열의 길이가 중요하다.
* 1 2 5 10 9 라고 했을 때
* 1 2 5 10 이나 1 2 5 9 나 길이면에선 같다는 의미이다.
* 길이에 집중해서 문제를 풀어보도록 하자.
*/
public class Main {
static int length =0;
static int[] possible;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer st;
int N = Integer.parseInt(br.readLine());
possible = new int[N];
length =0;
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++) {
int num = Integer.parseInt(st.nextToken());
if (length==0 || possible[length-1] < num) {
possible[length++] = num;
}else
findPosition(0, length, num);
}
System.out.println(length);
}
public static void findPosition(int left, int right, int value) {
int l = left;
int r = right-1;
int mid =0;
while (l <= r) {
mid = (l+r)>>1;
if (possible[mid] <value) {
l = mid +1;
}else{
r = mid-1;
}
}
possible[l] = value;
}
}
'알고리즘 > 문제풀이' 카테고리의 다른 글
[백준] Q16724 피리 부는 사나이 JAVA (0) | 2022.03.01 |
---|---|
[백준] Q12100 2048(Easy) JAVA (0) | 2022.03.01 |
[백준] Q10775 공항 JAVA (0) | 2022.03.01 |
[백준] Q9527 1의 개수 세기 JAVA (0) | 2022.03.01 |
[백준] Q1647 도시 분할 계획 JAVA (0) | 2022.03.01 |