1. 程式人生 > >算法模板——線段樹

算法模板——線段樹

pri include href [1] line 二叉 des 查詢 fin

前言

線段樹作為高級數據結構,可以做非常非常多的事情,那麽線段樹到底是什麽呢,我們就此了解下

一.基本概念

線段樹並非什麽特別高級的東西,顧名思義,它也就是一棵樹。那麽為什麽叫線段樹呢?因為樹的節點上存的就是一些區間,也就是線段。那麽它長啥樣呢?
技術分享圖片
嗯,如上圖,就是一個區間[1,9]的線段樹。有些節點是葉子節點,葉子節點長度為1,不能繼續往下分。葉子節點記錄的信息是最基本的信息,而其他非葉子節點記錄的就是兩個兒子信息的合並(合並的方法有很多,具體情況具體分析)。線段樹的左右區間分別為\([l,mid],(mid,r]\)。而且,由於線段樹是一顆二叉樹,並且線段樹是二分構造,所以它非常平衡,深度也是\(log_n\)

級別的

怎麽記錄?記錄的話,可以學習堆的建造方法,當前點是\(p\),左兒子即是\(p*2\),右兒子就是\(p*2+1\)

二.操作

線段樹被發明出來,肯定有它的道理,線段樹由於能快速的支持一些操作,因此被廣泛使用

1.單點修改

高級數據結構必然要能修改值,修改的話,只需要從線段樹的根開始,一路查詢到葉子節點,更新完葉子節點後,再將葉子節點到根的路徑上的點一路更新一下即可。時間復雜度最大是線段樹的深度,即\(O(log_n)\)

void change(){//醜陋的偽代碼,x是我要修改的點的位置
    if (到達葉子節點)
        修改當前節點;
        return;
    }
    int mid=區間中點;
    if (x<=mid) 對左兒子進行操作;
    if (x>mid)  對右兒子進行操作;
    更新;
}

2.區間查詢

在了解區間查詢之前,我們先要知道區間分解
技術分享圖片
如圖就是區間[2,8]的分解,紅色的節點表示終止節點。我們只要把所有的終止節點合並起來,就是我所要分解的區間。並且,每層的終止節點一定不會超過2個。所以說,區間查詢的時候只需要找到所有的終止節點即可,否則復雜度就上升到\(O(n log_n)\),比暴力還差。終止節點每層做多2個,所以查找的復雜度也是\(log_n\)量級的

那麽如何保證我只找到這些終止區間呢?

int query(){//依然是醜陋的偽代碼
//l,r是線段樹的區間,x,y是查詢區間
    if (x<=l&&r<=y) 返回節點信息;//線段樹的區間完全包含在查詢區間內
    int mid=區間中點,ans;
//如果不是完全包含則只需要做兩個判斷
    if (x<=mid) ans=ans+左兒子信息;//查詢的區間有部分在左兒子內
    if (y>mid)  ans=ans+右兒子信息;//查詢的區間有部分在右兒子內
    return ans;
}

(偽代碼實在太醜……下面有一個真正的代碼)

三.例題

1.

Description

給定一數列,規定有兩種操作,一是修改某個元素,二是求區間的連續和。

Input

輸入數據第一行包含兩個正整數n,m(n<=100000,m<=500000),以下是m行,
每行有三個正整數k,a,b(k=0或1, a,b<=n).
k=0時表示將a處數字加上b,k=1時表示詢問區間[a,b]內所有數的和。

Output

對於每個詢問輸出對應的答案。

暴力的話,\(O(n^2)\)的復雜度,如果\(n\)到了\(10^6\)的話,這顯然是不行的。這個時候,我們就需要用線段樹來完成這道題了。單點修改,區間查詢,用線段樹是很容易實現的

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
using namespace std;
const int N=1e5;
int val[N*4+10];
int read(){
    int x=0,f=1;char ch=getchar();
    for (;ch<‘0‘||ch>‘9‘;ch=getchar())    if (ch==‘-‘)    f=-1;
    for (;ch>=‘0‘&&ch<=‘9‘;ch=getchar())  x=x*10+ch-‘0‘;
    return x*f;
}
void change(int p,int l,int r,int x,int t){
    if (l==r){
        val[p]+=t;
        return;
    }
    int mid=(l+r)>>1;
    if (x<=mid)  change(p*2,l,mid,x,t);
    else    change(p*2+1,mid+1,r,x,t);//修改左右兒子,因為建樹的原因,所以在x<=mid的時候修改左兒子
    val[p]=val[p*2]+val[p*2+1];
}
int get(int p,int l,int r,int x,int y){//對照偽代碼即可
    if (x<=l&&r<=y)   return val[p];
    int mid=(l+r)>>1;
    int ans=0;
    if (x<=mid)  ans+=get(p*2,l,mid,x,y);
    if (y>mid)   ans+=get(p*2+1,mid+1,r,x,y);
    return ans;
}
int main(){
    int n=read(),m=read();
    for (int i=1;i<=m;i++){
        int k=read(),x=read(),y=read();
        if (!k) change(1,1,n,x,y);
        if (k)  printf("%d\n",get(1,1,n,x,y));
    }
    return 0;
}

對於簡單的單點修改和區間查詢,我們只需要考慮好節點上維護的信息是什麽,該怎麽修改,詢問這些值即可。

2.

Description
給定一行n個正整數a[1]..a[n]。
m次詢問,每次詢問給定一個區間[L,R],輸出a[L]..a[R]的最大公因數。

Input
第一行兩個整數n,m。
第二行n個整數表示a[1]..a[n]。
以下m行,每行2個整數表示詢問區間的左右端點。

Output
共m行,每行表示一個詢問的答案。

這題不牽涉到修改操作,只有查詢操作。但是查詢是查找\(gcd\)。因此我們對於葉子節點維護點的值,非葉子節點就維護兩個兒子節點的\(gcd\)即可

#include<cmath>
#include<cstdio>
#include<cstring>
#include<iostream>
#include<algorithm>
#define inf 0x7f7f7f7f
using namespace std;
typedef long long ll;
typedef unsigned int ui;
typedef unsigned long long ull;
inline int read(){
    int x=0,f=1;char ch=getchar();
    for (;ch<‘0‘||ch>‘9‘;ch=getchar())  if (ch==‘-‘)    f=-1;
    for (;ch>=‘0‘&&ch<=‘9‘;ch=getchar())    x=(x<<1)+(x<<3)+ch-‘0‘;
    return x*f;
}
inline void print(int x){
    if (x>=10)     print(x/10);
    putchar(x%10+‘0‘);
}
const int N=1e3,limit=1e9;
int tree[N*4+10];
#define ls (p<<1)
#define rs (p<<1|1)
int gcd(int a,int b){return !b?a:gcd(b,a%b);}
void updata(int p){tree[p]=gcd(tree[ls],tree[rs]);}
void build(int p,int l,int r){
    if (l==r){
        tree[p]=read();
        return;
    }
    int mid=(l+r)>>1;
    build(ls,l,mid),build(rs,mid+1,r);
    updata(p);
}
int query(int p,int l,int r,int x,int y){
    if (x<=l&&r<=y) return tree[p];
    int mid=(l+r)>>1,ans=0;
    if (x<=mid) ans=gcd(ans,query(ls,l,mid,x,y));
    if (y>mid)  ans=gcd(ans,query(rs,mid+1,r,x,y));
    return ans;
}
int main(){
    int n=read(),m=read();
    build(1,1,n);
    for (int i=1,x,y;i<=m;i++)  x=read(),y=read(),printf("%d\n",query(1,1,n,x,y));
    return 0;
}

四.尾聲

我們討論了這麽多,只是講了線段樹的單點修改和區間查詢。那麽區間修改該如何解決?請見線段樹之Lazy標記

算法模板——線段樹