I. Szegmensfa definició

A szegmensfa egy hatékony adatszerkezet, amelyet főként tartományokra vonatkozó műveletek gyors elvégzésére használnak. Egy bináris fa struktúrát használ, amely lehetővé teszi a tartományokra vonatkozó műveletek (pl. összegzés, minimum, maximum, LNKO) hatékony végrehajtását.

Fontos: A vizualizációk során kizárólag az összegzés műveletével fogunk dolgozni.

Amikor egy tömbben ismétlődő intervallum-lekérdezéseket végzünk (pl. egy adott tartomány összegét számítjuk), a naiv megoldás O(n) időben fut, ami nagyobb adathalmazoknál lassú lehet. Egy szegmensfa használatával viszont a lekérdezések és módosítások O(log n) idő alatt végrehajthatók, így sokkal hatékonyabb megoldást biztosít.

Tekintsük a következő ábrát:
         [36]
        /    \
     [7]      [29]
    /  \      /   \    
 [4]  [3] [18]   [11]  

Jól látható, hogy az ábrán a levelek képviselik a tartomány egyes elemeit, továbbá mindegyik szülő node a két alatta lévő gyermek node összegéből adódik. Ebből következik, hogy a tömb amiből a fa épült: [4, 3, 18, 11]


II. Szegmensfa felépítése

Tegyük fel, hogy van egy tömbünk: [1, 3, 5, 7, 9, 11]. És tekintsük a következő kódot:


build(nodeIndex, left, right) {
  if (left === right) {
      this.tree[nodeIndex] = this.inputArray[left];
  } else {
      let mid = Math.floor((left + right) / 2);
      this.build(nodeIndex * 2, left, mid);
      this.build(nodeIndex * 2 + 1, mid + 1, right);
      this.tree[nodeIndex] = this.tree[nodeIndex * 2] + this.tree[nodeIndex * 2 + 1];
  }
}

Megjegyzés: A kód a statikus szegmensfa építési algoritmusát szemlélteti.

II. 1. A kód értelmezése, magyarázat soronként:

build(nodeIndex, left, right) {

➡️ Ez a függvény egy szakaszt épít fel a fából: az inputArray tömb left és right közötti részét.

  • nodeIndex: annak a csúcsnak az indexe, amiben a bal-jobb tartomány összegét tároljuk.
  • left, right: ez a tartomány (szakasz) az eredeti tömbben.
if (left === right) {

➡️ Ha left == right, akkor ez egy levélcsúcs a fában, vagyis csak egyetlen elemről van szó.

this.tree[nodeIndex] = this.inputArray[left];

➡️ A levélcsúcs értéke maga az inputArray-ben lévő szám.

} else {
    let mid = Math.floor((left + right) / 2);

➡️ Ha a tartomány több elemet fed le, akkor kettéosztjuk: kiszámoljuk a közepét (mid).

this.build(nodeIndex * 2, left, mid);
this.build(nodeIndex * 2 + 1, mid + 1, right);

➡️ Rekurzívan építjük fel a bal és jobb részfát.

  • Bal gyerek: nodeIndex * 2
  • Jobb gyerek: nodeIndex * 2 + 1
this.tree[nodeIndex] = this.tree[nodeIndex * 2] + this.tree[nodeIndex * 2 + 1];

➡️ Miután a bal és jobb részfák készen vannak, a szülő csomópont értéke azok összege lesz.

II. 2. Fa felépítése lépésről lépésre

➡️ Induló hívás:

build(1, 0, 5)

A teljes tömb: [1, 3, 5, 7, 9, 11]

Első lépés: Felosztjuk a tömböt:

  • mid = Math.floor((0 + 5)/2) = 2
  • Bal rész: [1, 3, 5]build(2, 0, 2)
  • Jobb rész: [7, 9, 11]build(3, 3, 5)
➡️ Bal oldali rész továbbosztása (build(2, 0, 2)):
  • mid = 1
  • build(4, 0, 1) és build(5, 2, 2)

build(4, 0, 1) továbboszt:

  • mid = 0
  • build(8, 0, 0) → levél → tree[8] = 1
  • build(9, 1, 1) → levél → tree[9] = 3
  • tree[4] = tree[8] + tree[9] = 1 + 3 = 4

build(5, 2, 2) → levél → tree[5] = 5

tree[2] = tree[4] + tree[5] = 4 + 5 = 9

➡️ Jobb oldali rész (build(3, 3, 5)):
  • mid = 4
  • build(6, 3, 4) és build(7, 5, 5)

build(6, 3, 4):

  • mid = 3
  • build(12, 3, 3)tree[12] = 7
  • build(13, 4, 4)tree[13] = 9
  • tree[6] = 7 + 9 = 16

build(7, 5, 5)tree[7] = 11

tree[3] = tree[6] + tree[7] = 16 + 11 = 27

Végső lépés:

tree[1] = tree[2] + tree[3] = 9 + 27 = 36

A fa vizuálisan
                  [0,5] (36)
                /         \
            [0,2] (9)     [3,5] (27)
            /               /      
              
        /   \              /   \
  [0,0](1) [1,1](3)  [3,3](7) [4,4](9)

II. 3. Összegzés

A szegmensfa egy rendkívül hatékony és hasznos adatszerkezet, amely lehetővé teszi a gyors intervallum-lekérdezéseket. Kiemelkedő előnye az O(log n) időbonyolultságú műveletek, amelyek révén nagy adathalmazokkal is hatékonyan dolgozhatunk.

A szegmensfák tehát nem csupán egyszerű adatstruktúrák, hanem kulcsfontosságú szerepet játszanak a modern számítástechnika egyes területein, különösen a versenyprogramozásban és algoritmus-tervezésben. Alkalmazásuk számos különféle területen elterjedt, és lehetővé teszik a rendkívül hatékony adatkezelést és számításokat, még akkor is, ha az adatok dinamikusan változnak.


III. Különböző műveletek bemutatása

III. 1. Maximum művelet

A szegmensfa minden csúcsába a tartomány maximuma kerül. A lekérdezéshez a Math.max() használatos.

              [0,7] (30)
              /           \
        [0,3] (18)     [4,7] (30)
        /     \         /     \
      [0,1] [2,3]     [4,5] [6,7]
      (18)   (9)      (21)   (30)
    /   \   /   \    /   \   /   \
  (18)   (6)(9) (3) (15) (21) (12) (30)
          

Megjegyzés: Hatékony maximum keresés bármely intervallumban.

III. 2. LNKO művelet (Legnagyobb közös osztó)

Itt minden csomópont a szakasz LNKO-ját tárolja. A lekérdezésekhez az LNKO(a, b) műveletet alkalmazzuk.

                [0,7] (3)
                /          \
          [0,3] (3)      [4,7] (3)
          /     \         /     \
        [0,1] [2,3]     [4,5] [6,7]
        (6)    (3)      (3)    (6)
      /   \   /   \    /   \   /   \
  (18)   (6)(9)  (3)(15) (21) (12) (30)
          

Megjegyzés: Gyors és hatékony LNKO lekérdezések intervallumokra.

III. 3. Minimum művelet

A minimum szegmensfa minden szakasz legkisebb értékét tárolja, Math.min() alapján.

              [0,7] (3)
              /           \
        [0,3] (3)      [4,7] (12)
        /     \         /     \
      [0,1] [2,3]     [4,5] [6,7]
      (6)    (3)      (15)   (12)
    /   \   /   \    /   \   /   \
  (18)   (6)(9) (3) (15) (21) (12) (30)
      

Megjegyzés: Hasznos minimumok gyors kiszámításához bármely tartományban.


IV. Típusok összehasonlítása (statikus, dinamikus, perzisztens)

Tulajdonság Statikus szegmensfa Dinamikus szegmensfa Perzisztens szegmensfa
Adatkezelés Fix adatokkal dolgozik, amelyek nem változnak Támogatja az adatok frissítését és módosítását Minden frissítés új verziót hoz létre – nincs adatvesztés
Lekérdezés ideje O(log n) O(log n) O(log n)
Frissítés ideje Nem támogatott (újraépítés szükséges) O(log n) O(log n) – új verzió jön létre
Memóriahasználat Alacsony Közepes – dinamikus struktúra miatt Magas – minden verzióhoz új faágak tárolódnak
Implementáció összetettsége Könnyű Közepes – rekurzió + mutáció kezelése Magas – verziókezelés, funkcionális szemlélet szükséges
Tipikus használat Statikus adatok, például előre ismert konfigurációk Real-time rendszerek, pénzügyi alkalmazások Verziókövetés, időutazós lekérdezések (pl. undo/redo)
Frissítés visszavonása Nem lehetséges Csak utolsó állapot elérhető Igen, bármely korábbi verzióhoz vissza lehet térni

V. Szegmensfa alkalmazási területei

➕ Tartományösszeg lekérdezése

Gyorsan kiszámíthatjuk egy tömb adott tartományában (pl. [3,7]) szereplő számok összegét.

A szegmensfa csomópontjai előre kiszámított részösszegeket tartalmaznak, így a lekérdezés ideje O(log n).

sum(3, 7) → 42

📉 Minimum/maximum keresés

Meghatározhatjuk egy intervallum legkisebb vagy legnagyobb elemét valós időben.

A fa csomópontjai a gyermekek minimumát/maximumát tárolják. Így frissítés és lekérdezés is O(log n).

min(2, 5) → 3

🔁 Fordított párok számlálása

Rendezés közben segíthet meghatározni, hány inverzió (fordított sorrendű pár) van egy tömbben.

A szegmensfát prefix összegek és előfordulások tárolására használjuk.

[4, 1, 3, 2] → 4 inverzió

📊 Real-time rendszerek

Pénzügyi adatok, érzékelők, vagy valós idejű streamelt értékek esetén fontos, hogy gyorsan frissíthető és lekérdezhető legyen a tartomány.

A dinamikus szegmensfa lehetővé teszi az adatok gyors frissítését O(log n) időben.

update(5, +10)

📅 Események kezelése idővonalon

Gyorsan meghatározható, hogy egy adott időintervallumban mennyi esemény történt.

Minden időpillanatra eltárolunk egy számlálót, így lekérdezhetők a tartományi összesítések.

events(10, 20) → 7 esemény

🏙️ Intervallum gyakoriság (pl. Metropolisz szám)

Egy játékban vagy modellben gyorsan lekérdezhetjük, hogy egy adott érték hányszor fordul elő egy tartományban.

Módosított szegmensfában minden értékhez külön gyakoriságot tárolunk.

count(3, 6, 7) → 2 db 7-es


VI. Elméleti háttér

Tétel: A szegmensfa felépítése \( O(n) \) idő alatt elvégezhető.

Bizonyítás: A szegmensfa egy teljes bináris fa, amelynek csomópontjait egy \( 2n \) méretű tömbben tárolhatjuk.

1. Levelek inicializálása

Az eredeti tömb elemeit a levelekhez másoljuk:

\[ v[n+i] = a[i], \quad 0 \leq i < n \]

Ez \( O(n) \) időt igényel.

2. Belső csomópontok feltöltése

Alulról építjük a fát, minden belső csomópont értékét a gyerekeiből számítjuk ki:

\[ v[i] = f(v[2i], v[2i+1]) \]

Minden csomópontot pontosan egyszer számolunk ki, így az időbeli komplexitás:

\[ B(n) = B(n/2) + O(n) \Rightarrow O(n) \]

Következtetés

A szegmensfa felépítése \( O(n) \) idő alatt végrehajtható.

Tétel: A szegmensfa egy elemének módosítása \( O(\log n) \) idő alatt végrehajtható.

Bizonyítás: Egy módosítás során:

1. Levél megkeresése

A keresett indexű levélcsomópontot \( O(\log n) \) lépés alatt elérjük.

2. Érték frissítése

Az új értéket beállítjuk a levélben.

3. Felmenő csomópontok frissítése

A módosítás hatását végighúzzuk a szülőkön:

\[ v[i] = f(v[2i], v[2i+1]) \]

Mivel a fa magassága \( O(\log n) \), az időbeli komplexitás:

\[ U(n) = U(n/2) + O(1) \Rightarrow O(\log n) \]

Következtetés

A módosítás \( O(\log n) \) idő alatt végrehajtható.

Tétel: A szegmensfa lekérdezése \( O(\log n) \) idő alatt végrehajtható.

Bizonyítás: Egy adott \([L, R]\) intervallumra vonatkozó lekérdezés során:

1. Levelek elérése

Az indexeket a megfelelő levelekhez mozgatjuk.

2. Intervallum felbontása

A teljes intervallum lefedéséhez csak azokat a csomópontokat választjuk, amelyek teljes átfedésben vannak:

\[ \text{Ha } L \text{ páratlan, hozzáadjuk } v[L] \text{-t és } L++ \] \[ \text{Ha } R \text{ páros, hozzáadjuk } v[R] \text{-t és } R-- \]

Ezután \( L \) és \( R \) szülőire lépünk:

\[ L = L/2, \quad R = R/2 \]

Következtetés

Mivel a fa magassága \( O(\log n) \), a művelet legfeljebb ennyi lépést végez, és minden lépés \( O(1) \).

Az időbeli komplexitás:

\[ Q(n) = Q(n/2) + O(1) \Rightarrow O(\log n) \]

Tehát egy lekérdezés \( O(\log n) \) idő alatt végrehajtható.