레이지 프로파게이션 (Lazy Propagation)
정의
레이지 프로파게이션 (Lazy Propagation) 은 세그먼트 트리의 확장으로, 구간 갱신 (range update) 와 구간 쿼리 (range query) 를 모두 O(log N) 에 처리.
“lazy” 의 핵심: 갱신을 즉시 자식에게 전파하지 않고, 필요할 때만 (쿼리/갱신 시) 전파. 각 노드에 lazy 배열 (pending update) 을 두어 “아직 자식에게 전달 안 한 갱신” 을 캐싱.
기본 세그먼트 트리는 점 갱신 O(log N), 구간 갱신은 O(N log N). 레이지 프로파게이션으로 구간 갱신도 O(log N) 로 단축.
문제 상황과 동기
길이 N 배열에서:
- 구간 갱신:
a[l..r] += k(또는= k) - 구간 쿼리:
sum(l, r),min(l, r),max(l, r)등
| 방식 | 구간 갱신 | 구간 쿼리 | 비고 |
|---|---|---|---|
| naive | O(N) | O(N) | 직접 순회 |
| [[Segtree | Segtree]] (점 갱신) | O(N log N) | O(log N) |
| Lazy Propagation | O(log N) | O(log N) | 균형 |
핵심 통찰: 구간 [l, r] 을 덮는 O(log N) 개 노드에만 표시. 자식은 나중에 필요할 때 계산 (lazy).
PS에서 “구간에 +k / 구간 합” 같은 갱신+쿼리 동시 요구 → 레이지 프로파게이션 필수.
시각화
핵심 아이디어
invariant: 노드 값 = 현재까지 전파된 갱신의 결과, lazy[node] = 아직 자식에게 안 보낸 갱신.
Lazy 배열:
lazy[node] = 이 노드 관할 구간에 적용할 pending 값
전파 (propagate):
if lazy[node] ≠ 0:
tree[node] += lazy[node] * (구간 길이)
if 리프 아님:
lazy[2*node] += lazy[node]
lazy[2*node+1] += lazy[node]
lazy[node] = 0
구간 갱신 [L, R] += val:
- 현재 노드 [l, r] 이 [L, R] 에 완전히 포함 → lazy[node] 에만 표시하고 종료.
- 겹치지 않으면 스킵.
- 부분 겹침 → 전파 후 왼쪽/오른쪽 재귀.
구간 쿼리 [L, R]:
- 현재 노드 접근 전 lazy 전파.
- 나머지는 기본 세그먼트 트리와 동일.
알고리즘
전파
propagate(node, l, r):
if lazy[node] = 0: return
tree[node] += lazy[node] * (r - l + 1)
if l ≠ r:
lazy[2*node] += lazy[node]
lazy[2*node+1] += lazy[node]
lazy[node] = 0
구간 갱신
range_update(node, l, r, L, R, val):
propagate(node, l, r)
if r < L or R < l: return
if L ≤ l and r ≤ R:
lazy[node] += val
propagate(node, l, r)
return
mid = (l + r) / 2
range_update(2*node, l, mid, L, R, val)
range_update(2*node+1, mid+1, r, L, R, val)
tree[node] = tree[2*node] + tree[2*node+1]
구간 쿼리
range_query(node, l, r, L, R):
if r < L or R < l: return 0
propagate(node, l, r)
if L ≤ l and r ≤ R:
return tree[node]
mid = (l + r) / 2
left = range_query(2*node, l, mid, L, R)
right = range_query(2*node+1, mid+1, r, L, R)
return left + right
구현
// 구간 합 + 구간 갱신 레이지 프로파게이션
#include <bits/stdc++.h>
using namespace std;
typedef long long ll;
class LazySegTree {
vector<ll> tree, lazy;
int n;
void propagate(int node, int l, int r) {
if (lazy[node] == 0) return;
tree[node] += lazy[node] * (r - l + 1);
if (l != r) {
lazy[2*node] += lazy[node];
lazy[2*node+1] += lazy[node];
}
lazy[node] = 0;
}
void build(vector<ll>& a, int node, int l, int r) {
if (l == r) {
tree[node] = a[l];
} else {
int mid = (l + r) / 2;
build(a, 2*node, l, mid);
build(a, 2*node+1, mid+1, r);
tree[node] = tree[2*node] + tree[2*node+1];
}
}
void update(int node, int l, int r, int L, int R, ll val) {
propagate(node, l, r);
if (r < L || R < l) return;
if (L <= l && r <= R) {
lazy[node] += val;
propagate(node, l, r);
return;
}
int mid = (l + r) / 2;
update(2*node, l, mid, L, R, val);
update(2*node+1, mid+1, r, L, R, val);
tree[node] = tree[2*node] + tree[2*node+1];
}
ll query(int node, int l, int r, int L, int R) {
if (r < L || R < l) return 0;
propagate(node, l, r);
if (L <= l && r <= R) return tree[node];
int mid = (l + r) / 2;
return query(2*node, l, mid, L, R) + query(2*node+1, mid+1, r, L, R);
}
public:
LazySegTree(vector<ll>& a) : n(a.size() - 1), tree(4 * n), lazy(4 * n) {
if (n > 0) build(a, 1, 1, n);
}
void update(int l, int r, ll val) { update(1, 1, n, l, r, val); }
ll query(int l, int r) { return query(1, 1, n, l, r); }
};
int main() {
ios::sync_with_stdio(0); cin.tie(0);
int n, q; cin >> n >> q;
vector<ll> a(n + 1);
for (int i = 1; i <= n; i++) cin >> a[i];
LazySegTree seg(a);
while (q--) {
int t; cin >> t;
if (t == 1) {
int l, r; ll val; cin >> l >> r >> val;
seg.update(l, r, val);
} else {
int l, r; cin >> l >> r;
cout << seg.query(l, r) << "\n";
}
}
}5 5
1 2 3 4 5
2 1 3
1 2 4 3
2 1 3
2 2 5
1 1 5 -16
15
21복잡도
| 항목 | 값 |
|---|---|
| 빌드 | O(N) |
| 구간 쿼리 | O(log N) |
| 구간 갱신 | O(log N) |
| 공간 | O(N), 실제 4N tree + 4N lazy |
각 연산이 O(log N) 개 노드만 방문. lazy 덕분에 구간 갱신도 log 시간.
변형 / 활용
1. 구간 대입 (range set)
a[l..r] = k (덮어쓰기).
// lazy 의미: "이 노드 구간 전부를 k 로"
propagate(node, l, r):
if lazy[node] == -1: return // -1 = no pending
tree[node] = lazy[node] * (r - l + 1)
if l ≠ r:
lazy[2*node] = lazy[node]
lazy[2*node+1] = lazy[node]
lazy[node] = -1
초기 lazy = -1 (또는 flag).
2. Min / Max 구간 갱신
a[l..r] += k 후 구간 최솟값.
tree[node] = min(tree[2*node], tree[2*node+1]);
propagate:
tree[node] += lazy[node];
// min 이므로 +k 는 단순 shift
3. 2D 레이지 프로파게이션
2D 세그먼트 트리 + lazy. 구간 사각형 갱신/쿼리 O(log² N).
4. 구간 곱셈 + 구간 덧셈
두 종류 갱신 (곱 / 덧셈) 을 동시에. lazy 두 개 (mul, add).
propagate:
tree[node] = tree[node] * lazy_mul + lazy_add * (r - l + 1)
if l ≠ r:
lazy_mul[2*node] *= lazy_mul[node]
lazy_add[2*node] = lazy_add[2*node] * lazy_mul[node] + lazy_add[node]
// 오른쪽도 동일
갱신 순서 주의: 먼저 곱, 나중에 덧셈.
함정
1. 전파 순서
range_update / range_query 진입 시 반드시 propagate 먼저. 안 하면 lazy 가 쌓여서 틀린 답.
2. 리프 여부
if (l != r) 로 자식 존재 확인. 리프에서는 자식 갱신 금지.
3. lazy 초기값
구간 덧셈: 0, 구간 대입: -1 또는 flag. 잘못 초기화하면 첫 쿼리부터 오답.
4. long long 범위
N=10^5, val=10^9, Q=10^5 구간 갱신 → 합이 매우 큼. C++ 에서 long long 필수.
5. 갱신 타입 혼용
덧셈 / 대입 / 곱셈을 동시에 쓸 때 lazy 구조 설계 복잡. 각 연산의 결합 법칙 / 교환 법칙 확인 필수.
BOJ 연습 문제
| 번호 | 제목 | 정답률 | 링크 |
|---|---|---|---|
| BOJ 10999 | 구간 합 구하기 2 | 27.5% | kokoa-lab |
| BOJ 12844 | XOR | 30.8% | kokoa-lab |
| BOJ 16975 | 수열과 쿼리 21 | 36.2% | kokoa-lab |
| BOJ 14427 | 수열과 쿼리 15 | 32.1% | kokoa-lab |
참고
이 글의 용어 (3개)
- 세그먼트 트리 (Segment Tree)algorithm
- 정의 세그먼트 트리 (Segment Tree) 는 배열의 구간 쿼리 (range query) 와 점 갱신 (point update) 를 모두 O(log N) 에 처리하는 이진 트…
- 차분 배열 (Difference Array)algorithm
- 정의 차분 배열 (Difference Array) 은 배열 의 인접 차 (adjacent difference) 를 저장하는 배열 (단, ). 원 배열은 , 즉 d 의 누적 합 (…
- Fenwick Tree (Binary Indexed Tree): 구간 합 O(log N)algorithm
- 정의 Fenwick Tree (또는 BIT, Binary Indexed Tree) 는 배열의 prefix sum 을 O(log N) 에 갱신·조회 하는 자료구조입니다. Peter…
💬 댓글