세그먼트 트리는 배열의 구간에 대한 정보를 빠르게 계산하고 업데이트하기 위한 트리 자료구조입니다. 주로 다음과 같은 작업을 효율적으로 수행할 수 있습니다:
세그먼트 트리는 특히 배열의 값이 자주 변하고, 구간 쿼리가 빈번하게 발생하는 상황에서 매우 유용합니다.
세그먼트 트리는 다음과 같은 규칙을 따릅니다:
다음은 구간 합을 위한 세그먼트 트리의 구현입니다:
#include <vector>
#include <iostream>
class SegmentTree {
private:
std::vector<int> tree;
int n;
// 세그먼트 트리 구축
void build(const std::vector<int>& arr, int node, int start, int end) {
// 리프 노드인 경우
if (start == end) {
tree[node] = arr[start];
return;
}
// 내부 노드인 경우
int mid = (start + end) / 2;
// 왼쪽 자식 노드 구축
build(arr, 2 * node, start, mid);
// 오른쪽 자식 노드 구축
build(arr, 2 * node + 1, mid + 1, end);
// 부모 노드 값 계산 (두 자식 노드의 합)
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
// 구간 합 쿼리
int query(int node, int start, int end, int left, int right) {
// 쿼리 범위가 현재 노드 범위와 겹치지 않는 경우
if (right < start || end < left) {
return 0;
}
// 쿼리 범위가 현재 노드 범위를 완전히 포함하는 경우
if (left <= start && end <= right) {
return tree[node];
}
// 쿼리 범위가 현재 노드 범위와 일부 겹치는 경우
int mid = (start + end) / 2;
int left_sum = query(2 * node, start, mid, left, right);
int right_sum = query(2 * node + 1, mid + 1, end, left, right);
return left_sum + right_sum;
}
// 값 업데이트
void update(int node, int start, int end, int idx, int val) {
// 리프 노드에 도달한 경우
if (start == end) {
tree[node] = val;
return;
}
int mid = (start + end) / 2;
if (idx <= mid) {
// 왼쪽 자식으로 이동
update(2 * node, start, mid, idx, val);
} else {
// 오른쪽 자식으로 이동
update(2 * node + 1, mid + 1, end, idx, val);
}
// 부모 노드 값 갱신
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
public:
SegmentTree(const std::vector<int>& arr) {
n = arr.size();
// 세그먼트 트리 배열의 크기는 4*n으로 설정 (충분한 공간)
tree.resize(4 * n, 0);
build(arr, 1, 0, n - 1);
}
// 간편한 인터페이스를 위한 래퍼 함수들
int get_sum(int left, int right) {
return query(1, 0, n - 1, left, right);
}
void update_value(int idx, int val) {
update(1, 0, n - 1, idx, val);
}
};
다음 배열을 사용하여 세그먼트 트리의 작동을 살펴보겠습니다:
arr = [1, 3, 5, 7, 9, 11]
세그먼트 트리를 구축하면 다음과 같은 구조가 됩니다:
이는 arr[2] + arr[3] + arr[4] = 5 + 7 + 9 = 21과 일치합니다.
최소값을 구하는 세그먼트 트리는 다음과 같이 구현할 수 있습니다:
#include <vector>
#include <iostream>
#include <climits>
#include <algorithm>
class MinSegmentTree {
private:
std::vector<int> tree;
int n;
void build(const std::vector<int>& arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
build(arr, 2 * node, start, mid);
build(arr, 2 * node + 1, mid + 1, end);
tree[node] = std::min(tree[2 * node], tree[2 * node + 1]);
}
int query(int node, int start, int end, int left, int right) {
if (right < start || end < left) {
return INT_MAX;
}
if (left <= start && end <= right) {
return tree[node];
}
int mid = (start + end) / 2;
int left_min = query(2 * node, start, mid, left, right);
int right_min = query(2 * node + 1, mid + 1, end, left, right);
return std::min(left_min, right_min);
}
void update(int node, int start, int end, int idx, int val) {
if (start == end) {
tree[node] = val;
return;
}
int mid = (start + end) / 2;
if (idx <= mid) {
update(2 * node, start, mid, idx, val);
} else {
update(2 * node + 1, mid + 1, end, idx, val);
}
tree[node] = std::min(tree[2 * node], tree[2 * node + 1]);
}
public:
MinSegmentTree(const std::vector<int>& arr) {
n = arr.size();
tree.resize(4 * n, INT_MAX);
build(arr, 1, 0, n - 1);
}
int get_min(int left, int right) {
return query(1, 0, n - 1, left, right);
}
void update_value(int idx, int val) {
update(1, 0, n - 1, idx, val);
}
};
대규모 구간 업데이트가 필요한 경우, 지연 전파(Lazy Propagation) 기법을 사용하여 효율성을 높일 수 있습니다:
#include <vector>
#include <iostream>
class LazySegmentTree {
private:
std::vector<int> tree, lazy;
int n;
void build(const std::vector<int>& arr, int node, int start, int end) {
if (start == end) {
tree[node] = arr[start];
return;
}
int mid = (start + end) / 2;
build(arr, 2 * node, start, mid);
build(arr, 2 * node + 1, mid + 1, end);
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
void propagate(int node, int start, int end) {
if (lazy[node] != 0) {
// 현재 노드에 지연된 업데이트 적용
tree[node] += (end - start + 1) * lazy[node];
// 리프 노드가 아니면 자식 노드로 지연 전파
if (start != end) {
lazy[2 * node] += lazy[node];
lazy[2 * node + 1] += lazy[node];
}
// 현재 노드의 지연 값 초기화
lazy[node] = 0;
}
}
void update_range(int node, int start, int end, int left, int right, int val) {
// 지연된 업데이트 먼저 처리
propagate(node, start, end);
// 업데이트 범위와 겹치지 않는 경우
if (right < start || end < left) {
return;
}
// 업데이트 범위가 현재 노드 범위를 완전히 포함하는 경우
if (left <= start && end <= right) {
// 현재 노드에 업데이트 적용
tree[node] += (end - start + 1) * val;
// 리프 노드가 아니면 자식 노드로 지연 전파
if (start != end) {
lazy[2 * node] += val;
lazy[2 * node + 1] += val;
}
return;
}
// 업데이트 범위가 현재 노드 범위와 일부 겹치는 경우
int mid = (start + end) / 2;
update_range(2 * node, start, mid, left, right, val);
update_range(2 * node + 1, mid + 1, end, left, right, val);
// 부모 노드 값 갱신
tree[node] = tree[2 * node] + tree[2 * node + 1];
}
int query(int node, int start, int end, int left, int right) {
// 지연된 업데이트 먼저 처리
propagate(node, start, end);
// 쿼리 범위가 현재 노드 범위와 겹치지 않는 경우
if (right < start || end < left) {
return 0;
}
// 쿼리 범위가 현재 노드 범위를 완전히 포함하는 경우
if (left <= start && end <= right) {
return tree[node];
}
// 쿼리 범위가 현재 노드 범위와 일부 겹치는 경우
int mid = (start + end) / 2;
int left_sum = query(2 * node, start, mid, left, right);
int right_sum = query(2 * node + 1, mid + 1, end, left, right);
return left_sum + right_sum;
}
public:
LazySegmentTree(const std::vector<int>& arr) {
n = arr.size();
tree.resize(4 * n, 0);
lazy.resize(4 * n, 0);
build(arr, 1, 0, n - 1);
}
// 간편한 인터페이스를 위한 래퍼 함수들
void range_update(int left, int right, int val) {
update_range(1, 0, n - 1, left, right, val);
}
int get_sum(int left, int right) {
return query(1, 0, n - 1, left, right);
}
};
세그먼트 트리는 다음과 같은 문제에서 자주 활용됩니다: