標籤:lin type data 使用 gpo 描述 單點 htm .com
前言
線段樹作為進階資料結構,可以做非常非常多的事情,那麼線段樹到底是什麼呢,我們就此瞭解下
一.基本概念
線段樹並非什麼特別進階的東西,顧名思義,它也就是一棵樹。那麼為什麼叫線段樹呢?因為樹的節點上存的就是一些區間,也就是線段。那麼它長啥樣呢?
嗯,如,就是一個區間[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 0x7f7f7f7fusing 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標記
演算法模板——線段樹