Workspace IndexAlgorithms › Day 10

Advanced Segment Trees TODO

Algorithms · Day 10 / 100 · A. Advanced Algorithms & Data Structures (Day 1-19)

Concept

A segment tree is a binary-tree structure that answers queries and updates over an associative operation on array ranges (sum, min, gcd, and so on) in O(log n). Lazy propagation handles whole-range updates by storing, at each node, a pending operation that hasn't yet been pushed down to its children, and only pushing it down when that child is actually visited — which keeps range updates at O(log n) too. For this to work, pending operations must be composable, and it must be defined how they affect a node's stored value depending on the range size (for a range-add, for instance, you add delta times the range length to the sum). A persistent segment tree creates only the O(log n) nodes along the root-to-leaf path on each update and shares the rest of the subtrees with the previous version via path copying, preserving every past version at only O(log n) extra space per update. That makes it possible to query any past version, which is widely used for finding the k-th element or computing a rank within a range.

For state aggregation that needs rollback or point-in-time snapshots, and for workloads with heavy range updates, a naive implementation collapses under O(n) cost per update.

Code & Formula

# Day 10: 세그먼트 트리 심화 — Lazy Propagation으로 구간 갱신·구간 합을 O(log n)에 처리
# 보류값(lazy)을 노드에 쌓아두고 실제 방문 시점에만 자식으로 밀어내려 구간 갱신 비용을 낮춘다.

class LazySegTree:
    def __init__(self, n):
        self.sum = [0] * (4 * n)
        self.lazy = [0] * (4 * n)

    def _push_down(self, node, lo, hi):
        if self.lazy[node] == 0:
            return
        mid = (lo + hi) // 2
        for child, clo, chi in ((node * 2, lo, mid), (node * 2 + 1, mid + 1, hi)):
            self.lazy[child] += self.lazy[node]
            self.sum[child] += self.lazy[node] * (chi - clo + 1)
        self.lazy[node] = 0

    def range_add(self, node, lo, hi, l, r, delta):
        if r < lo or hi < l:
            return
        if l <= lo and hi <= r:
            self.sum[node] += delta * (hi - lo + 1)
            self.lazy[node] += delta
            return
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        self.range_add(node * 2, lo, mid, l, r, delta)
        self.range_add(node * 2 + 1, mid + 1, hi, l, r, delta)
        self.sum[node] = self.sum[node * 2] + self.sum[node * 2 + 1]

    def range_sum(self, node, lo, hi, l, r):
        if r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return self.sum[node]
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        return (self.range_sum(node * 2, lo, mid, l, r) +
                self.range_sum(node * 2 + 1, mid + 1, hi, l, r))

n = 10
tree = LazySegTree(n)
tree.range_add(1, 0, n - 1, 2, 5, 3)   # 인덱스 2~5에 +3
tree.range_add(1, 0, n - 1, 0, 9, 1)   # 전체 구간에 +1
print("구간 합 [0,9] =", tree.range_sum(1, 0, n - 1, 0, 9), "(기대값: 3*4 + 1*10 = 22)")
print("구간 합 [2,5] =", tree.range_sum(1, 0, n - 1, 2, 5), "(기대값: (3+1)*4 = 16)")
print("구간 합 [6,9] =", tree.range_sum(1, 0, n - 1, 6, 9), "(기대값: 1*4 = 4)")

Exercise

Implement a lazy segment tree that supports range-add and range-sum, then build a persistent version of the same operations, and cross-check queries against arbitrary past versions with randomized tests against a brute-force reference.

Practical Connection

When an indexer has to roll back to the aggregated state at a specific block height due to a chain reorg, a version-sharing structure lets it maintain snapshots at O(log n) cost instead of copying everything.

If you study this on a given day, add a note link and a ✅ to this line in the source curriculum (docs/knowledge/dev-100-curriculum.md) and this spot will lead straight to the note body. You can also write directly on this page — but regenerating overwrites it, so it's safer to keep anything you want to save as markdown under docs/algorithms/.


한국어

세그먼트 트리 심화 TODO

Algorithms · Day 10 / 100 · A. 고급 알고리즘·자료구조 (Day 1–19)

lazy propagation·영속 세그먼트 트리

개념

세그먼트 트리는 배열 구간에 대해 결합법칙이 성립하는 연산(합, 최솟값, gcd 등)을 O(log n)에 질의·갱신하는 이진 트리 구조다. Lazy propagation은 구간 전체를 갱신할 때 각 노드에 "아직 자식에게 내려보내지 않은 연산"을 보류값으로 저장하고, 그 자식을 실제로 방문하는 시점에 밀어내려 구간 갱신도 O(log n)에 처리한다. 이 기법이 성립하려면 보류 연산끼리 합성 가능해야 하고, 구간 크기에 따라 노드 값에 어떻게 반영되는지가 정의되어야 한다(예: 구간 덧셈이면 합에 delta 곱하기 구간 길이를 더한다). 영속(persistent) 세그먼트 트리는 갱신 시 루트에서 잎까지의 경로에 있는 O(log n)개 노드만 새로 만들고 나머지 서브트리는 이전 버전과 공유하는 path copying 기법으로, 모든 과거 버전을 O(log n) 추가 공간에 보존한다. 덕분에 임의 시점 버전에 대한 질의가 가능해져 구간 내 k번째 원소나 랭크 질의에 널리 쓰인다.

롤백이나 시점 스냅샷이 필요한 상태 집계, 그리고 구간 갱신이 대량으로 들어오는 워크로드에서 naive 구현은 갱신당 O(n)으로 무너진다.

코드 · 수식

# Day 10: 세그먼트 트리 심화 — Lazy Propagation으로 구간 갱신·구간 합을 O(log n)에 처리
# 보류값(lazy)을 노드에 쌓아두고 실제 방문 시점에만 자식으로 밀어내려 구간 갱신 비용을 낮춘다.

class LazySegTree:
    def __init__(self, n):
        self.sum = [0] * (4 * n)
        self.lazy = [0] * (4 * n)

    def _push_down(self, node, lo, hi):
        if self.lazy[node] == 0:
            return
        mid = (lo + hi) // 2
        for child, clo, chi in ((node * 2, lo, mid), (node * 2 + 1, mid + 1, hi)):
            self.lazy[child] += self.lazy[node]
            self.sum[child] += self.lazy[node] * (chi - clo + 1)
        self.lazy[node] = 0

    def range_add(self, node, lo, hi, l, r, delta):
        if r < lo or hi < l:
            return
        if l <= lo and hi <= r:
            self.sum[node] += delta * (hi - lo + 1)
            self.lazy[node] += delta
            return
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        self.range_add(node * 2, lo, mid, l, r, delta)
        self.range_add(node * 2 + 1, mid + 1, hi, l, r, delta)
        self.sum[node] = self.sum[node * 2] + self.sum[node * 2 + 1]

    def range_sum(self, node, lo, hi, l, r):
        if r < lo or hi < l:
            return 0
        if l <= lo and hi <= r:
            return self.sum[node]
        self._push_down(node, lo, hi)
        mid = (lo + hi) // 2
        return (self.range_sum(node * 2, lo, mid, l, r) +
                self.range_sum(node * 2 + 1, mid + 1, hi, l, r))

n = 10
tree = LazySegTree(n)
tree.range_add(1, 0, n - 1, 2, 5, 3)   # 인덱스 2~5에 +3
tree.range_add(1, 0, n - 1, 0, 9, 1)   # 전체 구간에 +1
print("구간 합 [0,9] =", tree.range_sum(1, 0, n - 1, 0, 9), "(기대값: 3*4 + 1*10 = 22)")
print("구간 합 [2,5] =", tree.range_sum(1, 0, n - 1, 2, 5), "(기대값: (3+1)*4 = 16)")
print("구간 합 [6,9] =", tree.range_sum(1, 0, n - 1, 6, 9), "(기대값: 1*4 = 4)")

연습

구간 덧셈과 구간 합을 지원하는 lazy 세그먼트 트리를 구현한 뒤, 같은 연산을 영속 버전으로도 만들어 임의 과거 버전 질의 결과를 무작위 테스트로 브루트포스와 대조 검증하라.

실무 · Verex 연결

인덱서가 체인 재구성으로 특정 블록 높이 시점의 집계 상태로 되돌아가야 할 때, 버전 공유형 자료구조는 전체 복사 대신 O(log n) 비용으로 스냅샷을 유지하게 해준다.

공부한 날 원본 커리큘럼(docs/knowledge/dev-100-curriculum.md)의 이 줄에 노트 링크와 ✅ 를 붙이면, 이 자리는 노트 본문으로 바로 이어집니다. 노트 없이 이 페이지에 바로 적어도 됩니다 — 다만 다시 생성하면 덮어쓰이므로, 남길 글은 docs/algorithms/ 의 마크다운으로 쓰는 편이 안전합니다.

← 9. 문자열 인덱스11. 위상정렬·DAG 스케줄링 →