- 자료구조
- 느리게 갱신되는 세그먼트 트리
느리게 갱신되는 세그먼트 트리 (Segment Tree with Lazy Propagation)
문제
크기가 $N$인 정수 배열 $A$가 있고, 여기서 다음과 같은 연산을 최대 $M$번 수행해야 하는 문제가 있습니다.
- 구간 $l$, $r$ ($l ≤ r$)이 주어졌을 때, $A[l] + A[l+1] + \dots + A[r-1] + A[r]$을 구해서 출력하기
- $i$번째 수부터 $j$번째 수에 $v$를 더하기
세그먼트 트리에 설명한 문제에서 2번 연산은 $i$번째 수에 $v$를 더하는 것이었습니다. 이번 문제의 2번 연산은 구간에 수를 더하는 것입니다. 세그먼트 트리에서 수를 변경하는 연산을 $i$번째 수부터 $j$번째 수까지 하나씩 하는 방식을 이용할 수 있습니다. 1번 연산은 구간의 합을 구하는 연산이라 $O(\lg{ N })$입니다. 2번 연산은 수 하나를 변경하는 연산을 $j-i+1$번 해야하니 $O(N\lg{ N })$입니다. 세그먼트 트리를 사용하지 않고 배열에서 1번, 2번 연산을 수행하는 경우 시간 복잡도는 $O(N)$인데, 오히려 시간이 더 걸립니다.
느리게 갱신되는 세그먼트 트리 설명을 위해 세그먼트 트리에서 구간을 변경하는 함수 update_range
를 만들어봅시다.
소스 1
update_range(tree, node, start, end, left, right, diff)
는 left
번째부터 right
번째 수에 diff
를 더하는 소스입니다.
리프 노드를 찾아 diff
를 더하고, 리프 노드가 아닌 나머지 노드가가 저장하는 합도 변경해 줍니다.
효율적으로 보이지만, 모든 수를 변경해야 하면 모든 노드를 다 변경해야 합니다. 따라서, 시간 복잡도는 $O(N\lg { N })$입니다.
느리게 갱신되는 세그먼트 트리를 사용하면 구간 변경을 효율적으로 수행할 수 있습니다.
변경해야 하는 노드
다음은 $N = $, left =
, right =
인 경우 변경해야 하는 노드를 표시했습니다.
- , , 은
[left,right]
를 변경하기 위해서 합을 변경해야 하는 노드입니다. - 여기서 을 루트로하는 서브트리는 모든 노드에 들어있는 합을 변경해야 합니다. 이런 경우 은 합을 변경하지 않고 나중에 다시 변경을 수행하러 그 노드에 방문했을 때 변경을 진행해도 됩니다.
lazy
나중에 변경해야 하는 값을 lazy[i]
에 저장합시다.
[3,4]
의 합을 저장하는 노드의 lazy[i]
에 $10$이 저장되어 있다면, $A[3]$과 $A[4]$에 $10$을 더해야 하는데, 나중에 $10$을 더하겠다는 의미를 갖습니다.
예시
$A$가 다음과 같은 경우를 살펴봅시다.
$i$ | $0$ | $1$ | $2$ | $3$ | $4$ | $5$ | $6$ | $7$ | $8$ | $9$ |
---|---|---|---|---|---|---|---|---|---|---|
$A[i]$ | $3$ | $6$ | $2$ | $5$ | $3$ | $1$ | $8$ | $9$ | $7$ | $3$ |
이 정보를 이용해서 세그먼트 트리를 만들어봅시다. 노드의 위에는 합을 저장하고 있는 구간, 아래에는 저장된 합이 있습니다.
- 여기서
[3,7]
에 2를 더하는 경우 변경해야 하는 노드는 아래 그림의 와 입니다.
- 은 노드의 구간이
[3,7]
에 일부만 포함되는 경우이고, 는 모두 포함되는 경우입니다. - 은 세그먼트 트리에서 변경을 처리하는 과정과 똑같이 처리하면 되지만, 은 조금 특별하게 진행해야 합니다.
- 의 아래에 있는 노드는 모두 변경하려는 구간
[3,7]
에 포함됩니다. 따라서, 이러한 노드의 변경은 나중에 필요할때 하기로 하고, 그 값을lazy[i]
에 저장합니다.
이제 $A$는 다음과 같고, 세그먼트 트리는 아래 그림과 같습니다. 세그먼트 트리 노드의 왼쪽 대각선 상단 또는 오른쪽 대각선 상단에 있는 값은 그 노드의 lazy[i]
값입니다. lazy[i]
가 $0$인 경우 표시하지 않습니다.
$i$ | $0$ | $1$ | $2$ | $3$ | $4$ | $5$ | $6$ | $7$ | $8$ | $9$ |
---|---|---|---|---|---|---|---|---|---|---|
$A[i]$ | $3$ | $6$ | $2$ | $7$ | $5$ | $3$ | $10$ | $11$ | $7$ | $3$ |
앞으로 어떤 노드를 방문할 때마다 lazy[i]
값이 있는지 확인해야 합니다. lazy[i]
값이 $0$이 아닌 경우에는 노드의 합을 변경하고, 자식 노드에게 lazy[i]
값을 전달해야 합니다.
lazy[i]
에는 그 노드가 담당하는 구간의 각 수에 더해야 하는 값이 저장되어 있으니, 합에는 그 구간에 포함된 수의 개수만큼 곱해서 더해야 합니다.
예를 들어, 어떤 노드 node
에 구간 [start,end]
의 합이 저장되어 있다면, tree[node]
에는 lazy[node]*(end-start+1)
을 더해야 합니다.
이제 [4,9]
에 $1$을 더해봅시다. 여기서 변경해야 하는 노드를 표시해보면 다음과 같습니다.
[3,4]
의 합을 저장하고 있는 노드를 방문했을 때, 항상 두 자식 모두를 호출합니다. 따라서,[3,3]
의 합을 저장하고 있는 노드도 호출하게 됩니다. 이 노드는 로 표시했습니다.
[3,3]
의 합과 [4,4]
의 합을 저장하고 있는 노드를 방문했을 때는 lazy
값이 $0$보다 크기 때문에, lazy
값을 이용해 합을 변경하고, 그 다음에 $1$을 더해주게 됩니다. 트리는 다음과 같이 변합니다.
마지막으로 [6,8]
의 합을 구해보겠습니다. 합을 구하려면 아래 그림의 색칠된 노드를 모두 방문하게 됩니다.
- 가장 처음 루트 노드 입니다.
[0,9]
는[6,8]
와 겹치기 때문에, 자식 노드 와 를 호출합니다. - 의 구간
[0,4]
는[6,8]
과 겹치지 않기 때문에, $0$을 리턴합니다. - 의 구간
[5,9]
는[6,8]
과 겹치기 때문에, 자식 노드 와 를 호출합니다. - 에는
lazy
값이 있습니다. 먼저 현재 노드의 합을 변경해주고, 자식 노드 와 에lazy
값을 넘겨줍니다.
- 이 저장하고 있는 합은 $24$이고,
lazy
의 값은 $1$입니다. 구간의 크기는 $7-5+1 = 3$이기 때문에, 변경되는 합은 $24 + (7-5+1) \times 1 = 27$이 됩니다. 자식 노드에게lazy
를 넘겨준 이후 트리는 다음과 같습니다.
- 에는
lazy
값이 있습니다. 현재 노드의 합을 변경해 가 되고, 두 자식 노드에게lazy
를 넘겨줍니다.
- 에는
lazy
값이 있고, 먼저 합을 변경합니다. 합을 구하려는 구간[6,8]
와 겹치지 않으니 $0$을 리턴합니다. - 에도
lazy
값이 있으니 합을 변경해 가 됩니다.[6,8]
에 포함되니 $11$을 리턴합니다.
- 는
lazy
값도 있고, 구간[6,8]
에도 포함됩니다. 합을 변경해 가 되고, $12$를 리턴합니다.
- 은
lazy
을 이용해 합을 변경해 가 되고, 두 자식 노드 와 에게lazy
를 넘겨준 다음 호출합니다.
- 에도
lazy
값이 있으니 합을 변경해 가 됩니다.[6,8]
에 포함되니 $8$을 리턴합니다. - 에는
lazy
값이 있고, 먼저 합을 변경합니다. 합을 구하려는 구간[6,8]
와 겹치지 않으니 $0$을 리턴합니다.
합을 모두 구한 이후 트리는 다음과 같습니다.
직접 해보기
- $A = $ 로
- $1 \le N \le 16$
- $1 \le A[i] \le 99$
$i$ | $0$ | $1$ | $2$ | $3$ | $4$ | $5$ | $6$ | $7$ | $8$ | $9$ |
---|---|---|---|---|---|---|---|---|---|---|
$A[i]$ | $3$ | $6$ | $2$ | $5$ | $3$ | $1$ | $8$ | $9$ | $7$ | $3$ |
- 변경:
left
= ,right
= , 더할 값 = - 합 구하기:
left
= ,right
=
시간 복잡도
수를 변경할 때 방문하는 노드는 합을 구할때 방문하는 노드와 같습니다. 따라서, 두 연산 모두 $O(\lg{ N })$ 입니다.
10999 - 구간 합 구하기 2
이 글의 가장 처음에 설명한 문제와 같은 문제입니다. 소스 2로 해결할 수 있습니다.