1. 程式人生 > 實用技巧 >線段樹從入門到跳樓

線段樹從入門到跳樓

首先先讓我們認識離散化用的函式,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\)
還沒完咕咕咕