Segment Tree
Dizinin aralık toplamlarını tutan, hem aralık sorgusunu hem tek eleman güncellemesini O(log n)'de yapan ikili ağaç.
Önce bunlara göz at
#Nasıl çalışır?
Segment tree (aralık ağacı), bir dizinin aralıkları üzerinde toplam, en küçük ya da en büyük gibi sorguları hızla cevaplayan ve diziyi değiştirmeye de izin veren bir ikili ağaçtır.
Kök tüm diziyi, iki çocuğu dizinin iki yarısını, onların çocukları dörtte birlerini kapsar; yapraklar tek tek elemanlardır. Her düğüm kendi aralığının toplamını tutar.
Bir a[l..r] sorgusunda tamamen içeride kalan bir düğümün toplamı doğrudan alınır, tamamen dışarıdaki bir düğüm atlanır, yalnızca kısmen örtüşen düğümlerin çocuklarına inilir. Böylece her sorgu en fazla yaklaşık 2·log n düğümle cevaplanır.
Bir eleman değişince yalnızca o yapraktan köke giden yoldaki düğümler yeniden hesaplanır.
#Kendin dene
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | |
|---|---|---|---|---|---|---|---|---|
| a | 5 | 2 | 7 | 3 | 6 | 1 | 4 | 8 |
Her düğüm, rozetindeki aralığın toplamını tutar. Aralık toplamı sorgusu yalnızca birkaç düğüme bakarak cevaplanır; bir eleman değişince sadece kökle arasındaki yol güncellenir.
#Temel işlemler
- query(l, r)
- a[l..r] aralığının toplamını O(log n) zamanda döndürür.
- update(i, x)
- a[i] = x yapar ve yoldaki toplamları O(log n) zamanda günceller.
- build(a)
- Ağacı alttan yukarı O(n) zamanda kurar.
- Tembel yayılım (lazy propagation)
- Bir aralığın tamamına ekleme gibi güncellemeleri de O(log n)'de yapmayı sağlayan genişletme.
#Örnek kod
Aynı algoritma 8 dilde. Her program derlenip çalıştırılarak test edildi ve hepsi aynı çıktıyı veriyor.
class SegmentTree:
"""Range sums with point updates; node k covers a range, its children are 2k and 2k + 1."""
def __init__(self, a: list) -> None:
self.n = len(a)
self.sums = [0] * (4 * self.n)
self._build(1, 0, self.n - 1, a)
def _build(self, k: int, lo: int, hi: int, a: list) -> None:
if lo == hi:
self.sums[k] = a[lo]
return
mid = (lo + hi) // 2
self._build(2 * k, lo, mid, a)
self._build(2 * k + 1, mid + 1, hi, a)
self.sums[k] = self.sums[2 * k] + self.sums[2 * k + 1]
def query(self, l: int, r: int, k: int = 1, lo: int = 0, hi: int = -1) -> int:
if hi == -1:
hi = self.n - 1
if r < lo or hi < l: # completely outside
return 0
if l <= lo and hi <= r: # completely inside
return self.sums[k]
mid = (lo + hi) // 2 # partly: ask both children
return self.query(l, r, 2 * k, lo, mid) + self.query(l, r, 2 * k + 1, mid + 1, hi)
def update(self, i: int, x: int, k: int = 1, lo: int = 0, hi: int = -1) -> None:
if hi == -1:
hi = self.n - 1
if lo == hi:
self.sums[k] = x
return
mid = (lo + hi) // 2
if i <= mid:
self.update(i, x, 2 * k, lo, mid)
else:
self.update(i, x, 2 * k + 1, mid + 1, hi)
self.sums[k] = self.sums[2 * k] + self.sums[2 * k + 1]
tree = SegmentTree([5, 2, 7, 3, 6, 1, 4, 8])
print(tree.query(2, 5)) # 7 + 3 + 6 + 1
tree.update(5, 10)
print(tree.query(2, 5)) # 7 + 3 + 6 + 10#Karmaşıklık
| İşlem | Süre |
|---|---|
| Kurma | O(n) |
| Aralık sorgusu | O(log n) |
| Tek eleman güncelleme | O(log n) |
| Bellek | O(n) (yaklaşık 2n–4n düğüm) |
Basit bir önek toplamı dizisi sorguyu O(1)'de cevaplar ama her güncellemede O(n) yeniden hesaplama ister. Segment tree ikisini de O(log n)'de yapar. Toplam yerine en küçük, en büyük, EBOB gibi birleşmeli (associative) her işlem kullanılabilir.
#Ne zaman kullanılır?
Hem sorgu hem güncelleme içeren aralık problemleri: canlı skor tabloları, zaman serilerinde aralık en büyüğü, oyunlarda bölge sorguları, hesaplamalı geometride tarama çizgisi algoritmaları ve yarışma programlamanın klasik problemleri.
#Alıştırma
Soruları cevaplamak ve ilerlemeni kaydetmek için giriş yap.
Kolay
Segment tree'de her düğüm neyi tutar?
- 1Kapsadığı aralığın toplamını (ya da min/max gibi bir özetini)
- 2Yalnızca bir elemanı
- 3Dizinin tamamını
- 4Aralığın uzunluğunu
Aralık toplamı sorgusu kaç adım sürer?
- 1O(log n)
- 2O(n)
- 3O(1)
- 4O(n log n)
Bir düğümün aralığı sorgunun tamamen dışındaysa ne yapılır?
- 10 döndürülür; aşağı inilmez
- 2Tüm çocuklarına bakılır
- 3Toplamı eklenir
- 4Ağaç yeniden kurulur
Orta
a = [5, 2, 7, 3, 6, 1, 4, 8] için a[2..5] toplamı kaçtır (0 tabanlı)?
- 117
- 218
- 323
- 410
Zor
Önek toplamı dizisi varken segment tree'ye neden ihtiyaç duyulur?
- 1Önek dizisi her güncellemede O(n) yeniden hesaplama ister; segment tree güncellemeyi de O(log n)'de yapar
- 2Önek dizisi yanlış sonuç verir
- 3Segment tree daha az bellek kullanır
- 4Önek dizisi yalnızca sıralı dizilerde çalışır
#Bundan sonra
Bu dersi bildiğini varsayan dersler: