펜윅 트리 — 세그먼트 트리보다 간결한 구간 쿼리
세그먼트 트리가 너무 복잡한데, 더 간단하게 구간 합을 처리할 수는 없을까요?
펜윅 트리란
펜윅 트리(Fenwick Tree), 또는 Binary Indexed Tree(BIT)는 배열의 접두사 합(prefix sum)을 효율적으로 관리 하는 자료구조입니다.
- 점 업데이트: O(log n)
- 접두사 합 쿼리: O(log n)
- 구현이 세그먼트 트리보다 훨씬 간결합니다
1994년 Peter Fenwick이 발표했으며, 코드가 매우 짧은 것이 특징입니다.
왜 필요한가
세그먼트 트리가 있는데 펜윅 트리가 필요한 이유는 다음과 같습니다.
| 특성 | 세그먼트 트리 | 펜윅 트리 |
|---|---|---|
| 구현 난이도 | 보통 | 쉬움 |
| 코드 길이 | 50~100줄 | 10~20줄 |
| 메모리 | 4n | n+1 |
| 상수 계수 | 보통 | ** 작음** |
| 구간 쿼리 | 직접 지원 | 접두사 합의 차로 계산 |
| Lazy Propagation | 가능 | 불편 |
| 구간 최솟값 | 가능 | 어려움 |
구간 합 문제만 다룬다면 펜윅 트리가 더 실용적입니다.
lowbit 연산 — 핵심 원리
펜윅 트리의 모든 것은 lowbit 연산에서 시작합니다.
// i의 최하위 1비트 값을 구하는 연산
int lowbit(int i) {
return i & (-i);
}
예시
i = 12 = 1100₂
-i = 0100₂ (2의 보수)
i & -i = 0100₂ = 4
i = 6 = 0110₂
-i = 1010₂
i & -i = 0010₂ = 2
i = 7 = 0111₂
-i = 1001₂
i & -i = 0001₂ = 1
lowbit(i)는 i가 ** 담당하는 구간의 길이 **를 의미합니다.
구조 이해
배열 a[1..8]에 대한 펜윅 트리 bit[1..8]:
bit[1] = a[1] (lowbit(1)=1, 1개)
bit[2] = a[1] + a[2] (lowbit(2)=2, 2개)
bit[3] = a[3] (lowbit(3)=1, 1개)
bit[4] = a[1] + a[2] + a[3] + a[4] (lowbit(4)=4, 4개)
bit[5] = a[5] (lowbit(5)=1, 1개)
bit[6] = a[5] + a[6] (lowbit(6)=2, 2개)
bit[7] = a[7] (lowbit(7)=1, 1개)
bit[8] = a[1] + ... + a[8] (lowbit(8)=8, 8개)
bit[i]는 a[i - lowbit(i) + 1]부터 a[i]까지의 합을 저장합니다.
인덱스: 1 2 3 4 5 6 7 8
│ │ │ │ │ │ │ │
bit[1] ──┘ │ │ │ │ │ │ │
bit[2] ───────┘ │ │ │ │ │ │
bit[3] ────────────┘ │ │ │ │ │
bit[4] ─────────────────┘ │ │ │ │
bit[5] ──────────────────────┘ │ │ │
bit[6] ───────────────────────────┘ │ │
bit[7] ────────────────────────────────┘ │
bit[8] ─────────────────────────────────────┘
구현
전체 코드가 놀라울 정도로 짧습니다.
public class FenwickTree {
private int[] bit;
private int n;
public FenwickTree(int n) {
this.n = n;
this.bit = new int[n + 1]; // 1-indexed
}
// a[i]에 delta를 더함: O(log n)
public void update(int i, int delta) {
for (; i <= n; i += i & (-i)) {
bit[i] += delta;
}
}
// a[1] + a[2] + ... + a[i] 접두사 합: O(log n)
public int query(int i) {
int sum = 0;
for (; i > 0; i -= i & (-i)) {
sum += bit[i];
}
return sum;
}
// a[l] + a[l+1] + ... + a[r] 구간 합
public int rangeQuery(int l, int r) {
return query(r) - query(l - 1);
}
}
이것이 전부입니다. update와 query 각각 3줄이면 됩니다.
동작 원리 상세
query(7) — 접두사 합 계산
i = 7 (0111₂): bit[7] 더하기 → i -= lowbit(7)=1 → i=6
i = 6 (0110₂): bit[6] 더하기 → i -= lowbit(6)=2 → i=4
i = 4 (0100₂): bit[4] 더하기 → i -= lowbit(4)=4 → i=0
i = 0: 종료
결과: bit[7] + bit[6] + bit[4]
= a[7] + (a[5]+a[6]) + (a[1]+a[2]+a[3]+a[4])
= a[1] + a[2] + a[3] + a[4] + a[5] + a[6] + a[7]
lowbit을 빼면서 이동하면, 겹치지 않는 구간들을 정확히 합칩니다.
update(3, +5) — 값 업데이트
인덱스 3에 5를 더합니다.
i = 3 (0011₂): bit[3] += 5 → i += lowbit(3)=1 → i=4
i = 4 (0100₂): bit[4] += 5 → i += lowbit(4)=4 → i=8
i = 8 (1000₂): bit[8] += 5 → i += lowbit(8)=8 → i=16
i = 16 > n: 종료
lowbit을 더하면서 이동하면, 해당 인덱스를 포함하는 모든 구간을 업데이트합니다.
배열로부터 펜윅 트리 구축
방법 1: 하나씩 update — O(n log n)
FenwickTree ft = new FenwickTree(n);
for (int i = 1; i <= n; i++) {
ft.update(i, arr[i]);
}
방법 2: 선형 구축 — O(n)
public FenwickTree(int[] arr) {
n = arr.length;
bit = new int[n + 1];
// 배열 복사
for (int i = 1; i <= n; i++) {
bit[i] = arr[i - 1]; // 0-indexed → 1-indexed
}
// 부모에게 값 전파
for (int i = 1; i <= n; i++) {
int parent = i + (i & (-i));
if (parent <= n) {
bit[parent] += bit[i];
}
}
}
활용 예제
역전 카운트 (Inversion Count)
배열에서 i < j이고 a[i] > a[j]인 쌍의 수를 구하는 문제입니다.
public long countInversions(int[] arr) {
int n = arr.length;
// 좌표 압축 (값의 범위를 1~n으로)
int[] sorted = arr.clone();
Arrays.sort(sorted);
Map<Integer, Integer> rank = new HashMap<>();
for (int i = 0; i < n; i++) {
rank.put(sorted[i], i + 1);
}
FenwickTree ft = new FenwickTree(n);
long inversions = 0;
// 뒤에서부터 순회
for (int i = n - 1; i >= 0; i--) {
int r = rank.get(arr[i]);
// 현재 값보다 작은 값 중 이미 등장한 것의 수
inversions += ft.query(r - 1);
ft.update(r, 1);
}
return inversions;
}
2D 펜윅 트리
2차원 배열에서의 구간 합도 처리할 수 있습니다.
public class FenwickTree2D {
private int[][] bit;
private int rows, cols;
public FenwickTree2D(int rows, int cols) {
this.rows = rows;
this.cols = cols;
this.bit = new int[rows + 1][cols + 1];
}
public void update(int r, int c, int delta) {
for (int i = r; i <= rows; i += i & (-i)) {
for (int j = c; j <= cols; j += j & (-j)) {
bit[i][j] += delta;
}
}
}
public int query(int r, int c) {
int sum = 0;
for (int i = r; i > 0; i -= i & (-i)) {
for (int j = c; j > 0; j -= j & (-j)) {
sum += bit[i][j];
}
}
return sum;
}
}
세그먼트 트리와의 트레이드오프
| 상황 | 추천 |
|---|---|
| 구간 합 쿼리 + 점 업데이트 | ** 펜윅 트리** (간결, 빠름) |
| 구간 최솟값/최댓값 | ** 세그먼트 트리** |
| 구간 업데이트 + 구간 쿼리 | ** 세그먼트 트리** (Lazy) |
| 메모리 제한이 빡빡 | ** 펜윅 트리** (n+1 vs 4n) |
| 빠른 구현이 필요 | ** 펜윅 트리** |
정리
- 펜윅 트리는 lowbit 연산 을 이용해 접두사 합을 O(log n)에 계산하는 자료구조입니다.
- 구현이 10~20줄로 매우 간결하며, 메모리도 세그먼트 트리의 1/4 수준입니다.
update는 lowbit을 더하며 위로,query는 lowbit을 빼며 아래로 이동합니다.- 구간 합 + 점 업데이트에 최적이지만, 구간 최솟값 등은 세그먼트 트리가 필요합니다.
- 역전 카운트, 2D 구간 합 등 다양한 문제에 응용할 수 있습니다.
댓글 로딩 중...