线段树——最好用的数据结构的学习笔记

· · 算法·理论

upd on 2025/11/27: 增加基础线段树、线段树合并

ps: 线段树分裂我还没学,学完加上!

线段树

实现原理

给你一个数组,让你区间加(修改),区间求和,要求 \le\mathcal O((n+q)\log n) 实现,可以怎么做?

树状数组线段树。

考虑每个区间分成 \log n 个小区间之和。

可以考虑将最大的大区间一分为二,小区间再一分为二,再分,直到一个节点为止。如字符画。

| 1 | 2 | 3 | 4 | 5 |
|-------------------|       -->一层 segment tree 总体管辖整个区间
|-----------|-------|       -->分成小线段管辖
|-------|---|---|---|       -->再分,线段内存储的是这些数的<询问类型>
|---|---|                   -->分到一个就不往下分了

先写出建树:

#define ls (p<<1) // 写成 1<<p 痛失 3 罚时
#define rs (p<<1|1)
const int maxn=2e5+5;
long long segtree[maxn<<2]/*注意是 4 倍空间*/,a[maxn],lazy[maxn],n,q;
void pushup(long long l,long long r,long long p){
    segtree[p]=segtree[ls]+segtree[rs];
}
void build(long long l,long long r,long long p){
    if(l==r){
        segtree[p]=a[l];
        return;
    }
    long long mid=l+r>>1;
    build(l,mid,ls);
    build(mid+1,r,rs);
    pushup(l,r,p);
}
接下来是查询: 其实查询的过程就是将小区间合并为大区间。 还是这个图: ``` | 1 | 2 | 3 | 4 | 5 | |-------------------| |-----------|-------| |-------|---|---|---| |---|---| |-----------| 要查询的区间为2~4 |-------------------| 有交集,下传 |-----------| 左右都有交集,下传 |-----------|-------| |-------|---| |-------|---|---|---| 最后一段没有交集,第二段与第三段是全部包含,第一段有交集,下传 |---| 是第二段的全部 |---|---| 于是就有小区间拼成大区间: |-------------------| |-----------|-------| |-------|===|===|---| |---|===| 如果是1~4: |-------------------| |===========|-------| |-------|---|===|---| |---|---| etc. ``` 然后就有代码: ```cpp long long query(int l,int r,int p,int s,int t){ if(t<l||s>r)return 0;// 避开无交集的区间 if(s<=l&&r<=t) return segtree[p]; int mid=l+r>>1; return query(l,mid,ls,s,t)+query(mid+1,r,rs,s,t); } ``` $O(\log n)$。 接下来是 `add` 函数。 --- 单点修: 直接递归到那个点修改: ```cpp void add(int l,int r,int p,int s,int t){ if(l==r){segtree[p]+=t;return;} int mid=l+r>>1; if(s<=mid)add(l,mid,p<<1,s,t); else add(mid+1,r,p<<1|1,s,t); pushup(l,r,p); } ``` $O(\log n)$。 没了。 :::success[[P3374 【模板】树状数组 1 - 洛谷](https://www.luogu.com.cn/problem/P3374) 实现:] ```cpp #include<bits/stdc++.h> using namespace std; #define ls (p<<1) #define rs (p<<1|1) const int maxn=5e5+5; long long segtree[maxn<<2],a[maxn],lazy[maxn],n,q; void pushup(long long l,long long r,long long p){ segtree[p]=segtree[ls]+segtree[rs]; } void build(long long l,long long r,long long p){ if(l==r){ segtree[p]=a[l]; return; } long long mid=l+r>>1; build(l,mid,ls); build(mid+1,r,rs); pushup(l,r,p); } long long query(int l,int r,int p,int s,int t){ if(t<l||s>r)return 0; if(s<=l&&r<=t) return segtree[p]; int mid=l+r>>1; return query(l,mid,ls,s,t)+query(mid+1,r,rs,s,t); } void add(int l,int r,int p,int s,int t){ if(l==r){segtree[p]+=t;return;} int mid=l+r>>1; if(s<=mid)add(l,mid,p<<1,s,t); else add(mid+1,r,p<<1|1,s,t); segtree[p]=segtree[p<<1]+segtree[p<<1|1]; } int main(){ cin>>n>>q; for(int i=1;i<=n;i++)cin>>a[i]; build(1,n,1); while(q--){ int op,x,y; cin>>op>>x>>y; if(op==1)add(1,n,1,x,y); else cout<<query(1,n,1,x,y)<<"\n"; } return 0; } ``` ::: --- 区间修改: ### lazy tag lazy tag,即懒标记,指在修改时找到一个包含的线段即可将其修改、打上懒标记,表示当前修改了 $lazy_{p}$ 个偏移量未下传,即分下去的线段还没有偏移,不用再下传造成 $O(len\log n)$ 的时间,而到了查询或修改时区间不完全被包含再下传懒标记,就是左右儿子的 `segtree` 数组。 然后没了,记得在 `query` 时也要 `pushdown`! :::success[[P3374 【模板】线段树 1 - 洛谷](https://www.luogu.com.cn/problem/P3372) 实现:] ```cpp #include<bits/stdc++.h> using namespace std; #define ls (p<<1) #define rs (p<<1|1) const long long maxn=2e5+5; long long segtree[maxn<<2],a[maxn],lazy[maxn],n,q; void pushup(long long l,long long r,long long p){ segtree[p]=segtree[ls]+segtree[rs]; } void pushdown(long long l,long long r,long long p){ lazy[ls]+=lazy[p]; lazy[rs]+=lazy[p]; segtree[ls]+=((l+r>>1)-l+1)*lazy[p]; segtree[rs]+=(r-(l+r>>1))*lazy[p]; lazy[p]=0; } void build(long long l,long long r,long long p){ if(l==r){ segtree[p]=a[l]; return; } long long mid=l+r>>1; build(l,mid,ls); build(mid+1,r,rs); pushup(l,r,p); } void add(long long l,long long r,long long p,long long s,long long t,long long v){ if(t<l||s>r)return; if(s<=l&&r<=t){ lazy[p]+=v; segtree[p]+=(r-l+1)*v; return; } pushdown(l,r,p); long long mid=l+r>>1; add(l,mid,ls,s,t,v); add(mid+1,r,rs,s,t,v); pushup(l,r,p); } long long query(long long l,long long r,long long p,long long s,long long t){ if(t<l||s>r)return 0; if(s<=l&&r<=t) return segtree[p]; pushdown(l,r,p); long long mid=l+r>>1; return query(l,mid,ls,s,t)+query(mid+1,r,rs,s,t); } int main(){ cin>>n>>q; for(long long i=1;i<=n;i++)cin>>a[i]; build(1,n,1); while(q--){ long long op,l,r; cin>>op>>l>>r; if(op==1){ long long x; cin>>x; add(1,n,1,l,r,x); }else{ cout<<query(1,n,1,l,r)<<"\n"; } } return 0; } ``` ::: ## 看例题 ### [P3373 【模板】线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3373) 毒瘤,但只是 `pushdown` 多亿点点而已。 考虑乘法懒标记下传。 左右儿子的乘法懒标记、加法懒标记、和都乘上当前乘法懒标记。 加法同上。 :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; #define ls (p<<1) #define rs (p<<1|1) const long long maxn=2e5+5; long long segtree[maxn<<2],a[maxn],lazy[maxn<<2],mul[maxn<<2],n,q,m,pos[maxn]; void pushup(long long l,long long r,long long p){ segtree[p]=segtree[ls]+segtree[rs]; segtree[p]%=m; } void pushdown(long long l,long long r,long long p){ if(mul[p]!=1){ lazy[ls]*=mul[p];lazy[ls]%=m; lazy[rs]*=mul[p];lazy[rs]%=m; mul[ls]*=mul[p];mul[ls]%=m; mul[rs]*=mul[p];mul[rs]%=m; segtree[ls]*=mul[p];segtree[ls]%=m; segtree[rs]*=mul[p];segtree[rs]%=m; mul[p]=1; } if(lazy[p]!=0){ lazy[ls]+=lazy[p];lazy[ls]%=m; lazy[rs]+=lazy[p];lazy[rs]%=m; segtree[ls]+=lazy[p]*((l+r>>1)-l+1);segtree[ls]%=m; segtree[rs]+=lazy[p]*(r-(l+r>>1));segtree[rs]%=m; lazy[p]=0; } } void build(long long l,long long r,long long p){ if(l==r){ segtree[p]=a[l]; return; } long long mid=l+r>>1; mul[p]=1; build(l,mid,ls); build(mid+1,r,rs); pushup(l,r,p); } void add(long long l,long long r,long long p,long long s,long long t,long long v){ if(t<l||s>r)return; if(s<=l&&r<=t){ lazy[p]+=v;lazy[p]%=m; segtree[p]+=(r-l+1)*v;segtree[p]%=m; return; } pushdown(l,r,p); long long mid=l+r>>1; add(l,mid,ls,s,t,v); add(mid+1,r,rs,s,t,v); pushup(l,r,p); } void multi(long long l,long long r,long long p,long long s,long long t,long long v){ if(t<l||s>r)return; if(s<=l&&r<=t){ mul[p]*=v;mul[p]%=m; lazy[p]*=v;lazy[p]%=m; segtree[p]*=v;segtree[p]%=m; return; } pushdown(l,r,p); long long mid=l+r>>1; multi(l,mid,ls,s,t,v); multi(mid+1,r,rs,s,t,v); pushup(l,r,p); } long long query(long long l,long long r,long long p,long long s,long long t){ if(t<l||s>r)return 0; if(s<=l&&r<=t) return segtree[p]; pushdown(l,r,p); long long mid=l+r>>1; return(query(l,mid,ls,s,t)+query(mid+1,r,rs,s,t))%m; } int main(){ cin>>n>>q>>m; for(long long i=1;i<=n;i++)cin>>a[i]; build(1,n,1); while(q--){ long long op,l,r; cin>>op>>l>>r; if(op==2){ long long x; cin>>x; add(1,n,1,l,r,x); }else if(op==1){ long long x; cin>>x; multi(1,n,1,l,r,x); }else{ cout<<query(1,n,1,l,r)<<"\n"; } } return 0; } ``` ::: ### [P1531 I Hate It - 洛谷](https://www.luogu.com.cn/problem/P1531) 维护最大值区间。这题不用 lazy tag,但是我还是打了。 :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; #define ls (p<<1) #define rs (p<<1|1) const long long maxn=2e5+5; long long segtree[maxn<<2],a[maxn],lazy[maxn<<1],n,q; void pushup(int l,int r,int p){ segtree[p]=max(segtree[ls],segtree[rs]); } void pushdown(int l,int r,int p){ lazy[ls]=max(lazy[ls],lazy[p]); lazy[rs]=max(lazy[rs],lazy[p]); segtree[ls]=max(segtree[ls],lazy[p]); segtree[rs]=max(segtree[rs],lazy[p]); lazy[p]=0; } void build(int l,int r,int p){ if(l==r){ segtree[p]=a[l]; return; } int mid=l+r>>1; build(l,mid,ls); build(mid+1,r,rs); pushup(l,r,p); } void add(int l,int r,int p,long long s,long long t,long long v){ if(t<l||s>r)return; if(s<=l&&r<=t){ lazy[p]=max(lazy[p],v); segtree[p]=max(segtree[p],v); return; } pushdown(l,r,p); int mid=l+r>>1; add(l,mid,ls,s,t,v); add(mid+1,r,rs,s,t,v); pushup(l,r,p); } long long query(int l,int r,int p,long long s,long long t){ if(t<l||s>r)return 0; if(s<=l&&r<=t) return segtree[p]; pushdown(l,r,p); int mid=l+r>>1; return max(query(l,mid,ls,s,t),query(mid+1,r,rs,s,t)); } int main(){ cin>>n>>q; for(int i=1;i<=n;i++)cin>>a[i]; build(1,n,1); while(q--){ char op; int l,r; cin>>op>>l>>r; if(op=='U'){ add(1,n,1,l,l,r); }else{ cout<<query(1,n,1,l,r)<<"\n"; } } return 0; } ``` ::: # 动态开点线段树 有些区间无需查询,那么就动态开点,需要查一个就开一个。 每次加点都是 $O(\log n)$ 级别的,总共 $O(q\log n)$,没必要开 $O(n\log n)$ 个点,只需要在 `add()` 时传递下去即可。`query()` 根本不用管没开的节点。空的就返回 $0$。 ## [P13825 【模板】线段树 1.5 - 洛谷](https://www.luogu.com.cn/problem/P13825) 这题……$n\le 10^9$,只能上动态开点。 `add()` 如果要懒标记下传,那么新开左右儿子存下懒标记; :::success[Template] ```cpp template<typename T> class segtree{ T L,R; struct node{ T sum{},ls{},rs{},lazy{}; }; vector<node>tree; void pushdown(T l,T r,int p,bool b=true){ if(b!=0){ if(!tree[p].ls)tree[p].ls=tree.size(),tree.emplace_back(); if(!tree[p].rs)tree[p].rs=tree.size(),tree.emplace_back(); if(!tree[p].lazy)return; T mid=l+r>>1; tree[tree[p].ls].sum+=(mid-l+1)*tree[p].lazy; tree[tree[p].rs].sum+=(r-mid)*tree[p].lazy; tree[tree[p].ls].lazy+=tree[p].lazy; tree[tree[p].rs].lazy+=tree[p].lazy; tree[p].lazy=0; }else{ T mid=l+r>>1; if(tree[p].ls){ tree[tree[p].ls].sum+=(mid-l+1)*tree[p].lazy; tree[tree[p].ls].lazy+=tree[p].lazy; } if(tree[p].rs){ tree[tree[p].rs].sum+=(mid-l+1)*tree[p].lazy; tree[tree[p].rs].lazy+=tree[p].lazy; } } } void pushup(int p){ tree[p].sum=0; if(tree[p].ls)tree[p].sum+=tree[tree[p].ls].sum; if(tree[p].rs)tree[p].sum+=tree[tree[p].rs].sum; } void add(T l,T r,int p,T s,T t,T c){ if(!p||l>t||r<s)return; if(s<=l&&r<=t){ tree[p].sum+=(r-l+1)*c,tree[p].lazy+=c; return; } pushdown(l,r,p); T mid=l+r>>1; add(l,mid,tree[p].ls,s,t,c); add(mid+1,r,tree[p].rs,s,t,c); pushup(p); } T query(T l,T r,int p,T s,T t){ if(!p||l>t||r<s)return 0; if(s<=l&&r<=t)return tree[p].sum; T mid=l+r>>1; T ans=0; pushdown(l,r,p,0); ans+=query(l,mid,tree[p].ls,s,t); ans+=query(mid+1,r,tree[p].rs,s,t); return ans; } public: segtree():L(numeric_limits<T>::min()),R(numeric_limits<T>::max()){tree=vector<node>(2);} segtree(T l,T r):L(l),R(r){tree=vector<node>(2);} void add(T l,T r,T c){ add(L,R,1,l,r,c); } T query(T l,T r){ return query(L,R,1,l,r); } }; segtree<int>sgtree; ``` ::: ## [P3184 [USACO16DEC] Counting Haybales S - 洛谷](https://www.luogu.com.cn/problem/P3184) 杀鸡用宰牛刀。 (甚至不需要 `add()`) ```cpp int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin>>n>>q; for(int i=1;i<=n;i++){ int x; cin>>x; sgtree.add(x,x,1); } for(int i=1;i<=q;i++){ int l,r; cin>>l>>r; cout<<sgtree.query(l,r)<<"\n"; } return 0; } ``` ## [P13825 【模板】线段树 1.5 - 洛谷](https://www.luogu.com.cn/problem/P13825) 板题。 :::warning[Trick]{open} 要开 `ull`。 ::: ```cpp int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cin>>n>>q; for(int i=1;i<=q;i++){ int op; cin>>op; ull l,r,k; if(op==1){ cin>>l>>r>>k; sgtree.add(l,r,k); }else{ cin>>l>>r; cout<<sgtree.query(l,r)+(l+r)*(r-l+1)/2<<"\n"; } } return 0; } ``` --- 重点在主席树。 # 主席树 接下来是主席树(其实是可持久化线段树)。 ## 先上板题[P3919 【模板】可持久化线段树 1(可持久化数组) - 洛谷](https://www.luogu.com.cn/problem/P3919)。 ### 思路 考虑如何更新。更新是一大重点。 每次更新一个节点。 一个节点。为什么? 增加严格 $O(\log n)$ 个节点吗? 不用 $lazy$ 数组吗? 考虑增加严格 $O(\log n)$ 个节点。 > 重建一棵线段树? > > $O(\log n)$ 个节点被改变,未免太过浪费。 > > 那么只更新 $O(\log n)$ 个节点,剩下的线段树不动? > > 嗯对!就这么干! 新建一个根节点,连上不动的旧节点为左或右儿子(原来是左儿子就是左儿子,原来是右儿子就是右儿子),剩下那个儿子新建一个节点出来就好了。 如 OI-Wiki 图: ![persistent-seg.png (2245×1075)](https://static.cdn.menci.xyz/oi-wiki/ds/images/persistent-seg.png) 就是新开一条链。 在上面的代码上改改就好了。 没写模板,见谅。 :::success[Template] ```cpp class psegtree{ struct pnode{ ll sum{},ls{},rs{}; }; vector<pnode>tree; vector<int>rt; int newrt(int t){ rt.push_back({t}); return rt.size()-1; } int newnode(){ tree.push_back({}); return tree.size()-1; } void pushup(int p){ tree[p].sum=0; if(tree[p].ls)tree[p].sum+=tree[tree[p].ls].sum; if(tree[p].rs)tree[p].sum+=tree[tree[p].rs].sum; } void add(int lst,int l,int r,int p,int x,int c){ if(l==r){ tree[p].sum/*+*/=c;// 若是修改就要两个 qwq 行 return; } int mid=l+r>>1; if(x<=mid){ tree[p].ls=newnode(); // tree[tree[p].ls].sum=tree[tree[lst].ls].sum; // qwq tree[p].rs=tree[lst].rs; add(tree[lst].ls,l,mid,tree[p].ls,x,c); }else{ tree[p].rs=newnode(); // tree[tree[p].rs].sum=tree[tree[lst].rs].sum; // qwq tree[p].ls=tree[lst].ls; add(tree[lst].rs,mid+1,r,tree[p].rs,x,c); } pushup(p); } ll query(int l,int r,int p,int s,int t){ if(!p||l>t||r<s)return 0; if(s<=l&&r<=t)return tree[p].sum; int mid=l+r>>1; ll ans=0; ans+=query(l,mid,tree[p].ls,s,t); ans+=query(mid+1,r,tree[p].rs,s,t); return ans; } public: psegtree(){ newnode(); newrt(newnode()); } void add(int k,int x,int c){ int p=newrt(newnode()); add(rt[k],0,1e9,rt[p],x,c); } ll query(int k,int l,int r){ int p=newrt(rt[k]);// qaq 这个标记后面会用到 return query(0,1e9,rt[p],l,r); } }; ``` 这题还要 `build()`。 ```cpp void build(int l,int r,int p){ if(l==r){ tree[p].sum=a[l]; return; } int mid=l+r>>1; build(l,mid,tree[p].ls=newnode()); build(mid+1,r,tree[p].rs=newnode()); pushup(p); } ``` ::: ## [P3834 【模板】可持久化线段树 2 - 洛谷](https://www.luogu.com.cn/problem/P3834) 主席树,全称可持久化权值线段树。 权值,指某个值出现的次数。主席树维护区间内每个数出现的次数。 一个一个加,就成了前缀和。 至于找第 $k$ 大,线段树上二分! :::info[如何线段树上二分?] 嗯对,线段树内部维护了区间和,如上做差分判断是否大于 $k$ 为 `check`。 ::: :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=1e6+5; int n,q,a[maxn]; typedef long long ll; template<typename T> class psegtree{ struct pnode{ ll sum{},ls{},rs{}; }; vector<pnode>tree; vector<int>rt; int newrt(int t){ rt.push_back({t}); return rt.size()-1; } int newnode(){ tree.push_back({}); return tree.size()-1; } void pushup(int p){ tree[p].sum=0; if(tree[p].ls)tree[p].sum+=tree[tree[p].ls].sum; if(tree[p].rs)tree[p].sum+=tree[tree[p].rs].sum; } void add(int lst,int l,int r,int p,int x,int c){ if(l==r){ tree[p].sum+=c; return; } int mid=l+r>>1; if(x<=mid){ tree[p].ls=newnode(); tree[tree[p].ls].sum=tree[tree[lst].ls].sum; tree[p].rs=tree[lst].rs; add(tree[lst].ls,l,mid,tree[p].ls,x,c); }else{ tree[p].rs=newnode(); tree[tree[p].rs].sum=tree[tree[lst].rs].sum; tree[p].ls=tree[lst].ls; add(tree[lst].rs,mid+1,r,tree[p].rs,x,c); } pushup(p); } ll query(int l,int r,int p,int s,int t){ if(!p||l>t||r<s)return 0; if(s<=l&&r<=t)return tree[p].sum; int mid=l+r>>1; ll ans=0; ans+=query(l,mid,tree[p].ls,s,t); ans+=query(mid+1,r,tree[p].rs,s,t); return ans; } ll kth(int l,int r,int L,int R,int pl,int pr,int k){// 线段树上二分 if(L==R)return L;// 边界 int M=L+R>>1; if(tree[tree[pr].ls].sum-tree[tree[pl].ls].sum>=k)return kth(l,r,L,M,tree[pl].ls,tree[pr].ls,k);// 差分 return kth(l,r,M+1,R,tree[pl].rs,tree[pr].rs,k-(tree[tree[pr].ls].sum-tree[tree[pl].ls].sum)); } public: psegtree(){ newnode(); newrt(newnode()); } void add(int k,int x,int c){ int p=newrt(newnode()); add(rt[k],0,1e9,rt[p],x,c); } ll query(int k,int l,int r){ int p=newrt(rt[k]); return query(0,1e9,rt[p],l,r); } ll kth(int l,int r,int k){ return kth(l-1,r,0,1e9,rt[l-1],rt[r],k); } }; int main(){ ios::sync_with_stdio(false); cin.tie(nullptr),cout.tie(nullptr); cin>>n>>q; psegtree<ll>sgt; for(int i=1;i<=n;i++) cin>>a[i],sgt.add(i-1,a[i],1); while(q--){ int l,r,k; cin>>l>>r>>k; cout<<sgt.kth(l,r,k)<<"\n"; } return 0; } ``` ::: ### [P3755 [CQOI2017] 老C的任务 - 洛谷](https://www.luogu.com.cn/problem/P3755) 先离散化。 主席树一个一个加,就当 $x_i$ 是 $i$,`sgtree.add(i-1,y,p)`。 然后就没有然后了。 查询时二分边界,而后差分查找。 :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=1e6+5; int n,q; typedef long long ll; struct pnode{ ll sum{},ls{},rs{}; }; class psegtree{ vector<pnode>tree; vector<int>rt; int newrt(int t){ rt.push_back({t}); return rt.size()-1; } int newnode(){ tree.push_back({}); return tree.size()-1; } void pushup(int p){ tree[p].sum=0; if(tree[p].ls)tree[p].sum+=tree[tree[p].ls].sum; if(tree[p].rs)tree[p].sum+=tree[tree[p].rs].sum; } void add(int lst,int l,int r,int p,int x,int c){ if(l==r){ tree[p].sum+=c; return; } int mid=l+r>>1; if(x<=mid){ tree[p].ls=newnode(); tree[tree[p].ls].sum=tree[tree[lst].ls].sum; tree[p].rs=tree[lst].rs; add(tree[lst].ls,l,mid,tree[p].ls,x,c); }else{ tree[p].rs=newnode(); tree[tree[p].rs].sum=tree[tree[lst].rs].sum; tree[p].ls=tree[lst].ls; add(tree[lst].rs,mid+1,r,tree[p].rs,x,c); } pushup(p); } ll query(int l,int r,int p,int s,int t){ if(!p||l>t||r<s)return 0; if(s<=l&&r<=t)return tree[p].sum; int mid=l+r>>1; ll ans=0; ans+=query(l,mid,tree[p].ls,s,t); ans+=query(mid+1,r,tree[p].rs,s,t); return ans; } ll kth(int l,int r,int L,int R,int pl,int pr,int k){ if(L==R)return L; int M=L+R>>1; if(tree[tree[pr].ls].sum-tree[tree[pl].ls].sum>=k)return kth(l,r,L,M,tree[pl].ls,tree[pr].ls,k); return kth(l,r,M+1,R,tree[pl].rs,tree[pr].rs,k-(tree[tree[pr].ls].sum-tree[tree[pl].ls].sum)); } public: psegtree(){ newnode(); newrt(newnode()); } void add(int k,int x,int c){ int p=newrt(newnode()); add(rt[k],INT_MIN,INT_MAX,rt[p],x,c); } ll query(int k,int x,int c){ int p=newrt(rt[k]); return query(INT_MIN,INT_MAX,rt[p],x,c); } ll kth(int l,int r,int k){ return kth(l-1,r,INT_MIN,INT_MAX,rt[l-1],rt[r],k); } }; tuple<int,int,int>a[maxn]; int main(){ ios::sync_with_stdio(false); cin.tie(nullptr),cout.tie(nullptr); cin>>n>>q; psegtree sgt; for(int i=1;i<=n;i++) cin>>get<0>(a[i])>>get<1>(a[i])>>get<2>(a[i]); sort(a+1,a+1+n); for(int i=1;i<=n;i++){ auto[x,y,z]=a[i]; sgt.add(i-1,y,z); } while(q--){ int l,r,u,d; cin>>u>>l>>d>>r; int Left=lower_bound(a+1,a+1+n,make_tuple(u,0,0))-a-1;// 左端点减 1 是差分的 int Right=upper_bound(a+1,a+1+n,make_tuple(d,INT_MAX,INT_MAX))-a-1;// 右端点减 1 是 upper_bound 的 cout<<sgt.query(Right,l,r)-sgt.query(Left,l,r)<<"\n"; } return 0; } ``` ::: ### [P1383 高级打字机 - 洛谷](https://www.luogu.com.cn/problem/P1383) 简单维护一下每个文本的长度就好。 因为 `Q` 不算修改,所以将板子的 `qaq` 行删除。 :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; const int maxn=1e6+5; int q; vector<int>length; typedef long long ll; class psegtree{ struct pnode{ ll sum{},ls{},rs{}; }; vector<pnode>tree; vector<int>rt; int newrt(int t){ rt.push_back({t}); return rt.size()-1; } int newnode(){ tree.push_back({}); return tree.size()-1; } void pushup(int p){ tree[p].sum=0; if(tree[p].ls)tree[p].sum+=tree[tree[p].ls].sum; if(tree[p].rs)tree[p].sum+=tree[tree[p].rs].sum; } void add(int lst,int l,int r,int p,int x,int c){ if(l==r){ tree[p].sum=c; return; } int mid=l+r>>1; if(x<=mid){ tree[p].ls=newnode(); tree[p].rs=tree[lst].rs; add(tree[lst].ls,l,mid,tree[p].ls,x,c); }else{ tree[p].rs=newnode(); tree[p].ls=tree[lst].ls; add(tree[lst].rs,mid+1,r,tree[p].rs,x,c); } pushup(p); } ll query(int l,int r,int p,int s,int t){ if(!p||l>t||r<s)return 0; if(s<=l&&r<=t)return tree[p].sum; int mid=l+r>>1; ll ans=0; ans+=query(l,mid,tree[p].ls,s,t); ans+=query(mid+1,r,tree[p].rs,s,t); return ans; } public: psegtree(){ newnode(); newrt(newnode()); } void add(int k,int x,int c){ int p=newrt(newnode()); add(rt[k],1,114514,rt[p],x,c); } ll query(int k,int x,int c){ return query(1,114514,rt[k],x,c); } }; int main(){ ios::sync_with_stdio(false); cin.tie(nullptr),cout.tie(nullptr); cin>>q; length.push_back(0); psegtree sgt; while(q--){ char op; cin>>op; if(op=='U'){ int k; cin>>k; int lst=length.size()-k-1; sgt.add(lst,length[lst]+1,0); length.push_back(length[lst]); }else if(op=='T'){ char k; cin>>k; int lst=length.size()-1; sgt.add(lst,length[lst]+1,k); length.push_back(length[lst]+1); }else{ int k; cin>>k; cout<<char(sgt.query(length.size()-1,k,k))<<"\n"; } } return 0; } ``` ::: # 线段树合并 一个很有意思的算法。 ## 先看一道经典例题[P3201 [HNOI2009] 梦幻布丁 - 洛谷](https://www.luogu.com.cn/problem/P3201) 考虑暴力地线段树。 开 $10^6$ 棵线段树,存下每个颜色所在的所有位置(这你要不动态开点就会有大大的 $\Large\colorbox{#052242}{\color{white}MLE}$),然后修改直接暴力一个一个删、一个一个加过去。 > 定义有点抽象,举个例子。 > > 比如 $a=\{1,1,4,5,1,4\}$,那么就 `add(1,6,rt[1],1,1),add(1,6,rt[1],2,1),add(1,6,rt[4],3,1),add(1,6,rt[5],4,1),add(1,6,rt[1],5,1),add(1,6,rt[4],6,1);`($rt_i$ 表示第 $i$ 棵线段树的根)。 如何求答案呢?设 $cl$ 为区间左端点,$cr$ 为区间右端点颜色,$ans$ 为区间颜色段数,那么 $ans_i=ans_{lson_i}+ans_{rson_i}-[cr_{lson_i}=1\text{ 且 }cl_{lson_i}=1]$。最终答案就是 $\sum ans_{rt_i}$。 求答案就是 `pushup`,但是修改就成了 $O(\log^2n)$ 的了。 如何变成 $O(\log n)$ 的呢? 想到每个位置一一可以对应,那么可以一一对应地 `dfs`。 形式化地说,就是存两种颜色的下标,合并时直接挪过来就好,不用再 `dfs`。 然后就有了 `merge`。 ```cpp int merge(int p0,int p1,int l,int r){ if(!p0||!p1)return p0|p1; if(l==r){ cl[p0]|=cl[p1];// 有一个点有就行 cr[p0]|=cr[p1]; ans[p0]=cl[p0];// 实际上就是如果有,ans[p0]=1,否则 ans[p0]=0; // do something... return p0; } int mid=l+r>>1; ls[p0]=merge(ls[p0],ls[p1],l,mid); rs[p0]=merge(rs[p0],rs[p1],mid+1,r); pushup(p0); return p0; } ``` :::warning[Trick]{open} 会有 $x=y$ 的情况! ::: :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=4e6+5; int ans[maxn],cl[maxn],cr[maxn],ls[maxn],rs[maxn],n,cnt,rt[maxn],m,mxcolor,sum; void pushup(int p){ cl[p]=cl[ls[p]]; cr[p]=cr[rs[p]]; ans[p]=ans[ls[p]]+ans[rs[p]]-(cr[ls[p]]&&cl[rs[p]]); } void add(int l,int r,int p,int x){ if(l==r){ cl[p]=cr[p]=ans[p]=1; return; } int mid=l+r>>1; if(x<=mid){ if(!ls[p])ls[p]=++cnt; add(l,mid,ls[p],x); }else{ if(!rs[p])rs[p]=++cnt; add(mid+1,r,rs[p],x); } pushup(p); } int query(){ return sum; } int merge(int p0,int p1,int l,int r){ if(!p0||!p1)return p0|p1; if(l==r){ cl[p0]|=cl[p1];// 有一个点有就行 cr[p0]|=cr[p1]; ans[p0]=cl[p0];// 实际上就是如果有,ans[p0]=1,否则 ans[p0]=0。 return p0; } int mid=l+r>>1; ls[p0]=merge(ls[p0],ls[p1],l,mid); rs[p0]=merge(rs[p0],rs[p1],mid+1,r); pushup(p0); return p0; } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>n>>m; for(int i=1,x;i<=n;i++)cin>>x,add(1,n,rt[x]=(rt[x]==0?++cnt:rt[x]),i); for(int i=1;i<=1e6+1;i++)sum+=ans[rt[i]]; while(m--){ int op; cin>>op; if(op==1){ int x,y; cin>>x>>y; if(x==y)continue; sum-=ans[rt[y]],sum-=ans[rt[x]]; rt[y]=merge(rt[x],rt[y],1,n); sum+=ans[rt[y]]; rt[x]=0; }else{ cout<<query()<<"\n"; } } return 0; } ``` ::: ## [P4556 【模板】线段树合并 / [Vani有约会] 雨天的尾巴 - 洛谷](https://www.luogu.com.cn/problem/P4556) 前置知识:[P3128 [USACO15DEC] Max Flow P - 洛谷](https://www.luogu.com.cn/problem/P3128)。 这个差分好像啊。 设 $sum$ 表示区间(粮食编号区间)粮食最大值,$mx$ 表示区间粮食最大值对应最小编号。 然后就是差分了。 合并的 `// do something...` 部分就是 `sum[p0]+=sum[p1];`,因为当前是一个点,这个点当然是求和呀。 然后没了。 蓝板子加绿板子等于紫板子。 :::success[AC code] ```cpp #include<bits/stdc++.h> using namespace std; typedef long long ll; const int maxn=5e6+5; int sum[maxn],mx[maxn],ls[maxn],rs[maxn],n,cnt,rt[maxn],m; int anc[maxn][20]; int dep[maxn]; vector<int>G[maxn]; void pushup(int p){ if(sum[ls[p]]>=sum[rs[p]]){ sum[p]=sum[ls[p]]; mx[p]=mx[ls[p]]; }else{ sum[p]=sum[rs[p]]; mx[p]=mx[rs[p]]; } } void add(int l,int r,int p,int x,int c){ if(l==r){ sum[p]+=c; mx[p]=x; return; } int mid=l+r>>1; if(x<=mid){ if(!ls[p])ls[p]=++cnt; add(l,mid,ls[p],x,c); }else{ if(!rs[p])rs[p]=++cnt; add(mid+1,r,rs[p],x,c); } pushup(p); } int merge(int p0,int p1,int l,int r){ if(!p0||!p1)return p0|p1; if(l==r){ sum[p0]+=sum[p1]; return p0; } int mid=l+r>>1; ls[p0]=merge(ls[p0],ls[p1],l,mid); rs[p0]=merge(rs[p0],rs[p1],mid+1,r); pushup(p0); return p0; } void dfs(int c,int fa){ dep[c]=dep[fa]+1; anc[c][0]=fa; for(int i=1;i<20;i++){ anc[c][i]=anc[anc[c][i-1]][i-1]; } for(auto i:G[c]){ if(i!=fa){ dfs(i,c); } } } int lca(int u,int v){ if(dep[u]>dep[v])swap(u,v); int y=dep[v]-dep[u],cnt=0; while(y){ if(y&1)v=anc[v][cnt]; ++cnt; y>>=1; } if(u==v)return u; for(int i=19;i>=0;i--){ if(anc[u][i]!=anc[v][i]){ u=anc[u][i]; v=anc[v][i]; } } return anc[u][0]; } void getans(int u,int fa){ for(auto v:G[u]){ if(v==fa)continue; getans(v,u); rt[u]=merge(rt[u],rt[v],1,114514); } if(!sum[rt[u]])mx[rt[u]]=0; } int main(){ ios::sync_with_stdio(false); cin.tie(nullptr); cout.tie(nullptr); cin>>n>>m; iota(rt+1,rt+1+n,1); cnt=n; for(int i=1;i<n;i++){ int u,v; cin>>u>>v; G[u].push_back(v); G[v].push_back(u); } dfs(1,0); for(int i=1;i<=m;i++){ int u,v,w; cin>>u>>v>>w; add(1,114514,rt[u],w,1); add(1,114514,rt[v],w,1); add(1,114514,rt[lca(u,v)],w,-1); add(1,114514,rt[anc[lca(u,v)][0]],w,-1); } getans(1,0); for(int i=1;i<=n;i++)cout<<mx[rt[i]]<<"\n"; return 0; } ``` ::: $1272$ 行,$24934$ 字符,制作不易,如果本篇文章对你有帮助的话,帮忙点个小小的赞,谢谢!