이진 탐색 Bineary Search (예제)

다히·2023년 2월 3일
0

Algorithm

목록 보기
8/8

예제 1) 떡볶이 떡 만들기

문제

오늘 동빈이는 여행 가신 부모님을 대신해서 떡집 일을 하기로 했다. 오늘은 떡볶이 떡을 만드는 날이다. 동빈이네 떡볶이 떡은 재밌게도 떡볶이 떡의 길이가 일정하지 않다. 대신에 한 봉지 안에 들어가는 떡의 총 길이는 절단기로 잘라서 맞춰준다.

절단기에 높이 H를 지정하면 줄지어진 떡을 한 번에 절단한다. 높이가 H보다 긴 떡은 H 위의 부분이 잘릴 것이고, 낮은 떡은 잘리지 않는다.

예를 들어 높이가 19, 14, 10, 17cm인 떡이 나란히 있고 절단기를 높이를 15cm로 지정하면 자른 뒤 떡의 높이는 15, 14, 10, 15cm가 될 것이다. 잘린 떡의 길이는 차례대로 4, 0, 0, 2cm 이다, 손님은 6cm만큼의 길이를 가져간다.

손님이 왔을 때 요청한 총 길이가 M일때, 적어도 M만큼의 떡을 얻기 위해 절단기에 설정할 수 있는 높이의 최댓값을 구하는 프로그램을 작성하시오.

조건

시간 제한: 2초, 메모리 제한: 128MB

입력

첫째 줄에 떡의 개수 N과 요청한 떡의 길이 M이 주어진다. (1 <= N <= 1000000, 1 <= M <= 2000000000)

둘째 줄에는 떡의 개별 높이가 주어진다. 떡 높이의 총합은 항상 M 이상이므로, 손님은 필요한 양만큼 떡을 사갈 수 있다. 높이는 10억보다 작거나 같은 양의 정수 또는 0이다.

4 6
19 15 10 17

출력

적어도 M만큼의 떡을 집에 가져가기 위해 절단기에 설정할 수 있는 높이의 최댓값을 출력한다.

15


문제 해결 아이디어

적절한 높이를 찾을 때까지 이진 탐색을 수행하여 높이 H를 반복해서 조정

현재 이 높이로 자르면 조건을 만족할 수 있는가?를 확인
→ 조건의 만족 여부('예' or '아니오')에 따라 탐색 범위를 좁혀서 해결

범위를 좁힐 때는 이진 탐색의 원리를 이용해 절반씩 좁혀나가기

  • 절단기의 높이가 0~10억까지의 정수 중 하나로 매우 큰 수

    → 선형 탐색을 수행하면 시간 초과가 날 수 있으므로 이진 탐색을 수행

  • 큰 수를 보면 당연하다는 듯이 가장 먼저 이진 탐색을 떠올려야 함!


문제 풀이

조건: 떡을 잘랐을 때 잘린 떡의 합 >= 손님이 요구하는 떡의 길이

1) 가장 긴 떡의 길이가 19이므로, 시작점은 0 끝점은 19 중간점은 9로 설정
중간점이 현재 우리가 자르고자 하는 높이 H

각각 떡에 대해서 잘린 떡의 길이가 나오게 되고, 모두 합했을 때 25로 M보다 크기 때문에 손님이 요구하는 최소한의 길이 조건을 만족 → 결과 저장


2) 높이를 더 높였을 때도 조건을 만족할 수 있는지 확인: 시작점 = 이전 중간점 + 1

시작점이 10이고 끝점이 19이므로 중간점(H)은 14가 되고, 잘린 떡의 길이를 모두 합했을 때 9로 M보다 크기 때문에 손님이 요구하는 최소한의 길이 조건을 만족 → 결과 저장


3) 높이를 더 높였을 때도 조건을 만족할 수 있는지 확인: 시작점 = 이전 중간점 + 1

시작점이 15이고 끝점이 19이므로 중간점(H)은 17이 되고, 잘린 떡의 길이를 모두 합했을 때 2로 M보다 작기 때문에 손님이 요구하는 최소한의 길이 조건을 만족하지 못함 → 결과 저장하지 않음


4) 이전 단계에서 조건을 만족하지 못했으므로 높이 줄이기: 끝점 = 이전 중간점 - 1

시작점이 15이고 끝점이 16이므로 중간점(H)은 15가 되고, 잘린 떡의 길이를 모두 합했을 때 6으로 M과 같기 때문에 손님이 요구하는 최소한의 길이 조건을 만족 → 결과 저장, 탐색 종료


과정 정리

이진 탐색을 수행해서 더이상 탐색 범위를 줄일 수 없을 때까지 시작점과 끝점의 위치를 바꿔가며 조건을 만족하는지 체크하는 방식으로 최적의 해를 구할 수 있음

중간점의 값은 시간이 지날수록 '최적화된 값'이 되기 때문에, 과정을 반복하면서 얻을 수 있는 떡의 길이의 합이 필요한 떡의 길이보다 크거나 같을 때마다 중간점의 값을 기록하면 됨

이때, 떡의 양을 줄이기 위해 절단기의 높이를 높였는데 떡의 양이 부족해질 수 있기 때문에 중간점의 값을 기록하는 것임


코드

n, m = map(int, input().split())
a = list(map(int, input().split()))

# 시작점, 끝점 초기화
start = 0
end = max(a)

# 절단기 높이(H) 기록용
result = 0

while start <= end:
    total = 0   # 잘린 떡의 길이 총합
    mid = (start + end) // 2   # 중간점(H)
    # 모든 떡을 높이 H로 잘랐을 때 남은 떡의 길이 계산
    for x in a:
        if x > mid:  # 떡이 남을 때만 자를 수 있음
            total += x - mid
    # 남은 떡의 길이 < 요구 사항 -> 조건 만족 X
    # => 높이 낮추기: 끝점 낮추기 (왼쪽 탐색)
    if total < m:
        end = mid - 1
    # 남은 떡의 길이 >= 요구 사항 -> 조건 만족 O
    # => 높이 기록 후 높이기: 시작점 높이기 (오른쪽 탐색)
    else:
        result = mid
        start = mid + 1
    
print(result)



정렬된 배열에서 특정 수의 개수 구하기

문제

N개의 원소를 포함하고 있는 수열이 오름차순으로 정렬되어 있습니다. 이때 이 수열에서 x가 등장하는 횟수를 계산하세요. 예를 들어 수열 {1,1,2,2,2,2,3}이 있을 때 x = 2라면, 현재 수열에서 값이 2인 원소가 4개이므로 4를 출력합니다.

단, 이 문제는 시간 복잡도 O(logN)으로 알고리즘을 설계하지 않으면 '시간 초과' 판정을 받습니다.

조건

시간 제한: 1초, 메모리 제한: 128MB

입력
첫째 줄에 N과 x가 정수 형태로 공백으로 구분되어 입력됩니다. (1 <= N <= 1,000,000, -10e9 <= x < 10e9)

둘째 줄에 N개의 원소가 정수 형태로 공백으로 구분되어 입력됩니다. (-10e9 <= 각 원소의 값 <= 10e9)

7 2
1 1 2 2 2 2 3

7 4
1 1 2 2 2 2 3

출력
수열의 원소 중에서 값이 x인 원소의 개수를 출력합니다. 단, 값이 x인 원소가 하나도 없다면 -1을 출력합니다.

4

-1


문제 해결 아이디어

  • O(logN) 요구 → 일반적인 선형 탐색 불가

  • 데이터가 정렬되어 있으므로 이진 탐색 수행 가능

    원소들은 모두 정렬되어 있기 때문에, 수열 내에 x가 존재한다면 연속적으로 나열되어 있을 것으로 예상할 수 있음

특정 값이 등장하는 첫 번째 위치와 마지막 위치를 찾아 위치 차이를 계산해 문제 해결 가능

  • 알고리즘을 직접 짤 수도 있지만, 단순히 정렬된 배열에서 특정한 데이터를 찾도록 요구하는 문제에서는 직접 구현할 필요 없이 파이썬의 표준 라이브러리인 bisect 모듈을 사용할 수도 있음

코드: 표준 라이브러리 이용

bisect 모듈 기반의 count_by_range() 함수 정의

from bisect import bisect_left, bisect_right

# 값이 [left_value, right_value]인 데이터의 개수를 반환하는 함수
def count_by_range(array, left_v, right_v):
    right_idx = bisect_right(array, right_v)
    left_idx = bisect_left(array, left_v)
    return right_idx - left_idx

n, x = map(int, input().split())
a = list(map(int, input().split()))

# 값이 [x, x] 범위에 있는 데이터 개수 계산
count = count_by_range(a, x, x) 

if count == 0:
    print(-1)
else:
    print(count)

코드: 이진 탐색 직접 구현

구현해야 할 함수

  • 데이터가 존재한다면 가장 첫 번째 위치를 찾는 이진 탐색 함수

  • 데이터가 존재한다면 가장 마지막 위치를 찾는 이진 탐색 함수

  • 위의 두 함수를 이용해 데이터의 값으로 개수를 계산하는 함수

# 정렬된 배열에서 값이 x인 데이터의 개수를 반환하는 함수
def count_by_value(array, x):
    n = len(array)

    # x가 처음 등장한 인덱스 계산
    a = first(array, x, 0, n-1)

    # 수열에 x 존재하지 않는 경우
    if a == None:
        return 0 
    
    # x가 마지막으로 등장한 인덱스 계산
    b = last(array, x, 0, n-1)

    return b - a + 1


# 처음 등장한 인덱스를 찾는 이진 탐색 함수
def first(array, target, start, end):
    if start > end:
        return None
    mid = (start + end) // 2

    # 해당 값을 가지는 원소 중에서 가장 왼쪽에 있는 경우에만 인덱스 반환
    if (mid == 0 or target > array[mid - 1]) and array[mid] == target:
        return mid
    # 중간점의 값보다 찾고자 하는 값이 작거나 같은 경우 왼쪽 확인
    elif array[mid] >= target:
        return first(array, target, start, mid - 1)
    # 중간점의 값보다 찾고자 하는 값이 큰 경우 오른쪽 확인
    else:
        return first(array, target, mid + 1, end)


# 마지막 위치를 찾는 이진 탐색 메서드
def last(array, target, start, end):
    if start > end:
        return None
    mid = (start + end) // 2

    # 해당 값을 가지는 원소 중에서 가장 오른쪽에 있는 경우에만 인덱스 반환
    if (mid == n-1 or target < array[mid + 1]) and array[mid] == target:
        return mid
    # 중간점의 값보다 찾고자 하는 값이 작은 경우 왼쪽 확인
    elif array[mid] > target:
        return last(array, target, start, mid - 1)
    # 중간점의 값보다 찾고자 하는 값이 크거나 같은 경우 오른쪽 확인
    else:
        return last(array, target, mid + 1, end)


n, x = map(int, input().split())
a = list(map(int, input().split()))

# 값이 [x, x] 범위에 있는 데이터 개수 계산
count = count_by_value(a, x) 

if count == 0:
    print(-1)
else:
    print(count)





Source
이코테 2021 강의 몰아보기

0개의 댓글