樹狀數組和線段樹都是用於維護數列信息的數據結構,支持單點/區間修改,單點/區間詢問信息。以增加權值與詢問區間權值和為例,其余的信息需要維護也都類似。時間復雜度均為\(O(logn)\)。
樹狀數組
對於樹狀數組,編號為\(x\)的結點上統計着[\(x-lowbit(x)+1,x\)]這一段區間的信息,\(x\)的父親就是\(x+lowbit(x)\)。
如果不知道\(lowbit\)是啥的話可以去看看這個:https://www.cnblogs.com/AKMer/p/9698694.html
畫出來就長這樣:
假設我們要維護的區間是\(A\)數組,那么\(C\)數組就是樹狀數組里存的東西。每個結點掌管的區間都是[\(x-lowbit(x)+1,x\)]。
單點修改區間查詢
假設我們要令\(A_x\)增加\(v\),那么\(x\)以及\(x\)的所有祖先全部都需要增加\(v\),因為這些結點的統計區間全部都覆蓋了\(x\)這個位置,而其他結點沒有。
假設我們要詢問區間[\(l,r\)]的權值和,我們可以轉化為前綴和相減,也就是\(sum[r]-sum[l-1]\)。
假設我們要求\(sum[x]\),那么我們只需要每次加上當前結點\(x\)的權值,然后令\(x\)等於\(x-lowbit(x)\),直到\(x\)為1時停下來。因為\(x\)統計的是區間[\(x-lowbit(x)+1,x\)]的信息,所以前綴和就由若干個這樣的區間組成,每次令\(x-=lowbit(x)\)就相當於去訪問前面一個區間了。由於\(lowbit\)與\(x\)的二進制最低位的\(1\)有關,所以復雜度是\(O(logn)\)的。
代碼如下:
#define low(i) ((i)&(-i))
void add(int pos,int v) {
for(int i=pos;i<=n;i+=low(i))
c[i]+=v;//單點修改
}
int query(int pos) {
int res=0;
for(int i=pos;i;i-=low(i))
res+=c[i];
return res;//詢問區間[1,pos]的權值和
}
區間修改單點詢問
這個要利用差分的思想就行了。每次在數組的\(l\)處加\(v\),\(r+1\)處減\(v\),然后一個數的權值就是\([1,x]\)的差分和。
代碼如下:
#define low(i) ((i)&(-i))
int l=read(),r=read(),v=read();
add(l,v);add(r+1,-v);
int pos=read();
printf("%d\n",query(pos));//add與query函數見單點修改區間詢問
區間修改區間詢問
假設\(a\)是差分數組,那么前綴權值和就是\(a\)的前綴和的前綴和,也就是:
化開就是:
\(\sum\limits_{i=1}^{x}(x-i+1)a[i]=(x+1)\sum\limits_{i=1}^{x}a[i]-\sum\limits_{i=1}^{x}i*a[i]\)
同單點修改,我們只需要開兩個樹狀數組,一個維護\(a[i]\),一個維護\(i*a[i]\)就行了。
代碼如下:
#include <cstdio>
using namespace std;
typedef long long ll;
#define low(i) ((i)&(-i))
const int maxn=1e5+5;
int n,m;
ll a[maxn],sum[maxn];
ll read() {
ll 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;
}
struct TreeArray {
ll c[maxn];
void add(int pos,ll v) {
for(int i=pos;i<=n;i+=low(i))
c[i]+=v;
}
ll query(int pos) {
ll res=0;
for(int i=pos;i;i-=low(i))
res+=c[i];
return res;
}
}T1,T2;//T1維護a[i]的前綴和,T2維護i*a[i]的前綴和
int main() {
n=read(),m=read();
for(int i=1;i<=n;i++)
a[i]=read(),sum[i]=sum[i-1]+a[i];
for(int i=1;i<=m;i++) {
int opt=read(),l=read(),r=read();
if(opt==1) {//區間加
ll k=read();
T1.add(l,k);T1.add(r+1,-k);
T2.add(l,l*k);T2.add(r+1,-(r+1)*k);
}
else {//區間查詢
ll ans=sum[r]-sum[l-1];
ans+=(r+1)*T1.query(r)-T2.query(r);
ans-=l*T1.query(l-1)-T2.query(l-1);
printf("%lld\n",ans);
}
}
return 0;
}
線段樹
線段樹是基於分治思想的數據結構,功能比樹狀數組更強大,長這樣:
對於一個節點\(p\),如果他統計的區間是[\(l,r\)],\(mid=(l+r)/2\),那么他左兒子統計的區間就是\([l,mid]\),右兒子是\([mid+1,r]\)。對於某些節點統計的區間是\([x,x]\),那么就直接是單點的信息,每個點的信息可以由子節點合並更新。因為線段樹是一顆二叉樹,所以我們可以用\(p*2\)來記錄\(p\)的左兒子,\(p*2+1\)記錄右兒子。這樣子的話,因為最后一層可能前面全部空出來,單出一個區間[\(n,n\)]在這一層的最后面,所以空間要開到\(4*n\)才不會段錯誤。
單點修改
直接從根開始,以覆蓋\(x\)這個位置的區間為路徑,將一條鏈上的節點全部更新。復雜度是\(O(logn)\)的。
代碼如下:
void updata(int p) {
tree[p]=tree[p<<1]+tree[p<<1|1];
}
void change(int p,int l,int r,int pos,int v) {//更新p號節點,p號節點統計了[l,r]的信息,我要把pos位置的值增加v
if(l==r) {
tree[p]+=v;
return;
}//此時到一條鏈的最底部了就return
int mid=(l+r)>>1;
if(pos<=mid)change(p<<1,l,mid,pos,v);
else change(p<<1|1,mid+1,r,pos,v);//選擇覆蓋pos的路徑遞歸
updata(p);//更新p節點的信息
}
//更改的時候調用change(1,1,n,x,v)就行了。
區間查詢
假如當前區間被我需要訪問的區間全部覆蓋了,那么直接返回當前區間的權值和就行了。如果不是,再分情況討論,分別去遞歸詢問左兒子右兒子,再合並起來。顯然,我會訪問的節點全部是包含\(l\)與\(r\)的,不包含的話會在一開始就返回統計的權值,不會進行遞歸,所以復雜度也是\(O(logn)\)的。
代碼如下:
int query(int p,int l,int r,int L,int R) {
if(L<=l&&r<=R)return tree[p];//如果當前區間是詢問區間子區間就直接返回統計信息
int mid=(l+r)>>1,res=0;
if(L<=mid)res+=query(p<<1,l,mid,L,R);//如果L<=mid就返回[L,mid]的和
if(R>mid)res+=query(p<<1|1,mid+1,r,L,R);//如果R>mid就返回[mid+1,R]的和
return res;
}
延遲標記與區間修改
對於區間修改,如果我們一個一個值的去改的話,還不如\(n^2\)暴力統計信息的算法。所以就有了延遲標記這種東西。如果一個節點上面有延遲標記,就表示這個節點已經被修改過了,但是這個節點的子節點還沒有被修改過,如果要進行遞歸必須要把延遲標記的影響一起帶下去,然后把當前結點的延遲標記清空。對於一個區間[\(l,r\)],如果是我要修改的區間的子區間,那么我就直接把當前節點\(p\)的統計信息更新掉,然后打上延遲標記,就不進行遞歸一個一個改了。根據區間查詢的復雜度,區間修改也只會在包含\(l\)與\(r\)的路徑上進行遞歸,復雜度是\(O(logn)\)的。
代碼如下:
void updata(int p) {
tree[p]=tree[p<<1]+tree[p<<1|1];
}
void add_tag(int p,int l,int r,int v) {
tree[p]+=(r-l+1)*v;tag[p]+=v;//標記只對兒子有影響,自己在打標記的同時一起把統計信息更改了。
}
void push_down(int p,int l,int r) {
int mid=(l+r)>>1;
add_tag(p<<1,l,mid,tag[p]);
add_tag(p<<1|1,mid+1,r,tag[p]);
tag[p]=0;//把當前標記分別傳給兩個兒子然后清空
}
void change(int p,int l,int r,int L,int R,int v) {//[l,r]為當前區間,[L,R]為要修改的區間
if(L<=l&&r<=R) {
add_tag(p,l,r,v);//打標記
return;
}
int mid=(l+r)>>1;push_down(p,l,r);//下傳標記
if(L<=mid)change(p<<1,l,mid,L,R,v);
if(R>mid)change(p<<1|1,mid+1,r,L,R,v);//遞歸更改
updata(p);//更新當前結點的信息
}