[백준 2357] 최솟값과 최댓값
[백준 2357] 최솟값과 최댓값
▪︎ 문제
▪︎ 알고리즘 설계
- 세그먼트 트리를 이용한다.
- 배열을 트리 형태로 만들어서 미리 초기화를 통해 값을 계산한 후 저장해 빠른 쿼리가 가능하도록 한다. 제한 시간 1초이므로 선형 탐색을 통해 설계하면 시간 초과가 발생한다.
- 최솟값, 최댓값 트리 배열 초기화, 쿼리 계산 메서드 각각 따로 분리하기!
▪︎ 코드
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
import java.util.*;
import java.io.*;
class Main {
static int[] numbers;
static int[] minTree;
static int[] maxTree;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken()); // 정수의 개수
int M = Integer.parseInt(st.nextToken()); // a, b의 쌍의 개수
numbers = new int[N]; // 정수 배열
minTree = new int[N*4];
maxTree = new int[N*4];
for(int i = 0; i < N; i++) {
numbers[i] = Integer.parseInt(br.readLine());
}
// 세그먼트 트리 초기화
initMinTree(1, 0, N - 1); // 루트 노드, 현재 노드가 담당하는 구간의 시작, 끝
initMaxTree(1, 0, N - 1); // 루트 노드, 현재 노드가 담당하는 구간의 시작, 끝
for(int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken()) - 1;
int b = Integer.parseInt(st.nextToken()) - 1;
int min = queryMin(1, 0, N - 1, a, b);
int max = queryMax(1, 0, N - 1, a, b);
bw.write(min + " " + max + "\n");
}
bw.flush();
bw.close();
br.close();
}
// 최솟값 트리 초기화
static int initMinTree(int node, int start, int end) {
if (start == end) { // 현재 구간이 단일 원소일 때 -> 배열 값 저장
return minTree[node] = numbers[start];
}
int mid = (start + end) / 2;
return minTree[node] = Math.min( // 두 자식 구간의 최소값 중 작은 값 저장
initMinTree(node * 2, start, mid), // 왼쪽 자식
initMinTree(node * 2 + 1, mid + 1, end) // 오른쪽 자식
);
}
// 최댓값 트리 초기화
static int initMaxTree(int node, int start, int end) {
if (start == end) { // 현재 구간이 단일 원소일 때 -> 배열 값 저장
return maxTree[node] = numbers[start];
}
int mid = (start + end) / 2;
return maxTree[node] = Math.max( // 두 자식 구간의 최댓값 중 큰 값 저장
initMaxTree(node * 2, start, mid), // 왼쪽 자식
initMaxTree(node * 2 + 1, mid + 1, end) // 오른쪽 자식
);
}
// 구간 최솟값 쿼리
static int queryMin(int node, int start, int end, int left, int right) {
if (right < start || end < left) return Integer.MAX_VALUE; // 구간 벗어남
if (left <= start && end <= right) return minTree[node]; // 완전히 포함
int mid = (start + end) / 2;
return Math.min(
queryMin(node * 2, start, mid, left, right), // 왼쪽
queryMin(node * 2 + 1, mid + 1, end, left, right) // 오른쪽
);
}
// 구간 최댓값 쿼리
static int queryMax(int node, int start, int end, int left, int right) {
if (right < start || end < left) return Integer.MIN_VALUE; // 구간 벗어남
if (left <= start && end <= right) return maxTree[node]; // 완전히 포함
int mid = (start + end) / 2;
return Math.max(
queryMax(node * 2, start, mid, left, right),
queryMax(node * 2 + 1, mid + 1, end, left, right)
);
}
}
- 왜 세그먼트 트리를 쓰면 시간 복잡도가 줄어들까?
- 세그먼트 트리는 배열을 구간 정보로 나눠서 저장한다.
- 즉, 배열을 트리로 만들어서 부분 구간의 최솟값/최댓값을 미리 계산해 놓는 것.
- 왜 초기화를 꼭 해줘야 할까?
- 초기화 함수는 세그먼트 트리에 각 노드가 담당하는 구간의 최소값/최댓값을 미리 계산해서 저장하는 과정
- 만약 초기화를 하지 않으면, 쿼리할 때마다 배열 전체를 다시 탐색해야 한다. 즉, 세그먼트 트리가 미리 계산된 값을 제공하지 못해서, 결국 선형 탐색과 다를 게 없다.
▪︎ 시간복잡도
O(log N)
▪︎ 틀린 이유
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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
import java.util.*;
import java.lang.*;
import java.io.*;
class Main {
static int[] numbers;
static int[] tree;
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out));
StringTokenizer st = new StringTokenizer(br.readLine());
int N = Integer.parseInt(st.nextToken()); // 정수의 개수
int M = Integer.parseInt(st.nextToken()); // a, b의 쌍의 개수
numbers = new int[N]; // 정수 배열
tree = new int[N*4]; // 트리
for(int i = 0; i < N; i++) {
numbers[i] = Integer.parseInt(br.readLine());
}
for(int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int a = Integer.parseInt(st.nextToken());
int b = Integer.parseInt(st.nextToken());
bw.write(findMin(a, b) + " " + findMax(a, b) + "\n");
}
bw.flush();
bw.close();
br.close();
}
public static int findMin(int a, int b) {
int min = numbers[a-1];
for(int i = a; i < b; i++) {
if (min > numbers[i]) {
min = numbers[i];
}
}
return min;
}
public static int findMax(int a, int b) {
int max = numbers[a-1];
for(int i = a; i < b; i++) {
if (max < numbers[i]) {
max = numbers[i];
}
}
return max;
}
}
- 시간 초과 발생 !
- 왜? 모든 쿼리마다 배열을 선형 탐색(O(N))하기 때문
▪︎ 느낀점 / 기억할 정보
- 세그먼트 트리는 구간의 합, 구간의 최솟값, 구간의 최댓값 등을 빠르게 구할 때 사용한다.
- 세그먼트 트리 → 초기화 과정 핵심 !!
- 값을 미리 계산하고 저장하여 불필요하게 여러 번 탐색하는 걸 줄여준다.
This post is licensed under CC BY 4.0 by the author.