線段樹從入門到跳樓
首先先讓我們認識離散化用的函式,STL給我們提供了便利:
unique(start,end);//取出有序序列重複元素,左閉右開,返回去重複序列最後一個元素位置
lower_bound(start,end,key);//左閉右開中尋找第一個大等於key的數,返回值
std::sort(a+1,a+n+1);
cnt = std::unique(a+1,a+n+1) - (a+1)//去重並獲得去重後長度cnt,注意減的是初始地址qwq我錯了一次
for(int i=1;i<=n;i++)
a[i] = lower_bound(a+1,a+1+cnt,a[i]) - a
我們需要對原陣列進行一次備份(cp
p3755 老c的任務
題解:
樹狀陣列
將點按\(x\)遞增;將詢問拆成2個,離線,離散化,按\(x\)遞增排序。按x軸排序,y軸維護樹狀陣列。
然後每次找到一個詢問,就判斷有哪些點可以加到樹狀陣列中,然後查詢一下就好了。把區間詢問轉化為字首和相減.
注意題目中邊上也算是包含,所以應將左下邊界-1;同時座標必須是正的,還需要+2。
P4054計數問題
題解:多維樹狀陣列板子
可以發現 , 矩陣中值的值域很小
\(w\le 100\)考慮暴力思路,
構建\(100\)個二維樹狀陣列,分別儲存各數值 ,在矩陣中出現的次數.
單點修改:將a修改成b,先將a對應的樹狀陣列改掉,對應位置出現次數-1,再對b對應的樹狀陣列對應位置+1
區間查詢:直接查詢對應子矩陣出現次數即可
#include<cstdio> #define maxn 301 #define lowbit(x) -x&x #define ll long long using namespace std; inline int read() { char ch=getchar();int i=0,f=1; while(ch<'0'||ch>'9'){if(ch=='-')f=-1;ch=getchar();} while(ch>='0'&&ch<='9'){i=(i<<1)+(i<<3)+ch-'0';ch=getchar();} return i*f; } int n,m,q; int map[maxn][maxn],tree[101][maxn][maxn]; inline void add(int val,int x,int y,int z){//修改操作,將此點權值設為val,並使其次數+=z map[x][y] = val; for(int i = x;i <= n;i += lowbit(i)) for(int j = y;j <= m;j += lowbit(j)) tree[val][i][j] += z; } inline ll sum(int type,int x,int y){//查詢1,1到x,y的type出現次數 int cnt = 0; for(int i = x;i;i -= lowbit(i)) for(int j = y;j;j -= lowbit(j)) cnt += tree[type][i][j]; return cnt; } int main(){ n=read(),m=read(); for(int i = 1;i <= n;i++) for(int j = 1;j <= m;j++) add(read(),i,j,1); q=read(); for(int i=1;i<=q;i++) { int opt = read(); if(opt == 1){ int x= read(),y=read(),tem=read(); add(map[x][y],x,y,-1);//原值出現次數-1 add(tem,x,y,1);//新值出現次數+1 } else{ int x1=read(),x2=read(),y1=read(),y2=read(),type=read(); ll ans1 = sum(type,x2,y2),ans2=sum(type,x1-1,y1-1); ll ans = ans2 + ans1 - sum(type,x1-1,y2) - sum(type,x2,y1-1);//容斥 //sum是不含邊界的 printf("%lld\n",ans); } } }
線段樹:滿足區間可加性\(f(x,y)=f(f(x,z),f(z,y)),z\in[x,y]\)
線段樹以常數大,空間大,難寫難調而臭名遠揚,但有時也很給力,當然能不寫線段樹就不寫啦,而且記得要開四倍空間
以下部分參考LightHouseOfficial的內容
區間和,區間最大最小值,區間LCA,區間質數個數......可以用線段樹維護
假設陣列是{1,2,3,4,5,6}查詢區間[2,4],計算出[2,4]的mid值,mid=(l+r)<<1=3
然後我們查詢[l,mid]和[mid+1,r]區間[2,3]和[4,4]
[4,4]是葉子,返回它的值4,回到[2,3]繼續遞迴
p<<1是左兒子,p<<1|1為右兒子
建樹:
#define l(x) tree[x].l
#define r(x) tree[x].r
#define val(x) tree[x].val
void pushup(int p){
val(p)=val(p<<1)+val(p<<1|1);
}
void build(int p,int l,int r){
l(p)=l,r(p)=r;//儲存每個節點的左右兒子的編號
if(l==r){
val(p)=a[l];
return;
}pushdown(p);
int mid=(l+r)>>1;
build(p<<1,l,mid);build(p<<1|1,mid+1,r);//遞迴建樹
pushup(p);//更新值
}
int query(int p,int l,int r){//查詢區間和 p是fa
if(l<=tree[p].l && r>=tree[p].r)return tree[p].val;
pushdown(p);
int mid=(tree[p].l+tree[p].r)>>1;
int ret=0;
if(l<=mid) ret+=query(p<<1,l,r);
if(r>mid) ret+=query(p<<1|1,l,r);
return ret;
}
區間修改,打lazytag 然後pushdown
#define val(x) tree[x].val
#define add(x) tree[x].add
void pushdown(int p){
if(add(p)){
val(p<<1)+=add(p)*(r(p<<1)-l(p<<1)+1);
val(p<<1|1)+=add(p)*(r(p<<1|1)-l(p<<1|1)+1);
add(p<<1)+=add(p);
add(p<<1|1)+=add(p);
add(p)=0;
}
}//加法
void update(int p,int l,int r,int d){//修改
if(l<=l(p) && r>=r(p)){
val(p)+=d*(r(p)-l(p)+1);add(p)+=d;
return;
}
pushdown(p);
int mid=(l(p)+r(p))>>1;
if(l<=mid)update(p<<1,l,r,d);
if(r>mid)update(p<<1|1,l,r,d);
pushup(p);
}
動態開點線段樹
由於線段樹要maxn<<2,maxn過大顯然MLE,這時就要離散化或動態開點了
意思是,你要用到一個點才開那個點,不用的點不開,可以大幅節省空間,空間複雜度降為O(nlogn)
\(log_210^6\approx20\)是不是很給力
接下來是實現:
一開始,你只有一個根節點。
通過update函式往樹裡面插點,開兩個陣列記錄每個節點的左右兒子編號。
遞迴進入左右兒子,如果要用新點,就開新點。
可持久化線段樹
對於每個詢問我們每次不開一整棵樹,而是每次把修改節點插入,也就是插入一些歷史節點來儲存歷史值。
空間複雜度成了\(nlog^2n\),一般開32倍空間<<5
一般可持久化線段樹的題目只要求單點修改,區間修改可以實現不過複雜度比較高。
真的遇到了需要寫主席樹而且要求區間修改的題目,一般可以轉化為單點修改進行操作
時間複雜度\((n+m)logn\)
還沒完咕咕咕