Segmenttipuu

Kokeneet kirjoittajat eivät ole vielä tarkistaneet sivun nykyistä versiota, ja se voi poiketa merkittävästi 6. lokakuuta 2016 tarkistetusta versiosta . vahvistus vaatii 41 muokkausta .

Segmenttipuu on tietorakenne , jonka avulla voit löytää assymptotiikkaa varten jonkin assosiatiivisen funktion arvon mielivaltaisesta taulukon segmentistä . Yleisimmin käytetyt funktiot ovat summa, tulo, maksimi ja minimi.

Rakenteen kuvaus

Segmenttipuu on juurtunut puu, jonka lehdet ovat alkuperäisen taulukon elementtejä, ja muilla kärjeillä on kullakin 2 lasta. Jokaiselle kärjelle on määritetty väli, joka on sen lapsien välien liitto (jos kärjessä on lapsia), tai väli, joka sisältää tietyn taulukon elementin (lehdille). Lisäksi jokaiselle kärjelle tallennetaan jonkin assosiatiivisen funktion arvo tietyllä aikavälillä. Tällä puulla on logaritminen korkeus, koska tasojen lukumäärä ei ylitä

Segmenttipuu muistissa

Olkoon taulukossamme elementtejä : .

Valitaan sellainen . Täydennetään oikealla olevaa taulukkoamme neutraaleilla elementeillä niin, että sen pituus on yhtä suuri kuin . Sitten taulukon elementteihin rakennetun segmenttipuun tallentamiseksi tarvitsemme joukon soluja .

Emme käytä nollasolua taulukossa , ja solut ensimmäisestä -: nteen vastaavat binääripuun huippuja vastaavilla numeroilla. Yleensä segmenttipuun kärkien numerointia käytetään läpikulkujärjestyksessä leveys , eli puun juurella on numero 1 ja kärjen vasemmalla ja oikealla pojilla numerolla on numerot ja vastaavasti. Tällä numeroinnilla kärki, jonka numero on at , vastaa segmenttiä , eli numero tallennetaan soluun .


Artikkelissa käytetään tätä segmenttipuun kärkien numerointia. Vaihtoehtoisesti voit numeroida kärjet syvyystraversal järjestyksessä , jolloin kärjen vasemmalla ja oikealla pojilla on numerot ja , missä on kärkeä vastaava segmentti . Samanaikaisesti, jos rakennat segmenttipuun heti alkuperäisen taulukon mukaan etkä laajenna sitä pituuteen (sellaisen puun kaikki segmentin pituudet eivät ole kahden potenssit eivätkä kaikki lehdet sijaitse maksimissaan syvyys), niin kaikki taulukon solut riittävät sen tallentamiseen . Tallennettaessa puuta, jonka kärjet on numeroitu leveyden läpikulkujärjestyksessä, taulukon pituus voi olla .

Segmenttipuun rakentaminen

Alla on C++-koodi rekursiiviselle funktiolle segmenttipuun muodostamiseksi taulukon elementtien summalle . Puun rakentamisen monimutkaisuus on toimia.

tyhjä rakenne () { rakentaa(1, 0, (1 << h) - 1); } void build(int v, int L, int R) { jos (L == R){ b[v] = a[L]; } else { int C = (L + R)/2; rakentaa (v * 2, L, C); rakentaa (v * 2 + 1, C + 1, R); b[v] = b[v*2] + b[v*2 + 1]; } }

Segmenttipuu yhdellä muutoksella

Elementin muuttaminen

Muutetaan arvoa . Sitten meidän on päivitettävä arvot soluissa , , ,..., . Siten se ryhtyy toimiin elementin muuttamiseksi.

Alla on C++-koodi rekursiiviselle proseduurille segmenttipuun päivittämiseksi summalle , kun lähdetaulukon -. elementti muuttuu .

mitätön päivitys (int i, int uusi arvo) { päivitys(1, 0, (1 << h) - 1, i, uusiArvo); } void update (int v, int L, int R, int i, int uusi arvo) { jos (L == R){ b[v] = uusi Arvo; a[i] = uusi Arvo; } else { int C = (L + R)/2; if (i <= C){ päivitys(v * 2, L, C, i, uusiArvo); } else { päivitä(v * 2 + 1, C + 1, R, i, uusiArvo); } b[v] = b[v*2] + b[v*2 + 1]; } }

Funktion laskeminen segmentille

Funktion laskemiseen elementeistä käytetään seuraavaa rekursiivista funktiota argumenteista , joka laskee funktion arvon segmentille . Tässä on sellainen puun kärkipiste, että solu sisältää segmentin funktion arvon .

Jos segmentit ja eivät leikkaa, niin vastaus on yhtä suuri kuin funktion neutraali elementti (0 summalle, 1 tulolle, maksimille, minimille).

Jos , niin vastaus on .

Muussa tapauksessa jaamme segmentin kahtia asettamalla . Pelkistetään ongelma funktion laskemiseen segmenteille ja . Sitten vastaus on .

Näin ollen segmentin funktion laskenta supistuu funktion laskemiseen joitakin segmenttejä vastaavien taulukon elementeistä .

Osoitetaan, että kun funktio lasketaan, saadaan tulokset. Jokaisella syvyydellä palautamme arvon enintään kahdesta puun kärjestä. Päinvastoin, oletamme, että niitä on vähintään kolme. Mutta sitten kaksi kutsua kahdesta naapuripisteestä voitaisiin korvata yhdellä kutsulla heidän yhteiseltä vanhemmalta. Siksi kullakin syvyydellä palautamme korkeintaan kaksi arvoa. Rakenteesta johtuen puun korkeus ei ylitä . Siksi palautuksia ei tehdä enempää.

Samanlainen päättely osoittaa, että yhdessä puun kyselyssä ohitamme korkeintaan kärjet.

Alla on C++-koodi segmentin summan laskemiseen .

int getSum(int l, int r) { return getSum(1, 0, (1 << h) - 1, l, r); } int getSum(int v, int L, int R, int l, int r) { jos (L > r || R < l){ paluu 0; } jos (l <= L && R <= r){ paluu b[v]; } int C = (L + R)/2; return getSum(v * 2, L, C, l, r) + getSum(v * 2 + 1, C + 1, R, l, r); }

Segmenttipuu intervallin muokkauksella

Oletetaan, että haluamme muuttaa ei yhden taulukon solun arvoa , vaan koko intervallin (esimerkiksi kasvattaa kaikkien solujen arvoja väliltä annetulla numerolla ). Silloin pelkän taulukon tallentaminen ei riitä. Kuitenkin segmenttipuut, jotka pystyvät laskemaan summan ja maksimin, voidaan toteuttaa tallentamalla kaksi samanpituista taulukkoa ja toteuttamalla muutosoperaatio rekursiivisesti.

Segment Tree for Sum (RSQ)

Tallennamme taulukoita samalla osoitteella kuin taulukko (katso yllä).


Rekursiivinen menettely koostuu segmentin kaikkien elementtien arvon kasvattamisesta numerolla . voi olla sekä positiivista että negatiivista. Tässä on puun kärkipiste, jossa solu sisältää välin kaikkien elementtien summan .

Alla on menettelykoodi C++:ssa.

void muokata(int l, int r, int X) { muokata(1, 0, (1 << h) - 1, l, r, X); } void muokata(int v, int L, int R, int l, int r, int X) { jos (L > r || R < l){ palata; } jos (l <= L && R <= r){ summa[v] += X* (R - L + 1); add[v] += X; } else { int C = (L + R)/2; modifioi (v*2, L, C, l, r, X); modifioi (v * 2 + 1, C + 1, R, l, r, X); summa[v] = summa[v * 2] + summa[v * 2 + 1] + summa[v] * (R - L + 1); } }

Rekursiivista funktiota segmentin summan laskemiseksi muutetaan seuraavasti. Hänellä on vielä yksi argumentti , joka kuvaa sitä, kuinka paljon sinun on lisättävä kaikkia segmentin numeroita summaa laskettaessa.

int getSum(int l, int r) { return getSum(1, 0, (1 << h) - 1, l, r, 0); } int getSum(int v, int L, int R, int l, int r, int additive) { jos (L > r || R < l){ paluu 0; } jos (l <= L && R <= r){ palautussumma[v] + lisäys * (R - L + 1); } int C = (L + R)/2; return getSum(v * 2, L, C, l, r, additive + add[v]) + getSum(v * 2 + 1, C + 1, R, l, r, additiivinen + additiivinen [v]); }


Toiminnan monimutkaisuus on .

Suurin segmenttipuu (RMQ)

Kuten edellisessä tapauksessa, tallennamme taulukot ja . Menettelyllä on sama merkitys ja samat argumentit.

void muokata(int l, int r, int X) { muokata(1, 0, (1 << h) - 1, l, r, X); } void muokata(int v, int L, int R, int l, int r, int X) { jos (L > r || R < l){ palata; } jos (l <= L && R <= r){ max[v] += X; add[v] += X; } else { int C = (L + R)/2; modifioi (v*2, L, C, l, r, X); modifioi (v * 2 + 1, C + 1, R, l, r, X); max[v] = std::max(max[v * 2], max[v * 2 + 1]) + add[v]; } }

Rekursiivinen funktio segmentin maksimin laskemiseksi toteutetaan samalla tavalla kuin summan segmenttipuufunktio.

int getMax(int ​​l, int r) { return getMax(1, 0, (1 << h) - 1, l, r, 0); } int getMax(int ​​​​v, int L, int R, int l, int r, int additive) { jos (L > r || R < l){ paluu -INF; // Miinus ääretön, ts. numero, joka on varmasti pienempi kuin mikään segmenttimme numero. Jos esimerkiksi kaikki luvut eivät ole negatiivisia, voit laittaa INF = 0. } jos (l <= L && R <= r){ paluu max[v] + lisäaine; } int C = (L + R)/2; int max1 = getMax(v * 2, L, C, l, r, additive + add[v]); int max2 = getMax(v * 2 + 1, C + 1, R, l, r, additive + add[v]); return std::max(max1, max2); }


Toiminnan monimutkaisuus on .

RMQ:n ratkaiseminen harvalla taulukolla

Myös RMQ-ongelma voidaan ratkaista käyttämällä Sparse-taulukkoa. Tämän tietorakenteen avulla voit löytää segmentin maksimi-/minimiarvot O(1):ssä esikäsittelyllä O(n log n) ajassa.

Esikäsittely:

Merkitse - maksimi/minimi segmentissä välillä - . Taulukko voidaan täyttää dynaamisesti seuraavasti:

1) ;

2) tai vastaavasti.

Laskeminen:

Vastaus väliin on (vastaavasti ), jossa lg on kahden maksimiteho, joka ei ylitä .

Esimerkki:

Tarkastellaan vaihteluväliä 1 - 5. Sille sopiva kahden maksimiteho on kaksi, mutta se ei kata koko aluetta, vaan vain osan 1 - 4. Tämän segmentin maksimi saadaan vertaamalla arvoja f[1,2] ja f[2,2] (maksimiarvot segmenteillä 1-4 ja 2-5).

O(1)-ratkaisu O(N)-esikäsittelyllä ja muistilla

Tällaista ratkaisua varten riittää pelkistää RMQ-ongelma LCA -ongelmaksi rakentamalla karteesinen puu muodon elementeistä eli - avain, - prioriteetti. Prioriteetit on järjestettävä alhaalta ylöspäin, eli se elementti, jolla on pienin . Ilmeisesti tällainen puu on helppo rakentaa :lle , koska kaikki avaimet, jotka meillä on, ovat järjestyksessä (nämä ovat taulukon elementtien peräkkäisiä indeksejä).

Sen jälkeen vastaus mihin tahansa pyyntöön on kahden kärjen ja :n LCA . LCA-indeksi sijaitsee , koska karteesinen puu avaimen mukaan on binääripuu. Koska karteesinen puu on prioriteettikeko , samalla solmulla on alhaisin prioriteetti (elementtiarvo) kaikista

Esikäsittelyn ja muistin LCA - ongelmaan tunnetaan useita mahdollisia ratkaisuja . Tällainen ratkaisu on asymptoottisesti optimaalinen.

Linkit

Katso myös