본문 바로가기

Dot Algo∙ DS/PS

[BOJ] 백준 14003번 가장 긴 증가하는 부분 수열 5 LIS - 이진탐색 (Java)

#14003 가장 긴 증가하는 부분 수열5

난이도 : 플레 5 

유형 : 이진 탐색 / LIS / DP

 

14003번: 가장 긴 증가하는 부분 수열 5

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

www.acmicpc.net

▸ 문제

수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.

예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {1020, 10, 30, 20, 50} 이고, 길이는 4이다.

 입력

첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.

둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)

 출력

첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.

둘째 줄에는 정답이 될 수 있는 가장 긴 증가하는 부분 수열을 출력한다.

 

문제 풀이  

백준 12738번 가장 긴 증가하는 부분 수열3의 확장 문제이다. 주어진 수열의 크기가 작을 경우(1000이하) DP만 사용하여 이중포문으로 해결해도 되지만 해당 문제와 같이 수열의 크기가 100만 정도로 크게 주어진 경우는 더 효율적인 방식을 사용해야 한다.

 

LIS는 이진탐색으로도 구할 수 있다. (참고) 이진 탐색을 활용하면 O(n^2)에서 O(nlogn)으로 줄일 수 있게된다.

 

구상

이진탐색을 통해 구할 수 있는 데이터

lis[]는 1차원배열로 길이(len)에 따른 LIS 수열 원소 값을 저장한다.

  • lis[len] =0~i의 arr의 원소배열 중 LIS의 길이가 len인 부분수열들의 마지막값 중 최솟값
arr[i] 1 5 6 2 3 4
lis[i] 1 2 3 4 - -

 

해당 방식은 주어진 수열 데이터를 순차적으로 비교해가며 이진탐색을 통해 적절한 위치에 삽입을 해주는 방식이다. 그래서 바로 lis배열에 저장된 데이터를 출력하면 될 것 같지만 아니다. 반례는 다음과 같다.

  • 마지막 원소 5는 10보다 작으므로 맨 앞자리에 갱신되어 들어가게 된다. 
arr[i] 10 20 30 5
lis[i] 5 20 30 0

 

증가 부분 수열 크기를 담는 데이터 추가

그러므로 dp[] 1차원 배열을 하나 더 추가하여 데이터를 하나 더 수집할 것이다. 해당 배열에는 인덱스에 따른 증가 부분 수열의 크기를 저장해준다.

  • dp[i] = arr[i]의 증가 부분 수열 크기 
arr[i] 10 20 30 5
lis[i] 5 20 30 -
dp[i] 1 2 3 1

 

설계

    1. LIS를 구하는 과정은 LIS 이진탐색 풀이와 동일하다.
    2. 해당 과정에 부분 수열 크기(len)을 dp에 저장하는 부분만 추가해준다.
      1. if(arr[i] > lis[len]),  dp[i] = ++len;
      2. else, dp[i] = idx
    3. Stack 자료구조와 LIS 크기, dp의 데이터를 가지고 역추적으로 LIS 수열을 탐색한다.  if(dp[i] == len) s.push(arr[i])
    4. Stack에 저장된 데이터를 출력해준다.

 

시뮬레이션

arr[i] 10 20 30 5
lis[i] 5 20 30 -
dp[i] 1 2 3 1

LIS 길이 (len) : 3

  • dp를 i: n-1 ~ 0으로 역순으로 탐색한다.
    • dp[3] != 3
    • dp[2] == 3, s.push(arr[2])
    • dp[1] == 2, s.push(arr[1])
    • dp[0] == 1, s.push(arr[0])

 

풀이 코드 

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.Stack;
import java.util.StringTokenizer;

public class Main {
	static int[] lis;
	public static void main(String[] args) throws IOException{
		BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
		int n = Integer.parseInt(br.readLine());
	
		int[] arr = new int[n];
		StringTokenizer st = new StringTokenizer(br.readLine());
		for(int i=0; i<n; i++) {
			arr[i] = Integer.parseInt(st.nextToken());
		}
		
		lis = new int[n+1];  // 0~i의 arr의 원소배열 중 LIS의 길이가 len인 부분수열들의 마지막 값 중 최솟값
		lis[0] = -1_000_000_001;
		int[] dp = new int[n]; // 증가 부분 수열 크기 저장   
		int len =0;
		int idx =0;
		for(int i=0; i<n; i++) {
			if(arr[i] > lis[len]) {
				dp[i] = ++len;
				lis[len] =arr[i];
				
			}else {
				idx = binarySearch(0, len, arr[i]);
				lis[idx] = arr[i];
				dp[i] = idx;
			}
		}
		
		StringBuilder sb = new StringBuilder();
		sb.append(len+"\n");
		
		Stack<Integer> s = new Stack<>();
		for(int i=n-1; i>=0; i--) {
			if(dp[i] == len) {
				s.push(arr[i]);
				len--;
			}
		}
		
		while(!s.isEmpty()) {
			sb.append(s.pop()+" ");
		}
		System.out.println(sb.toString());
        
	}
    
	static int binarySearch(int left, int right, int key) {
		int mid =0;
		while(left < right) {
			mid = (left+right)/2;
			
			if(lis[mid] < key) {
				left = mid+1;
			}else {
				right = mid;
			}
		}
		return right;
	}
}