Publicado · en mejora
Guía de Árboles de segmentos · 2/6
Por ahora, este capítulo solo está disponible en inglés.
This chapter builds a segment tree for the array a = [5, 3, 7, 9, 6, 4, 1, 2] and traces, step by step, how a range-sum query and a point update move through it. It then shows the iterative bottom-up variant and the idea behind lazy propagation for range updates.
The most common layout numbers nodes from 1 and gives node k the children 2k and 2k+1. Elements go into the leaves, and each parent stores the sum of its two children.
[0,8) 37
/ \
[0,4) 24 [4,8) 13
/ \ / \
[0,2) 8 [2,4) 16 [4,6) 10 [6,8) 3
/ \ / \ / \ / \
5 3 7 9 6 4 1 2There are 2n - 1 nodes and each one is computed once, so building takes O(n).
We want a[2] + a[3] + a[4] + a[5]. Starting at the root, every node falls into one of three cases:
| Node visited | Relation to [2,6) | Returned |
|---|---|---|
| [0,8) | partial, recurse | 16 + 10 = 26 |
| [0,4) | partial, recurse | 0 + 16 = 16 |
| [0,2) | disjoint | 0 |
| [2,4) | fully inside | 16 |
| [4,8) | partial, recurse | 10 + 0 = 10 |
| [4,6) | fully inside | 10 |
| [6,8) | disjoint | 0 |
The answer is 26, and indeed 7 + 9 + 6 + 4 = 26. The query was covered by exactly two nodes, [2,4) and [4,6). On each level at most two nodes are "partial" and recurse further, so the number of visited nodes is O(log n).
When a[3] changes from 9 to 0, fix the leaf [3,4) and recompute only its ancestors:
[3,4): 9 becomes 0[2,4): 16 becomes 7 + 0 = 7[0,4): 24 becomes 8 + 7 = 15[0,8): 37 becomes 15 + 13 = 28Only one leaf-to-root path changed: nodes. Translating the three steps directly into recursive functions gives:
log2 8 + 1 = 4a = [5, 3, 7, 9, 6, 4, 1, 2]
n = len(a)
tree = [0] * (4 * n)
def build(node, lo, hi):
if hi - lo == 1:
tree[node] = a[lo]
return
mid = (lo + hi) // 2
build(2 * node, lo, mid)
build(2 * node + 1, mid, hi)
tree[node] = tree[2 * node] + tree[2 * node + 1]
def query(node, lo, hi, l, r):
if r <= lo or hi <= l: # disjoint
return 0
if l <= lo and hi <= r: # fully inside
return tree[node]
mid = (lo + hi) // 2 # partial overlap
return query(2 * node, lo, mid, l, r) + query(2 * node + 1, mid, hi, l, r)
def update(node, lo, hi, i, value):
if hi - lo == 1:
tree[node] = value
return
mid = (lo + hi) // 2
if i < mid:
update(2 * node, lo, mid, i, value)
else:
update(2 * node + 1, mid, hi, i, value)
tree[node] = tree[2 * node] + tree[2 * node + 1]
build(1, 0, n)
print(query(1, 0, n, 2, 6)) # 26
update(1, 0, n, 3, 0)
print(query(1, 0, n, 2, 6)) # 17When n is not a power of two the tree becomes slightly lopsided and node numbers can exceed 2n, which is why the array is allocated with 4n slots.
The same work can be done without recursion. Put the elements in the second half of an array of size 2n, tree[n..2n), then compute tree[i] = tree[2i] + tree[2i+1] for i from n-1 down to 1. A query starts at the two ends lo = l + n and hi = r + n and climbs toward the root, adding only the nodes that sit on the boundary.
a = [5, 3, 7, 9, 6, 4, 1, 2]
n = len(a)
tree = [0] * n + a
for i in range(n - 1, 0, -1):
tree[i] = tree[2 * i] + tree[2 * i + 1]
lo, hi, total = 2 + n, 6 + n, 0
while lo < hi:
if lo & 1: # lo is a right child: take it, step right
total += tree[lo]; lo += 1
if hi & 1: # hi is a right child: take its left neighbor
hi -= 1; total += tree[hi]
print(f"lo={lo} hi={hi} total={total}")
lo //= 2; hi //= 2
print(total) # 26| Round | lo, hi at start | Nodes added | total |
|---|---|---|---|
| 1 | 10, 14 | none | 0 |
| 2 | 5, 7 | tree[5]=16, tree[6]=10 | 26 |
| 3 | 3, 3 | loop ends | 26 |
Handling "add 2 to every element of [0,4)" as four point updates costs O(k log n). Lazy propagation instead stops at nodes that are fully covered by the update and leaves them a note (the lazy value) saying "my children still owe +2".
add +2 to [0,4)
[0,4): 24 + 2 * 4 = 32, lazy = 2 (children untouched)
[0,8): 32 + 13 = 45
later, a query for [2,4) passes through [0,4) and pushes the note down
[0,2): 8 + 2 * 2 = 12, lazy = 2
[2,4): 16 + 2 * 2 = 20, lazy = 2
[0,4): lazy = 0A note is only resolved when some operation needs to descend below that node, so range updates also finish in O(log n). The full code appears in the practice problems chapter.
A segment tree stores the sum of every halved range; queries descend according to overlap, and updates recompute a single leaf-to-root path. The iterative variant does the same job with a 2n array and short loops, while lazy propagation defers range updates and pushes them to children only when needed.
0 comentarios
Iniciar sesión · Inicia sesión para dejar un comentario.
Sé el primero en comentar.