| 일 | 월 | 화 | 수 | 목 | 금 | 토 | 
|---|---|---|---|---|---|---|
| 1 | ||||||
| 2 | 3 | 4 | 5 | 6 | 7 | 8 | 
| 9 | 10 | 11 | 12 | 13 | 14 | 15 | 
| 16 | 17 | 18 | 19 | 20 | 21 | 22 | 
| 23 | 24 | 25 | 26 | 27 | 28 | 29 | 
| 30 | 
- designPattern
 - El
 - 프로토타입 패턴
 - F
 - 코틀린
 - ㅋㅁ
 - 옵저버 패턴
 - factory method
 - android designsystem
 - 안드로이드 디자인시스템
 - 추상 팩토리
 - 추상팩토리패턴
 - 디자인패턴 #
 - Abstract Factory
 - 팩토리 메소드
 - ㅓ
 - compose
 - r
 - Functional Programming
 - material3
 - 디자인패턴
 - 싱글톤
 - Singleton
 - 함수형프로그래밍
 - 빌터패턴
 - PrototypePattern
 - Kotlin
 - builderPattern
 - Design Pattern
 - Observer Pattern
 
- Today
 
- Total
 
오늘도 더 나은 코드를 작성하였습니까?
상한선(upper_bound) & 하한선(lower_bound) feat.kotlin 본문
이진탐색의 응용으로 자주 나오지는 않지만, 알아두면 유용하고 반대로 모르면 구현자체가 쉽지 않을 수도 있다.
*이진탐색의 응용으로 반드시 오름차순 정렬은 기본적으로 되어있는 배열 및 리스트를 이용한다.
상한선 (upper_bound)
- key 보다 "큰 값"이 가장 처음 나오는 index
val arr = arrayOf(1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 7, 7)
위 와같은 배열이 있다면 key = 6이라면 상한선은 무엇일까?
첫번째 7인 index 10일 것이다.
key = 7 또는 그 이상의 값이라면 상한선은 배열의 마지막 Index + 1, 반대로 배열의 최소값이 들어온다면 무조건 index 0이다.
여기 핵심적으로 봐야할것
1. 조건을 만족하는 key값을 찾기 위해 left는 right를 넘을 수 없습니다.
key 보다 큰 값을 가르키는 right index보다 작은 값을 가르키는 left index가 같아 진다라는 것은 곧 key를 찾은 것이기 때문이다.
그래서 우리가 찾는건 "key보다 큰 값이 처음 나오는 index"이다.
while (left < right) {
...
}
2. 이진탐색하고 다른 부분은 현재 검사하고 있는 index (mid)가, key보다 크다면,
right = mid - 1이 아니라 1칸 덜 줄어든 (일단 커야되니까) mid로 할당된다.
그리고 (arr[mid] == key)처리하는 분기는 없어야 한다.
if (arr[mid] > key) {
    right = mid
} else {
    left = mid + 1
}
code
fun upperBound(arr: Array<Int>, start: Int = 0, end: Int = arr.size, key: Int): Int {
    var left = start
    var right = end
    while (left < right) {
        val mid = (left + right) / 2
        if (arr[mid] > key) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return right
}
fun main() {
    val arr = arrayOf(1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 7, 7)
    println(upperBound(arr, key= 6))
    println(upperBound(arr, key= 10))
    println(upperBound(arr, key= 0))
}
하한선 (lower_bound)
- key 보다 "크거나 같은 값" 중 가장 처음 나오는 index
중요하게 봐야될 점은 상한선과 유사한데, while 반복문 안쪽의
if (arr[mid] >= key) {
            right = mid
        }
이 부분이다.
정의에 따라서 "크거나 같은 값" 중
같은이 추가되었다 보면된다.
code
fun lowerBound(arr: Array<Int>, start: Int = 0, end: Int = arr.size, key: Int): Int {
    var left = start
    var right = end
    while (left < right) {
        val mid = (left + right) / 2
        if (arr[mid] >= key) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return right
}
fun main() {
    val arr = arrayOf(1, 2, 2, 3, 3, 3, 4, 5, 5, 6, 7, 7)
    println(lowerBound(arr, key = 3))
    println(lowerBound(arr, key = 0))
    println(lowerBound(arr, key = 8))
}
예시문제
https://www.acmicpc.net/problem/7795
7795번: 먹을 것인가 먹힐 것인가
심해에는 두 종류의 생명체 A와 B가 존재한다. A는 B를 먹는다. A는 자기보다 크기가 작은 먹이만 먹을 수 있다. 예를 들어, A의 크기가 {8, 1, 7, 3, 1}이고, B의 크기가 {3, 6, 1}인 경우에 A가 B를 먹을
www.acmicpc.net
A의 크기가 B보다 큰 쌍이 몇 개나 있는지
(1 ≤ N, M ≤ 20,000)
단순히 A의 모든 원소를 각각 반복하면서 B의 모든 원소와 각각 비교하면 모든 쌍을 구할 수 있다.
하지만 이렇게되면 20_000 * 20_000 = 400_000_000 최악의 경우 4억번의 반복 횟수가 생기기 때문에 시간초과가 생긴다.
즉, 단순 2중 반복문을 사용하면 안된다는 것.
일단 "크기"의 순서쌍 이기 때문에 먹이가 되는 B집단을 정렬한다.
자신과 "같거나 큰것"은 못 먹는다 그럼 lowerBound를 쓴 결과의 Index는 이전은 "무조건 작음"
code
var n = 0
var m = 0
lateinit var arrA: Array<Int>
lateinit var arrB: Array<Int>
fun lowerBound(key: Int): Int {
    var left = 0
    var right = arrB.size
    while (left < right) {
        val mid = (left + right) / 2
        if (arrB[mid] >= key) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return right
}
fun main() = with(System.`in`.bufferedReader()) {
    val tc = readLine().toInt()
    repeat(tc) { _ ->
        val info = readLine().split(" ").map { it.toInt() }
        n = info[0]
        m = info[1]
        arrA = readLine().split(" ").map { it.toInt() }.toTypedArray()
        arrB = readLine().split(" ").map { it.toInt() }.toTypedArray()
        arrB.sort()
        var cnt = 0
        for (i in 0 until arrA.size) {
            val idx = lowerBound(arrA[i])
            cnt += idx
        }
        println(cnt)
    }
    Unit
}
https://www.acmicpc.net/problem/12015
12015번: 가장 긴 증가하는 부분 수열 2
첫째 줄에 수열 A의 크기 N (1 ≤ N ≤ 1,000,000)이 주어진다. 둘째 줄에는 수열 A를 이루고 있는 Ai가 주어진다. (1 ≤ Ai ≤ 1,000,000)
www.acmicpc.net
문제분석
LIS 를 사용할때 DP로 풀면 시간복잡도가 O(N^2)이지만, lowerBound를 사용하면 그 시간을 O(N logN)으로 줄일 수 있다.
단, 아래와같이 그 길이를 알수는 있지만, 어떤 요소로 LIS가 구성되어 있는지는 알 수 없다.
var n = 0
lateinit var arr: IntArray
val lis = mutableListOf<Int>()
fun lowerBound(key: Int): Int {
    var left = 0
    var right = lis.size
    while (left < right) {
        val mid = (left + right) / 2
        if (lis[mid] >= key) {
            right = mid
        } else {
            left = mid + 1
        }
    }
    return right
}
fun main() = with(System.`in`.bufferedReader()) {
    n = readLine().toInt()
    arr = readLine().split(" ").map { it.toInt() }.toIntArray()
    lis.add(arr[0])
    for (i in 1 until arr.size) {
        if (arr[i] == 0) continue
        if (arr[i] > lis.last()) {
            lis.add(arr[i])
        } else {
            val idx = lowerBound(arr[i])
            lis[idx] = arr[i]
        }
    }
    println(lis.size)
}'Algorithm > 개념' 카테고리의 다른 글
| 최단거리 알고리즘 - 플로이드 와샬(Floyd-Warshall) feat.kotlin (0) | 2024.03.18 | 
|---|---|
| 최단거리 알고리즘 - 다익스트라(dijkstra) feat.kotlin (0) | 2024.03.13 | 
| 이분탐색(Binary Search) (1) | 2023.11.29 | 
| 투포인터(two pointer) (1) | 2023.11.28 | 
| 탐욕법(Greedy) (1) | 2023.11.26 |