Elinde bir milyon elemanlı bir dizi var. İki tür işlem geliyor: "şu aralıktaki sayıların toplamı kaç?" ve "şu indeksteki değeri değiştir". İkisi de milyonlarca kez tekrarlanıyor.
Naif çözüm her sorguda aralığı taramaktır: güncelleme O(1), sorgu O(n). Milyonlarca sorguda bu kabul edilemez. Akıllı bir alternatif önek toplamlarıdır: Her konuma kadar olan toplamı önceden hesaplarsan sorgu O(1)'e iner — ama tek bir güncelleme, ondan sonraki tüm önekleri bozar ve yeniden hesaplamak O(n) sürer.
İki uç da aynı sorunu gösterir: Biri sorguyu, diğeri güncellemeyi ucuzlatır ama diğerini pahalılaştırır. Segment ağacı, ikisini birden O(log n)'de tutarak bu ödünleşmeyi kırar. Bu derste önek toplamlarının sınırını, segment ağacının yapısını, kurma/sorgu/güncelleme işlemlerini, aralık güncellemeleri için tembel yayılımı ve daha az kodla benzer işi yapan Fenwick ağacını öğreneceksin.
Önek toplamları ve sınırı
Önce basit çözümü anlayalım, çünkü pek çok durumda gerçekten yeterlidir.
dizi: [3, 1, 4, 1, 5, 9, 2, 6]
önek: [0, 3, 4, 8, 9, 14, 23, 25, 31]
↑ prefix[i] = ilk i elemanın toplamı
toplam(2..5) = prefix[6] − prefix[2] = 23 − 4 = 19
(4 + 1 + 5 + 9 = 19 ✓)package main
import "fmt"
type PrefixSum struct {
prefix []int
data []int
}
func NewPrefixSum(data []int) *PrefixSum {
p := &PrefixSum{data: append([]int(nil), data...), prefix: make([]int, len(data)+1)}
for i, v := range data {
p.prefix[i+1] = p.prefix[i] + v
}
return p
}
// Query: [lo, hi) aralığının toplamı — O(1)
func (p *PrefixSum) Query(lo, hi int) int {
return p.prefix[hi] - p.prefix[lo]
}
// Update: O(n) — sonraki tüm önekler bozulur
func (p *PrefixSum) Update(i, value int) {
diff := value - p.data[i]
p.data[i] = value
for j := i + 1; j < len(p.prefix); j++ {
p.prefix[j] += diff
}
}
func main() {
data := []int{3, 1, 4, 1, 5, 9, 2, 6}
ps := NewPrefixSum(data)
fmt.Println("dizi:", data)
fmt.Println("önek toplamları:", ps.prefix)
fmt.Println()
for _, r := range [][2]int{{0, 8}, {2, 6}, {5, 6}, {3, 3}} {
fmt.Printf("toplam[%d..%d) = %d\n", r[0], r[1], ps.Query(r[0], r[1]))
}
fmt.Println()
ps.Update(2, 10) // 4 → 10
fmt.Println("indeks 2 → 10 güncellendi")
fmt.Println("yeni önekler:", ps.prefix)
fmt.Println("toplam[2..6) =", ps.Query(2, 6))
fmt.Println()
fmt.Println("sorgu: O(1) — mükemmel")
fmt.Println("güncelleme: O(n) — sorun burada")
}dizi: [3 1 4 1 5 9 2 6] önek toplamları: [0 3 4 8 9 14 23 25 31] toplam[0..8) = 31 toplam[2..6) = 19 toplam[5..6) = 9 toplam[3..3) = 0 indeks 2 → 10 güncellendi yeni önekler: [0 3 4 14 15 20 29 31 37] toplam[2..6) = 25 sorgu: O(1) — mükemmel güncelleme: O(n) — sorun burada
Önek toplamları, dizi hiç değişmiyorsa en iyi çözümdür: hazırlık O(n), her sorgu O(1). Ama güncelleme varsa çöker. Segment ağacı tam olarak bu boşluğu doldurur.
Segment ağacı yapısı
Fikir şudur: Diziyi ikiye böl, her yarıyı yine ikiye böl, tek elemana inene kadar devam et. Her düğüm, kapsadığı aralığın özetini (toplam, minimum, maksimum…) saklar.
dizi: [3, 1, 4, 1, 5, 9, 2, 6]
[0..7]
31
╱ ╲
[0..3] [4..7]
9 22
╱ ╲ ╱ ╲
[0..1] [2..3] [4..5] [6..7]
4 5 14 8
╱ ╲ ╱ ╲ ╱ ╲ ╱ ╲
3 1 4 1 5 9 2 6
Her düğüm: kendi aralığının toplamı
Yaprak: tek elemanBu yapının iki kilit özelliği vardır. Birincisi, herhangi bir aralık en fazla O(log n) düğümle kaplanabilir. İkincisi, bir yaprağı güncellemek yalnızca kökten ona giden yoldaki düğümleri etkiler — yani yine O(log n).
toplam(2..5) sorgusu:
[0..7] ← çok geniş, çocuklara in
╱ ╲
[0..3] [4..7] ← çok geniş, çocuklara in
╱ ╲ ╱ ╲
[0..1] [2..3]✓ [4..5]✓ [6..7]
(dışarıda) 5 14 (dışarıda)
sonuç: 5 + 14 = 19
yalnızca 2 düğüm okunduAşağıdaki görselleştirmede sorgu aralıklarını değiştirerek hangi düğümlerin ziyaret edildiğini izleyebilirsin:
Kurma, sorgu ve güncelleme
Ağacı diziyle temsil etmek en pratik yoldur: Kök 1. indekste, i. düğümün çocukları 2i ve 2i+1'dedir.
package main
import "fmt"
type SegmentTree struct {
tree []int
n int
visits int // sorgu sırasında ziyaret edilen düğüm sayısı
}
func NewSegmentTree(data []int) *SegmentTree {
n := len(data)
st := &SegmentTree{tree: make([]int, 4*n), n: n} // 4n her zaman yeterlidir
if n > 0 {
st.build(data, 1, 0, n-1)
}
return st
}
// build: O(n) — her düğüm bir kez hesaplanır
func (st *SegmentTree) build(data []int, node, lo, hi int) {
if lo == hi {
st.tree[node] = data[lo]
return
}
mid := (lo + hi) / 2
st.build(data, 2*node, lo, mid)
st.build(data, 2*node+1, mid+1, hi)
st.tree[node] = st.tree[2*node] + st.tree[2*node+1]
}
// Query: [l, r] kapalı aralığının toplamı — O(log n)
func (st *SegmentTree) Query(l, r int) int {
if st.n == 0 || l > r {
return 0
}
st.visits = 0
return st.query(1, 0, st.n-1, l, r)
}
func (st *SegmentTree) query(node, lo, hi, l, r int) int {
st.visits++
if r < lo || hi < l {
return 0 // tamamen dışarıda
}
if l <= lo && hi <= r {
return st.tree[node] // tamamen içeride: hazır cevap
}
mid := (lo + hi) / 2
return st.query(2*node, lo, mid, l, r) + st.query(2*node+1, mid+1, hi, l, r)
}
// Update: tek noktayı günceller — O(log n)
func (st *SegmentTree) Update(i, value int) {
if st.n == 0 {
return
}
st.update(1, 0, st.n-1, i, value)
}
func (st *SegmentTree) update(node, lo, hi, i, value int) {
if lo == hi {
st.tree[node] = value
return
}
mid := (lo + hi) / 2
if i <= mid {
st.update(2*node, lo, mid, i, value)
} else {
st.update(2*node+1, mid+1, hi, i, value)
}
st.tree[node] = st.tree[2*node] + st.tree[2*node+1] // geri dönerken güncelle
}
func main() {
data := []int{3, 1, 4, 1, 5, 9, 2, 6}
st := NewSegmentTree(data)
fmt.Println("dizi:", data)
fmt.Println("toplam:", st.Query(0, 7))
fmt.Println()
for _, r := range [][2]int{{0, 7}, {2, 5}, {5, 5}, {0, 3}, {4, 7}} {
sum := st.Query(r[0], r[1])
fmt.Printf("toplam[%d..%d] = %-3d (ziyaret edilen düğüm: %d)\n", r[0], r[1], sum, st.visits)
}
fmt.Println()
st.Update(2, 10) // 4 → 10, fark +6
fmt.Println("indeks 2: 4 → 10")
fmt.Println("yeni toplam[0..7] =", st.Query(0, 7), "(önceki 31 + 6)")
fmt.Println("yeni toplam[2..5] =", st.Query(2, 5))
st.Update(7, 0)
fmt.Println()
fmt.Println("indeks 7: 6 → 0")
fmt.Println("toplam[6..7] =", st.Query(6, 7))
fmt.Println("toplam[0..7] =", st.Query(0, 7))
}dizi: [3 1 4 1 5 9 2 6] toplam: 31 toplam[0..7] = 31 (ziyaret edilen düğüm: 1) toplam[2..5] = 19 (ziyaret edilen düğüm: 7) toplam[5..5] = 9 (ziyaret edilen düğüm: 7) toplam[0..3] = 9 (ziyaret edilen düğüm: 3) toplam[4..7] = 22 (ziyaret edilen düğüm: 3) indeks 2: 4 → 10 yeni toplam[0..7] = 37 (önceki 31 + 6) yeni toplam[2..5] = 25 indeks 7: 6 → 0 toplam[6..7] = 2 toplam[0..7] = 31
Sorgu fonksiyonundaki üç durum, segment ağacının tüm mantığını içerir: Düğümün aralığı sorguyla hiç kesişmiyorsa nötr değer döndürülür (toplam için 0), tamamen kapsanıyorsa hazır cevap kullanılır, kısmen kesişiyorsa iki çocuğa inilir. Ziyaret edilen düğüm sayısına dikkat: Sekiz elemanlı bir ağaçta bile sorgular yalnızca birkaç düğüme dokunuyor.
4*n boyutunun sebebi şudur: Ağaç, dizi uzunluğu ikinin kuvveti değilse tam dolu olmaz ve dizi temsilinde boşluklar oluşur. 4n, her durumda yeterli olduğu kanıtlanmış güvenli bir üst sınırdır.
Farklı işlemler
Segment ağacının en güzel yanı, yalnızca toplamla sınırlı olmamasıdır. Birleştirme işlemi birleşmeli (associative) olduğu sürece her şey çalışır: minimum, maksimum, en büyük ortak bölen, bit işlemleri, hatta daha karmaşık özetler.
package main
import "fmt"
type Op struct {
name string
combine func(a, b int) int
neutral int
}
type SegmentTree struct {
tree []int
n int
op Op
}
func NewSegmentTree(data []int, op Op) *SegmentTree {
st := &SegmentTree{tree: make([]int, 4*len(data)), n: len(data), op: op}
for i := range st.tree {
st.tree[i] = op.neutral
}
if st.n > 0 {
st.build(data, 1, 0, st.n-1)
}
return st
}
func (st *SegmentTree) build(data []int, node, lo, hi int) {
if lo == hi {
st.tree[node] = data[lo]
return
}
mid := (lo + hi) / 2
st.build(data, 2*node, lo, mid)
st.build(data, 2*node+1, mid+1, hi)
st.tree[node] = st.op.combine(st.tree[2*node], st.tree[2*node+1])
}
func (st *SegmentTree) Query(l, r int) int {
if st.n == 0 || l > r {
return st.op.neutral
}
return st.query(1, 0, st.n-1, l, r)
}
func (st *SegmentTree) query(node, lo, hi, l, r int) int {
if r < lo || hi < l {
return st.op.neutral
}
if l <= lo && hi <= r {
return st.tree[node]
}
mid := (lo + hi) / 2
return st.op.combine(
st.query(2*node, lo, mid, l, r),
st.query(2*node+1, mid+1, hi, l, r),
)
}
func (st *SegmentTree) Update(i, value int) {
st.update(1, 0, st.n-1, i, value)
}
func (st *SegmentTree) update(node, lo, hi, i, value int) {
if lo == hi {
st.tree[node] = value
return
}
mid := (lo + hi) / 2
if i <= mid {
st.update(2*node, lo, mid, i, value)
} else {
st.update(2*node+1, mid+1, hi, i, value)
}
st.tree[node] = st.op.combine(st.tree[2*node], st.tree[2*node+1])
}
func gcd(a, b int) int {
for b != 0 {
a, b = b, a%b
}
if a < 0 {
return -a
}
return a
}
func main() {
data := []int{12, 8, 20, 4, 16, 24}
ops := []Op{
{"toplam", func(a, b int) int { return a + b }, 0},
{"minimum", func(a, b int) int { return min(a, b) }, 1 << 60},
{"maksimum", func(a, b int) int { return max(a, b) }, -(1 << 60)},
{"OBEB", gcd, 0},
}
fmt.Println("dizi:", data)
fmt.Println()
fmt.Printf("%-10s %10s %10s %10s\n", "işlem", "[0..5]", "[1..3]", "[2..4]")
for _, op := range ops {
st := NewSegmentTree(data, op)
fmt.Printf("%-10s %10d %10d %10d\n",
op.name, st.Query(0, 5), st.Query(1, 3), st.Query(2, 4))
}
// Güncelleme her işlemde çalışır
fmt.Println()
minTree := NewSegmentTree(data, ops[1])
fmt.Println("minimum[0..5] =", minTree.Query(0, 5))
minTree.Update(3, 100) // en küçük değer olan 4'ü büyüt
fmt.Println("indeks 3: 4 → 100")
fmt.Println("yeni minimum[0..5] =", minTree.Query(0, 5))
}dizi: [12 8 20 4 16 24] işlem [0..5] [1..3] [2..4] toplam 84 32 40 minimum 4 4 4 maksimum 24 20 20 OBEB 4 4 4 minimum[0..5] = 4 indeks 3: 4 → 100 yeni minimum[0..5] = 8
Burada kritik olan nötr değerdir: Sorgu aralığının tamamen dışında kalan düğümler için döndürülen ve sonucu etkilemeyen değer. Toplamda 0, çarpımda 1, minimumda çok büyük bir sayı, maksimumda çok küçük bir sayı. Yanlış nötr değer seçmek, sessiz ve bulunması zor hatalara yol açar.
Birleşmeli olma koşulunun sebebi de budur: Ağaç, aralığı farklı parçalara bölüp sonuçları birleştirir. İşlem birleşmeli değilse, bölme biçimine göre farklı sonuçlar çıkar.
Tembel yayılım
Şimdiye kadar yalnızca tek nokta güncelledik. Peki "1000 ile 5000 arasındaki tüm elemanlara 7 ekle" demek istersen? Her elemanı tek tek güncellemek O(n log n) sürer.
Tembel yayılım (lazy propagation) fikri şudur: Güncellemeyi hemen aşağıya yaymak yerine, ilgili düğümde bir not bırak. O alt ağaca gerçekten ihtiyaç duyulduğunda notu aşağı it.
[1..6] aralığına +5 ekle
[0..7]
lazy: 0 ← kısmi kesişim: çocuklara in
╱ ╲
[0..3] [4..7]
lazy:0 lazy:0
╱ ╲ ╱ ╲
[0..1] [2..3] [4..5] [6..7]
lazy:+5 ✓ lazy:+5 ✓ ← tam kapsanıyor: NOT BIRAK, inme
(çocuklar hiç dokunulmadı)
Bu alt ağaçlara sonra erişilirse, not önce aşağı itilir.package main
import "fmt"
type LazySegmentTree struct {
tree []int
lazy []int
n int
}
func NewLazy(data []int) *LazySegmentTree {
st := &LazySegmentTree{
tree: make([]int, 4*len(data)),
lazy: make([]int, 4*len(data)),
n: len(data),
}
if st.n > 0 {
st.build(data, 1, 0, st.n-1)
}
return st
}
func (st *LazySegmentTree) build(data []int, node, lo, hi int) {
if lo == hi {
st.tree[node] = data[lo]
return
}
mid := (lo + hi) / 2
st.build(data, 2*node, lo, mid)
st.build(data, 2*node+1, mid+1, hi)
st.tree[node] = st.tree[2*node] + st.tree[2*node+1]
}
// push: bekleyen güncellemeyi çocuklara aktarır
func (st *LazySegmentTree) push(node, lo, hi int) {
if st.lazy[node] == 0 {
return
}
st.tree[node] += st.lazy[node] * (hi - lo + 1) // aralıktaki her elemana eklenir
if lo != hi {
st.lazy[2*node] += st.lazy[node]
st.lazy[2*node+1] += st.lazy[node]
}
st.lazy[node] = 0
}
// RangeAdd: [l, r] aralığındaki her elemana delta ekler — O(log n)
func (st *LazySegmentTree) RangeAdd(l, r, delta int) {
if st.n == 0 || l > r {
return
}
st.rangeAdd(1, 0, st.n-1, l, r, delta)
}
func (st *LazySegmentTree) rangeAdd(node, lo, hi, l, r, delta int) {
st.push(node, lo, hi)
if r < lo || hi < l {
return
}
if l <= lo && hi <= r {
st.lazy[node] += delta // NOT BIRAK, aşağı inme
st.push(node, lo, hi)
return
}
mid := (lo + hi) / 2
st.rangeAdd(2*node, lo, mid, l, r, delta)
st.rangeAdd(2*node+1, mid+1, hi, l, r, delta)
st.tree[node] = st.tree[2*node] + st.tree[2*node+1]
}
func (st *LazySegmentTree) Query(l, r int) int {
if st.n == 0 || l > r {
return 0
}
return st.query(1, 0, st.n-1, l, r)
}
func (st *LazySegmentTree) query(node, lo, hi, l, r int) int {
st.push(node, lo, hi) // okumadan önce notu uygula
if r < lo || hi < l {
return 0
}
if l <= lo && hi <= r {
return st.tree[node]
}
mid := (lo + hi) / 2
return st.query(2*node, lo, mid, l, r) + st.query(2*node+1, mid+1, hi, l, r)
}
func main() {
data := []int{1, 2, 3, 4, 5, 6, 7, 8}
st := NewLazy(data)
fmt.Println("dizi:", data)
fmt.Println("toplam[0..7] =", st.Query(0, 7))
fmt.Println()
st.RangeAdd(1, 6, 5) // 1..6 arasındaki 6 elemana +5
fmt.Println("[1..6] aralığına +5 eklendi")
fmt.Println("toplam[0..7] =", st.Query(0, 7), "(önceki 36 + 6×5 = 66)")
fmt.Println("toplam[1..6] =", st.Query(1, 6))
fmt.Println("toplam[0..0] =", st.Query(0, 0), "← etkilenmedi")
fmt.Println("toplam[7..7] =", st.Query(7, 7), "← etkilenmedi")
fmt.Println()
st.RangeAdd(0, 3, 10)
fmt.Println("[0..3] aralığına +10 eklendi")
for i := range 8 {
fmt.Printf(" eleman[%d] = %d\n", i, st.Query(i, i))
}
fmt.Println("toplam[0..7] =", st.Query(0, 7))
}dizi: [1 2 3 4 5 6 7 8] toplam[0..7] = 36 [1..6] aralığına +5 eklendi toplam[0..7] = 66 (önceki 36 + 6×5 = 66) toplam[1..6] = 57 toplam[0..0] = 1 ← etkilenmedi toplam[7..7] = 8 ← etkilenmedi [0..3] aralığına +10 eklendi eleman[0] = 11 eleman[1] = 17 eleman[2] = 18 eleman[3] = 19 eleman[4] = 10 eleman[5] = 11 eleman[6] = 12 eleman[7] = 8 toplam[0..7] = 106
Tembel yayılımın anahtar kuralı şudur: Bir düğüme dokunmadan önce bekleyen notunu uygula. Hem sorgu hem güncelleme fonksiyonunun ilk satırında push çağrısı bulunmasının sebebi budur. Bu kuralı bir yerde atlarsan, ağaç sessizce yanlış değerler döndürmeye başlar — ve bu tür hataları bulmak gerçekten zordur.
Fenwick ağacı (BIT)
Segment ağacı güçlüdür ama uzundur. Yalnızca toplam (ya da tersi olan bir işlem) gerekiyorsa, çok daha kısa bir alternatif vardır: Fenwick ağacı, diğer adıyla ikili indeksli ağaç (Binary Indexed Tree).
Fikir, her indeksin en düşük anlamlı bitine dayanır. i & -i işlemi, bir sayının en sağdaki 1 bitini verir ve bu, o indeksin "sorumlu olduğu aralığın uzunluğudur".
i ikilik i & -i sorumlu aralık
1 0001 1 [1..1]
2 0010 2 [1..2]
3 0011 1 [3..3]
4 0100 4 [1..4]
5 0101 1 [5..5]
6 0110 2 [5..6]
7 0111 1 [7..7]
8 1000 8 [1..8]
Sorgu(6) = tree[6] + tree[4] (6 → 4 → 0)
Güncelle(3) → tree[3], tree[4], tree[8] güncellenirpackage main
import "fmt"
type Fenwick struct {
tree []int // 1 tabanlı
n int
}
func NewFenwick(n int) *Fenwick {
return &Fenwick{tree: make([]int, n+1), n: n}
}
func FromSlice(data []int) *Fenwick {
f := NewFenwick(len(data))
for i, v := range data {
f.Add(i, v)
}
return f
}
// Add: i. elemana delta ekler — O(log n)
func (f *Fenwick) Add(i, delta int) {
for i++; i <= f.n; i += i & -i { // en düşük biti ekleyerek ilerle
f.tree[i] += delta
}
}
// PrefixSum: [0, i] toplamı — O(log n)
func (f *Fenwick) PrefixSum(i int) int {
sum := 0
for i++; i > 0; i -= i & -i { // en düşük biti çıkararak geri git
sum += f.tree[i]
}
return sum
}
// RangeSum: [l, r] kapalı aralığı — O(log n)
func (f *Fenwick) RangeSum(l, r int) int {
if l > r {
return 0
}
if l == 0 {
return f.PrefixSum(r)
}
return f.PrefixSum(r) - f.PrefixSum(l-1)
}
func main() {
data := []int{3, 1, 4, 1, 5, 9, 2, 6}
f := FromSlice(data)
fmt.Println("dizi:", data)
fmt.Println("iç dizi (1 tabanlı):", f.tree[1:])
fmt.Println()
fmt.Println("önek toplamları:")
for i := range data {
fmt.Printf(" [0..%d] = %d\n", i, f.PrefixSum(i))
}
fmt.Println()
for _, r := range [][2]int{{0, 7}, {2, 5}, {5, 5}, {3, 6}} {
fmt.Printf("toplam[%d..%d] = %d\n", r[0], r[1], f.RangeSum(r[0], r[1]))
}
fmt.Println()
f.Add(2, 6) // indeks 2'ye +6 (4 → 10)
fmt.Println("indeks 2'ye +6 eklendi")
fmt.Println("toplam[0..7] =", f.RangeSum(0, 7))
fmt.Println("toplam[2..5] =", f.RangeSum(2, 5))
// En düşük bit hilesi
fmt.Println()
fmt.Println("i & -i değerleri:")
for i := 1; i <= 8; i++ {
fmt.Printf(" %d (%04b) → %d\n", i, i, i&-i)
}
}dizi: [3 1 4 1 5 9 2 6] iç dizi (1 tabanlı): [3 4 4 9 5 14 2 31] önek toplamları: [0..0] = 3 [0..1] = 4 [0..2] = 8 [0..3] = 9 [0..4] = 14 [0..5] = 23 [0..6] = 25 [0..7] = 31 toplam[0..7] = 31 toplam[2..5] = 19 toplam[5..5] = 9 toplam[3..6] = 17 indeks 2'ye +6 eklendi toplam[0..7] = 37 toplam[2..5] = 25 i & -i değerleri: 1 (0001) → 1 2 (0010) → 2 3 (0011) → 1 4 (0100) → 4 5 (0101) → 1 6 (0110) → 2 7 (0111) → 1 8 (1000) → 8
Fenwick ağacının tüm çekirdeği iki döngüden ibarettir. i += i & -i yukarı doğru ilerler, i -= i & -i aşağı. Bu kısalık, onu yarışma programlamasında ve performans kritik kodda çok popüler yapar.
Bedeli esnekliktir: Fenwick ağacı, tersi olan işlemlerle çalışır. Toplamda çıkarma mümkün olduğu için prefix[r] - prefix[l-1] işe yarar. Minimumda bu mümkün değildir — bir minimumu "geri alamazsın". Bu yüzden minimum/maksimum sorguları için segment ağacı gerekir.
Hangisini seçmeli?
Pratik karar ağacı basittir. Dizi hiç değişmiyorsa önek toplamı kullan; en basit ve en hızlı çözümdür. Yalnızca toplam sorgusu ve nokta güncellemesi varsa Fenwick yeterlidir ve çok daha az kod gerektirir. Minimum, maksimum gibi tersi olmayan işlemler ya da aralık güncellemesi gerekiyorsa segment ağacına geç.
Bir de gözden kaçan seçenek vardır: Sorgu sayısı gerçekten az ise, hiçbir yapı kurmadan doğrudan taramak en iyi çözüm olabilir. n = 10.000 ve toplam 100 sorgu varsa, milyon işlem demektir ve bu hiçbir şeydir. Karmaşık bir yapı kurmadan önce, naif çözümün gerçekten yetersiz olduğundan emin ol.
Sık yapılan hatalar
- Ağaç dizisini küçük ayırmak. Dizi temsilinde 4n boyut güvenli sınırdır; 2n her zaman yetmez.
- Yanlış nötr değer seçmek. Toplamda 0, minimumda çok büyük bir sayı olmalı; yanlış seçim sessiz hatalara yol açar.
- Tembel yayılımda
pushçağrısını atlamak. Düğüme dokunmadan önce bekleyen not uygulanmalıdır. - Aralık sınırlarını karıştırmak. Kapalı
[l, r]mi yarı açık[l, r)mi kullandığına karar ver ve her yerde tutarlı ol. - Fenwick ağacını 0 tabanlı yazmaya çalışmak. En düşük bit hilesi 1 tabanlı indekslemeyi gerektirir.
- Fenwick ile minimum sorgusu yapmaya çalışmak. İşlemin tersi olmalıdır; minimum bunu sağlamaz.
- Basit çözüm yeterken karmaşık yapı kurmak. Dizi değişmiyorsa önek toplamı, sorgu azsa doğrudan tarama daha iyidir.
Alıştırmalar
Segment ağacı kullanarak aralık minimumu sorgulayan bir yapı yaz. Nokta güncellemesini de destekle ve birkaç güncelleme sonrası sonuçları kaba kuvvetle karşılaştırarak doğrula.
İpucu
Birleştirme işlemi min, nötr değer ise çok büyük bir sayı olmalı. Toplam sürümünden tek farkı budur.
Çözümü göster
package main
import (
"fmt"
"slices"
)
const inf = 1 << 60
type MinTree struct {
tree []int
n int
}
func NewMinTree(data []int) *MinTree {
st := &MinTree{tree: make([]int, 4*len(data)), n: len(data)}
for i := range st.tree {
st.tree[i] = inf
}
if st.n > 0 {
st.build(data, 1, 0, st.n-1)
}
return st
}
func (st *MinTree) build(data []int, node, lo, hi int) {
if lo == hi {
st.tree[node] = data[lo]
return
}
mid := (lo + hi) / 2
st.build(data, 2*node, lo, mid)
st.build(data, 2*node+1, mid+1, hi)
st.tree[node] = min(st.tree[2*node], st.tree[2*node+1])
}
func (st *MinTree) Query(l, r int) int {
if st.n == 0 || l > r {
return inf
}
return st.query(1, 0, st.n-1, l, r)
}
func (st *MinTree) query(node, lo, hi, l, r int) int {
if r < lo || hi < l {
return inf // nötr değer
}
if l <= lo && hi <= r {
return st.tree[node]
}
mid := (lo + hi) / 2
return min(st.query(2*node, lo, mid, l, r), st.query(2*node+1, mid+1, hi, l, r))
}
func (st *MinTree) Update(i, value int) {
st.update(1, 0, st.n-1, i, value)
}
func (st *MinTree) update(node, lo, hi, i, value int) {
if lo == hi {
st.tree[node] = value
return
}
mid := (lo + hi) / 2
if i <= mid {
st.update(2*node, lo, mid, i, value)
} else {
st.update(2*node+1, mid+1, hi, i, value)
}
st.tree[node] = min(st.tree[2*node], st.tree[2*node+1])
}
func bruteMin(data []int, l, r int) int {
return slices.Min(data[l : r+1])
}
func main() {
data := []int{7, 2, 9, 4, 1, 8, 3, 6}
st := NewMinTree(data)
fmt.Println("dizi:", data)
fmt.Println()
ranges := [][2]int{{0, 7}, {1, 3}, {4, 6}, {2, 2}, {5, 7}}
allMatch := true
for _, r := range ranges {
got := st.Query(r[0], r[1])
want := bruteMin(data, r[0], r[1])
if got != want {
allMatch = false
}
fmt.Printf("min[%d..%d] = %-3d (kaba kuvvet: %d) ✓=%t\n", r[0], r[1], got, want, got == want)
}
fmt.Println("tüm sorgular doğru:", allMatch)
fmt.Println()
st.Update(4, 20)
data[4] = 20
fmt.Println("indeks 4: 1 → 20")
for _, r := range ranges {
got := st.Query(r[0], r[1])
want := bruteMin(data, r[0], r[1])
fmt.Printf("min[%d..%d] = %-3d ✓=%t\n", r[0], r[1], got, got == want)
}
}dizi: [7 2 9 4 1 8 3 6] min[0..7] = 1 (kaba kuvvet: 1) ✓=true min[1..3] = 2 (kaba kuvvet: 2) ✓=true min[4..6] = 1 (kaba kuvvet: 1) ✓=true min[2..2] = 9 (kaba kuvvet: 9) ✓=true min[5..7] = 3 (kaba kuvvet: 3) ✓=true tüm sorgular doğru: true indeks 4: 1 → 20 min[0..7] = 2 ✓=true min[1..3] = 2 ✓=true min[4..6] = 3 ✓=true min[2..2] = 9 ✓=true min[5..7] = 3 ✓=true
Toplam sürümünden tek farkın birleştirme fonksiyonu ve nötr değer olduğuna dikkat et. Segment ağacını bir kez yazdığında, farklı işlemler için yeniden yazman gerekmez — jenerik hâle getirip işlemi parametre olarak geçirmek yeterlidir.
Bir dizideki ters çiftleri (i < j iken a[i] > a[j] olan çiftler) Fenwick ağacıyla O(n log n) sürede say. Kaba kuvvet O(n²) çözümle karşılaştırarak doğrula.
İpucu
Diziyi sağdan sola gez. Her eleman için, kendisinden küçük kaç eleman zaten görüldüğünü Fenwick'e sor. Değerleri sıkıştırarak indeks olarak kullan.
Çözümü göster
package main
import (
"fmt"
"slices"
)
type Fenwick struct {
tree []int
n int
}
func NewFenwick(n int) *Fenwick { return &Fenwick{tree: make([]int, n+1), n: n} }
func (f *Fenwick) Add(i, delta int) {
for i++; i <= f.n; i += i & -i {
f.tree[i] += delta
}
}
func (f *Fenwick) PrefixSum(i int) int {
sum := 0
for i++; i > 0; i -= i & -i {
sum += f.tree[i]
}
return sum
}
// compress: değerleri 0..k-1 aralığına sıkıştırır
func compress(data []int) []int {
sorted := slices.Clone(data)
slices.Sort(sorted)
sorted = slices.Compact(sorted)
out := make([]int, len(data))
for i, v := range data {
idx, _ := slices.BinarySearch(sorted, v)
out[i] = idx
}
return out
}
// countInversions: O(n log n)
func countInversions(data []int) int {
if len(data) < 2 {
return 0
}
ranks := compress(data)
maxRank := slices.Max(ranks) + 1
f := NewFenwick(maxRank)
count := 0
for i := len(ranks) - 1; i >= 0; i-- {
if ranks[i] > 0 {
count += f.PrefixSum(ranks[i] - 1) // sağda kendisinden küçük kaç eleman var
}
f.Add(ranks[i], 1)
}
return count
}
// bruteForce: O(n²)
func bruteForce(data []int) int {
count := 0
for i := range data {
for j := i + 1; j < len(data); j++ {
if data[i] > data[j] {
count++
}
}
}
return count
}
func main() {
cases := [][]int{
{5, 4, 3, 2, 1}, // tamamen ters
{1, 2, 3, 4, 5}, // sıralı
{2, 4, 1, 3, 5}, // karışık
{3, 3, 3}, // tekrarlı
{10, 1, 10, 1}, // hem tekrar hem ters
{42}, // tek eleman
{}, // boş
}
fmt.Printf("%-22s %10s %12s %8s\n", "dizi", "Fenwick", "kaba kuvvet", "aynı")
for _, c := range cases {
fast := countInversions(c)
slow := bruteForce(c)
fmt.Printf("%-22v %10d %12d %8t\n", c, fast, slow, fast == slow)
}
}dizi Fenwick kaba kuvvet aynı [5 4 3 2 1 ] 10 10 true [1 2 3 4 5 ] 0 0 true [2 4 1 3 5 ] 3 3 true [3 3 3 ] 0 0 true [10 1 10 1 ] 3 3 true [42 ] 0 0 true [] 0 0 true
Değer sıkıştırma (coordinate compression) burada zorunlu bir adımdır: Fenwick ağacı indeks tabanlı çalışır, ama dizideki değerler çok büyük ya da negatif olabilir. Sıralayıp benzersizleştirerek değerleri 0'dan başlayan küçük indekslere eşlersin.
Ters çift sayma, bir dizinin "ne kadar sırasız" olduğunun ölçüsüdür ve pratik anlamı vardır: Ekleme sıralamasının yapacağı takas sayısı tam olarak ters çift sayısına eşittir. Aynı problem birleştirme sıralamasıyla da O(n log n) sürede çözülür; onu Böl ve Fethet dersinde göreceksin.
Tembel yayılımlı bir segment ağacı yaz ama bu kez "aralığa ekle" yerine "aralığı belirli bir değere ata" işlemini destekle. Toplam sorgusu da çalışsın. İki işlem türünün birlikte nasıl ele alınacağına dikkat et.
İpucu
Atama işleminde bekleyen not, "bu aralıktaki her eleman şu değere eşit" bilgisidir. Ekleme notundan farklı olarak, yeni bir atama eskisini tamamen geçersiz kılar; bu yüzden notun var olup olmadığını ayrı bir bayrakla takip et.
Çözümü göster
package main
import "fmt"
type AssignTree struct {
sum []int
lazy []int
hasLazy []bool
n int
}
func NewAssignTree(data []int) *AssignTree {
st := &AssignTree{
sum: make([]int, 4*len(data)),
lazy: make([]int, 4*len(data)),
hasLazy: make([]bool, 4*len(data)),
n: len(data),
}
if st.n > 0 {
st.build(data, 1, 0, st.n-1)
}
return st
}
func (st *AssignTree) build(data []int, node, lo, hi int) {
if lo == hi {
st.sum[node] = data[lo]
return
}
mid := (lo + hi) / 2
st.build(data, 2*node, lo, mid)
st.build(data, 2*node+1, mid+1, hi)
st.sum[node] = st.sum[2*node] + st.sum[2*node+1]
}
// apply: bir düğüme atama notunu uygular
func (st *AssignTree) apply(node, lo, hi, value int) {
st.sum[node] = value * (hi - lo + 1)
st.lazy[node] = value
st.hasLazy[node] = true
}
// push: bekleyen atamayı çocuklara aktarır
func (st *AssignTree) push(node, lo, hi int) {
if !st.hasLazy[node] || lo == hi {
return
}
mid := (lo + hi) / 2
st.apply(2*node, lo, mid, st.lazy[node])
st.apply(2*node+1, mid+1, hi, st.lazy[node])
st.hasLazy[node] = false
}
// RangeAssign: [l, r] aralığındaki her elemanı value yapar
func (st *AssignTree) RangeAssign(l, r, value int) {
if st.n == 0 || l > r {
return
}
st.assign(1, 0, st.n-1, l, r, value)
}
func (st *AssignTree) assign(node, lo, hi, l, r, value int) {
if r < lo || hi < l {
return
}
if l <= lo && hi <= r {
st.apply(node, lo, hi, value)
return
}
st.push(node, lo, hi)
mid := (lo + hi) / 2
st.assign(2*node, lo, mid, l, r, value)
st.assign(2*node+1, mid+1, hi, l, r, value)
st.sum[node] = st.sum[2*node] + st.sum[2*node+1]
}
func (st *AssignTree) Query(l, r int) int {
if st.n == 0 || l > r {
return 0
}
return st.query(1, 0, st.n-1, l, r)
}
func (st *AssignTree) query(node, lo, hi, l, r int) int {
if r < lo || hi < l {
return 0
}
if l <= lo && hi <= r {
return st.sum[node]
}
st.push(node, lo, hi)
mid := (lo + hi) / 2
return st.query(2*node, lo, mid, l, r) + st.query(2*node+1, mid+1, hi, l, r)
}
func (st *AssignTree) Slice() []int {
out := make([]int, st.n)
for i := range out {
out[i] = st.Query(i, i)
}
return out
}
func main() {
data := []int{1, 2, 3, 4, 5, 6, 7, 8}
st := NewAssignTree(data)
fmt.Println("başlangıç:", st.Slice(), "toplam:", st.Query(0, 7))
st.RangeAssign(2, 5, 10)
fmt.Println()
fmt.Println("[2..5] → 10 atandı")
fmt.Println("dizi:", st.Slice())
fmt.Println("toplam[0..7] =", st.Query(0, 7), "(1+2+40+7+8)")
fmt.Println("toplam[2..5] =", st.Query(2, 5))
st.RangeAssign(0, 3, 0)
fmt.Println()
fmt.Println("[0..3] → 0 atandı (önceki atamayı kısmen geçersiz kılar)")
fmt.Println("dizi:", st.Slice())
fmt.Println("toplam[0..7] =", st.Query(0, 7))
st.RangeAssign(0, 7, 3)
fmt.Println()
fmt.Println("[0..7] → 3 atandı")
fmt.Println("dizi:", st.Slice())
fmt.Println("toplam:", st.Query(0, 7), "= 8 × 3")
fmt.Println("tek eleman[4]:", st.Query(4, 4))
}başlangıç: [1 2 3 4 5 6 7 8] toplam: 36 [2..5] → 10 atandı dizi: [1 2 10 10 10 10 7 8] toplam[0..7] = 58 (1+2+40+7+8) toplam[2..5] = 40 [0..3] → 0 atandı (önceki atamayı kısmen geçersiz kılar) dizi: [0 0 0 0 10 10 7 8] toplam[0..7] = 35 [0..7] → 3 atandı dizi: [3 3 3 3 3 3 3 3] toplam: 24 = 8 × 3 tek eleman[4]: 3
Atama ile ekleme arasındaki temel fark, notların birikme biçimidir. Ekleme notları toplanır: Önce +5, sonra +3 gelirse sonuç +8'dir. Atama notları ise birbirinin üzerine yazar: Önce 10, sonra 0 atanırsa sonuç 0'dır ve ilk atamanın hiçbir izi kalmaz. Bu yüzden atamada "not var mı?" bilgisini ayrı bir bayrakla tutmak gerekir — sıfır değeri geçerli bir atama olabileceği için, notun yokluğunu sıfırla temsil edemezsin.
İki işlem türünün birlikte desteklenmesi gerekirse (hem ekle hem ata) iş daha da incelir: Bekleyen notlar arasında bir öncelik sırası tanımlaman gerekir, çünkü bir atamadan sonra gelen ekleme ile bir eklemeden sonra gelen atama farklı sonuçlar verir. Bu tür birleşik tembel yayılım, segment ağacı uygulamalarının en zor kısmıdır ve dikkatli test gerektirir.
Kısa sınav
Önek toplamları yaklaşımının temel kısıtı nedir?
Segment ağacında bir aralık sorgusu neden O(log n) sürer?
Segment ağacında hangi işlemler desteklenebilir?
Tembel yayılım (lazy propagation) ne sağlar?
Fenwick ağacındaki i & -i işlemi ne verir?
Fenwick ağacı ile aralık minimumu sorgulanabilir mi?
Özet
- Önek toplamları sabit dizilerde O(1) sorgu verir ama güncelleme O(n)'dir.
- Segment ağacı hem sorguyu hem güncellemeyi O(log n)'de tutar; kurma O(n) sürer.
- Her düğüm bir aralığın özetini saklar; sorgu üç duruma ayrılır: tamamen dışarıda, tamamen içeride, kısmen kesişen.
- Dizi temsilinde kök 1. indekstedir, çocuklar 2i ve 2i+1; güvenli boyut 4n'dir.
- Birleşmeli olan her işlem desteklenir: toplam, minimum, maksimum, OBEB, bit işlemleri. Doğru nötr değer seçmek kritiktir.
- Tembel yayılım, aralık güncellemelerini O(log n)'e indirir; her düğüme dokunmadan önce bekleyen not uygulanmalıdır.
- Fenwick ağacı çok daha kısadır ve az bellek kullanır ama yalnızca tersi olan işlemlerle çalışır.
i & -ihilesi Fenwick ağacının tüm mantığını taşır: yukarı çıkarken ekle, aşağı inerken çıkar.- Karar sırası: dizi sabitse önek toplamı, yalnızca toplam gerekiyorsa Fenwick, diğer her durumda segment ağacı.