문제
수열 A가 주어졌을 때, 가장 긴 증가하는 부분 수열을 구하는 프로그램을 작성하시오.
예를 들어, 수열 A = {10, 20, 10, 30, 20, 50} 인 경우에 가장 긴 증가하는 부분 수열은 A = {10, 20, 10, 30, 20, 50} 이고, 길이는 4이다.
입력
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다.
둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (-1,000,000,000 ≤ Ai ≤ 1,000,000,000)
출력
첫째 줄에 수열 A의 가장 긴 증가하는 부분 수열의 길이를 출력한다.
둘째 줄에는 정답이 될 수 있는 가장 긴 증가하는 부분 수열을 출력한다.
내가 푼 풀이
이제까지 가장 긴 증가하는 부분수열(LIS)를 구하는 방법은 두가지가 있었다.
첫번째는 DP방법으로 해당 인덱스의 원소까지의 부분수열 길이를 최신화 하는 방법이였다.
이는 시간복잡도가 $O(N^2)$가 걸리므로 주어진 문제의 입력은 최대 100만개의 원소이므로 시간초과가 일어났다.
두번째 방법으론 이분탐색이 있다.
이분탐색을 통해 이문제를 해결했지만 몇가지 주의해야할 점이 있었다.
이전에 이분탐색으로 구하면 최장증가 부분수열의 "길이"만 구할 수 있었다.
이분탐색을 통해 배열에서 원소가 위치하는 자리를 구하고 해당위치에 넣은결과 길이는 정답이지만, 부분수열이 옳지 않게 구하게 되었다.
예로 수열 [10, 20, 10, 30, 15, 50]이 주어졌을 때, 이분탐색을 이용하여 구한다면
[10, 15, 30, 50]배열이 구해지고 이는 최장증가 부분수열이 아니다.
이유는 15는 50이전의 원소로 30보다 나중에 나오는 원소이므로 옳지 않다.
부분수열은 구할 수 없지만 길이는 알고있다.
이 점을 이용하여 이분탐색을 통해 부분수열을 구할 때, 해당 원소의 인덱스를 저장한다.
괄호안 숫자는 해당원소가 주어진 수열의위치를 의미한다.
첫번째 원소 10
첫번째 원소는 비교대상이 없으므로 첫번째에 넣는다.
result = [10(1)]
idx = [1]
두번째 원소 20
두번쨰 원소는 이분탐색을 이용하여 10뒤에 오는 숫자임을 알 수 있다.
result = [10(1), 20(2)]
idx = [1, 2]
세번째 원소 10
세번째 원소는 이분탐색 결과 첫번째 오는 숫자와 같음을 알 수 있다.
result = [10(3), 20(2)]
idx = [1, 2, 1]
네번째 원소 30
네번째 원소는 20뒤에오는 숫자임을 알 수 있다.
result = [10(3), 20(2), 30(4)]
idx = [1, 2, 1, 3]
다섯번째 원소 15
다섯번째 원소는 이분탐색 결과 10과 20사이에 오는 숫자임을 알 수 있다.
이분탐색 결과 부분수열은 아래와 같다.
result = [10(3), 15(5), 30(4)]
idx = [1, 2, 1, 3, 2]
-> 이분탐색으로 만들어진 부분수열은 이미 정답과는 다른 부분수열이 만들어졌다. 해당원소의 위치를 배열안에 저장한다.
여섯번째 원소 50
여섯번째 원소는 이분탐색결과 마지막에 오는 숫자임을 알 수 있다.
result = [10(3), 15(5), 30(4), 50(6)]
idx = [1, 2, 1, 3, 2, 4]
-> 결과
result = [10(3), 15(5), 30(4), 50(6)]
idx = [1, 2, 1, 3, 2, 4]
이전의 이분탐색을 이용하면 result 배열을 얻고 길이는 정답이지만 부분 수열은 옳지않은 수열이 된다.
이분탐색을 통해 부분수열의 값이 변경될때 해당 원소가 위치하는 인덱스값을 저장함으로써 길이를 차감하며 부분수열을 구할 수 있게된다.
길이가 4이므로 역순으로 4 3 2 1의 원소를 뽑으면 [50, 30, 20 ,10]이고 이배열의 역순은 정답이 된다.
이분탐색을 이용하는 방법은 알았지만, 부분수열을 뽑아내는 과정은 쉽게 떠올리지 못했다.
코드로 구현하면 다음과 같다.
import Foundation
// 입력받기
let N = Int(readLine()!)!
var A = readLine()!.split(separator: " ").map{Int(String($0))!}
A.insert(0, at: 0)
let INF = 1000000001
// 이분탐색 결과배열과 인덱스 배열
var result = Array(repeating: INF, count: N+1)
var idxArr = [Int]()
result[1] = A[1]
// 이분탐색
func binarySearch(num: Int) {
var left = 1
var right = result.count-1
while left <= right {
let mid = (right + left) / 2
if result[mid] < num {
left = mid + 1
} else {
right = mid - 1
}
}
// 해당위치에 넣고, 인덱스배열에 저장
result[left] = num
idxArr.append(left)
}
for i in 1...N {
binarySearch(num: A[i])
}
var answer = [Int]()
var count = 0
// 부분수열의 길이 구하기
for i in 1...N {
if result[i] != INF {
count += 1
} else {
break
}
}
// 부분수열 구하기
for i in (0..<idxArr.count).reversed() {
if idxArr[i] == count {
count -= 1
answer.append(A[i+1])
}
}
// 출력
print(answer.count)
print(answer.map{String($0)}.reversed().joined(separator: " "))
'코딩테스트 > 백준' 카테고리의 다른 글
BOJ-17404 RGB거리 2 Swift (1) | 2024.07.16 |
---|---|
BOJ-2631 줄세우기 Swift (0) | 2024.06.17 |
BOJ-1450 냅색문제 Swift (0) | 2024.06.06 |
BOJ-1644 소수의 연속합 Swift (0) | 2024.06.06 |
BOJ-11657 타임머신 Swift (1) | 2024.06.04 |