프로그래머스: 징검다리 건너기 Swift 풀이(재밌는 자료구조)
요새는 Swift로 프로그래머스 문제를 주에 3개씩 풀고 있는데
재밌는 자료구조를 발견해서 가져왔슴니다
징검다리 건너기
https://school.programmers.co.kr/learn/courses/30/lessons/64062
대충 각 디딤돌의 사용 가능한 횟수를 주고, k개는 디딤돌 없어도 건너뛸 수 있다고 할 때 얼마나 많이 건널 수 있을까?? 하는 문제
조건
- 배열 크기 20만 이하
- 각 값은 2억 이하
풀이1
import Foundation
func solution(_ stones:[Int], _ k:Int) -> Int {
var ans = Int.max
var r = k-1
while r < stones.count {
r += 1
// print(stones[r-k..<r])
ans = min(ans, stones[r-k..<r].max()!)
}
return ans
}
대충 k만큼 이어진 구간을 윈도우로 해서 거기서 max 값들의 min 값을 찾으면 안 되나?? 해서 해 봄
채점 결과(효율성 시간 초과)
정확성 테스트
테스트 1 〉 통과 (0.03ms, 16.5MB)
테스트 2 〉 통과 (0.05ms, 16.3MB)
테스트 3 〉 통과 (0.06ms, 16.4MB)
테스트 4 〉 통과 (0.11ms, 16.6MB)
테스트 5 〉 통과 (0.08ms, 16.5MB)
테스트 6 〉 통과 (0.57ms, 16.5MB)
테스트 7 〉 통과 (3.76ms, 16.4MB)
테스트 8 〉 통과 (5.18ms, 16.5MB)
테스트 9 〉 통과 (9.38ms, 16.4MB)
테스트 10 〉 통과 (0.13ms, 16.5MB)
테스트 11 〉 통과 (0.06ms, 16.4MB)
테스트 12 〉 통과 (0.06ms, 16.1MB)
테스트 13 〉 통과 (0.09ms, 16.3MB)
테스트 14 〉 통과 (0.56ms, 16.4MB)
테스트 15 〉 통과 (3.50ms, 16.4MB)
테스트 16 〉 통과 (4.62ms, 16.2MB)
테스트 17 〉 통과 (8.88ms, 16.3MB)
테스트 18 〉 통과 (0.04ms, 16.3MB)
테스트 19 〉 통과 (0.11ms, 16.3MB)
테스트 20 〉 통과 (0.09ms, 16.2MB)
테스트 21 〉 통과 (0.60ms, 16.4MB)
테스트 22 〉 통과 (2.49ms, 16.4MB)
테스트 23 〉 통과 (5.52ms, 16.4MB)
테스트 24 〉 통과 (10.29ms, 16.2MB)
테스트 25 〉 통과 (0.05ms, 16.3MB)
효율성 테스트
테스트 1 〉 실패 (시간 초과)
테스트 2 〉 실패 (시간 초과)
테스트 3 〉 실패 (시간 초과)
테스트 4 〉 실패 (시간 초과)
테스트 5 〉 실패 (시간 초과)
테스트 6 〉 실패 (시간 초과)
테스트 7 〉 실패 (시간 초과)
테스트 8 〉 실패 (시간 초과)
테스트 9 〉 실패 (시간 초과)
테스트 10 〉 실패 (시간 초과)
테스트 11 〉 실패 (시간 초과)
테스트 12 〉 실패 (시간 초과)
테스트 13 〉 실패 (시간 초과)
테스트 14 〉 실패 (시간 초과)
논리는 맞긴 하네
복잡도는 (n-k) 순회, k개에서 max 찾기 해서 $O((n-k)k)$인데 시간 초과 남
k = n/2 일 때 최댓값이니까 $O(n^2/4)$
풀이2
위 풀이에서 매번 k개 중의 최댓값 구하는 게 문제인데…
그럼 큐 형태면서 최댓값 빠르게 구할 수 있는 거 없나? 뭔가 저장해 놓으면 어째 될 거 같은데… 싶어서 찾아 봄
그리고 스택 오버플로우에 누가 올린 정확히 똑같은 문제를 찾았슴니다 -> 어느 아마존의 인터뷰 문제
해당 기능을 위한 자료구조 설명은 여기 -> push, pop, getMin이 상수인 자료구조
class MaxQueue {
private var pushStack: [(val: Int, maxVal: Int)] = []
private var popStack: [(val: Int, maxVal: Int)] = []
func push(_ val: Int) {
var maxVal = val
if !pushStack.isEmpty {
maxVal = max(maxVal, pushStack.last!.maxVal)
}
pushStack.append((val: val, maxVal: maxVal))
}
func pop() -> Int? {
if popStack.isEmpty {
if pushStack.isEmpty {
return nil
} else {
var val = pushStack.popLast()!.val
popStack.append((val: val, maxVal: val))
while !pushStack.isEmpty {
val = pushStack.popLast()!.val
let maxVal = max(popStack.last!.maxVal, val)
popStack.append((val: val, maxVal: maxVal))
}
}
}
return popStack.popLast()!.val
}
func getMax() -> Int? {
if pushStack.isEmpty {
if popStack.isEmpty {
return nil
} else {
return popStack.last!.maxVal
}
} else if popStack.isEmpty {
return pushStack.last!.maxVal
} else {
return max(pushStack.last!.maxVal, popStack.last!.maxVal)
}
}
}
재밋네요…
일단 스택 2개로 큐 구현하고
각 스택은 자기 값과 현재 맥스 값을 튜플로 가집니다
푸시할 땐
- pushStack이 있다면 pushStack의 max를 max로 하여 넣기
팝할 땐
- popStack, pushStack 모두 비었다면 없는 거니까 nil
- popStack이 비었다면
- pushStack에서 꺼내서 popStack으로 옮기기(maxVal은 val로, 나중에 생각해 보면 push가 비었을 때 getMax하면 얘가 마지막에 팝될 애이므로 해당 구간에선 자기가 최고일 거고, push가 있을 땐 둘이 비교해서 max 리턴하면 되니까)
- 그리고 빌 때까지 계속 꺼내서 popStack의 maxVal과 비교하여 자신의 maxVal 정하고 popStack에 다시 넣기
- 이제 popStack이 있으니 마지막 걸 꺼내 주기
전부 amortized $O(1)$이라네요
이게 뭔 소리야?! pop할 때 다 옮기고 난리 났구만 뭔 O(1) 싶으시다면 아래 설명을 읽어 주세요
amortized
제일 많이 드는 예는 C++의 벡터에 push_back()
입니다
벡터가 일단 capacity만큼 자리 차지 하고, 거기서 넘어가면 크기가 2배인 곳에다가 옮기잖아요
근데 생각해보면 이거 다 옮겨 버리는데 시간 복잡도 최악의 경우엔 사실 $O(n)$아님?? 싶은데
저런 경우가 어쩌다 한 번밖에 안 나오니까 이걸 계산을 해 보면은
평소에는 $O(1)$로 하다가 마지막 딱 한 번만 옮겨야 해서 $O(n)$이면 사실상 $\frac{O(1) * (n-1) + O(n) * 1}{n}$ 해서 $O(1)$이나 마찬가지인 거임!!
위 자료구조도 마찬가지로 popStack이 비었을 때에만 pushStack을 popStack 쪽으로 옮기고, 평상시에는 옮겨 뒀던 거 쓰면 되니까 사실상 평균 내면 거의 $O(1)$이라 봐도 된다는 뜻임니다
import Foundation
func solution(_ stones:[Int], _ k:Int) -> Int {
var ans = Int.max
var window = MaxQueue()
stones[0..<k-1].forEach { window.push($0) }
var r = k-1
while r < stones.count {
window.push(stones[r])
r += 1
ans = min(ans, window.getMax()!)
window.pop()
}
return ans
}
해당 자료구조 써서 넣고 빼고 맥스 비교 해보면
채점 결과(통과)
정확성 테스트
테스트 1 〉 통과 (0.06ms, 16.6MB)
테스트 2 〉 통과 (0.06ms, 16.3MB)
테스트 3 〉 통과 (0.11ms, 16.3MB)
테스트 4 〉 통과 (0.10ms, 16.4MB)
테스트 5 〉 통과 (0.24ms, 16.4MB)
테스트 6 〉 통과 (0.55ms, 16.5MB)
테스트 7 〉 통과 (1.57ms, 16.6MB)
테스트 8 〉 통과 (1.11ms, 16.3MB)
테스트 9 〉 통과 (1.33ms, 16.3MB)
테스트 10 〉 통과 (0.24ms, 16.3MB)
테스트 11 〉 통과 (0.14ms, 16.5MB)
테스트 12 〉 통과 (0.11ms, 16.4MB)
테스트 13 〉 통과 (0.25ms, 16.4MB)
테스트 14 〉 통과 (0.80ms, 16.6MB)
테스트 15 〉 통과 (1.27ms, 16.5MB)
테스트 16 〉 통과 (1.90ms, 16.6MB)
테스트 17 〉 통과 (1.90ms, 16.5MB)
테스트 18 〉 통과 (0.11ms, 16.2MB)
테스트 19 〉 통과 (0.11ms, 16.4MB)
테스트 20 〉 통과 (0.16ms, 16.4MB)
테스트 21 〉 통과 (0.64ms, 16.3MB)
테스트 22 〉 통과 (2.06ms, 16.7MB)
테스트 23 〉 통과 (1.08ms, 16.2MB)
테스트 24 〉 통과 (1.12ms, 16.6MB)
테스트 25 〉 통과 (0.09ms, 16.6MB)
효율성 테스트
테스트 1 〉 통과 (194.50ms, 25.6MB)
테스트 2 〉 통과 (212.44ms, 25.7MB)
테스트 3 〉 통과 (211.92ms, 25.7MB)
테스트 4 〉 통과 (213.86ms, 25.6MB)
테스트 5 〉 통과 (211.39ms, 25.5MB)
테스트 6 〉 통과 (202.16ms, 25.5MB)
테스트 7 〉 통과 (193.14ms, 25.4MB)
테스트 8 〉 통과 (164.61ms, 28.5MB)
테스트 9 〉 통과 (168.79ms, 25.3MB)
테스트 10 〉 통과 (143.84ms, 28.6MB)
테스트 11 〉 통과 (167.61ms, 25.8MB)
테스트 12 〉 통과 (134.08ms, 28.6MB)
테스트 13 〉 통과 (192.35ms, 25.6MB)
테스트 14 〉 통과 (178.66ms, 25.7MB)
성공~~
$O(n-k)$와 amortized $O(1)$ 입니다
와… 이렇게 수준 높은 문제를 낸다고? 싶었는데(위 스택 오버플로우에서도 아마존에서 이상한 문제 냈다고 누가 뭐라 함)
이거 사실 거의 이분탐색으로 푼다네요
이분탐색 풀이는 구글링 해서 다른 분들 거 보셔요 저는 이분탐색으로 다시 풀어보러 가겟슴니다…
댓글남기기