메모리: 495940 KB, 시간: 3084 ms
이분 탐색, 자료 구조, 가장 긴 증가하는 부분 수열: O(n log n), 세그먼트 트리
2025년 1월 17일 03:38:31
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열과 개수를 구하는 프로그램을 작성하시오.
예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이고, 1개이다. A = {10, 20, 30, 10, 20, 30}인 경우에는 가장 긴 증가하는 부분 수열의 길이는 3이고, 4개가 있다.
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.
둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)
첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이와 개수를 출력한다. 개수는 매우 커질 수 있기 때문에 109+7로 나눈 나머지를 출력한다.
문제 풀이


일반적인 LIS는 이분 탐색을 사용하여 O(N log N)에 해결할 수 있다. 하지만 이 문제는 개수도 세야 하므로, 세그먼트트리를 사용해야한다.
세가지 전략으로 접근해보겠다.
1. 좌표 압축
2. 세그먼트 트리
3. 개수 세기를 위한 DP적 접근
TreeSet<Integer> uniqueSet = new TreeSet<>();
for(int n : arr) uniqueSet.add(n);
ArrayList<Integer> unique = new ArrayList<>(uniqueSet);
Why?
How?
static class Pair {
int length; // LIS의 길이
long count; // 해당 길이를 가진 LIS의 개수
public Pair(int length, long count) {
this.length = length;
this.count = count;
}
}
static Pair merge(Pair a, Pair b) {
if(a.length > b.length) return a;
else if(a.length < b.length) return b;
return new Pair(a.length, (a.count + b.count) % MOD);
}
for(int i=0; i<N; i++) {
Pair curr = query(1, compressed[i]-1, 1, uniqueSize, 1);
Pair newValue = new Pair(curr.length + 1, Math.max(curr.count, 1));
update(compressed[i], newValue, 1, uniqueSize, 1);
}
arr 수열의 각 숫자를 순회하면서 :
현재 숫자: 10 (압축값: 1)
1. 이전 작은 숫자들 조회 (query(1, 0))
- 결과: length = 0, count = 0 (아무것도 없음)
2. 새로운 상태
- length = 1 (0 + 1)
- count = 1 (새로운 수열 시작)
3. 세그먼트 트리 업데이트
- 위치 1에 (length=1, count=1) 저장
현재까지 찾은 LIS들:
- {10} (길이 1, 개수 1)
현재 숫자: 20 (압축값: 2)
1. 이전 작은 숫자들 조회 (query(1, 1))
- 결과: length = 1, count = 1 (10으로 끝나는 수열)
2. 새로운 상태
- length = 2 (1 + 1)
- count = 1 (이전 수열에서 이어짐)
3. 세그먼트 트리 업데이트
- 위치 2에 (length=2, count=1) 저장
현재까지 찾은 LIS들:
- {10} (길이 1, 개수 1)
- {10, 20} (길이 2, 개수 1)
현재 숫자: 10 (압축값: 1)
1. 이전 작은 숫자들 조회 (query(1, 0))
- 결과: length = 0, count = 0 (더 작은 수 없음)
2. 새로운 상태
- length = 1 (0 + 1)
- count = 1 (새로운 수열 시작)
3. 세그먼트 트리 업데이트
- 위치 1의 상태 유지 (merge 결과 변화 없음)
현재까지 찾은 LIS들:
- {10} (길이 1, 개수 1)
- {10, 20} (길이 2, 개수 1)
- {10} (새로운 시작점)
현재 숫자: 30 (압축값: 3)
1. 이전 작은 숫자들 조회 (query(1, 2))
- 결과: length = 2, count = 1 ({10, 20}으로 끝나는 수열)
2. 새로운 상태
- length = 3 (2 + 1)
- count = 1 (이전 수열에서 이어짐)
3. 세그먼트 트리 업데이트
- 위치 3에 (length=3, count=1) 저장
현재까지 찾은 LIS들:
- {10} (길이 1)
- {10, 20} (길이 2)
- {10, 20, 30} (길이 3, 개수 1)
현재 숫자: 20 (압축값: 2)
1. 이전 작은 숫자들 조회 (query(1, 1))
- 결과: length = 1, count = 1 (10으로 끝나는 수열)
2. 새로운 상태
- length = 2 (1 + 1)
- count = 1 (이전 수열에서 이어짐)
3. 세그먼트 트리 업데이트
- 위치 2의 상태 갱신 (merge로 count 합산)
현재까지 찾은 LIS들:
- {10} (길이 1)
- {10, 20} (길이 2)
- {10, 20, 30} (길이 3)
- {10, 20} (새로운 경로)
1. 이전 작은 숫자들 조회 (query(1, 3))
- 결과: length = 3, count = 1 ({10, 20, 30}으로 끝나는 수열)
2. 새로운 상태
- length = 4 (3 + 1)
- count = 1 (이전 수열에서 이어짐)
3. 세그먼트 트리 업데이트
- 위치 4에 (length=4, count=1) 저장
최종 LIS들:
- {10, 20, 30, 50} (길이 4, 개수 1)
위치 1 (값 10): length=1, count=1
위치 2 (값 20): length=2, count=1
위치 3 (값 30): length=3, count=1
위치 4 (값 50): length=4, count=1

코드
/**
* Author: nowalex322, Kim HyeonJae
*/
import java.io.*;
import java.util.*;
public class Main {
static final int MOD = 1000000007;
static class Pair{
int length;
long count;
public Pair(int length, long count){
this.length = length;
this.count = count;
}
}
static Pair merge(Pair a, Pair b){
if(a.length > b.length) return a;
else if (a.length < b.length) return b;
// 같은 길이일때는 count를 더함
return new Pair(a.length, (a.count + b.count) % MOD);
}
static BufferedReader br;
static BufferedWriter bw;
static StringTokenizer st;
static int N, arr[], compressed[];
static Pair segment[];
public static void main(String[] args) throws Exception {
new Main().solution();
}
public void solution() throws Exception {
br = new BufferedReader(new InputStreamReader(System.in));
// br = new BufferedReader(new InputStreamReader(new FileInputStream("src/main/java/BOJ_17411_가장긴증가하는부분수열6/input.txt")));
bw = new BufferedWriter(new OutputStreamWriter(System.out));
N = Integer.parseInt(br.readLine());
arr = new int[N];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++) {
arr[i] = Integer.parseInt(st.nextToken());
}
// 좌표압축
TreeSet<Integer> uniqueSet = new TreeSet<>();
for(int n : arr) uniqueSet.add(n);
ArrayList<Integer> unique = new ArrayList<>(uniqueSet); // 중복제거
compressed = new int[N];
for(int i = 0; i < N; i++){
int pos = binarySearch(unique, arr[i]);
compressed[i] = pos + 1; // 1-based
}
int uniqueSize = unique.size();
segment = new Pair[4*uniqueSize];
for (int i=0; i<4*uniqueSize; i++) {
segment[i] = new Pair(0, 0);
}
for(int i=0; i<N; i++){
// 지금i위치보다 앞에있는 작은수들 중 LIS 찾기
Pair curr = query(1, compressed[i]-1, 1, uniqueSize, 1);
/*
지금위치i까지 포함해서 길이 1 증가
count가 0일땐 새로운수열 시작 (1)
count가 0아닐땐 이전까지 count 다 더함
*/
Pair newValue = new Pair(curr.length + 1, Math.max(curr.count, 1));
update(compressed[i], newValue, 1, uniqueSize, 1);
}
Pair result = query(1, uniqueSize, 1, uniqueSize, 1);
bw.write(result.length + " " + result.count + "\n");
bw.flush();
bw.close();
br.close();
}
private int binarySearch(ArrayList<Integer> unique, int target) {
int left = 0, right = unique.size() - 1;
int res = 0;
while(left <= right){
int mid = left + (right-left)/2;
if(unique.get(mid) >= target) {
res = mid;
right = mid - 1;
}
else left = mid + 1;
}
return res;
}
/*
구간 [left, right]에서 최댓값 찾기
*/
static Pair query(int left, int right, int start, int end, int node){
if(left > end || right < start) return new Pair(0, 0);
if(left <= start && end <= right) return segment[node];
int mid = start + (end-start)/2;
Pair leftRes = query(left, right, start, mid, node*2);
Pair rightRes = query(left, right, mid+1, end, node*2 + 1);
return merge(leftRes, rightRes);
}
/*
pos 위치를 value로 업데이트
*/
static void update(int pos, Pair value, int start, int end, int node){
if(pos > end || pos < start) return;
if(start == end){
segment[node] = merge(segment[node], value);
return;
}
int mid = start + (end-start)/2;
update(pos, value, start, mid, node * 2);
update(pos, value, mid+1, end, node * 2 + 1);
segment[node] = merge(segment[node * 2], segment[node * 2 + 1]);
}
}