저는 이분 탐색하다 조건문 처리에서 low <= high
, low < high
, low + 1 < high
중 어떤걸 선택해야할지,
결과가 low
, high
, (low + high)/2
인지 헷갈리는 점을 해결하기 위해 이 글을 작성하게 되었습니다.
목차
🤔 이 글을 왜 적었을까? 무슨 상황이야?
📖 이분 탐색이란
🧽 이분 탐색 구현
🛠️ lower_bound, upper_bound
🙋🏻 현재 나의 상황
이분 탐색 문제에서 특정 값을 찾는 문제 같은 경우는 쉽게 풀었지만, 근삿값을 구하는 문제에서(백준 예산, 과자 나눠주기)
while(조건문)
조건문 처리에서
left < right
left <= right
둘 중 어떤 걸 사용해야할지 감을 잡지 못해
이와 같이 (밑 사진와 같이)
이분 탐색 문제에서 계속 틀리고 있다 😭
특히, 코딩 테스트에서 최근들어 자주 접했지만 매번 틀리는 안타까움에 해결하기 위해 이분 탐색을 제대로 정리해보자 라는 생각에 작성하게 되었다.
시작하기에 앞서 제가 이해한 내용을 간단하게 소스로 설명하면 이와 같습니다.
ex) 특정 범위내에서 조건에 만족하는 최소값을 찾아야 하는 경우
public class Main {
private static int N, M; //
private static int[] 배열;
private static boolean Check(int mid){
cnt 변수 선언
ex)
for(배열 길이){
cnt에는 배열[인덱스] / mid의 결과를 합으로 더해준다.
}
와 같이 cnt에 결과값 저장
cnt가 현재 찾는 값보다 크거나 같다면 true를 return 한다.
}
public static void main(String[] args){
int max = Integer.MAX_VALUE;
binarySearch(0, max);
}
public static int binarySearch(int low, int high) {
while (만약 low + 1이 high 보다 작은 경우) {
mid는 low와 high의 사이 중간 값이다.
Check함수에서는 mid(중간 값)을 활용해 조건에 해당한다면
- 조건에 해당한다면 low = mid
Check함수를 통해 확인한 결과 조건에 해당하지 않는다면
- 조건에 해당하지 않을 시 high = mid
}
현재 문제에서 최댓값을 구하라고 한다면 low을 return
현재 문제에서 최솟값을 구하라고 한다면 high을 return
}
주어진 자료에서 중복되지 않은 값이 주어졌을 때 그 데이터 내에 특정 값이 존재하는지 검색하는 방법
보통, 1개의 결과값을 가지는 문제일 때 많이 사용된다.
즉, 결정 문제의 답이 이분적일 때 사용할 수 있는 탐색 기법이다.
✔️ 결정 문제란?
결정 문제 : Yes 또는 No인 문제를 의미하며 보통 1개의 매개변수를 가진다.
F, F, F ~ , T, T, T ~, T일 때 처음으로 v[i] >= val
인 지점이 True가 되는 i 값이 결과값이다.
결정 문제의 매개변수(i, 안에서는 mid)에 대해 결정 문제의 답이 두 구간으로 나뉘는 것을 이분적이다 라고 하며 이런 경우 이분 탐색을 사용해 결정 문제의 답이 달라지는 경계를 찾을 수 있다.
이분 탐색은 경계를 포함하는 [low, high]
를 잡은 뒤 구간의 길이를 절반씩 줄여나가면서 low, high이 경계 지점에 위치하도록 하는 것이다.
이분 탐색이 끝난 뒤에는 low + 1 == high
이며, Check(low) != Check(high)
이다.
이때, Check(x)가 답이다.
Check(x)
: 결정 문제에서 파라미터가 x일 때, 결정 문제의 답을 의미이분 탐색은 구간의 범위가 클 때 효과적이라서 많이 사용된다.
또한, 최적화 문제에서도 사용되는데, 어떤 조건(Check(x))을 만족하는 x의 최댓값 또는 최솟값을 찾는 문제에서도 많이 사용된다.
O(log n)
(1) low, high의 반복문
low, high : 초기 값을 생각한다.
while(low + 1 < high)
mid = (low + high) / 2;
- if(Check(mid)) low = mid;
- else high = mid;
return 최댓값인경우 low, 최솟값인경우 high
(2) Check 함수
Check(int mid)
long sum = 0;
for 배열 크기 만큼 반복문을 돌린다.
- 특정 조건이라면 sum에 더해준다.
return sum >= 찾는 값
while(low + 1 < high)
- [low, mid] or [mid, high]으로 줄여나간다.
- Check(mid)가 low일 경우
- low 는 mid
- Check(mid)가 high일 경우
- high는 mid
현재 low + 1 < high
이기 때문에, low와 high 사이에는 무조건 한 개 이상의 칸이 있다.
low + 1 == high
가 되는 순간 반복문은 종료가 된다.
종료될 때,
를 반환(정답으로 출력)해주면 된다.
(개인적인 생각으로 정답을 만족하는 최댓값을 구해라는 경우 low, 최솟값을 구해라는 경우 high을 정답으로 반환해주면 될 것 같다.)
(1) Low, High을 통해 Mid 값을 구한다.
(2) Check 함수를 통해 확인해본 결과 Check(Low) == Mid 라는 결과를 얻게 되었다.
(3) Check 함수를 통해 확인해본 결과 Check(Low) != Mid 라는 결과를 얻게 되었다.
(4) Low + 1 == High 가 되었기에 탈출한다.
✔️ 나무 자르기 문제
결정 문제로 mid 높이에 절단기를 위치했을 때 m 이상의 나무를 얻을 수 있는가? 로 주면 된다.
Low의 초기 값은 0, High의 초기 값은 가장 큰 나무의 높이로 설정하면 된다.
package 나무_자르기;
import java.io.*;
import java.util.*;
public class BJ2805 {
private static int N;
private static long M;
private static long[] arr;
private static boolean Check(int mid){
long sum = 0;
// mid 절단기
for(int i = 0; i < arr.length; i++){
if(arr[i] > mid) sum += arr[i] - mid;
}
// System.out.println("sum : " + sum);
return sum >= M;
}
private static long binarySearch(){
int low = 0;
int high = Integer.MAX_VALUE;
while(low + 1 < high){
int mid = (low + high) / 2;
if(Check(mid)) low = mid;
else high = mid;
}
// System.out.println("low : " + low + " high : " + high);
return low;
}
public static void main(String[] args) throws IOException{
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
StringTokenizer tokenizer = new StringTokenizer(reader.readLine());
N = Integer.parseInt(tokenizer.nextToken());
M = Long.parseLong(tokenizer.nextToken());
arr = new long[N];
tokenizer = new StringTokenizer(reader.readLine());
for(int i = 0; i < N; i++){
long input = Long.parseLong(tokenizer.nextToken());
arr[i] = input;
}
Arrays.sort(arr);
System.out.println(binarySearch());
reader.close();
}
}
Lower Bound와 Upper Bound는 정렬된 배열에서 특정 값 이상 또는 초과인 원소가 처음으로 등장하는 위치를 찾는 문제에서 사용된다.
✔️ Lower Bound
Lower Bound는 v[i-1] < k <= v[i]
인 i
를 찾아주는 함수로, v[i] >= k
인 i
의 최솟값을 반환한다.
v의 모든 원소가 k보다 작다면 v의 마지막 다음 칸의 위치를 반환한다.
✔️ Upper Bound
Upper Bound는 v[i-1] <= k < v[i]
인 i
를 찾아주는 함수로, v[i] > k
인 i
의 최솟값을 반환한다.
v의 모든 원소가 k보다 작거나 같다면 v의 마지막 다음 칸의 위치를 반환한다.
✏️ Upper Bound와 Lower Bound는 이와 같은 규칙이 있다.
Upper Bound(v, v + n, x) - Lower Bound(v, v + n, x) == v 배열 내에서 성립하는 x의 개수
Upper Bound(v, v + n, x) : 배열 해당 구간 내에 원소 x에 해당하는 인덱스를 반환한다. (
v[i] >= 3인 경우 => i
)v 배열이 오름차순으로 정렬되어 있을 때
- v 내에 x가 없다면 0
- v 내에 x가 있다면
idx1[x의 처음 등장 위치]
-idx2[x의 마지막 등장 위치 + 1]
이 것은 v 배열 내에서 성립하는 x의 개수이다.
public class BinarySearch {
private static int lowerBound(int[] arr, int x){
int n = arr.length;
int low = -1; // high은 최소 0까지 감소할 수 있기 때문에, -1로 설정
int high = n; // 모든 원소가 x보다 작은 경우 n을 반환해야 한다.
while(low + 1 < high){
int mid = (low + high) / 2;
if(!(arr[mid] >= x)) low = mid;
else high = mid;
}
return high;
}
private static int upperBound(int[] arr, int x){
int n = arr.length;
int low = -1;
int high = n;
while(low + 1 < high){
int mid = (low + high) / 2;
if(!(arr[mid] > x)) low = mid;
else high = mid;
}
return high;
}
public static void main(String[] args) {
int[] arr = {1, 2, 3, 3, 4};
System.out.println(lowerBound(arr, 3)); // 2
System.out.println(upperBound(arr, 3)); // 4
System.out.println(upperBound(arr, 3) - lowerBound(arr, 3)); // 2
}
}
Lower Bound
는 주어진 값보다 같거나 큰 값이 처음으로 나오는 것을 찾는다.Upper Bound
는 주어진 값보다 큰 값이 처음으로 나오는 것을 찾는다.
밑의 내용은 개인적인 생각입니다.
✔️ 범위가 큰 곳에서 조건을 만족하는 특정 값을 찾을 때는
Binary Search 3개의 if문을 활용해 구현
✔ 최적화 문제, 어떤 조건(Check(x))을 만족하는 x의 최댓값 또는 최솟값을 찾는 문제
Check 함수를 활용해 구현한다.
low = mid
high = mid
✔️ 정렬된 배열에서 특정 값 이상 또는 초과인 원소가 등장 하는 처음 위치를 찾을 때
Lower Bound와 Upper Bound를 활용해 구현한다.
추가로, Lower Bound, Upper Bound에서도 Check 함수를 활용할 수 있을 것 같다.
구글링을 통해 학습하며 개인적인 생각으로 정리했습니다.
설명이 잘못된 게 있다면, 댓글로 알려주시면 감사하겠습니다!