1. 程式人生 > >普通線段樹——高階資料結構入門

普通線段樹——高階資料結構入門

一.線段樹引入.

線段樹,是一種基於分治思想的高階樹形資料結構,可以支援滿足快速合併性質的資訊維護.

線段樹作為較為容易也較傳統的高階資料結構,應用廣泛,在高階資料結構中佔有重要的地位.

 

二.線段樹概述.

我們先來回憶一下分治,分治的一個應用就是想要得到一個區間的值f(l,r),這個值不能直接求得,但是可以藉助f(l,mid)f(mid+1,r)來快速得到,而且對於任意一個x,f(x,x)的值也可以快速得到,那麼就可以通過分治來求得.

一個經典的演算法就是歸併排序,它要排序區間[1,n],那麼就先排好區間[1,mid][mid+1,n],合併區間[1,mid][mid+1,n]已經排好的兩個序列.然後[1,mid]也不能直接排好,就把這個區間也通過兩個子區間的資訊合併...直到區間變成了[x,x],這個區間直接有序了,就可以直接返回求得排好序的序列.

那麼我們把上述分治的過程畫成一張圖(假設是區間[1,8])的話:

我們發現這其實是一棵樹,而且我們可以把這個東西顯式建樹,就可以得到一棵線段樹.

 

三.基本操作Merge.

既然一個區間[l,r]的資訊要通過區間[l,mid][mid,r]的資訊合併來得到,那麼自然要寫一個函式來實現這個功能了.

雖然一般來說,這個函式的程式碼量通常很小,都不會單獨寫一個函式,但是在這裡還是寫一下吧,這裡以維護區間和為例:

tree Merge(tree a,tree b){
  tree c;
  c.l=a.l;c.r=b.r;
  c.sum=c.sum+b.sum;
  return c;
}

 

四.建樹Build.

建樹的過程其實就是一個分治,然後把每一個節點的資訊存下來.

這裡我們依然以維護區間和為例,設a陣列為初始的值的情況,那麼就分治到葉子(區間[l,r]滿足l=r),就把資訊直接存到葉子上,其它節點的資訊就通過兩個兒子的資訊合併就可以了.

由於我們的分治就是將所有點操作了一遍,而這棵完全二叉樹的節點個數為2n-1,所以建樹是O(n)的.

具體程式碼如下:

void Build(int L,int R,int k=1){
  tr[k].l=L;tr[k].r=R;
  if (L==R){
    tr[k].sum=a[L];
    return;
  }
  int mid=L+R>>1;
  Build(L,mid,k<<1);Build(mid+1,R,k<<1|1);
  tr[k]=Merge(tr[k<<1],tr[k<<1|1]);
}

 

五.單點操作.

我們考慮一個單點操作,比如說單點查詢一個點x的值.這個應該比較容易,可以從根開始不斷往下遞迴找到葉子節點[x,x],然後直接返回這個葉子的資訊即可.

我們在考慮一個單點修改的操作,比如說給一個點x加上v,那麼我們可以找到葉子[x,x],講葉子的資訊直接加上v.但是葉子發生改變後,會使該葉子的所有祖先的資訊發生改變,所以我們要在回溯的時候,再讓經過的節點進行merge操作.

由於每次操作最多隻會經過一條從根到葉子的鏈,長度為樹高,而樹高為O(logn),所以單點操作的時間複雜度為O(logn).

具體程式碼如下:

int Query(int x,int k=1){
  if (tr[k].l==tr[k].r) return tr[k].sum;
  int mid=tr[k].l+tr[k].r>>1;
  if (x<=mid) return Query(x,k<<1);
  else return Query(x,k<<1|1);
}

void Add(int x,int num,int k=1){
  if (tr[k].l==tr[k].r){
    tr[k].sum+=num;
    return;
  }
  int mid=tr[k].l+tr[k].r>>1;
  if (x<=mid) Add(x,num,k<<1);
  else Add(x,num,k<<1|1);
  tr[k]=Merge(tr[k<<1],tr[k<<1|1]);
}

 

六.區間查詢操作.

對於區間查詢[l,r]資訊並的操作,我們當然可以利用上面單點查詢每一個點,然後將資訊並起來,但是這樣就會使時間複雜度變為O(nlogn),十分不優美.

我們考慮從根節點向下的時候,若要查詢的區間包括在一個節點的左兒子,直接往左兒子向下;右兒子同理;若左右兒子都有,則左右兒子都遞迴進入,最後將兩個兒子中找到的答案合併起來就行了.

但是這樣的時間複雜度貌似還是O(n),但其實我們可以證明這是O(logn)的.

證明如下:

首先,這個區間[l,r]在每一層,都只會遍歷到最多4個節點.

若會遍歷到4個以上節點,則必定有兩個節點表示的區間是直接被[l,r]包含且是兄弟的關係,那麼這兩個節點應該會在上一層就被遍歷過了,不會在這一層再被遍歷了.

所以,每一層最多會遍歷到4個節點,所以時間複雜度為O(logn).

證畢.

那麼區間查詢程式碼如下(以區間加為例):

int Query(int L,int R,int k=1){
  if (L==tr[k].l&&R==tr[k].r) return tr[k].sum;
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) return Query(L,R,k<<1);
  else if (L>mid) return Query(L,R,k<<1|1);
    else return Query(L,mid,k<<1)+Query(mid+1,R,k<<1|1); 
}

 

七.區間修改與lazy-tag.

區間修改,我們以給區間[l,r]的每個數加上y為例.

我們考慮與區間查詢類似,直接找到每一個需要修改的修改,然後對每一個區間直接加上y*區間長度.

但是這樣子的話,被修改的節點下所有節點都要修改,但這樣做就會無法修改,暴力修改又太慢,我們該怎麼辦?

我們引入lazy-tag的概念,給每一個修改的節點在打上一個tag標記,表示這個節點下面的所有節點都要增加一個值.

那麼,當我們要查詢點的時候,對所有經過的節點的tag往下推,對下面的節點進行修改,我們把這個過程寫成一個函式pushdown.

那麼我們就可以做到O(logn)修改啦,但是要注意若有tag,最好在任何操作經過任何節點都pushdown.

程式碼如下:

void Update_add(int k,int num){      //為了簡潔寫一個Update函式 
  tr[k].sum+=num*(tr[k].r-tr[k].l+1);
  tr[k].tag+=num; 
}

void Pushdown(int k){
  Update_add(k<<1,tr[k].tag);Update_add(k<<1|1,tr[k].tag);
  tr[k].tag=0;
}

void Add(int L,int R,int num,int k=1){
  if (L==tr[k].l&&R==tr[k].r){
    Update_add(k,num);
    return;
  }
  Pushdown(k);
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) Add(L,R,num,k<<1);
  else if (L>mid) Add(L,R,num,k<<1|1);
    else Add(L,mid,num,k<<1),Add(mid+1,R,num,k<<1);
  tr[k]=Merge(tr[k<<1],tr[k<<1|1]);
}

 

八.例題與程式碼.

現在我們拿出三道例題,分別為luogu3372luogu3374luogu3368.

luogu3372程式碼:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=500000;

int n,m;
LL a[N+9];
struct tree{
  int l,r;
  LL sum,tag;
}tr[N*4+9];

void Update_add(int k,LL num){
  tr[k].sum+=num*(tr[k].r-tr[k].l+1);
  tr[k].tag+=num;
}

void Pushdown(int k){
  Update_add(k<<1,tr[k].tag);Update_add(k<<1|1,tr[k].tag);
  tr[k].tag=0LL; 
}

void Build(int L,int R,int k=1){
  tr[k].l=L;tr[k].r=R;
  if (L==R){
    tr[k].sum=a[L];
    return;
  }
  int mid=L+R>>1;
  Build(L,mid,k<<1);Build(mid+1,R,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}

void Add(int L,int R,LL num,int k=1){
  if (L==tr[k].l&&R==tr[k].r){
    Update_add(k,num);
    return;
  }
  Pushdown(k);
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) Add(L,R,num,k<<1);
  else if (L>mid) Add(L,R,num,k<<1|1);
    else Add(L,mid,num,k<<1),Add(mid+1,R,num,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}

LL Query(int L,int R,int k=1){
  if (L==tr[k].l&&R==tr[k].r) return tr[k].sum;
  Pushdown(k);
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) return Query(L,R,k<<1);
  else if (L>mid) return Query(L,R,k<<1|1);
    else return Query(L,mid,k<<1)+Query(mid+1,R,k<<1|1); 
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}

Abigail work(){
  Build(1,n);
}

Abigail getans(){
  int opt,l,r;
  LL x;
  for (int i=1;i<=m;++i){
    scanf("%d",&opt);
    if (opt==1){
      scanf("%d%d%lld",&l,&r,&x);
      Add(l,r,x);
    }else{
      scanf("%d%d",&l,&r);
      printf("%lld\n",Query(l,r));
    }
  }
}

int main(){
  into();
  work();
  getans();
  return 0;
} 

luogu3374程式碼:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=500000;

int n,m;
LL a[N+9];
struct tree{
  int l,r;
  LL sum;
}tr[N*4+9];

void Build(int L,int R,int k=1){
  tr[k].l=L;tr[k].r=R;
  if (L==R){
    tr[k].sum=a[L];
    return;
  }
  int mid=L+R>>1;
  Build(L,mid,k<<1);Build(mid+1,R,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}

void Add(int x,LL num,int k=1){
  if (tr[k].l==tr[k].r){
    tr[k].sum+=num;
    return;
  }
  int mid=tr[k].l+tr[k].r>>1;
  if (x<=mid) Add(x,num,k<<1);
  else Add(x,num,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum; 
}

LL Query(int L,int R,int k=1){
  if (tr[k].l==L&&tr[k].r==R) return tr[k].sum;
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) return Query(L,R,k<<1);
  else if (L>mid) return Query(L,R,k<<1|1);
    else return Query(L,mid,k<<1)+Query(mid+1,R,k<<1|1);
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}

Abigail work(){
  Build(1,n);
}

Abigail getans(){
  int opt,l,r;
  LL x;
  for (int i=1;i<=m;++i){
    scanf("%d",&opt);
    if (opt==1){
      scanf("%d%lld",&l,&x);
      Add(l,x);
    }else{
      scanf("%d%d",&l,&r);
      printf("%lld\n",Query(l,r));
    }
  }
}

int main(){
  into();
  work();
  getans();
  return 0;
} 

luogu3368程式碼:

#include<bits/stdc++.h>
  using namespace std;

#define Abigail inline void
typedef long long LL;

const int N=500000;

int n,m;
LL a[N+9];
struct tree{
  int l,r;
  LL sum,tag;
}tr[N*4+9];

void Update_add(int k,LL num){
  tr[k].sum+=num*(tr[k].r-tr[k].l+1);
  tr[k].tag+=num;
}

void Pushdown(int k){
  Update_add(k<<1,tr[k].tag);Update_add(k<<1|1,tr[k].tag);
  tr[k].tag=0;
}

void Build(int L,int R,int k=1){
  tr[k].l=L;tr[k].r=R;
  if (L==R){
    tr[k].sum=a[L];
    return;
  }
  int mid=L+R>>1;
  Build(L,mid,k<<1);Build(mid+1,R,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}

void Add(int L,int R,LL num,int k=1){
  if (tr[k].l==L&&tr[k].r==R){
    Update_add(k,num);
    return;
  }
  Pushdown(k);
  int mid=tr[k].l+tr[k].r>>1;
  if (R<=mid) Add(L,R,num,k<<1);
  else if (L>mid) Add(L,R,num,k<<1|1);
    else Add(L,mid,num,k<<1),Add(mid+1,R,num,k<<1|1);
  tr[k].sum=tr[k<<1].sum+tr[k<<1|1].sum;
}

LL Query(int x,int k=1){
  if (tr[k].l==tr[k].r) return tr[k].sum;
  Pushdown(k);
  int mid=tr[k].l+tr[k].r>>1;
  if (x<=mid) return Query(x,k<<1);
  else return Query(x,k<<1|1);
}

Abigail into(){
  scanf("%d%d",&n,&m);
  for (int i=1;i<=n;++i)
    scanf("%lld",&a[i]);
}

Abigail work(){
  Build(1,n);
}

Abigail getans(){
  int opt,l,r;
  LL x;
  for (int i=1;i<=m;++i){
    scanf("%d",&opt);
    if (opt==1){
      scanf("%d%d%lld",&l,&r,&x);
      Add(l,r,x);
    }else{
      scanf("%d",&l);
      printf("%lld\n",Query(l));
    }
  }
}

int main(){
  into();
  work();
  getans();
  return 0;
}