Segment Tree에서 Lazy Propagation(레이지 프로파게이션, 늦은 전파?)에 대해 알아보겠습니다
세그먼트 트리 중 RSQ(Range Sum Query) segment tree를 예로 들어보겠습니다.
Lazy Propagation 개념 및 예제
배열이 주어지고, 배열에서 아래 두 연산을 수행해야 하는 문제가 있습니다. (ex. 10999: 구간 합 구하기 2)
- Query: [l, r]의 합을 출력하기
- Update: [l, r]의 범위에 a라는 값을 더하기
- arr[l] += a, arr[l+1] += a, ..., arr[r] += a
세그먼트 트리를 쓰면 Query는 O(lgN)의 시간복잡도를 가집니다.
Update가 문제인데, 세그먼트 트리에서 수 하나를 변경하려면 O(lgN)의 시간이 걸리니, l-r+1개를 업데이트할 때는 O(NlgN)의 시간이 걸립니다. update를 여러번 하면 시간초과가 나올 겁니다.
Lazy Propagation은 구간 업데이트를 할 때, 당장은 리프 노드까지 이어지는 모든 노드를 업데이트 할 필요가 없다는 것에 아이디어를 착안합니다.
즉 [l, r]에 a씩 더하는 update 쿼리가 들어오면, 노드 표현 범위에 [l, r]에 완전히 포함되는 노드 중 부모 노드와 루트부터 부모노드까지의 노드 값만 업데이트 합니다. 업데이트한 노드의 자식 노드들은 아직 업데이트를 하지 않고 미뤄둡니다
업데이트 한 노드의 자식 노드의 업데이트는 나중에 필요할 때 하기로 하고, 그 값을 lazy[i]에 적어둡니다.
Fig 2.를 보시면 [l, r]에 포함되는 노드의 부모 노드인 파란색 노드와 루트부터 파란색 노드까지 방문하는 회색 노드만 합의 값이 업데이트 된 것을 확인할 수 있습니다.
이후에는 항상 어떤 노드를 방문할 때마다 lazy 값이 있는지 검사하고, lazy 값이 0이 아니라면 현재 노드에 해당하는 값을 노드의 표현 구간이 [start, end]라 할 때 (end-start+1)를 곱해서 더한 다음, 자식 노드에 lazy 값을 물려줘야 합니다. 이 과정을 propagation이라고 합니다.
propagation을 필요할 때, 나중에(lazy) 하기 때문에 lazy propagation이라고 합니다.
Lazy Propagation을 사용하면 범위 update 쿼리도 O(lgN)에 처리할 수 있다는 이점이 있습니다.
단점이라면 세그먼트 트리 만들때 사용한 배열과 동일한 크기의 lazy 배열이 필요해 메모리를 좀 더 쓴다는 겁니다.
소스코드
/////////////////////////////////////////////////////////////////////////
// RSQ with Lazy Propagation
template <typename T>
struct RSQ {
int n;
vector<long long> rangeSum;
vector<long long> lazy;
RSQ(const vector<T>& arr) {
n = arr.size();
rangeSum.resize(n * 4);
lazy.resize(n * 4);
init(arr, 0, n - 1, 1);
}
// arr[left, right]를 초기화
T init(const vector<T>& arr, int left, int right, int node) {
if (left == right)
return rangeSum[node] = arr[left];
int mid = (left + right) / 2;
T left_sum = init(arr, left, mid, node * 2);
T right_sum = init(arr, mid + 1, right, node * 2 + 1);
return rangeSum[node] = left_sum + right_sum;
}
// node의 표현 범위 arr[nodeLeft, nodeRight]가 주어졌을 때
// 쿼리의 arr[left, right]의 교집합 sum을 구함
T query(int left, int right,
int node, int nodeLeft, int nodeRight) {
// lazy propagation
propagate(node, nodeLeft, nodeRight);
// 두 구간이 겹치지 않은 경우
if (right < nodeLeft || nodeRight < left) return 0;
// node의 표현 범위가 query에 완전히 포함되는 경우
if (left <= nodeLeft && nodeRight <= right) return rangeSum[node];
int mid = (nodeLeft + nodeRight) / 2;
return query(left, right, node * 2, nodeLeft, mid) +
query(left, right, node * 2 + 1, mid + 1, nodeRight);
}
T query(int left, int right) {
return query(left, right, 1, 0, n - 1);
}
void propagate(int node, int nodeLeft, int nodeRight) {
if (lazy[node] != 0) {
rangeSum[node] += (nodeRight - nodeLeft + 1) * lazy[node];
// leaf가 아니면
if (nodeLeft != nodeRight) {
lazy[node * 2] += lazy[node];
lazy[node * 2 + 1] += lazy[node];
}
lazy[node] = 0;
}
}
// [left, right]에 val을 더함 (lazy propagation)
T update_range(int left, int right, T val,
int node, int nodeLeft, int nodeRight) {
// lazy propagation
propagate(node, nodeLeft, nodeRight);
// 두 구간이 겹치지 않는 경우
if (right < nodeLeft || nodeRight < left) return rangeSum[node];
// node 표현범위에 [left, right]가 완전히 포함될 경우 자식 노드는 lazy 배열에 넣음
if (left <= nodeLeft && nodeRight <= right) {
rangeSum[node] += val * (nodeRight-nodeLeft+1);
if (nodeLeft != nodeRight) {
lazy[node * 2] += val;
lazy[node * 2 + 1] += val;
}
return rangeSum[node];
}
int mid = (nodeLeft + nodeRight) / 2;
return rangeSum[node] = update_range(left, right, val, node*2, nodeLeft, mid) +
update_range(left, right, val, node*2+1, mid+1, nodeRight);
}
// [left, right]에 val을 더함 (lazy propagation)
T update_range(int left, int right, T val) {
return update_range(left, right, val, 1, 0, n-1);
}
};
소스입니다.
update를 update_range로 바꿨고, propagate 메소드가 추가됐습니다.
query와 update_range를 진행할 때 각 노드에서 propagate 메소드를 호출합니다. 노드 표현 범위에 update 쿼리 범위가 완전히 포함되는 경우(Fig. 2,3에서 파란색 노드) 노드의 값을 업데이트 하고 자식 노드의 lazy 값을 추가합니다.
'알고리즘 > 트리' 카테고리의 다른 글
서로소 집합(Disjoint set), 유니온 파인드(Union-Find tree) (0) | 2020.05.21 |
---|---|
세그먼트 트리(Segment Tree, 구간 트리) (0) | 2020.05.12 |