線段樹模板總結

bleso發表於2021-05-21

前言

一直以來都只會一些簡單的資料結構,像樹狀陣列、線段樹這樣的高階資料結構只在大佬口中聽過hhhh,今天自己也偷學一下線段樹,發現挺有意思而且並不是很難,理解思想程式碼就很好寫。

例題

最大數

題目描述

給定一個正整數數列 a1,a2,…,an,每一個數都在 0∼p−1 之間。

可以對這列數進行兩種操作:

  1. 新增操作:向序列後新增一個數,序列長度變成 n+1;
  2. 詢問操作:詢問這個序列中最後 LL 個數中最大的數是多少。

程式執行的最開始,整數序列為空。

寫一個程式,讀入操作的序列,並輸出詢問操作的答案。

輸入格式

第一行有兩個正整數 m,p,意義如題目描述;

接下來 mm 行,每一行表示一個操作。

如果該行的內容是 Q L,則表示這個操作是詢問序列中最後 LL 個數的最大數是多少;

如果是 A t,則表示向序列後面加一個數,加入的數是 (t+a) mod p。其中,t 是輸入的引數,a 是在這個新增操作之前最後一個詢問操作的答案(如果之前沒有詢問操作,則 a=0)。

第一個操作一定是新增操作。對於詢問操作,L>0L>0 且不超過當前序列的長度。

輸出格式

對於每一個詢問操作,輸出一行。該行只有一個數,即序列中最後 L個數的最大數。

資料範圍

\[1{\leq}m{\leq}2{\times}10{^5} \]

\[1 {\leq}p{\leq}2{\times}10{^5} \]

\[0{\leq}t{<}p \]

輸入樣例:

10 100
A 97
Q 1
Q 1
A 17
Q 2
A 63
Q 1
Q 1
Q 3
A 99

輸出樣例:

97
97
97
60
60
97

樣例解釋

最後的序列是 97,14,60,96。

程式碼

#include<iostream>
#include<algorithm>
using namespace std;

const int N = 2e5 + 10;
int m, p;

struct Node {
    int l, r;
    int v;
}tr[N * 4];

void build(int u, int l, int r) {
    tr[u].l = l, tr[u].r = r;
    if(l == r) return;
    int mid = l + r >> 1;
    build(u << 1, l, mid), build(u << 1 | 1, mid + 1, r);
}

void pushup(int u) {
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);
}

int query(int u, int l, int r) {
    if(l <= tr[u].l && r >= tr[u].r) return tr[u].v;
    
    int v = 0;
    int mid = tr[u].l + tr[u].r >> 1;
    if(l <= mid) v = query(u << 1, l , r);
    if(r > mid) v = max(v, query(u << 1 | 1, l, r));
    
    return v;
}

void modify(int u, int x, int v) {
    if(tr[u].l == x && tr[u].r == x) tr[u].v = v;
    else {
        int mid = tr[u].l + tr[u].r >> 1;
        if(x <= mid) modify(u << 1, x, v);
        else modify(u << 1 | 1, x, v);
        pushup(u);
    }
}

int main() {
    ios::sync_with_stdio(false);
    cin.tie(0);
    cin >> m >> p;
    build(1, 1, m);
    int last = 0, n = 0;
    while(m--) {
        char op;
        int x;
        cin >> op >> x;
        if(op == 'Q') {
            last = query(1, n - x + 1, n);
            cout << last << endl;
        }
        else {
            modify(1, n + 1, (last + x) % p);
            n++;
        }
    }
    return 0;
}

思考過程

  • 線段樹的寫法有點像用陣列模擬堆,將n號節點的子節點分別對映到n<<1號節點和n<<1|(就是2n + 1) 號節點。

  • 每個節點儲存的是一個區間的左右端點,以及該區間的最大值。特別的如果左端點等於右端點最大值其實就相當於我們想要儲存的數值

  • 實際上只有2n個節點儲存了資料,在如果是個完全二叉樹2n個節點就夠用,但是實際上不會有這麼好的運氣。以n等於5為例最大下標用到了13,顯然2n是不夠用的。 保險起見空間應該是4nimage-20210521180434020

  • 查詢時我們直至所以很放心的在節點端點在l和r範圍內就返回v是因為我們是遞迴進行的這一步操作實際上是把所有在l和r內的節點返回值中取的最大值。當l<=當前節點的mid時他的左節點才有分治的必要,當r> mid時右節點才有分治的必要,這兩步操作可以保證我們在tltr中完整地擷取出若干個自己點使他們的左右端點正好完整湊成lr,然後返回最大值即可image-20210521182313641

  • 修改操作就很簡單,根節點就是我們儲存的數,當l=r時說明是根節點,直接修改即可。如果不是根節點判斷一下該修改位置是在左子樹還是右子樹遞迴一下就行了。最後要記得更新資料pushup一下

相關文章