線段樹學習筆記

什麼是線段樹

圖片來自百度百科

線段樹是一種二叉搜索樹,與區間樹類似,它將一個區間劃分紅一些單元區間,每一個單元區間對應線段樹中的一個葉結點。使用線段樹能夠快速的查找某一個節點在若干條線段中出現的次數,時間複雜度爲 $O(\log N)$ 。而未優化的空間複雜度爲 $2N$ ,所以有時須要離散化讓空間壓縮。——by 百度數組

怎麼構造線段樹

首先明確一件事,根($root$)的左孩子是$root \cdot 2 $或$root << 1$,右孩子是$root \cdot 2+1$或$root<< 1 | 1$函數

咱們有個大小爲 $5$ 的數組 $a={10,11,12,13,14}$ 要進行區間求和操做,如今咱們要怎麼把這個數組存到線段樹中(也能夠說是轉化成線段樹)呢?咱們這樣子作:設線段樹的根節點編號爲 $1$ ,用數組 $d$ 來保存咱們的線段樹, $d[i]$ 用來保存編號爲 $i$ 的節點的值(這裏節點的值就是這個節點所表示的區間總和),如圖所示:

上圖來自$oi-wiki$優化

void build(int root,int l,int r) {
    if(l==r) {//若是到了葉子節點就直接賦值
        tree[root]=a[l];
        return;
    }
    int mid=(l+r)/2;
    build(root*2,l,mid);//遞歸左子樹
    build(root*2+1,mid+1,r);//遞歸右子樹
    tree[root]=tree[root*2]+tree[root*2+1];//注意須要更新根節點
}

咱們來看一下$build$函數的運行過程,當從$root$開始遞歸時遞歸了左子樹,左子樹又遞歸左子樹,一直到葉節點返回了左葉節點的值,而後和上面的同樣去遞歸右子樹,一直到葉而後返回了右葉節點的值(上面描述的可能不是太清楚,能夠本身結合一下上圖圖),而後一層一層的返回就能夠了ui

線段樹的區間查詢

區間查詢,好比求區間 $[l,r]$ 的總和(即 $a[l]+a[l+1]+ \cdots +a[r]$ )、求區間最大值/最小值……還有不少不少……怎麼作呢?
3d

如上圖舉例
若是要查詢區間 $[1,5]$ 的和,那直接獲取 $d[1]$ 的值( $60$ )便可。那若是我就不查詢區間 $[1,5]$ ,我就查區間 $[3,5]$ 呢?code

傻了吧。但其實呢咱們確定仍是有辦法的!htm

你要查的不是 $[3,5]$ 嗎?我把 $[3,5]$ 拆成 $[3,3]$ 和 $[4,5]$ 不就好了嗎?blog

int query(int root,int l,int r,int x,int y) {
    if(x<=l && r<=y) return tree[root];
    int mid=(l+r)/2;
    int ans=0;
    pushdown(root,l,r,mid);
    if(x<=mid)  ans+=query(root*2,l,mid,x,y);
    if(mid<y)   ans+=query(root*2+1,mid+1,r,x,y);
    return ans;
}

線段樹的區間修改與懶惰標記

這裏就是線段樹的精髓了,請仔細理解遞歸

區間修改是個頗有趣的東西……你想啊,若是你要修改區間 $[l,r]$ ,難道把全部包含在區間[l,r]中的節點都遍歷一次、修改一次?那估計這時間複雜度估計會上天。這怎麼辦呢?咱們這裏要引用一個叫作 「懶惰標記」 的東西。

咱們設一個數組 $b$ , $b[i]$ 表示編號爲 $i$ 的節點的懶惰標記值。啥是懶惰標記、懶惰標記值呢?這裏我再舉個例子:

A 有兩個兒子,一個是 B,一個是 C。

有一天 A 要建一個新房子,沒錢。恰好過年嘛,有人要給 B 和 C 紅包,兩個紅包的錢數相同都是 $(1000000000000001\bmod 2)$ 圓(好多啊!……不就是 $1$ 元嗎……),然而由於 A 是父親因此紅包確定是先塞給 A 咯~

理論上來說 A 應該把兩個紅包分別給 B 和 C,可是……缺錢嘛,A 就把紅包偷偷收到本身口袋裏了。

A 高興地說:「我如今有 $2$ 份紅包了!我又多了 $2\times (1000000000000001\bmod 2)=2$ 元了!哈哈哈~」

可是 A 知道,若是他不把紅包給 B 和 C,那 B 和 C 確定會不爽而後致使家庭矛盾最後崩潰,因此 A 對兒子 B 和 C 說:「我欠大家每人 $1$ 份 $(1000000000000001\bmod 2)$ 圓的紅包,下次有新紅包給過來的時候再給大家!這裏我先作下記錄……嗯……我錢大家各 $(1000000000000001\bmod 2)$ 圓……」

兒子 B、C 有點惱怒:「但是若是有同窗問起咱們咱們收到了多少紅包咋辦?你把咱們的紅包都收了,咱們還怎麼裝X?」

父親 A 趕緊說:「有同窗問起來我就會給大家的!我欠條都寫好了不會不算話的!」

這樣 B、C 才放了心。

在這個故事中咱們不難看出,A 就是父親節點,B 和 C 是 A 的兒子節點,並且 B 和 C 是葉子節點,分別對應一個數組中的值(就是以前講的數組 $a$ ),咱們假設節點 A 表示區間 $[1,2]$ (即 $a[1]+a[2]$ ),節點 B 表示區間 $[1,1]$ (即 $a[1]$ ),節點 C 表示區間 $[2,2]$ (即 $a[2]$ ),它們的初始值都爲 $0$ (如今纔剛開始呢,還沒拿到紅包,因此都沒錢~)。

如圖:


注:這裏 D 表示當前節點的值(即所表示區間的區間和)。
爲何節點 A 的 D 是 $2\times (1000000000000001\bmod 2)$ 呢?緣由很簡單:節點 A 表示的區間是 $[1,2]$ ,一共包含 $2$ 個元素。咱們是讓 $[1,2]$ 這個區間的每一個元素都加上 $1000000000000001\bmod 2$ ,因此節點 A 的值就加上了 $2\times (1000000000000001\bmod 2)$ 咯。

若是這時候咱們要查詢區間 $[1,1]$ (即節點 B 的值)怎麼辦呢?不是說了嗎?若是 B 要用到的時候,A 就把它欠的還給 B!

具體是這樣操做(如圖):

注:爲何是加上 $1\times (1000000000000001\bmod 2)$ 呢?

緣由和上面同樣——B 和 C 表示的區間中只有 $1$ 個元素啊!

由此咱們能夠獲得,區間 $[1,1]$ 的區間和就是 $1$ 啦!O(∩_∩)O 哈哈~!

PS:上述解釋來自$Oi-wiki$,我以爲解釋的很好能夠看看,附上上面解釋的原版代碼

void update(int l, int r, int c, int s, int t,int p){
  // [l,r] 爲修改區間,c 爲被修改的元素的變化量,[s,t] 爲當前節點包含的區間,p 爲當前節點的編號
  if (l <= s && t <= r) {
    d[p] += (t - s + 1) * c, b[p] += c;
    return;
  }// 當前區間爲修改區間的子集時直接修改當前節點的值,而後打標記,結束脩改
  int m = (s + t) / 2; 
  if (b[p] && s!=t){
    // 若是當前節點的懶標記非空,則更新當前節點兩個子節點的值和懶標記值
    d[p * 2] += b[p] * (m - s + 1), d[p * 2 + 1] += b[p] * (t - m);
    b[p * 2] += b[p], b[p * 2 + 1] += b[p]; // 將標記下傳給子節點
    b[p] = 0; // 清空當前節點的標記
  }
  if (l <= m) update(l, r, c, s, m, p * 2);
  if (r > m) update(l, r, c, m + 1, t, p * 2 + 1);
  d[p] = d[p * 2] + d[p * 2 + 1];
}

下面是個人代碼:

區間修改

void update(int root,int l,int r,int x,int y,int v) {
    if(x<=l && r<=y) return add(root,l,r,v);
    int mid=(l+r)/2;
    pushdown(root,l,r,mid);
    if(x<=mid)  update(root*2,l,mid,x,y,v);
    if(y>mid)   update(root*2+1,mid+1,r,x,y,v);
    tree[root]=tree[root*2]+tree[root*2+1];
}

$add$函數

void add(int root,int l,int r,int v) {
    tree[root]+=v*(r-l+1);
    lazy[root]+=v;
}

下放懶標記

void pushdown(int root,int l,int r,int mid) {
    if(lazy[root]==0) return ;
    add(root*2,l,mid,lazy[root]);
    add(root*2+1,mid+1,r,lazy[root]);
    lazy[root]=0;
}

單點修改

單節點更新是指只更新線段樹的某個葉子節點的值,可是更新葉子節點會對其父節點的值產生影響,所以更新子節點後,要回溯更新其父節點的值

/*
功能:更新線段樹中某個葉子節點的值
root:當前線段樹的根節點下標
[nstart, nend]: 當前節點所表示的區間
index: 待更新節點在原始數組arr中的下標
addVal: 更新的值(原來的值加上addVal)
*/
void updateOne(int root, int nstart, int nend, int index, int addVal)
{
    if(nstart == nend)
    {
        if(index == nstart)//找到了相應的節點,更新之
            segTree[root].val += addVal;
        return;
    }
    int mid = (nstart + nend) / 2;
    if(index <= mid)//在左子樹中更新
        updateOne(root*2+1, nstart, mid, index, addVal);
    else updateOne(root*2+2, mid+1, nend, index, addVal);//在右子樹中更新
    //根據左右子樹的值回溯更新當前節點的值
    segTree[root].val = segTree[root*2+1].val+segTree[root*2+2].val;
}

參考資料

1.oi-wiki
2.一步一步理解線段樹 3.信息學奧賽一本通

相關文章
相關標籤/搜索