進階資料結構

 

 

Outline

  • Sparse table
  • 線段樹
  • Fenwick Tree
  • 資料結構和樹
  • Treap

已經會的可以去寫 讀書會練習題、刷 cses

 

RMQ問題

  • 給定一序列 \(a\) , 會有 \(Q\) 筆詢問
  • 每次會詢問區間 \([l,r]\) 
  • 動態
    • 帶修改 
  • 靜態
  • 在線
    • 要輸出答案後才會有下一筆輸入
  • 離線

前綴和、差分與離散化

基礎演算法的課有講過,當複習

前綴和

  • 靜態區間和詢問,給定數列 \(a\) 和 \(q\) 次訊問
  • 每次求區間 \([l,r]\) 的和
  • 維護前綴和 \(S_i=sum_{k=0} ^{i} {a_k}\)
  • 對詢問 \([l,r]\)所求即為 \(S_{r}-S_{l-1}\)
  • 預處理 \(S\)
  • 時間複雜度為 \(O(n+q)\)

有一個二維圖 \(N\times N\) 每個位置是 \(0/1\)

之後有 \(q\) 筆詢問求介於 \((x1,y1) \sim (x2,y2)\) 的和

\(N\leq 1000\) , \(q \leq 2\cdot 10^5\)

二維前綴和

和一維前綴和類似,只是多一個維度

\(sum[x][y]\) 代表 \((1,1) \sim (x,y)\) 的總和

考慮從 \((x,y-1),(y-1,x)\) 轉移到 \(x,y\) 的過程

\(sum[x][y]=sum[x-1][y]+sum[x][y-1]-sum[x-1][y-1]+a[x][y]\)

\(sum[i][j]=sum[i-1][j]+sum[i][j-1]-sum[i-1][j-1]+a[i][j]\)

區間詢問時答案即為 \(sum[x_2][y_2]-sum[x_1-1][y_2]-sum[x_2][y_1-1]+sum[x_1-1][y_1-1]\)

差分

  • 序列 \(a\) 初始皆為 \(0\)
  • 會有 \(q\) 筆操作
  • 每次給定 \(l \ r  \ val\) 代表 \(\forall {p \in [l,r]} \ a_{p}:=a_{p}+val\)
  • 操作結束後輸出 \(n\) 個數代表序列 \(a\) 之值
  • 建立差分序列 \(d\) , \(d_i=a_i-a_{i-1}\)
  • 那對操作 \(l \ r \ val\) 等價 \(d_l+val,d_{r+1}-val\)
  • 因此只需按照操作更新差分序列 \(d\) 即可
  • 最後 \(a_i=\sum_{k=0}^{i} {d_i}\)
  • 時間複雜度為 \(O(n+q)\)

離散化

  • 當序列數值不重要,只在乎數字間的大小
  • 做為初步序列處理
  • 通常把序列映射到 \(1 \sim n\)
  • 求序列 \(x\) 的最長遞增子序列
  • \(1\leq x_i \leq 10^9\)
  • 不在乎序列數值,只在乎相對大小

Code

vector<int> v;
int a[N];
for(int i=0;i<n;i++) cin>>a[i],v.eb(a[i]);
sort(v.begin(),v.end());
v.erase(unique(v.begin(),v.end()),v.end()); 
// 刪除重複的數字
for(int i=0;i<n;i++) a[i]=upper_bound(v.begin(),v.end())-v.begin(); 
// 按照 a[i] 大小映射成 1 ~ |v|

Sparse table

  • 解決靜態 RMQ 問題
  • 通常是區間極值詢問
  • 可以做到預處理 \(O(n\log{n})\) , 詢問 \(O(1)\)
  • 給定長度 \(n\) 序列 \(a\)
  • 和 \(q\) 筆詢問
  • 每次給定 \(l,r\) 求 \(\min_{i \in [l,r]}\{a_i\}\)

預處理

  • 利用倍增法
  • \(mn[x][k]=min([x,x+2^k))\)
  • 轉移式:
    • \(mn[x][k]=min(mn[x][k-1],mn[x+2^{k-1}][k-1])\)
  • 時間複雜度 \(O(n\log{n})\) 預處理

詢問

  • 對詢問 \([l,r]\) 分割成兩部分: \([l,l+2^k],[r-2^k,r]\)
  • \(min([l,r))=\min([l,l+2^k),[r-2^k,r))\)

要如何選 \(k\)

滿足條件 :

  • \(l\) 所在的方格右界不得超過 \(r\)
  • \(l\) 右界、 \(r\) 左界必須相交
  • \(l+2^k\leq r \land l+2^k \geq r-2^k\)
  • \(l+2^k \leq r \leq l+2^{k+1}\)
  • \(2^k\leq r-l \leq 2^{k+1}\)
  • \(k=\lfloor \log_{2}{(r-l)}\rfloor\) 
    • c++ 語法 __lg(r-l)

詢問

  • 對詢問 \([l,r]\) 輸出
  • \(\min(mn[k][l],mn[k][r-2^k]) \) ,
  • \( k=\lfloor\log_2{(r-l)}\rfloor\)
  • 時間複雜度為 \(O(1)\)

Code

const int N=2e5+7;
const int K=20;
int a[N];
struct Sparsetable{
	int mn[N][K];	
	void build(int n){
		rep(i,1,n) mn[i][0]=a[i];
		rep(i,1,K-1){
			rep(j,1,n) mn[j][i]=min(mn[j][i-1],mn[j+(1<<(i-1))][i-1]);
		}
	}
	int qry(int l,int r){
		int k=__lg(r-l);
		return min(mn[l][k],mn[r-(1<<k)][k]);
	}
}st;

樹狀數組(Fenwick Tree /BIT)

如果詢問只是前綴 / 後綴 , 單點修改的話就可以使用。 線段樹的替代品

  • 給定序列 \(a\), 長度 \(n\) , \(q\) 筆操作
  • 第 \(i\) 次操作如下:
    • \(1 \ k \ u \) 
      • \(a[k]:=a[k]+u\)
    • \(2 \ l \ r \)
      • 詢問 \(\sum_{i=l}^r {a[i]}\)
  • 先介紹BIT的 lowbit 函數 \(lowbit(x)=x\&-x\)
  • 也就是 \(x\) 在二進位最後面的 \(1\)
  • \(lowbit(110)_{2}=(10)_2\)
  • \(lowbit(111001)_{2}=(1)_2\)

 

BIT 畫成樹狀結構如上圖

每個節點的父節點即為 \(x-lowbit(x)\)

右兄弟節點為 \(x+lowbit(x)\)

深度為 \(2\) 進位 \(1\) 的個數 \(\leq \log{n}\)

同一層的節點數 \(\leq \log{n}\)

如果把 \(x\) 的值同時在該層的右兄弟節點儲存

詢問 \(x\) 的前綴和即為從該點到根結點路徑上數字的總和

lowbit(0)=0,通常根節點不會儲存值

const int N=2e5+7;
int bit[N]; // bit[x]不等於前綴 1~x 之總和
#define lowbit(x) (x&-x)
void add(int x,int val){
    for(int i=x;i<N;i+=lowbit(i)) bit[x]+=val;
    // 在其右兄弟節點加上該值
}
int query(int x){
    int sum=0;
    for(int i=x;i>0;i-=lowbit(i)) sum+=bit[x];
    // 從該點到根結點的路徑上之總和
    return sum;
}

單次時間複雜度和線段樹相同都是 \(O(\log{N})\)

但是 BIT 常數較小,跑飛快

當然可以變通,在求後綴時改成在父節點加值、右兄弟節點求值亦可

若題目求前綴最小值、單點更新亦可用 BIT

動態第 \(k\) 小

  • 有 \(q\) 次操作和一個空集合 \(v\)

  • 每次操作會有兩種可能:

    • + x k 在集合加入 \(x\)

    • - x k 在集合刪除 \(x\) (保證 \(x\)) 存在

    • 之後請輸出一行代表集合 \(v\) 的第 \(k\) 小

  • \(q,x,k\leq 10^6\)

在線段樹的部分有利用值域線段樹+樹上二分搜解過這題

在這裡改用 BIT 試試看

和值域線段樹做法類似,\(cnt[x]\) 代表 \(x\) 在集合出現幾次

之後就等價找 \(\min\{x \ |\ sum_{i=0}^{x} {cnt[i]} \geq k\}\)

二分搜即可

因為 BIT 的結構問題,無法樹上二分搜

所以時間複雜度為 \(O(q\cdot \log^2{n})\)

const int N=1e6+7;
int bit[N]; // bit[x]不等於前綴 1~x 之總和
#define lowbit(x) (x&-x)
void add(int x,int val){
    for(int i=x;i<N;i+=lowbit(i)) bit[x]+=val;
    // 在其右兄弟節點加上該值
}
int query(int x){
    int sum=0;
    for(int i=x;i>0;i-=lowbit(i)) sum+=bit[x];
    // 從該點到根結點的路徑上之總和
    return sum;
}
int bsearch(int k){
    int l=0,r=1e6+5;
    while(l+1<r){
        int mid=(l+r)>>1;
        if(query(mid)>=k) r=mid;
        else l=mid;
    }
    return r;
}

高維BIT

  • 單點修改區間和詢問
  • 只是陣列是二維
  • 一個 \(N \times N\) 的 \(0-1\) 矩陣、 \(q\) 次操作
    • 改變 \(x,y\) 的狀態
    • 詢問 \(sum([x_1,y_1] \sim [x_2,y2])\)
#pragma GCC optimzize("Ofast,no-stack-protector")
#include<bits/stdc++.h>
#define int long long
#define quick ios::sync_with_stdio(0);cin.tie(0);
#define rep(x,a,b) for(int x=a;x<=b;x++)
#define repd(x,a,b) for(int x=a;x>=n;x--)
#define lowbit(x) (x&-x)
const int N=1e3+7;
const int INF=1e18;
int bit[N][N];
void add(int x,int y,int val){
	for(int i=x;i<N;i+=lowbit(i)){
		for(int j=y;j<N;j+=lowbit(j)) {
			bit[i][j]+=val;
		}
	}
}
int query(int x,int y){
	int ans=0;
	for(int i=x;i>0;i-=lowbit(i)){
		for(int j=y;j>0;j-=lowbit(j)){
			ans+=bit[i][j];
		}
	}
	return ans;
}
 
bool f[N][N];
signed main(){
    quick
    int n,q;
    cin>>n>>q;
    rep(i,1,n){
    	rep(j,1,n){
    		char c;
    		cin>>c;
    		if(c=='*') add(i,j,1),f[i][j]=1;
		}
	}
	while(q--)
    {
    	int r,x,y,x2,y2;
    	cin>>r>>x>>y;
    	if(r==1){
    		if(f[x][y]) add(x,y,-1);
    		else add(x,y,1);
    		f[x][y]^=1;
		}
		else{
			cin>>x2>>y2;
			--x,--y;
			cout<<query(x2,y2)-query(x,y2)-query(x2,y)+query(x,y)<<"\n";
		}
	}
    return 0;
}

時間複雜度一樣是 \(O(Q\cdot \log^{2}{N})\)

但常數遠小於 樹套樹

線段樹

  • 給定序列 \(a\), 長度 \(n\) , \(q\) 筆操作
  • 第 \(i\) 次操作如下:
    • \(1 \ k \ u \) 
      • \(a[k]:=a[k]+u\)
    • \(2 \ l \ r \)
      • 詢問 \(\sum_{i=l}^r {a[i]}\)

有修改,所以不能使用前綴和

區間和好像有分治性質:

\(sum(l,r) \ =   \ sum(l,mid)+sum(mid+1,r)\)

先不考慮修改

只考慮儲存

  • 線段樹的核心概念就是分治
  • 每個節點維護一段區間和
  • 父節點存 \([l,r]\) 的答案
    • 兩子節點分別為 \([l,mid]\) , \([mid+1,r]\)
    • \(mid=\lfloor\frac{l+r}{2} \rfloor \)
  • 可發現每個點到根節點的深度是 \(O(\log{N})\)
  • 如下圖

節點數會有多少呢?

可注意到第 \(i\) 層至多 \(2^i\) 個節點

共 \(1+2+2^2+...+2^{\log{n}}\ \simeq 4n\) 

區間和

區間Min

修改

因為從圖可發現一個點的位置只需從根結點走 \(O(\log{N})\) 

因此單點修改時只要從該節點往下走到該點位置、修改後再往上更新即可

修改

下圖是設 \(N=16\),現在有兩筆 modify 操作:將第\(3\) 個位置改成 \(5\) ,將 \(1\) 的位置改成 \(2\)

實作

實作

  • build
  • push
  • update
  • query

Build

  • 對初始序列 \(a\) 建線段樹
  • 陣列開 \(4n\)
  • 以下使用 0-base , 區間為 \([ \ , \ )\)
  • 使用完全二元樹編號:
    • 父節點為 \(x\)
    • 則其左右子點為 \(2x+1 , \ 2x+2\) 

以區間和為例

建樹使用遞迴,先建左右子樹再合併結果

\(sum(x)=sum(2x+1)+sum(2x+2)\)

函式 build(x,lx,rx) 代表節點編號 x 、 其維護區間為 [lx,rx)
void pull(int x){
	sum[x]=sum[2*x+1]+sum[2*x+2];
}
void build(int x,int lx,int rx){
	if(lx==rx-1){
		sum[x]=a[lx];
		return ;
	}
	int mid=(lx+rx)>>1;
	build(2*x+1,lx,mid);
	build(2*x+2,mid,rx);
	pull(x);
}

之後的 code 用 L 代表 2*x+1,lx,mid

R 代表 2*x+2,mid,rx

修改

 

更新的話就直接沿著線段樹...,

最深層即為該位置 (區間長度為 1 )

更改後一路 pull 更新子父節點

update (x,lx,rx,pos,val) 
代表目前所在節點 x, 維護區間 [lx,rx) 、 要更新 a[pos]+val
 
void update(int x,int lx,int rx,int pos,int val){
	if(lx==rx-1) {
		sum[x]+=val;
	}
	int mid=(lx+rx)>>1;
	if(pos<mid) {// pos 在左子樹
		update(L,pos,val);
	}
	else update(R,mid,rx)
	pull(x); 
}

Update

詢問時會有當前節點區間 [lx,rx] 

以及目標區間[l,r]

會有三種關係如下

Query

int query(int x,int lx,int rx,int l,int r){
	if(l>=rx||lx>=r) return 0;
	if(l<=lx&&rx<=r) return sum[x];
	int mid=(lx+rx)>>1;
	return query(L,l,r)+query(R,l,r);
}

如果是包含的話直接加上該節點的值

相離則 return

部分相交往下遞迴

合併Code

#define L 2*x+1,lx,mid
#define R 2*x+2,mid,rx
int sum[4*N];
void pull(int x){
	sum[x]=sum[2*x+1]+sum[2*x+2];
}
void build(int x,int lx,int rx){
	if(lx==rx-1){
		sum[x]=a[lx];
		return ;
	}
	int mid=(lx+rx)>>1;
	build(L);
	build(R);
	pull(x);
}
void update(int x,int lx,int rx,int pos,int val){
	if(lx==rx-1) {
		sum[x]+=val;
	}
	int mid=(lx+rx)>>1;
	if(pos<mid) {// pos 在左子樹
		update(L,pos,val);
	}
	else update(R,pos,val)
	pull(x); 
}
int query(int x,int lx,int rx,int l,int r){
	if(l>=rx||lx>=r) return 0;
	if(l<=lx&&rx<=r) return sum[x];
	int mid=(lx+rx)>>1;
	return query(L,l,r)+query(R,l,r);
}

上面講述的是單點修改的情況

那區間修改呢

 

  • 給定長度為 \(n\) 初始序列 \(a\) 和 \(q\) 筆操作
  • 操作有兩種
    • \(l,r,u\) 在區間 \([l,r]\)  加 \(u\) 
    • 詢問區間 \([l,r]\) 的和

直接當作 \(O(n)\) 次單點加值

時間複雜度退化回 \(O(qn\log{n})\)

善用線段樹的區間分割性質

打懶標

 

在區間設置一個標記 \(tag\) 代表這個區間裡的每個位置都要加上 \(tag\)

在區間修改時若節點區間被目標區間完全包含則打懶標並同時更新該區間的答案

在之後要遍歷下方節點時再把懶標往下推

打懶標

實作時一個很重要的點就是讓你的陣列恰存實際值,因此在打懶標時順便更新 sum 之值進區間時 push , 離開後 pull
void push(int x,int lx,int rx){
	int mid=(lx+rx)>>1;
	sum[2*x+1]+=(mid-lx)*tag[x];
	sum[2*x+2]+=(rx-mid)*tag[x];
	tag[2*x+1]+=tag[x];
	tag[2*x+2]+=tag[x];
	tag[x]=0;
}
void update(int x,int lx,int rx,int l,int r,int val){
	if(l<=lx&&rx<=r){
		tag[x]+=val;
		sum[x]+=(rx-lx)*val;
		return ;
	}
	if(l>=rx||lx>=r) return ;
	int mid=(lx+rx)>>1;
	push(x,lx,rx); // 要走訪往左右子節點,把懶標往下打
	update(L,l,r,val);
	update(R,l,r,val);
	pull(x);
}

對區間更新時是根據其計算方式更新

  • 區間和
    • 加上長度乘其值
  • 區間 max/min
    • 加上該值

 

支援區間加值的完整code

* 在詢問時也必須加入 push

#define L 2*x+1,lx,mid
#define R 2*x+2,mid,rx
int sum[N],tag[x];
void pull(int x){
	sum[x]=sum[2*x+1]+sum[2*x+2];
}
void push(int x,int lx,int rx){
	int mid=(lx+rx)>>1;
	sum[2*x+1]+=(mid-lx)*tag[x];
	sum[2*x+2]+=(rx-mid)*tag[x];
	tag[2*x+1]+=tag[x];
	tag[2*x+2]+=tag[x];
	tag[x]=0;
}
void build(int x,int lx,int rx){
	if(lx==rx-1){
		sum[x]=a[lx];
		return ;
	}
	int mid=(lx+rx)>>1;
	build(L);
	build(R);
	pull(x);
}
void update(int x,int lx,int rx,int l,int r,int val){
	if(l<=lx&&rx<=r){
		tag[x]+=val;
		sum[x]+=(rx-lx)*val;
		return ;
	}
	if(l>=rx||lx>=r) return ;
	int mid=(lx+rx)>>1;
	push(x,lx,rx); // 要走訪往左右子節點,把懶標往下打
	update(L,l,r,val);
	update(R,l,r,val);
	pull(x);
}
int query(int x,int lx,int rx,int l,int r){
	if(l>=rx||lx>=r) return 0;
	if(l<=lx&&rx<=r) return sum[x];
	int mid=(lx+rx)>>1;
	push(x,lx,rx); // 要走訪往左右子節點,把懶標往下打
	return query(L,l,r)+query(R,l,r);
}

線段樹的基礎支援即為區間操作/詢問

只要操作/詢問具有分治性及可用分治解決

接下來介紹一些線段樹應用例題和一些技巧

區間反轉(沒有judge)

  • 一個長度為 \(n\) 的 \(0-1\) 陣列 , 有 \(q\) 筆操作
    • 選擇區間 \([l,r]\) 把內部的數值反轉
    • 詢問該區間的總和
    • 詢問該區間的最小值

區間反轉(沒有judge)

  • 一個長度為 \(O(n)\) 的 \(0-1\) 陣列 , 有 \(q\) 筆操作
    • 選擇區間 \([l,r]\) 把內部的數值反轉
    • 詢問該區間的總和
    • 詢問該區間的最小值

區間修改所以需要打懶標,懶標直接表示是否反轉

問題剩下反轉後區間和該如何修改

整個區間 \(0-1\) 反轉

\(sum^{'}=rx-lx-sum\)

區間反轉(沒有judge)

  • 一個長度為 \(O(n)\) 的 \(0-1\) 陣列 , 有 \(q\) 筆操作
    • 選擇區間 \([l,r]\) 把內部的數值反轉
    • 詢問該區間的總和
    • 詢問該區間的最小值

因為數字只有 \(0/1\) 所以只要判斷有沒有 \(0\) 

有的化最小值為 \(0\) , 反之為 \(1\)

用區間和即可判斷

 

長度為 \(n\) 的序列、 \(q\) 筆操作

  • 修改位置 \(k\) 的值為 \(u\)
  • 求區間 \([l,r]\) 的最大前綴和

 

嘗試看最大前綴和的分治性質

把陣列切一半

那整個陣列的最大前綴和可分成兩種 case:

  1. 右界超過 \(mid\)
    • 左邊總和+右邊最大前綴和
  2. 右界小於 \(mid\)
    • 左邊最大前綴和
void pull(int x){
	mx[x]=max(mx[2*x+1],sum[2*x+1]+mx[2*x+2]);
	sum[x]=sum[2*x+1]+sum[2*x+2];
}

維護區間總和、最大前綴和即可

類似題自己練習: 最大區間子序列和

樹上二分搜

動態第 \(k\) 小

  • 有 \(q\) 次操作和一個空集合 \(v\)

  • 每次操作會有兩種可能:

    • + x k 在集合加入 \(x\)

    • - x k 在集合刪除 \(x\) (保證 \(x\) 存在)

    • 之後請輸出一行代表集合 \(v\) 的第 \(k\) 小

  • \(q,x,k\leq 10^6\)

\(cnt[x]\) 代表 \(x\) 在集合出現幾次

等價找 \(\min\{x \ |\ sum_{i=0}^{x} {cnt[i]} \geq k\}\)

二分搜

但是線段樹本身的分治性即可直接二分搜

時間複雜度還是 \(O(q\cdot \log{N})\)

Code

#define L 2*x+1,lx,mid
#define R 2*x+2,mid,rx
int query(int x,int lx,int rx,int k){
    if(lx==rx-1) return lx; 
    // 區間只剩 lx 即為答案
    if(sum[2*x+1]>=k) return query(L,k); 
    // 代表答案在左區間內
    return query(R,k-sum[2*x+1]); 
    // 左區間數量不足 k , 剩下 k-sum[2*x+1] 在右邊找尋
}

掃描線

當題目是二維計算時,可以枚舉其中一維度

剩下一維計算再利用線段樹之類的

  • 輸入包含一個 \(n\) 代表矩形數量
  • 之後有 \(n\) 行 , 每行有 \(L,R,D,U\) 代表該矩形的四頂點
  • \(1\leq n,L,D,U \leq 10^6\)
  • 求矩形覆蓋的面積

相交的只能算一次,二維不太好計算。

利用掃描線枚舉 \(x\) 軸計算 \(y\) 軸的長度

轉成一維後會發現原先的矩形變成線段覆蓋

如下圖,會發現要計算的可能被很多塊覆蓋到。但只能當作 \(1\)

於是用一個技巧:

計算所求的補集合(沒被覆蓋的) 

 

Code

實作時還有一個技巧是讓他先加後減,使得其數值必定 \(\geq 0\)

//Author: Woody
#include<bits/stdc++.h>
#define int long long
#define mp make_pair
#define eb emplace_back
#define rep(n) for(int i=0;i<n;i++)
#define rep2(n) for(int j=0;j<n;j++)
#define F first
#define S second
#define all(v) v.begin(),v.end()
#define SZ(x) (int)(x.size())
#define lowbit(x) (x&-x)
#define SETIO(s) ifstream cin(s+".in");ofstream cout(s+".out");
#define quick ios::sync_with_stdio(0);cin.tie(0);
using namespace std;
typedef pair<int,int> pii;
template <class t1,class t2> 
inline const pair<t1,t2> operator + (const pair<t1,t2>&p1,const pair<t1,t2>&p2){
	return pair<t1,t2>(p1.F+p2.F,p1.S+p2.S);
}
template <class t1,class t2> 
inline const pair<t1,t2> operator - (const pair<t1,t2>&p1,const pair<t1,t2>&p2){
	return pair<t1,t2>(p1.F-p2.F,p1.S-p2.S);
}
const int INF=1e18;
const int N=1e6+7;
struct segment{
	vector<int> lazy;
	vector<int> st;
	int size;
	void init(int n){
		size=1;
		while(size<n) size*=2;
		lazy.assign(size*2,0LL);
		st.assign(size*2,0LL);
	}
	void push(int x,int lx,int rx){
		if(lazy[x]) st[x]=rx-lx;
		else if(lx!=rx-1){
			st[x]=st[2*x+1]+st[2*x+2];
		}
		else st[x]=0LL;
	}
	void add(int l,int r,int val){
		add(l,r,val,0,0,size);
	}
	void add(int l,int r,int val,int x,int lx,int rx){
		if(l>=rx||lx>=r) return ;
		if(l<=lx&&rx<=r){
			lazy[x]+=val;
			push(x,lx,rx);
			return ;
		}
		int middle=(lx+rx)>>1;
		add(l,r,val,2*x+1,lx,middle);
		add(l,r,val,2*x+2,middle,rx);
		push(x,lx,rx);
	}
}tree;
vector<tuple<int,int,int,int> > v;
signed main(){
	quick
	int n;
	cin>>n;
	for(int i=0;i<n;i++){
		int l,r,d,u;
		cin>>l>>r>>d>>u;
		v.push_back({l,d,u,1});
		v.push_back({r,d,u,-1});
	}
    tree.init(N);
	sort(all(v));
	int pre=0;
    int ans=0;
	for(auto [pos,a,b,val]:v){
		if(pos!=pre){
			ans+=(pos-pre)*tree.st[0];
            pre=pos;
		}
        // cout<<pos<<" "<<a<<" "<<b<<" "<<val<<"\n";
        // cout<<ans<<"ans\n";
		tree.add(a,b,val);
	}
	cout<<ans<<"\n";
	return 0;
}

動態開點

有時候題目要求的是值域線段樹,但是他的值域範圍是 \([0,10^9]\)

 

動態第 \(k\) 小

  • 有 \(q\) 次操作和一個空集合 \(v\)

  • 每次操作會有兩種可能:

    • + x k v 在集合加入 \(v\) 個 \(x\)

    • - x k 在集合刪除 \(x\) (保證 \(x\)) 存在

    • 之後請輸出一行代表集合 \(v\) 的第 \(k\) 小

  • \(q,k\leq 10^6\)

  • \(x \leq 10^9\)

  • 可以注意到即使你的值域是 \([0,10^9]\) 但數字的個數還是 \(\leq 10^6\)
  • 動態開點的想法就是不要預先開好,有需要才開
  • 那因為個數 \(\leq 10^6\) , 所以節點數一樣會是 \(\leq 10^6\log{10^6}\)

Code

int L[N];
int R[N];
int sum[4*N];
int cnt=1;
void pull(int x){
	sum[x]=sum[L[x]]+sum[R[x]];
}

void update(int &x,int lx,int rx,int pos,int val){
	if(!x) x=cnt++;
	int mid=(lx+rx)>>1;
	if(pos<mid){
		update(L[x],lx,mid,pos,val);
	}
	else update(R[x],mid,rx,pos,val);
	pull(x);
}
int query(int x,int lx,int rx,int k){
	if(lx==rx-1) return lx;
	int mid=(lx+rx)>>1;
	if(sum[L[x]]>=k) return query(L[x],lx,mid,k);
	return query(R[x],mid,rx,k-sum[L[x]]); 
}

樹套樹

  • 單點修改區間和詢問
  • 只是陣列是二維
  • 一個 \(N \times N\) 的 \(0-1\) 矩陣、 \(q\) 次操作
    • 改變位置 \((x,y)\) 的狀態
    • 詢問 \(sum([x_1,y_1] \sim [x_2,y2])\)

如果題目是一維的會做,問題是現在是兩個維度

線段樹是維護區間,其維護的不一定是數值,也可以是線段樹

 

開一個線段樹,每個節點都是一個一維度的區間和線段樹。

總時間複雜度為 \(O((N+Q)\log^2{N}\))

實際實作就留作練習

後面會介紹用 2維 bit 實作二維動態區間和

持久化線段樹

線段樹可以回到過去

長度 \(n\) 的序列, 有 \(q\) 筆操作

  • 設定 \(k\) 版本的陣列的位置 \(p\) 的值為 \(u\)
  • 詢問 \(k\) 版本的陣列區間 \([l,r]\) 的總和
  • 複製 \(k\) 版本的陣列為新版本

直接暴力開 \(n\) 棵線段樹 ?

光記憶體就不夠了

 

想法就是每次操作時直接回傳新的節點。每棵線段樹共用未被修改的節點

因為每次修改時只會改到 \(O(\log{n})\) 個節點。

時間,空間複雜度是 \(O((n+q)\log{n})\)

 

通常會使用指標型線段樹實作

修改後直接 new node

const int N=2e5+7;
const int INF=1e18;
struct Node{
	Node *l,*r;	
	int sum=0;
	Node(Node*l,Node*r) : l(l),r(r){
		if(l) sum+=l->sum;
		if(r) sum+=r->sum;
	}
	Node(Node*v) : l(v->l),r(v->r),sum(v->sum){
	}
	Node(int x) : sum(x),l(nullptr),r(nullptr){
	}
};
#define L now->l,lx,mid
#define R now->r,mid,rx
Node* Set(int pos,int val,Node*now,int lx,int rx){
	if(lx==rx-1){
		return new Node(val);
	}
	if(!(now->l)){
		now->l=new Node(0LL);
		now->r=new Node(0LL);
	}
	int mid=(lx+rx)>>1;
	if(pos<mid) return new Node(Set(pos,val,L),now->r);
	else return new Node(now->l,Set(pos,val,R));
}
int query(int l,int r,Node*now,int lx,int rx){
	if(!now) return 0;
	if(l>=rx||lx>=r) return 0;
	if(l<=lx&&rx<=r) return now->sum;
	int mid=(lx+rx)>>1;
	return query(l,r,L)+query(l,r,R);
}
Node* root[N];
signed main(){
	quick
 
	root[1]=new Node(0LL);
	int n,q;
	int cnt=1;
	cin>>n>>q;
	rep(i,1,n) {
		int xi;
		cin>>xi;
		root[1]=Set(i-1,xi,root[1],0,n);
	}
	while(q--){
		int ti;
		cin>>ti;
		if(ti==1){
			int k,a,x;
			cin>>k>>a>>x;
			root[k]=Set(--a,x,root[k],0,n);
		}
		else if(ti==2){
			int k,a,b;
			cin>>k>>a>>b;
			cout<<query(--a,b,root[k],0,n)<<"\n";
		}
		else{
			int k;cin>>k;
			root[++cnt]=new Node(root[k]);
		}
	}
	return 0;
}

剛剛那題還沒有展現持久化線段樹的魅力

 

長度 \(n\) 序列、 \(q\) 次詢問

每次詢問 區間 \([l,r]\) 第 \(k\) 小的數字

  • \(1 \leq n,q \leq 2\cdot 10^5\)

 

p.s. 洛谷題目有時候會亂卡常數 / 記憶體

題目假如只是問整個序列第

\(k\) 小

值域線段樹即可解決

問題是有很多區間

持久化線段樹開砸

利用持久化線段樹的版本、維護前綴版本的線段樹

(第 \(i\) 個版本的線段樹 \(st_i\) 代表 \([1\sim i]\) 的序列的值域線段樹 \([l,r]\) 即為 \(st_r-st_{l-1}\)

樹上二分搜即可

BIT + 持久化線段樹即可

BIT 在接下來會教

例題/練習題

資料結構和樹

樹壓平

 

樹壓平

  • 之前其實有講過,
  • 維護每個點第一次進入 \(in[i]\) , 最後離開 \(out[i]\) 這兩個時間戳記
  • 若 \((in[c],out[c]) \in (in[x],out[x])\) 則 \(x\) 為 \(c\) 祖先

樹壓平

  • 若 \((in[c],out[c]) \in (in[x],out[x])\) 則 \(x\) 為 \(c\) 祖先
  • 若把點當作在 \(in[x]\) , 則 \((in[x],out[x])\) 恰包含 \(x\) 的子孫
  • 會更改一個點的值、求某個點子樹的總和
  • 由於樹壓平的性質,利用樹壓平轉成序列
  • 子樹總和等價詢問 \((in[x],out[x])\) 總和
  • 變成區間詢問問題  => 線段樹
#include<bits/stdc++.h>
#define Woody
#define int long long 
#define lowbit(x) (x&-x)
#define rep(n) for(int i=0;i<n;i++)
#define mp make_pair
#define eb emplace_back
#define F first
#define S second
#define SZ(a) (int)(a.size())
#define all(v) v.begin(),v.end()
#define SETIO(s) ifstream cin(s+".in");ofstream cout(s+".out");
#ifdef Woody
#define quick ios::sync_with_stdio(0);cin.tie(0);
#else
#define quick
#endif
#define INF INT64_MAX
using namespace std;
typedef pair<int,int> pii;
vector<int> in;
vector<int> out;
int t;
const int N=2e5+7;
vector<int> V[N];
vector<bool> visited;
void dfs(int x){
	in[x]=t++;
	visited[x]=true;
	for(int i:V[x]){
		if(!visited[i]) dfs(i);
	}
	out[x]=t++;
	return ;
}
struct BIT{
	vector<int> bit;
	int n;
	void init(int x){
		n=x;
		bit.assign(n+1,0);
	}
	void add(int x,int val){
		for(int i=x;i<=n;i+=lowbit(i))	bit[i]+=val;
	}
	int query(int x){
		int sum=0;
		for(int i=x;i>0;i-=lowbit(i)) sum+=bit[i];
		return sum;
	}
}tree;
signed main(){
	quick
	int n,q;
	cin>>n>>q;
	vector<int> v(n);
	rep(n) cin>>v[i];
	in.resize(n+1);
	out.resize(n+1);
	visited.assign(n+1,0);
	t=1;
	rep(n-1){
		int a,b;
		cin>>a>>b;
		V[a].eb(b);
		V[b].eb(a);
	}
	dfs(1);
 
	tree.init(t);
	for(int i=0;i<n;i++){
		tree.add(in[i+1],v[i]);
	}
	rep(q){
	//	for(int i=1;i<=t;i++) cout<<tree.query(i)<<" ";cout<<"\n";
		//system("pause");
		int r;
		cin>>r;
		if(r==1){
			int s,x;
			cin>>s>>x;
			if(v[s-1]!=x){
				tree.add(in[s],x-v[s-1]);
				v[s-1]=x;
			}
		}
		else{
			int s;
			cin>>s;
			cout<<tree.query(out[s])-tree.query(in[s]-1)<<"\n";
		}
	}
}

Treap

 

  • Treap=tree+heap
  • 結合二元搜尋樹、樹堆性質
  • 每個節點維護兩個值 \(key,pri\)
    • \(key_l<key_{x}<key_R\)
    • \(pri_{x}<pri_l,pri_r\) 
      •  (若是使用 max heap,父節點會大於左右節點)

 

藍字為 \(pri\) , 紅字為 \(key\)

\(key\)是真正要維護的、\(pri\) 是隨機產生已維護期望高度 \(\log{N}\)

實作

Treap 主要由節點、兩個主要函式組成

  • Treap node struct
  • merge
  • Split
struct Treap{
	int key,pri;
	Treap *l,*r;
	Treap(int _key) : key(_key),pri(rand()),l(nullptr),r(nullptr){
	}
};

Treap 節點架構

Merge

把兩個 Treap \(a,b\) 合併且 \(a\) 所有的 \(key\) 值保證小於 \(b\)

Treap merge(Treap*a,Treap*b){
    // a->key < b->key
    if(!a||!b) return a ? a : b;
    if(a->pri>b->pri){
        // a 為 b 的父節點
        a->r=merge(a->r,b);
        pull(a);
        // 更新值
        return a;
    }
    else{
        // a 為 b 的子節點
        b->l=merge(a,b->l);
        pull(b);
        // 更新值
        return b;
    }
}

圖解

Split

把 Treap \(t\) key 值 \(\leq k\) 放在 \(a\) , 其餘在 \(b\)  且 \(a,b\) 依然滿足 Treap 性質

void split(Treap*t,int k,Treap*&a,Treap*&b){
    if(!t) {a=b=nullptr;return ;}
    if(t->key<=k){
        a=t;
        split(t->r,k,a->r,b);
        pull(a);    
    }
    else{
        b=t;
        split(t->l,k,a,b->l);
        pull(b);
    }
}

圖解

struct Treap{
    int key,pri,sum;
    Treap *l,*r;
    Treap(int _key,int _val) : key(_key),sum(_val),pri(rand()),l(nullptr),r(nullptr){
    }
}
Treap merge(Treap*a,Treap*b){
    // a->key < b->key
    if(!a||!b) return a ? a : b;
    if(a->pri>b->pri){
        // a 為 b 的父節點
        a->r=merge(a->r,b);
        pull(a);
        // 更新值
        return a;
    }
    else{
        // a 為 b 的子節點
        b->l=merge(a,b->l);
        pull(b);
        // 更新值
        return b;
    }
}
void split(Treap*t,int k,Treap*&a,Treap*&b){
    if(!t) {a=b=nullptr;return ;}
    if(t->key<=k){
        a=t;
        split(t->r,k,a->r,b);
        pull(a);    
    }
    else{
        b=t;
        split(t->l,k,a,b->l);
        pull(b);
    }
}

Treap 完整 Code

 

把 \(key\) 設置成它的位置即可

即可把序列對照在 Treap

Treap 的中序順序即為序列順序

Merge,Split能幹嘛

  • 當你做修改時,可以把該位置給 split 出來更新再 merge回去
  • ex: 區間 \([l,r]\) 加值 
    • 先 split 出 \(\leq r\) \(t_1,R\) 
    • 再 split 出 \(\geq l\) \(L,t_2\)
    • 再 \(t_2\) 上打懶標
    • 再 \(merge(L,t_2,R)\)

insert

Treap* insert(Treap*t,int k,int _val){
    Treap *tl;
    split(t,k,tl,t);
    merge(tl,merge(new Treap(k,val)),t);
}

erase

Treap* erase(Treap*t,int k,int _val){
    Treap *tl,*tr;
    split(t,k-1,tl,t);
    split(t,k,t,tr);
    return merge(tl,tr);
}

和上方所述相同,更新值時把該位置 split 修改完再 merge

 

 

#pragma GCC optimzize("Ofast,no-stack-protector")
#include<bits/stdc++.h>
#define int long long
#define quick ios::sync_with_stdio(0);cin.tie(0);
#define rep(x,a,b) for(int x=a;x<=b;x++)
#define repd(x,a,b) for(int x=a;x>=b;x--)
#define lowbit(x) (x&-x)
#define sz(x) (int)(x.size())
#define F first
#define S second
#define all(x) x.begin(),x.end()
#define mp make_pair
#define eb emplace_back
using namespace std;
typedef complex<int> P;
#define X real()
#define Y imag()
typedef pair<int,int> pii;
void debug(){
    cout<<"\n";
}
template <class T,class ... U >
void debug(T a, U ... b){
    cout<<a<<" ",debug(b...);
}
const int N=2e5+7;
const int INF=1e18;
struct Treap{
	int pri,key,val;
	int sum;
	Treap *l,*r;
	Treap (int _key,int _val ) : key(_key),val(_val),sum(_val),pri(rand()),l(nullptr),r(nullptr){
	}
};
int Sum(Treap *x){
	return x ? x->sum : 0LL;
}
void pull(Treap *x){
	x->sum=Sum(x->l)+Sum(x->r)+x->val;
}
Treap* merge(Treap*a,Treap*b){
	//保證 a 之key<b之key
	if(!a||!b) return a ? a:b;
	if(a->pri > b->pri){
		a->r=merge(a->r,b);
		pull(a);
		return a;
	}
	else{
		b->l=merge(a,b->l);
		pull(b);
		return b;
	}
}
void split(Treap*t,int k,Treap* &a,Treap* &b){
	//split key<=k to a, key>k to b
	if(!t) a=b=nullptr;
	else if(t->key<=k){
		a=t;
		split(t->r,k,a->r,b);
		pull(a);
	}
	else{
		b=t;
		split(t->l,k,a,b->l);
		pull(b);
	}
}
void print(Treap*t){
	if(!t) return ;
	print(t->l);
	debug(t->key,t->sum);
	print(t->r);
}
Treap* insert(Treap *t, int k,int val){
	Treap *tl,*tr;
	split(t,k,tl,tr);
	return merge(tl,merge(new Treap(k,val),tr));
}
Treap* remove(Treap *t,int k){
	Treap* tl,*tr;
	split(t,k-1,tl,t);
	split(t,k,t,tr);
	return merge(tl,tr);
}
signed main(){
	quick
	int n,q;
	cin>>n>>q;
	Treap *t=nullptr;
	rep(i,1,n){
		int xi;
		cin>>xi;
		t=insert(t,i,xi);
		//debug("printt");print(t);
	}
	while(q--){
		int c,a,b;
		cin>>c>>a>>b;
		if(c==1){
			t=remove(t,a);
			t=insert(t,a,b);
			continue;
		}
		Treap *tl,*tr;
		split(t,a-1,tl,t);
		split(t,b,t,tr);
		cout<<Sum(t)<<"\n";
		t=merge(tl,merge(t,tr));
	}
 
 
	return 0;
}

使用 Treap 解只要記得暴力把詢問給 split 、 解完再 merge 

Treap 當然不只處理 RMQ

還支援反轉等操作

長度 \(n\) 序列和 \(m\) 次操作

  • 反轉一個區間
  • 計算區間和

考慮反轉操作:

可以利用 Treap 把區間 \([l,r]\) 剪下。 反轉後再放回

一區間反轉其實就等價左右子樹對調

但這樣就無法維護 \(key\) 的性質

把 key 丟掉

\(key\) 的維護在區間反轉時就會爛掉

但 \(key\) 好像沒有那麼重要 \(?\)

因為位置的順序可直接由中序來判定。

所以可以把 \(key\) 丟掉、改成維護 \(size\)

\(x\) 的編號即為左子樹大小 +1

#pragma GCC optimzize("Ofast,no-stack-protector")
#include<bits/stdc++.h>
#define int long long
#define quick ios::sync_with_stdio(0);cin.tie(0);
#define rep(x,a,b) for(int x=a;x<=b;x++)
#define repd(x,a,b) for(int x=a;x>=b;x--)
#define lowbit(x) (x&-x)
#define F first
#define S second
#define all(x) x.begin(),x.end()
#define mp make_pair
#define eb emplace_back
using namespace std;
typedef complex<int> P;
#define X real()
#define Y imag()
typedef pair<int,int> pii;
void debug(){
    cout<<"\n";
}
template <class T,class ... U >
void debug(T a, U ... b){
    cout<<a<<" ",debug(b...);
}
const int N=2e5+7;
const int INF=1e18;
struct Treap{
	int sz,sum,val,pri;
	bool rev;
	Treap *l,*r;
	Treap(int _val) : sz(1),sum(_val),val(_val),pri(rand()),l(nullptr),r(nullptr){
	}
};
int Sz(Treap *x){
	return x ? x->sz : 0;
}
int Sum(Treap *x){
	return x ? x->sum : 0;
}
void pull(Treap *x){
	if(!x) return ;
	x->sz=Sz(x->l)+Sz(x->r)+1;
	x->sum=Sum(x->l)+Sum(x->r)+x->val;
}
void Rev(Treap *x){
	if(!x) return ;
	x->rev^=1;
	swap(x->l,x->r);
}
void push(Treap *x){
	if(!x) return ;
	if(!x->rev) return ;
	Rev(x->l);
	Rev(x->r);
	x->rev=false;
}
Treap *merge(Treap *a,Treap *b){
	if(!a||!b) return a ? a : b;
	push(a);push(b);
	if(a->pri > b->pri){
		a->r=merge(a->r,b);
		pull(a);
		return a;
	}
	else{
		b->l=merge(a,b->l);
		pull(b);
		return b;
	}
}
void split(Treap *t,int k,Treap *&a,Treap *&b){
	push(t);	
	if(!t) a=b=nullptr;
	else if(Sz(t->l)+1<=k){
		a=t;
		split(t->r,k-Sz(t->l)-1,a->r,b);
		pull(a);
		return ;
	}
	else{
		b=t;
		split(t->l,k,a,b->l);
		pull(b);
		return ;
	}
}
Treap *insert(Treap *t,int pos,int val){
	Treap *tl,*tr;
	split(t,pos,tl,tr);
	return merge(tl,merge(new Treap(val),tr));
}
Treap* opt(Treap*t,int a,int b){
	Treap *tl,*tr;
	split(t,b,t,tr);
	split(t,a-1,tl,t);
	Rev(t);
	return merge(tl,merge(t,tr));
}
Treap* query(Treap*t,int a,int b,int &ans){
	Treap *tl,*tr;
	split(t,b,t,tr);
	split(t,a-1,tl,t);
	ans=Sum(t);
	return merge(tl,merge(t,tr));;
}
void print(Treap *t){
	if(!t) return ;
	print(t->l);
	cout<<t->val<<" ";
	print(t->r);
}
signed main(){
	quick
	int n,m;
	cin>>n>>m;
	Treap *t=nullptr;
	rep(i,1,n){
		int x;
		cin>>x;
		t=insert(t,i,x);
	}
	while(m--){
		int k,a,b;
		cin>>k>>a>>b;
		if(k==1){
			t=opt(t,a,b);
		}
		else{
			int ans=0;
			t=query(t,a,b,ans);
			cout<<ans<<"\n";
		}
	}
 
	return 0;
}  

線段樹的題目 Treap 基本都可以寫

但 Treap 其實不常用,線段樹基本足夠了

不要亂耍毒

Treap 練習題

樹鍊剖分

 

  • 給定一顆樹和 \(q\) 次操作
  • 每次操作是下列兩種
    • \(s_i\) 值加上 \(val_i\)
    • 詢問 \((a,b)\) 路徑上的最大值
  • \(1≤n,q≤2⋅10^5\)

樹鍊(重鍊)剖分

  • 當然題目也可以是要求路徑加值、路徑詢問...
  • 想法是把樹切成一堆鍊
  • 鍊稱為重鍊,而連接兩鍊的邊稱為輕邊

樹鍊(重鍊)剖分

  • 重節點 : \(x\) 為 重節點則 \(x\) 為 \(p_x\) 中子樹最大的子節點
  • 其餘為輕點
  • 重點會和祖先在同一條鍊
  • 輕點會是新鍊的起點
  • 祖先和輕點的邊為輕邊

樹鍊(重鍊)剖分

  • 重點會和祖先在同一條鍊
  • 輕點會是新鍊的起點
  • 祖先和輕點的邊為輕邊

樹鍊(重鍊)剖分性質

  • 每個節點恰在一條重鍊
  • 每經過輕邊則子樹大小會除以二
  • 經過 \(O(\log{n})\) 條輕邊
  • 經過 \(O(\log{n})\) 條重鍊
  • 也就是可以暴力走訪重鍊

重剖求 \(LCA(a,b)\)

  • 可以每次 \(O(1)\) 把重鍊深度較深的點往上跳
  • 直到兩人在同一鍊停止,此時較淺者為 \(LCA\)
  • 因為鍊至多 \(O(\log{n})\) 條,所以只需 \(O(\log{n})\) 時間

重剖求 路徑 \(a,b\)

  • 那因為一樣至多 \(O(\log{n})\) 條鍊
  • 所以假設可以維護每條鍊的答案 (ex: 總和、最大值)
  • 就可以暴力跳時順便更新答案
  • 如何維護 ?
    • 線段樹

維護重鍊

  • 最暴力想法每條重鍊維護一棵線段樹
  • 重鍊有性質? 會是一堆相連的點
  • 所以只要讓重鍊先 dfs 就可以讓其進入(\(in\))編號連續
  • 一棵線段樹搞定

維護重鍊

實作

  • 子樹大小 \(sz[x]\)
    • 來決定一個點是不是重點
  • 所屬鍊的起點 \(top[x]\)
  • 所屬鍊的深度 \(dep[x]\)
    • 非節點的深度
  • dfs 進入順序 \(in[x]\)
  • 他的父節點 \(fa[x]\)
    • 跳鍊時會需要
  • 一棵線段樹維護答案...

實作

cses 會TLE的 code

#pragma GCC optimize("O3,unroll-loops")
#pragma GCC target("avx,popcnt,sse4,abm")
#include<bits/stdc++.h>
#define int long long
#define quick ios::sync_with_stdio(0);cin.tie(0);
#define rep(x,a,b) for(int x=a;x<=b;x++)
#define repd(x,a,b) for(int x=a;x>=b;x--)
#define lowbit(x) (x&-x)
#define sz(x) (int)(x.size())
#define F first
#define S second
#define all(x) x.begin(),x.end()
#define mp make_pair
#define eb emplace_back
using namespace std;
typedef complex<int> P;
#define X real()
#define Y imag()
typedef pair<int,int> pii;
void debug(){
    cout<<"\n";
}
template <class T,class ... U >
void debug(T a, U ... b){
    cout<<a<<" ",debug(b...);
}
const int N=2e5+7;
const int INF=1e18;
#define ll 2*x+1
#define rr 2*x+2
#define L ll,lx,mid
#define R rr,mid,rx
struct segment{
	int sz;
	vector<int> mx;
	void init(int n){
		sz=1;
		while(sz<n) sz<<=1;
		mx.assign(sz*2,0LL);
		sz=n;
		return;
	}
	void pull(int x){
		mx[x]=max(mx[ll],mx[rr]);
	}
	void build(const vector<int>&a,int x,int lx,int rx){
		if(lx==rx-1){
			if(lx<sz(a)) mx[x]=a[lx];
			return ;
		}
		int mid=(lx+rx)>>1;
		build(a,L);
		build(a,R);
		pull(x);
	}
	void update(int pos,int val,int x,int lx,int rx){
		if(lx==rx-1){
			mx[x]=val;
			return ;
		}
		int mid=(lx+rx)>>1;
		if(pos<mid) update(pos,val,L);
		else update(pos,val,R);
		pull(x);
	}
	int qry(int l,int r,int x,int lx,int rx){
		if(l<=lx&&rx<=r) return mx[x];
		if(l>=rx||lx>=r) return 0LL;
		int mid=(lx+rx)>>1;
		return max(qry(l,r,L),qry(l,r,R));
	}
	void build(const vector<int>&a){
		build(a,0,0,sz);
	}
	void update(int pos,int val){
		update(pos,val,0,0,sz);
	}
	int qry(int l,int r){
		return qry(l,r+1,0,0,sz);
	}
}st;
int top[N];
int fa[N];
int in[N];
int sz[N];
int mxc[N];
int t=0;
int V[N];
int dep[N];
vector<int> v[N];
void dfs_sz(int x,int p=-1){
	mxc[x]=-1;
	sz[x]=1;
	for(int i:v[x]){
		if(i==p) continue;
		dfs_sz(i,x);
		sz[x]+=sz[i];
		if(mxc[x]==-1||sz[mxc[x]]<sz[i]) mxc[x]=i;
	}
}
void dfs(int x,int p=1,int tp=1,int d=0){
	in[x]=t++;
	top[x]=tp;
	dep[x]=d;
	fa[x]=p;
	if(mxc[x]!=-1) dfs(mxc[x],x,tp,d);
	else return ;
	for(int i:v[x]){
		if(i==p||i==mxc[x]) continue;
		dfs(i,x,i,d+1);
	}
}
int Query(int a,int b){
	int ret=0;
	while(top[a]!=top[b]){
		if(dep[a]>dep[b]){
			ret=max(ret,st.qry(in[top[a]],in[a]));
			a=fa[top[a]];
		}
		else{
			ret=max(ret,st.qry(in[top[b]],in[b]));
			b=fa[top[b]];
		}
	}
	if(in[a]>in[b]) swap(a,b);
	return max(ret,st.qry(in[a],in[b]));
}
signed main(){
	quick
	int n,q;
	cin>>n>>q;
	rep(i,1,n) cin>>V[i];
	rep(i,1,n-1){
		int a,b;
		cin>>a>>b;
		v[a].eb(b);
		v[b].eb(a);
	}
	dfs_sz(1);
	dfs(1);
 
	vector<int> res(n);
	rep(i,1,n){
		res[in[i]]=V[i];
	}
	st.init(n);
	st.build(res);
	while(q--){
		int ti,a,b;
		cin>>ti>>a>>b;
		if(ti==1){
			st.update(in[a],b);
		}
		else{
			cout<<Query(a,b)<<" ";
		}
	}cout<<"\n";
 
	return 0;
}

值在邊上

  • 則可以把樹邊值對應給子節點
  • 詢問和操作路徑時忽略 \(LCA(a,b)\) 即可
  • 樹剖是很強大的毒瘤工具
  • 單次時間複雜度會是 \(O(\log^2{n})\) 
  • 可以做路徑修改、路徑詢問
  • 簡單來講就是原先區間可做的,重剖後一樣可做
  • 甚至可以把原先樹壓平題目耍毒
  • 要求每個點 \(i\) 有幾個子孫節點的值\(a_c\)小於他 \(a_i>a_c\)
  • \(1≤N≤10^5\)
  • \(1≤ai≤10^9\)
  • 當你按照點權由小到大排序,且相同點權時深度較小先
  • 那當掃到 \(i\) 時就直接詢問目前子樹總和、並在 \(i\) 值 +1
  • 那其實也也可以當掃到 \(i\) 時暴力在其祖先都 +1
  • 就可以利用樹剖在路徑 \((root,i)\) +1

總路徑長度

  • 給一棵 \(n\) 點的樹,每條邊有其值和 \(q\) 次詢問
  • 每次給點集 \(S_i\) , 求連接\(S_i\) 所有點的最少邊權總和
  • \(1\leq n,q,\sum{S_i} \leq 2\cdot 10^5\)

總路徑長度

  • 首先按照dfs序排好,之後把相鄰點的路徑總和加總(要包含(尾-投))
  • 因為此時會是一個樹上尤拉路徑,因此所得總合會是原先兩倍,再除以二即可

總路徑長度/點權和

  • 當題目沒有總和除以二性質(可能求點且有些點沒有值)
  • 嘗試用樹剖解
  • 再按照 \(in\) 排序後等價相鄰路徑的總和(經過多次只能算一次)
  • 利用樹剖詢問路徑和,並把路徑的價值都歸零即可
  • 圓方樹配樹剖練習題

進階資料結構

By yuhung94

進階資料結構

  • 251