Theme:

세그먼트 트리가 너무 복잡한데, 더 간단하게 구간 합을 처리할 수는 없을까요?

펜윅 트리란

펜윅 트리(Fenwick Tree), 또는 Binary Indexed Tree(BIT)는 배열의 접두사 합(prefix sum)을 효율적으로 관리 하는 자료구조입니다.

  • 점 업데이트: O(log n)
  • 접두사 합 쿼리: O(log n)
  • 구현이 세그먼트 트리보다 훨씬 간결합니다

1994년 Peter Fenwick이 발표했으며, 코드가 매우 짧은 것이 특징입니다.

왜 필요한가

세그먼트 트리가 있는데 펜윅 트리가 필요한 이유는 다음과 같습니다.

특성세그먼트 트리펜윅 트리
구현 난이도보통쉬움
코드 길이50~100줄10~20줄
메모리4nn+1
상수 계수보통** 작음**
구간 쿼리직접 지원접두사 합의 차로 계산
Lazy Propagation가능불편
구간 최솟값가능어려움

구간 합 문제만 다룬다면 펜윅 트리가 더 실용적입니다.

lowbit 연산 — 핵심 원리

펜윅 트리의 모든 것은 lowbit 연산에서 시작합니다.

JAVA
// i의 최하위 1비트 값을 구하는 연산
int lowbit(int i) {
    return i & (-i);
}

예시

PLAINTEXT
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]:

PLAINTEXT
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]까지의 합을 저장합니다.

PLAINTEXT
인덱스:  1    2    3    4    5    6    7    8
         │    │    │    │    │    │    │    │
bit[1] ──┘    │    │    │    │    │    │    │
bit[2] ───────┘    │    │    │    │    │    │
bit[3] ────────────┘    │    │    │    │    │
bit[4] ─────────────────┘    │    │    │    │
bit[5] ──────────────────────┘    │    │    │
bit[6] ───────────────────────────┘    │    │
bit[7] ────────────────────────────────┘    │
bit[8] ─────────────────────────────────────┘

구현

전체 코드가 놀라울 정도로 짧습니다.

JAVA
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);
    }
}

이것이 전부입니다. updatequery 각각 3줄이면 됩니다.

동작 원리 상세

query(7) — 접두사 합 계산

PLAINTEXT
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를 더합니다.

PLAINTEXT
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)

JAVA
FenwickTree ft = new FenwickTree(n);
for (int i = 1; i <= n; i++) {
    ft.update(i, arr[i]);
}

방법 2: 선형 구축 — O(n)

JAVA
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]인 쌍의 수를 구하는 문제입니다.

JAVA
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차원 배열에서의 구간 합도 처리할 수 있습니다.

JAVA
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 구간 합 등 다양한 문제에 응용할 수 있습니다.
댓글 로딩 중...