[백준 14428] 수열과 쿼리 16
[백준 14428] 수열과 쿼리 16
▪︎ 문제
▪︎ 알고리즘 설계
- 세그먼트 트리를 사용하여 각 노드에 해당 구간의 최솟값의 인덱스를 저장한다.
- 구간이 주어진 쿼리 시, 최솟값을 비교하는 것이 아니라 인덱스를 비교하여 트리 값을 관리한다.
▪︎ 코드
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
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
import java.io.*;
import java.util.*;
public class Main {
static int[] arr; // 실제 수열 값이 저장된 배열
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;
int N = Integer.parseInt(br.readLine());
arr = new int[N];
tree = new int[N * 4];
st = new StringTokenizer(br.readLine());
for (int i = 0; i < N; i++) {
arr[i] = Integer.parseInt(st.nextToken()); // 수열 원소 입력
}
init(1, 0, N - 1); // 초기화
int M = Integer.parseInt(br.readLine());
for (int i = 0; i < M; i++) {
st = new StringTokenizer(br.readLine());
int cmd = Integer.parseInt(st.nextToken()); // 쿼리 종류 번호
if (cmd == 1) {
int idx = Integer.parseInt(st.nextToken()) - 1; // 바꿀 수열의 원소 인덱스
int value = Integer.parseInt(st.nextToken()); // 바꿀 값
arr[idx] = value; // 배열 값 변경
update(1, 0, N - 1, idx); // 트리 값 변경
} else {
int l = Integer.parseInt(st.nextToken()) - 1; // 수열 원소 시작 인덱스
int r = Integer.parseInt(st.nextToken()) - 1; // 수열 원소 끝 인덱스
int result = query(1, 0, N - 1, l, r); // 크기가 가장 작은 값의 인덱스
bw.write((result + 1) + "\n");
}
}
bw.flush();
bw.close();
}
// 초기화
static int init(int node, int start, int end) {
if (start == end) {
return tree[node] = start; // 리프노드
}
int mid = (start + end) / 2;
// 좌/우 자식 노드 재귀 호출
int leftIdx = init(node * 2, start, mid);
int rightIdx = init(node * 2 + 1, mid + 1, end);
// 더 작은 값을 가진 인덱스를 현재 노드에 저장
if (arr[leftIdx] < arr[rightIdx]) return tree[node] = leftIdx;
else if (arr[leftIdx] > arr[rightIdx]) return tree[node] = rightIdx;
else return tree[node] = Math.min(leftIdx, rightIdx); // 값 같으면 인덱스 작은 것
}
// 값 변경
static int update(int node, int start, int end, int idx) {
// 변경 대상 인덱스가 현재 노드가 담당하는 구간 밖이면 무시
if (idx < start || idx > end) return tree[node];
// 리프노드
if (start == end) {
return tree[node] = idx;
}
int mid = (start + end) / 2;
// 좌/우 자식 노드를 재귀적으로 갱신
int leftIdx = update(node * 2, start, mid, idx);
int rightIdx = update(node * 2 + 1, mid + 1, end, idx);
// 더 작은 값을 가진 인덱스를 부모 노드에 저장
if (arr[leftIdx] < arr[rightIdx]) return tree[node] = leftIdx;
else if (arr[leftIdx] > arr[rightIdx]) return tree[node] = rightIdx;
else return tree[node] = Math.min(leftIdx, rightIdx);
}
// 구간 내에서 크기가 가장 작은 값의 인덱스
static int query(int node, int start, int end, int l, int r) {
if (r < start || end < l) return -1; // 현재 노드 구간이 [l, r]과 겹치지 않음
if (l <= start && end <= r) return tree[node]; // 현재 노드 구간이 [l, r]에 완전히 포함
int mid = (start + end) / 2;
// 좌/우 자식 노드를 재귀적으로 탐색
int leftIdx = query(node * 2, start, mid, l, r);
int rightIdx = query(node * 2 + 1, mid + 1, end, l, r);
// 양쪽 중 유효한 값만 있는 경우 처리
if (leftIdx == -1) return rightIdx;
if (rightIdx == -1) return leftIdx;
// 둘 다 유효할 경우 더 작은 값을 가진 인덱스를 반환
if (arr[leftIdx] < arr[rightIdx]) return leftIdx; // 왼쪽 구간에서의 최솟값이 오른쪽 값보다 작을 때
else if (arr[leftIdx] > arr[rightIdx]) return rightIdx; // 오른쪽 구간에서의 최솟값이 왼쪽 값보다 작을 때
else return Math.min(leftIdx, rightIdx); // 두 구간의 최솟값이 같은데 인덱스가 다를 경우
}
}
▪︎ 시간복잡도
O(log N)
▪︎ 틀린 이유
- 최솟값이 아니라 최솟값의 인덱스를 저장해야 한다.
- 단순히 값을 저장하면 동일한 값이 있을 때 문제가 된다. (값이 같을 때는 인덱스에 대한 조건임)
- 따라서 인덱스를 저장하고, 비교는 값으로 한다.
- 1-based / 0-based 인덱스 혼동
- 입력은 보통 1-based
- 배열 인덱스는 0-based →
-1
빼주는 처리 안 하면 IndexOutOfBounds 발생
▪︎ 느낀점 / 기억할 정보
- 세그먼트 트리를 초기화할 때는 문제에서 구하고자 하는 값을 저장한다.
- 트리 구조 설계 시, 리프 노드는 직접적인 데이터를, 내부 노드는 비교 결과(인덱스 등)를 저장하는 설계 방식을 사용한다.
This post is licensed under CC BY 4.0 by the author.