funbinarySearch(target: Int): Int { var st = 0// start var en = n - 1// end
while (st <= en) { val mid = (st + en) / 2 when { a[mid] < target -> st = mid + 1 a[mid] > target -> en = mid - 1 else -> return1 } } return0 }
funinit() = with(Scanner(System.`in`)) { n = nextInt() a = IntArray(n) for (i in0 until n) { a[i] = nextInt() } m = nextInt() b = IntArray(m) for (i in0 until m) { b[i] = nextInt() } }
kotlin.collections의 binarySearch 사용
JVM을 기반으로 하는 코틀린에서의 컬렉션은 자바에서 제공하는 클래스들을 그대로 사용한다.
1 2 3 4
b.forEach { if (a.binarySearch(it) >= 0) println(1) else println(0) }
contains 사용
1 2 3 4
b.forEach { if (a.contains(it)) println(1) else println(0) }
BOJ를 기준으로 binarySearch를 이용한 풀이는 2164ms, contains는 4940ms 시간이 소요되었다.
publicstaticintbinarySearch(int[] a, int fromIndex, int toIndex, int key){ rangeCheck(a.length, fromIndex, toIndex); return binarySearch0(a, fromIndex, toIndex, key); } privatestaticintbinarySearch0(int[] a, int fromIndex, int toIndex, int key){ int low = fromIndex; int high = toIndex - 1; while(low <= high) { int mid = low + high >>> 1; int midVal = a[mid]; if (midVal < key) { low = mid + 1; } else { if (midVal <= key) { return mid; } high = mid - 1; } } return -(low + 1); }
funlowerBound(target: Int): Int { var st = 0 var en = a.size
while (st < en) { val mid = (st + en) / 2 if (a[mid] >= target) en = mid else st = mid + 1 } return st }
funupperBound(target: Int): Int { var st = 0 var en = a.size
while (st < en) { val mid = (st + en) / 2 if (a[mid] > target) en = mid else st = mid + 1 } return st }
funinit() = with(Scanner(System.`in`)) { n = nextInt() a = IntArray(n + 1) for (i in0 until n) { a[i] = nextInt() } m = nextInt() b = IntArray(m) for (i in0 until m) { b[i] = nextInt() } }
readLine()을 통한 입력 받기 (통과)
가독성이 좋아 Scanner를 통해 입력을 받는 방식을 BOJ에서 자주 사용하지만 다른 입력 방식들에 비해 느리기 때문에 시간 초과가 발생할 경우 readLine()을 이용하는 방법 등을 사용한다.
funmain() { init() val sb = StringBuilder() val sorted = list.distinct().sorted() for (i in list.indices) { sb.append(lowerBound(sorted, list[i])).append(" ") } }
funlowerBound(list: List<Int>, target: Int): Int { var st = 0 var en = list.lastIndex
while (st < en) { val mid = (st + en) / 2 if (list[mid] >= target) en = mid else st = mid + 1 }
var n = 0 val list = mutableListOf<Int>() val two = mutableListOf<Int>()
funmain() { init() list.sort() for (i in0 until n) { for (j in i until n) { two.add(list[i] + list[j]) } } two.sort() for (i in n - 1 downTo 0) { for (j in0 until i) { if (two.binarySearch(list[i] - list[j]) >= 0) { println(list[i]) return } } } }
이 문제의 상황은 N개를 만들 수 있는 랜선의 최대 길이를 구하는 최적화 문제이다. 이걸 결정 문제로 바꾸면 반대로 우리가 구해야 하는 답이 인자로 들어가고, 조건의 참/거짓 여부를 판단하는 문제로 만들 수 있다.
랜선의 길이가 줄어들수록 개수가 많아지므로 간단하게 그래프를 그려보면 랜선의 길이가 x축에 놓이고 개수가 y축에 놓인다. 그리고 그래프는 x가 커질수록 y가 감소하는 형태이다. 그래프에서 답은 표시한 지점으로, 개수가 N개 이상인 지점들 중에서 가장 길이가 긴 곳이다. 이 답을 기점으로 왼쪽은 개수가 N 이상이고 오른쪽은 N 미만이다. 랜선의 길이는 최소 1, 최대 231-1인데, 우리는 여기서 이분탐색으로 답을 빠르게 찾아낼 수 있다.
이렇게 st, mid, en을 놓고 범위를 줄여가며 답을 찾는다. 최대 길이를 구해야하는 문제에서 랜선의 길이가 X일 때 조건을 만족하는지 확인하는 문제로 변환해서 풀이를 해낼 수 있다.
이 문제의 경우, 랜선의 길이를 X로 두고나면 조각의 개수를 구하는건 O(N)이고 랜선의 길이로 가능한 범위는 231이어서 시간복잡도는 O(log(231N)) = O(31N)
여기서 주의해야하는건, 지금처럼 이분탐색을 수행할 변수를 가지고 함수를 세웠을 때 그 함수가 감소함수거나 증가함수여야 한다. 만약 위의 그래프처럼 함수가 감소 혹은 증가함수 형태가 아니라 뒤죽박죽이면 이분탐색 자체가 불가능하다. 그래서 parametric search를 할 때에는 최적화 문제를 결정 문제로 바꿀 수 있는지 생각하고 그 결정 문제로 얻어낸 함수가 감소 혹은 증가함수인지를 따져봐야 한다. 문제에서 최소 혹은 최대 얘기가 있고 범위가 무지막지하게 크거나, 시간복잡도에서 값 하나를 로그로 어떻게 잘 떨구면 될 것 같을 때 parametric search 풀이가 가능하지는 않을까 고민을 해볼 필요가 있다.
var st: Long = 1 var en = Int.MAX_VALUE.toLong() // 2^31 - 1 while (st < en) { val mid = (st + en + 1) / 2 if (solve(mid)) st = mid else en = mid - 1 } println(st) }
funsolve(x: Long): Boolean { // 결정 문제 var cur = 0L for (i in0 until k) cur += arr[i] / x return cur >= n }
funinit() = with(Scanner(System.`in`)) { k = nextInt() n = nextInt() arr = IntArray(k) for (i in0 until k) { arr[i] = nextInt() } }
mid = (st + en + 1) / 2로 둬야 무한 루프에 빠지지 않는다. mid = (st + en) / 2로 두면 st와 en이 1 차이날 때 st가 계속 값이 똑같아버릴 수 있다.