1. 程式人生 > 其它 >線段樹原理以及一些模板題

線段樹原理以及一些模板題

線段樹

線段樹

定義

百度百科定義:線段樹是一種二叉搜尋樹,與區間樹相似,它將一個區間劃分成一些單元區間,每個單元區間對應線段樹中的一個葉結點
藍書定義:線段樹是一種基於分治思想的二叉樹結構,用於在區間上進行資訊統計

原理

  • 線段樹的每個節點都代表一個區間
  • 線段樹具有唯一的根節點,代表的區間是整個統計範圍,如[1, N]
  • 線段樹的每個葉節點都代表一個長度為1的元區間[x, x]
  • 對於每個內部節點[l, r],它的左子節點是[l, mid],右子節點是[mid + 1, r],其中 mid = l + r >> 1(下取整)

圖示

由上面倆圖展示了一顆區間長度為10的線段樹
不難發現我們可以由一個struct陣列來儲存線段樹的每個節點

struct Node
{
    int l, r;  //左右區間端點
    int date;  //線段樹要維護的區間性質(一般為最大值,最小值,求和等)
}Tr[4 * N];  //N 為長度為1的區間節點的個數

一般我們Tr[]陣列的長度要求不小於4 * N

原因(瞭解即可):由上面倆幅圖我們不難發現,線段樹的最後一層是不滿的,有多餘位置。而除去最後一層
後的線段樹一定是一顆完全二叉樹, 樹的深度為\(O(log)N\)
一共有N個長度為1的區間節點, N > 倒數第二層的節點個數
因為最後一層的節點個數為N的一顆滿二叉樹的所有節點個數 = N + N/2 + N/ 4 +.... + 2 + 1 = 2N - 1


所以我們的線段樹去掉最後一層後節點的個數是嚴格小於2N - 1的(因為我們的線段樹最後一層有空餘位置)
而最後一層最多不會超過上一層的倆倍 即最壞情況下有 < 2*N 個節點
所以我們長度一般取4 * N 即可


基本操作

  • pushup 由子節點更新父節點資訊
  • pushdown 由子節點更新父節點資訊 lazytag(懶標記)
  • build 由區間建立線段樹
  • modify 修改某一個點(easy)或者區間(hard)
  • query 查詢某一端區間資訊

1.build操作

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); //遞迴建立右子樹
    pushup(u);                  //一般在這裡pushup(即更新區間所要維護的資訊/屬性)
}

2.query

int query(int u, int l, int r)
{
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].v;  //當前查詢區間[l,r]完全覆蓋了u節點所代表的區間,直接返回
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if(l <= mid) v = query(u << 1, l, r);             //否則,若[l,r]和左子節點有重疊,遞迴訪問左子節點
    if(r > mid) v = max(query(u << 1 | 1, l, r), v);  //否則,若[l,r]和右子節點有重疊,遞迴訪問左子節點
    return v;
}

3. modify 操作和query類似,具體問題具體分析如何修改區間資訊/屬性
4. pushup 直接看模板題更好理解
5. pushdown 直接看模板提更好理解

題目連結 AcWing1275. 最大數

第一個模板題,用線段樹維護區間最大值, 只需用到pushup操作,暫時沒用到pushdown

題目思路

1.我們可以提前把m個數給填好
2.因此我們的第一個操作 在第n個數後面加一個數x == 修改第n + 1個數
3.第二個操作即 查詢[n - l + 1, n]內的最大值

時間複雜度

#include <iostream>
#include <algorithm>
#include <cstring>

using namespace std;

const int N = 2e5 + 10;

int m, p;
struct Node
{
    int l, r;
    int v;               //[l,r]區間內最大的數
}tr[N * 4];

void pushup(int u)     //由子節點資訊更新父節點資訊
{
    tr[u].v = max(tr[u << 1].v, tr[u << 1 | 1].v);   //父節點的最大值 = max(左子節點的最大值,右子節點的最大值)
}

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

int query(int u, int l, int r)  //query基本操作
{
    if(tr[u].l >= l && tr[u].r <= r) return tr[u].v;    //當前區間[l, r]已經覆蓋u節點區間,直接返回
    int mid = tr[u].l + tr[u].r >> 1;
    int v = 0;
    if(l <= mid) v = query(u << 1, l, r);           //[l, r]和左子節點有重疊,遞迴查詢左子節點
    if(r > mid) v = max(query(u << 1 | 1, l, r), v); //[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()
{
    int n = 0, last = 0;
    scanf("%d%d", &m, &p);
    build(1, 1, m);        //建立一個長度為m的線段樹
    int x;
    char op[2];
    while(m -- )
    {
        scanf("%s%d", op, &x);
        if(op[0] == 'Q')
        {
            last = query(1, n - x + 1, n);
            printf("%d\n", last);
        }
        else
        {
            modify(1, n + 1, (last + x) % p);
            n ++;
        }
    }
    return 0;
}

2021.8.17 持續更新中 還沒學完