비내림차순으로 정렬된 수열이 주어질 때, 다음 조건을 만족하는 부분 수열을 찾으려고 합니다.
수열을 나타내는 정수 배열 sequence와 부분 수열의 합을 나타내는 정수 k가 매개변수로 주어질 때, 위 조건을 만족하는 부분 수열의 시작 인덱스와 마지막 인덱스를 배열에 담아 return 하는 solution 함수를 완성해주세요. 이때 수열의 인덱스는 0부터 시작합니다.
처음에 문제를 보고 최근에 탐색 알고리즘이 많이 나왔으니.. 너비 우선 탐색 알고리즘을 사용해야겠다 생각했다. 그런데 이 문제의 경우 "길이가 미정인 부분 배열"을 추출해 그 합이 k인지를 확인해야 되기 때문에 원소를 순서에 상관없이 하나씩 뽑아 탐색하는 너비 우선 탐색과는 거리가 멀었다. 멀어도 많이.. 멀었다.
아무튼, 문제를 풀이한 방식은 다음과 같다.
sequence.size + 1
로, 만약 전체 배열이 답인 경우에도 갱신이 될 수 있게 해주었다.class Solution {
fun solution(sequence: IntArray, k: Int): IntArray {
val dq = ArrayDeque<Int>()
var sum = 0
var l = sequence.size + 1
var (start, end) = 0 to 0
var answer = intArrayOf()
sequence.mapIndexed { i, s ->
dq.addLast(i)
end = i
sum += s
// 합이 k를 넘어간다면,
// k 이하가 될 때까지 큐 앞에서부터 제거
while (sum > k) {
sum -= sequence[dq.first()]
dq.removeFirst()
start++
}
// 합이 k인 부분 배열의 길이가 기존보다 더 짧다면 답 갱신
if (sum == k && end - start < l) {
answer = intArrayOf(start, end)
l = end - start
}
}
return answer
}
}
나는 그냥 큐를 이용해 풀었는데, 투 포인터와 이분 탐색으로 푸는 방법도 있다고 한다.
⭐ 투 포인터 알고리즘, Two Pointers
리스트에 순차적으로 접근해야 할 때 두 개의 점의 위치를 기록하면서 처리하는 알고리즘
시간복잡도
⭐ 이분 탐색(이진 탐색) 알고리즘, Binary Search
정렬된 배열에서 특정 값을 찾을 때 정중앙에 위치한 값을 활용해 탐색하는 알고리즘
원리
"중앙값 < 탐색값"이면 탐색값 보다 작은 부분(중앙값의 왼쪽)은 고려할 필요가 없다.
시간 복잡도
class Solution {
fun solution(sequence: IntArray, k: Int): IntArray {
var startPtr = 0
var endPtr = 0
var sum = sequence[0]
val answer = intArrayOf(0, sequence.lastIndex)
//투 포인터로 경우 탐색
while(startPtr <= endPtr && endPtr < sequence.size){
if(sum < k && ++endPtr < sequence.size)
sum += sequence[endPtr]
else if(sum > k)
sum -= sequence[startPtr++]
//길이가 짧다면(앞으로만 이동하므로 더 앞에 있을 가능성은 무시)
if(sum == k){
if(answer[1] - answer[0] > endPtr - startPtr) {
answer[0] = startPtr
answer[1] = endPtr
}
sum -= sequence[startPtr++]
}
}
return answer
}
}
이 풀이를 보기 전에 도대체 합을 구하는 건데 어떻게 이분 탐색으로 푸는 건가 싶었다. 그런데 미리 합을 구해놓는 것이었고... 넘나 똑똑한것..
class Solution {
fun solution(sequence: IntArray, k: Int): IntArray {
var answer: IntArray = intArrayOf()
// 누적합 미리 구해놓기
val pSum = LongArray(sequence.size+1)
for(i in 1 .. sequence.size) {
// 이전 인덱스까지의 합과 현재 인덱스의 값 더하기
pSum[i] = pSum[i-1] + sequence[i-1]
}
// cnt로 k를 만드는 게 가능하다면 stop
for(cnt in 1 .. sequence.size) {
// cnt개로 k를 만드는 데 이분 탐색하여 가장 낮은 인덱스 탐색
var left = -1
var right = -1
var s = 0
var e = sequence.size-cnt
while(s<=e){
val mid = (s+e)/2
// mid ~ mid+cnt까지의 합
val sum = pSum[mid+cnt] - pSum[mid]
if(sum >= k){
if(sum==k.toLong()){
left = mid
right = mid+cnt-1
}
e = mid-1
}else{
s = mid+1
}
}
if(left != -1 && right != -1) return intArrayOf(left,right)
}
return answer
}
}
투 포인터가 가장 기억에 남는다. 굳이 큐를 사용하지 않고도 풀 수 있었구나..