01 線段樹

zerocloud01發表於2024-04-25

目錄
  • 線段樹
    • 簡介
    • 節點
    • 加法線段樹
      • 1. 準備變數
      • 2. 上拉操作
      • 3. 建樹
      • 4. 懶標記
      • 5. 下放操作
      • 6. 區間修改
      • updata
    • 異或線段樹
      • pushup
      • updata
    • 最值線段樹
      • updata
      • pushup

線段樹

簡介

  • 線段樹(一個二叉樹)是一個非常重要的資料結構,利用分治的思想。可以用於維護一些滿足結合律區間的資訊,例如區間元素之和,區間異或和。
  • 線段樹可以實現O(logn)(需要Lazy tag,樸素版只允許單點修改和區間查詢)的時間複雜度的區間修改區間查詢,與樹狀陣列相比,它更具有通用性,但是常數略大。

節點

線段樹由多個節點組成,每個節點包含以下資訊

  1. 編號: O
  2. 管轄區間: [st,ed]
  3. 權值: 和/最值/異或和/(等滿足性質資料)

加法線段樹

字首和在查詢與加法交替出現時複雜度很高,不適用
此處可使用樹狀陣列,但是線段樹可擴充性更強

因為線段樹一定是二叉樹,所以一般利用二叉樹編號性質構造整棵樹.對於編號為x的節點,左兒子為2x,右兒子為2x+1.所以不需要記錄左右兒子,但是也使得需要開四倍n的陣列才能存下整棵樹,一般以1號節點為根.

樹的大小為1時為葉節點,葉節點組成原陣列
線段樹每次操作從根節點開始,每個節點的管轄區間可以由根算出來s

1. 準備變數

#define int long long
const int N = 2e5+10;
int a[N];//原陣列
int n;//原陣列大小
int t[N << 2];//開四倍空間,t[x]表示節點x所表示的區間元素大小

2. 上拉操作

purhup(上拉操作)是所有線段樹所有操作都需要用到的工具函式,是用兒子資訊來更新自己的資訊.

void pushup(int o)
{
    t[o] = t[o<<1] + t[0<<1 | 1];//可能不是加法
}

3. 建樹

建樹採用分治思想, 分別將左右子樹建立,然後利用pushup來更新當前節點.

void bulidTree(int s = 1,int e = n,int o = 1)
{
    if(s == e)
    {
        t[o] = a[s];
        return ;
    }
    int mid = s + e >> 1;
    bulidTree(s,mid,o<<1),bulidTree(mid+1,e,o<<1 | 1);
    pushup(o);
}

4. 懶標記

懶標記(lazytag)用於表示某個節點尚未更新給子節點的值, 懶標記是保證線段樹的區間修改和區間查詢的複雜度正確性的關鍵.

int lz[N<<2]; 
//lz[o]表示: 節點o還有lazy[o]這麼大的一個數字,還沒有加給左右兒子. 

當lz[o] == 0,說明當前節點的左右兒子都已經被更新,得到了正確的值,使用懶標記必須配合下放操作

5. 下放操作

pushdown(下放操作)需要三個引數, 分別是當前的操作區間([st,ed])和節點編號(O).

void pushdown(int s,int e,int o)
{
    if(!lz[o])  return ;//如果lz[o] == 0,無需下放
    int mid = s + 1 >> 1,ls = 0<<!,rs = o<<1 | 1;//ls,rs為左右兒子編號
    t[ls] += lz[o] * (mid - s + 1);//可改為updata()
    t[rs] += lz[o] * (e-mid);
    //每個t(區間和)都需要乘上一個區間長度. 
    lz[ls] += lz[o],lz[rs] += lz[o];//標記也要下放
    lz[o] = 0;//lz下放完畢
}

6. 區間修改

將區間[l,r]中的數字都加上x,採用遞迴形式,當走到了目標區域內,就將對應的t[o]和lz[o]做出修改.
此函式是線段樹精髓

void add(int l,int r,int x,int s = 1,int e = n,int o = 1)
{
    if(l <= s && e <= r)
    {
        //當前操作區間已經完全進入目標區間,應該直接被修改並且打算lz標記,不再向下
        t[o] += (e-s+1)*x;
        lz[o] += x;
        return; 
    }
    pushdown(e,s,o);//向下走前,必須下放
    int mid = s + e >> 1;
    if(mid >= l)    add(l,r,x,s,mid,o<<1);//判斷是否需要往左走
    if(mid + 1 <= r)    add(l,r,x,mid+1,e,o<<1 | 1);//判斷是否需要往右走
    pushup(o);//遞迴回來,上拉
}

updata

void updata(int s,int e,int o,int x)
{
    t[o] += (e-s+1)*x;
    lz[o] += x;
}

異或線段樹

只需要修改"pushup"和"updata"

pushup

將"+"改為"^"即可

void pushup(int o)
{
    t[o] = t[o<<1] ^ t[o<<1 | 1];
}

updata

void updata(int s,int e,int o,int x)
{
    t[o] ^= ((e - s + 1)&1) ? x : 0;// 奇數才異或x
    //因為偶數次異或等同於未操作 
    lz[o] ^= x;
}

最值線段樹

updata

將區間[s,e],節點o加上x

void updata(int s,int e,int o,int x)
{
    tmax[o] += x,tmin[o] += x;
    lz[o] += x;
}

pushup

void pushup(int o)
{
    tmax[o] = max(tmax[o<<1],tmax[o<<1 | 1]);
    tmin[o] = min(tmin[o<<1],tmin[o<<1 | 1]);
    lz[o] += x;
}