문제
숫자 카드는 정수 하나가 적혀져 있는 카드이다. 상근이는 숫자 카드 N개를 가지고 있다. 정수 M개가 주어졌을 때, 이 수가 적혀있는 숫자 카드를 상근이가 몇 개 가지고 있는지 구하는 프로그램을 작성하시오.
입력
첫째 줄에 상근이가 가지고 있는 숫자 카드의 개수 N(1 ≤ N ≤ 500,000)이 주어진다. 둘째 줄에는 숫자 카드에 적혀있는 정수가 주어진다. 숫자 카드에 적혀있는 수는 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다.
셋째 줄에는 M(1 ≤ M ≤ 500,000)이 주어진다. 넷째 줄에는 상근이가 몇 개 가지고 있는 숫자 카드인지 구해야 할 M개의 정수가 주어지며, 이 수는 공백으로 구분되어져 있다. 이 수도 -10,000,000보다 크거나 같고, 10,000,000보다 작거나 같다.
출력
첫째 줄에 입력으로 주어진 M개의 수에 대해서, 각 수가 적힌 숫자 카드를 상근이가 몇 개 가지고 있는지를 공백으로 구분해 출력한다.
예제 입력 1
10
6 3 2 10 10 10 -10 -10 7 3
8
10 9 -5 2 3 4 5 -10
예제 출력 1
3 0 0 1 2 0 0 2
풀이
처음에 한 생각은 배열에다 값을 할당하고, 우선 정렬을 한 다음 이진 탐색을 통하여 해당 값의 인덱스를 가져온다. 그리고, 그 인덱스를 전후로 동일한 값을 가지는 원소들을 탐색하는 식으로 알고리즘을 짰다.
그랬더니, 시간 초과가 발생하였다. 분석을 해보았다. 테스트 케이스의 경우는 아래와 같다.
- 값이 배열의 처음부터 존재하는 경우
- 값이 배열의 끝까지 존재하는 경우
- 값이 배열의 중간에만 존재하는 경우
인덱스의 처리를 어떻게 할까 고민하였다. 반복문을 통해 key값보다 크거나 작은 값을 가지는 원소에 도달하면 반복문 종료, 즉 key값을 가지는 원소들의 범위 하한값-1 부터 상한값+1까지 범위를 설정하게 된다. 또한, 1과 2의 경우를 설정해주어 인덱스가 배열의 크기나 -1을 가지면 반복문이 종료되어, 1의 경우에는 하한값이 -1로 설정되고, 2의 경우에는 상한값이 배열의 크기만큼 설정되어 Out of index가 발생하지 않도록 하였다.
위의 방법에서 시작초과가 발생하여서, 구글링을 한 결과, 이 문제는 이진 탐색을 변형하여 푸는 응용 문제였다. 기존, 이진 탐색의 경우 정렬된 배열에서 중간값을 key값과 비교해 범위를 반으로 줄여나가며 탐색을 하는 메커니즘이다. 이 문제에서는 상계와 하계를 결정짓는데에 이진 탐색을 변형하여 사용한다. 코드는 아래와 같다.
private static int lowerbound(int key){
int lo = 0;
int hi = arr.length;
while(lo < hi){
int mid = (lo + hi) / 2;
//등호를 넣어 key값과 mid값이 동일하여도 상계를 줄여나감
//최종적으로, key값보다 작은 값을 가지는 첫번째 index를 반환
if(key <= arr[mid])
hi = mid;
else
lo = mid + 1;
}
return lo;
}
private static int upperbound(int key){
int lo = 0;
int hi = arr.length;
while(lo < hi){
int mid = (lo + hi) / 2;
//위와 반대로 key값과 mid값이 동일하면 하계를 키워나감
//최종적으로, key값보다 큰 값을 가지는 첫번째 index를 반환
if(key < arr[mid]){
hi = mid;
}
else
lo = mid + 1;
}
return lo;
}
이진 탐색을 상계, 하계를 찾는 두개의 탐색 메소드로 분리하여 상계, 하계를 찾고 이 범위에 속하는 원소의 개수를 출력한다.
public class _10816 {
static BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
static StringBuilder sb = new StringBuilder();
static StringTokenizer st;
static int[] arr;
public static void main(String[] args) throws Exception{
int N = Integer.parseInt(br.readLine());
st = new StringTokenizer(br.readLine(), " ");
arr = new int[N];
for(int i = 0; i < N; i++){
arr[i] = Integer.parseInt(st.nextToken());
}
Arrays.sort(arr);
int M = Integer.parseInt(br.readLine());
st = new StringTokenizer(br.readLine(), " ");
for(int i = 0; i < M; i++){
int key = Integer.parseInt(st.nextToken());
sb.append(upperbound(key) - lowerbound(key)).append(" ");
}
System.out.println(sb);
}
}
결론
이진 탐색으로 찾고, 그 반환값으로 다시 인덱스 전후를 탐색하는 과정을 거쳤기에 기존에 알고리즘은 시간 초과가 발생했다. 좀 더 창의적으로 생각할 수 있도록 하자. 알고있는 내용도 문제에 맞게 변형하여 효율적으로 설계할 수 있는 힘을 기르면 멋있는 개발자가 될 것이다.
'Algorithm > Problems' 카테고리의 다른 글
[JAVA]Hashing(Baekjoon) (0) | 2023.02.15 |
---|---|
[JAVA]이항 계수 1(Baekjoon) (0) | 2023.02.15 |
[JAVA]수 찾기(Baekjoon) (0) | 2023.02.15 |
[JAVA]플로이드(Baekjoon) (0) | 2023.02.15 |
[JAVA]문자열 폭발(Baekjoon) (0) | 2023.02.14 |