线段树和树状数组总结
一大波线段树和树状数组预警。
树状数组基础部分
Binary Indexed Tree.BIT
线段树可以实现树状数组所有功能,但树状数组简单易写常数小。
树状数组利用了二进制数的特性,很精巧。
注意到一件事,若将当前点下标从低到高第一个
考虑任意正整数感性理解一下。
lowbit
利用了补码的特性,
加上
单次时间复杂度显然
树状数组只能维护前缀查询,不能维护没有可减性的信息,有局限性,但好写。
当然与值域线段树相对应有值域树状数组。
板子:
ll lowbit(int x){
return x&(-x);
}
ll getsum(int x){
int ans=0;
while(x){
ans+=c[x];
x-=lowbit(x);
}
return ans;
}
void update(int x,int v){
while(q<=n){
c[x]+=v;
x+=lowbit(x);
}
}
单点加,区间和
板子。
区间加,单点查
先差分,然后板子。
其他更多操作
考虑差分,考虑推式子,从式子中分离出几项可以用一般的树状数组维护的东西,再上树状数组即可。
二维树状数组
感觉很像BIT套BIT(或许就是)。
我们尝试用树状数组维护矩形的信息。
考虑tr[i][j]
记录以
于是其他操作就和一维的树状数组很像了,支持单点修改,前缀的一个矩形的查询。
单点修改,矩形查询
就是上面说的。
矩形修改,单点查询
考虑差分。
我们并不能直观地知道二维差分是一个什么东西,但是我们知道二维前缀和,以及前缀和与差分互逆。
已知:
我们把
稍作整理:
于是可以差分后将操作转化为单点修改,矩形查询,做完了。
矩形修改,矩形查询
我们需要推一下式子。
对于矩形修改,我们还是选择差分。
查询显然可以用前缀相减,于是考虑如何查前缀信息。
要求的显然是:
推一下:
于是维护四个树状数组
板子
#include<bits/stdc++.h>
using namespace std;
const int maxn=2070;
char op;
int n,m,tr1[maxn][maxn],tr2[maxn][maxn],tr3[maxn][maxn],tr4[maxn][maxn];
#define lowbit(x) (x)&(-x)
void add(int x,int y,int v){
for(int i=x;i<=n;i+=lowbit(i)){
for(int j=y;j<=m;j+=lowbit(j)){
tr1[i][j]+=v;
tr2[i][j]+=v*x;
tr3[i][j]+=v*y;
tr4[i][j]+=v*x*y;
}
}
}
int query(int x,int y){
int rs=0;
for(int i=x;i>0;i-=lowbit(i)){
for(int j=y;j>0;j-=lowbit(j)){
rs+=tr1[i][j]*(x+1)*(y+1)-tr2[i][j]*(y+1)-tr3[i][j]*(x+1)+tr4[i][j];
}
}
return rs;
}
int main(){
scanf("%c%d%d",&op,&n,&m);
while(cin>>op){
if(op=='L'){
int xa,xb,ya,yb,v;
scanf("%d%d%d%d%d",&xa,&ya,&xb,&yb,&v);
add(xa,ya,v);
add(xa,yb+1,-v);
add(xb+1,ya,-v);
add(xb+1,yb+1,v);
}
else if(op=='k'){
int xa,xb,ya,yb;
scanf("%d%d%d%d",&xa,&ya,&xb,&yb);
if(xa>xb) swap(xa,xb),swap(ya,yb);
printf("%d\n",query(xb,yb)-query(xb,ya-1)-query(xa-1,yb)+query(xa-1,ya-1));
}
}
return 0;
}
线段树基础部分
常用的维护区间信息的数据结构。
常见的区间和等东西以及一些神秘的信息都可以尝试用线段树。
通常用pushup
和pushdown
上传信息和下放标记。
通常用堆式储存,ls=k<<1,rs=k<<1|1
。
区间修改通常使用懒标记,查询或修改到后代时再下放标记。
也有值域线段树,可以支持部分平衡树的操作。
技巧:
-
动态开点:降低空间复杂度,据说可以只开
的空间。 -
标记永久化:若确定一个点上的标记不会爆范围,就可以永久化它,不下放,而是在经过的时候累计贡献。
抽象线段树
对线段树深刻理解一下。
线段树上维护的东西分成两类:信息和标记。这可以写两个结构体。
维护线段树时,首先是设计信息和标记,接下来要考虑的就是信息和标记之间的合并。
分为三类来讨论:标记与标记合并,信息和信息合并,信息和标记合并(标记作用在信息上)。
这样来考虑问题会更有条理一点。
我们按以下步骤来设计:
-
设计信息,同时得到了信息如何合并。
-
设计标记,同时得到标记如何作用在信息上。
-
实现标记之间的合并。这一步可以讨论两个具有先后关系的标记应当如何合并。(或许相当于合并两个操作序列)
维护区间加,区间乘
板子。
维护区间 ,区间赋值,区间加
赋值的优先级高于加法。
pushdown
中,先下放赋值标记,再下放加法标记。被赋值后,最大值要改成赋的值,加法标记要清空。下放加法标记时不修改赋值标记。
区间赋值函数中,赋值后修改最大值和赋值标记,清空加法标记。区间加函数中,下放赋值标记,再修改最大值和加法标记,不管赋值标记。
注意在区间加函数中,在叶子节点下放赋值标记可能越界,要么数组再开两倍,要么特判叶子节点。
板子
#include<bits/stdc++.h>
#define ls(k) k<<1
#define rs(k) k<<1|1
using namespace std;
typedef long long ll;
const int maxn=1e6+100;
const ll inf=1e18;
int n,q,a[maxn];
struct TREE{
ll val,tag1,tag2;
}t[maxn<<3];
inline void pushup(int k){
t[k].val=max(t[ls(k)].val,t[rs(k)].val);
}
inline void cover(int k){
if(t[k].tag2!=-inf){
t[ls(k)].val=t[k].tag2;
t[ls(k)].tag2=t[k].tag2;
t[rs(k)].val=t[k].tag2;
t[rs(k)].tag2=t[k].tag2;
t[ls(k)].tag1=t[rs(k)].tag1=0;
t[k].tag2=-inf;
}
}
inline void sum(int k){
if(t[k].tag1){
cover(k);
t[ls(k)].val+=t[k].tag1;
t[ls(k)].tag1+=t[k].tag1;
t[rs(k)].val+=t[k].tag1;
t[rs(k)].tag1+=t[k].tag1;
t[k].tag1=0;
}
}
inline void pushdown(int k){
cover(k),sum(k);
}
void build(int k,int l,int r){
t[k].tag2=-inf;
if(l==r){
t[k].val=a[l];
return;
}
int mid=(l+r)>>1;
build(ls(k),l,mid);
build(rs(k),mid+1,r);
pushup(k);
}
void modify(int k,int l,int r,int ql,int qr,ll v){
if(ql<=l&&r<=qr){
cover(k);
t[k].val+=v;
t[k].tag1+=v;
return;
}
if((r<ql)||(l>qr)) return;
pushdown(k);
int mid=(l+r)>>1;
if(ql<=mid) modify(ls(k),l,mid,ql,qr,v);
if(mid<qr) modify(rs(k),mid+1,r,ql,qr,v);
pushup(k);
}
void update(int k,int l,int r,int ql,int qr,ll v){
if(ql<=l&&r<=qr){
t[k].val=v;
t[k].tag2=v;
t[k].tag1=0;
return;
}
if((r<ql)||(l>qr)) return;
pushdown(k);
int mid=(l+r)>>1;
if(ql<=mid) update(ls(k),l,mid,ql,qr,v);
if(mid<qr) update(rs(k),mid+1,r,ql,qr,v);
pushup(k);
}
ll query(int k,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr){
return t[k].val;
}
if((r<ql)||(l>qr)) return -inf;
pushdown(k);
int mid=(l+r)>>1;
ll res=-inf;
if(ql<=mid) res=max(res,query(ls(k),l,mid,ql,qr));
if(mid<qr) res=max(res,query(rs(k),mid+1,r,ql,qr));
return res;
}
int main(){
scanf("%d%d",&n,&q);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
build(1,1,n);
for(int i=1,op,l,r;i<=q;++i){
scanf("%d",&op);
if(op==1){
ll x;
scanf("%d%d%lld",&l,&r,&x);
update(1,1,n,l,r,x);
}
else if(op==2){
ll x;
scanf("%d%d%lld",&l,&r,&x);
modify(1,1,n,l,r,x);
}
else if(op==3){
scanf("%d%d",&l,&r);
printf("%lld\n",query(1,1,n,l,r));
}
}
return 0;
}
标记其实是好维护的。
维护网格图最短路
Sol:
对于一段区间
考虑合并两个区间,以新区间左上到右下的最短路为例,可能不可达(
于是
其他一样讨论即可。
代码
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+10;
int n,m;
char c[2][maxn];
struct dis{
int d1,d2,d3,d4;
dis(){
d1=d2=d3=d4=0;
return;
}
};
struct TREE{
dis d;
int l,r;
}t[maxn<<2];
dis merge(dis a,dis b){
dis s;
s.d1=min(maxn*2,min(a.d1+b.d1,a.d2+b.d3)+1);
s.d2=min(maxn*2,min(a.d1+b.d2,a.d2+b.d4)+1);
s.d3=min(maxn*2,min(a.d3+b.d1,a.d4+b.d3)+1);
s.d4=min(maxn*2,min(a.d4+b.d4,a.d3+b.d2)+1);
return s;
}
#define ls(k) k<<1
#define rs(k) k<<1|1
void build(int u,int l,int r){
t[u].l=l,t[u].r=r;
if(l==r){
t[u].d.d1=t[u].d.d2=t[u].d.d3=t[u].d.d4=maxn*2;
if(c[0][l]=='.') t[u].d.d1=0;
if(c[1][l]=='.') t[u].d.d4=0;
if(c[0][l]==c[1][l]&&c[0][l]=='.') t[u].d.d2=t[u].d.d3=1;
return;
}
int mid=l+r>>1;
build(ls(u),l,mid);
build(rs(u),mid+1,r);
t[u].d=merge(t[ls(u)].d,t[rs(u)].d);
}
dis query(int u,int l,int r,int ql,int qr){
if(ql==l&&r==qr) return t[u].d;
int mid=l+r>>1;
if(mid>=qr) return query(ls(u),l,mid,ql,qr);
else if(mid<ql) return query(rs(u),mid+1,r,ql,qr);
else return merge(query(ls(u),l,mid,ql,mid),query(rs(u),mid+1,r,mid+1,qr));
}
void ask(int x,int y){
int u=(x-1)%n+1,v=(y-1)%n+1;
if(u>v) swap(x,y),swap(u,v);
dis ans=query(1,1,n,u,v);
int res;
if(x<=n&&y<=n) res=ans.d1;
else if(x<=n&&y>n) res=ans.d2;
else if(x>n&&y<=n) res=ans.d3;
else if(x>n&&y>n) res=ans.d4;
if(res<maxn*2) printf("%d\n",res);
else puts("-1");
}
int main(){
scanf("%d%d",&n,&m);
scanf("%s%s",c[0]+1,c[1]+1);
build(1,1,n);
for(int i=1;i<=m;++i){
int u,v;
scanf("%d%d",&u,&v);
ask(u,v);
}
return 0;
}
维护最大子段和
单点修改,查询区间最大子段和。
Sol:
对每个区间维护前缀最大和,后缀最大和,最大子段和,区间和。
考虑合并两个区间ls(k),rs(k)
到k
上。
区间和直接维护即可。
前缀最大和讨论一下,可能是ls(k)
的前缀最大和,可能是ls(k)
的区间和加上rs(k)
的前缀最大和。
后缀最大和同理。
最大子段和讨论一下,可能是ls(k)
的最大子段和,可能是rs(k)
的最大子段和,可能是ls(k)
的后缀最大和加上rs(k)
的前缀最大和。
然后就做完了。
板子
#include<bits/stdc++.h>
#define ls(k) k<<1
#define rs(k) k<<1|1
using namespace std;
const int maxn=5e5+5;
int n,m,a[maxn];
struct TREE{
int lmx,rmx,val,sum;
}t[maxn<<2];
inline void pushup(int k){
t[k].sum=t[ls(k)].sum+t[rs(k)].sum;
t[k].lmx=max(t[ls(k)].lmx,t[ls(k)].sum+t[rs(k)].lmx);
t[k].rmx=max(t[rs(k)].rmx,t[rs(k)].sum+t[ls(k)].rmx);
t[k].val=max(t[ls(k)].val,max(t[rs(k)].val,t[ls(k)].rmx+t[rs(k)].lmx));
}
void build(int k,int l,int r){
if(l==r){
t[k].sum=t[k].lmx=t[k].rmx=t[k].val=a[l];
return;
}
int mid=(l+r)>>1;
build(ls(k),l,mid);
build(rs(k),mid+1,r);
pushup(k);
}
void update(int k,int l,int r,int p,int v){
if(l==r){
t[k].sum=t[k].lmx=t[k].rmx=t[k].val=v;
return;
}
int mid=(l+r)>>1;
if(p<=mid) update(ls(k),l,mid,p,v);
else update(rs(k),mid+1,r,p,v);
pushup(k);
}
TREE query(int k,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return t[k];
int mid=(l+r)>>1;
if(ql<=mid&&mid<qr){
TREE tmp1=query(ls(k),l,mid,ql,qr),tmp2=query(rs(k),mid+1,r,ql,qr);
TREE res;
res.sum=tmp1.sum+tmp2.sum;
res.lmx=max(tmp1.lmx,tmp1.sum+tmp2.lmx);
res.rmx=max(tmp2.rmx,tmp2.sum+tmp1.rmx);
res.val=max(tmp1.val,max(tmp2.val,tmp1.rmx+tmp2.lmx));
return res;
}
else if(ql<=mid) return query(ls(k),l,mid,ql,qr);
else return query(rs(k),mid+1,r,ql,qr);
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
}
build(1,1,n);
for(int i=1,k,a,b;i<=m;++i){
scanf("%d%d%d",&k,&a,&b);
if(k==1){
if(a>b) swap(a,b);
TREE tmp=query(1,1,n,a,b);
printf("%d\n",max(tmp.lmx,max(tmp.rmx,tmp.val)));
}
else if(k==2){
update(1,1,n,a,b);
}
}
return 0;
}
区间加,区间
由更相减损术
差分后区间加变成单点修改。
于是做完了。
维护区间Hash/或者其他字符串相关的东西
Hash只需要改一下pushup
和pushdown
就好,但是很难写,不带修的话最好写倍增而不是上数据结构。
维护其他字符串的东西只是把线段树当成一个工具就行。
历史和线段树
配合扫描线之区间子区间问题食用。
区间加,区间历史版本和。
使用抽象线段树的思想,来设计一下信息和标记:
信息需要维护区间历史和
设每次求历史和之前打上的
标记合并:
标记作用在信息上:
信息合并:
按抽象线段树的思想写的
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
#define gc getchar
#define pc putchar
int rd(){
int f=1,r=0;
char ch=gc();
while(!isdigit(ch)){ if(ch=='-') f=-1;ch=gc();}
while(isdigit(ch)){ r=(r<<1)+(r<<3)+(ch^48);ch=gc();}
return f*r;
}
void wt(ll x){
static int stk[30],tp=0;
if(x<0) x=-x,pc('-');
do{
stk[++tp]=x%10,x/=10;
}while(x);
while(tp) pc(stk[tp--]+'0');
}
const int maxn=1e5+10;
int n,m,a[maxn];
struct TAG{
ll ds,t,hs;
TAG(){}
inline TAG(int x,int y,int z):ds(x),t(y),hs(z){}
inline TAG operator+(const TAG &tmp)const{
TAG rs;
rs.ds=ds+tmp.ds;
rs.t=t+tmp.t;
rs.hs=hs+tmp.hs+tmp.t*ds;
return rs;
}
}tag[maxn<<2];
struct DATA{
ll s,h,len;
DATA(){}
inline DATA(int x,int y,int z):s(x),h(y),len(z){}
inline DATA operator+(const TAG &tmp)const{
DATA rs;
rs.len=len;
rs.h=h+s*tmp.t+tmp.hs*len;
rs.s=s+tmp.ds*len;
return rs;
}
inline DATA operator+(const DATA &tmp)const{
DATA rs;
rs.len=len+tmp.len;
rs.s=s+tmp.s;
rs.h=h+tmp.h;
return rs;
}
}t[maxn<<2];
#define ls(u) (u<<1)
#define rs(u) (u<<1|1)
void build(int u,int l,int r){
if(l==r){
t[u]=DATA(a[l],a[l],r-l+1);
return;
}
int mid=(l+r)>>1;
build(ls(u),l,mid);
build(rs(u),mid+1,r);
t[u]=t[ls(u)]+t[rs(u)];
}
inline void addtag(int x,TAG v){
t[x]=t[x]+v;
tag[x]=tag[x]+v;
}
inline void pushdown(int u){
addtag(ls(u),tag[u]);
addtag(rs(u),tag[u]);
tag[u]=TAG(0,0,0);
}
void update(int u,int l,int r,int ql,int qr,TAG v){
if(ql<=l&&r<=qr){
addtag(u,v);
return;
}
pushdown(u);
int mid=(l+r)>>1;
if(ql<=mid) update(ls(u),l,mid,ql,qr,v);
if(mid<qr) update(rs(u),mid+1,r,ql,qr,v);
t[u]=t[ls(u)]+t[rs(u)];
}
DATA qry(int u,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return t[u];
pushdown(u);
int mid=(l+r)>>1;
DATA rs=DATA(0,0,0);
if(ql<=mid) rs=rs+qry(ls(u),l,mid,ql,qr);
if(mid<qr) rs=rs+qry(rs(u),mid+1,r,ql,qr);
return rs;
}
int main(){
n=rd(),m=rd();
for(int i=1;i<=n;++i) a[i]=rd();
build(1,1,n);
for(int i=1;i<=m;++i){
int op=rd();
if(op==1){
int l=rd(),r=rd(),x=rd();
update(1,1,n,l,r,TAG(x,0,0));
}
else{
int l=rd(),r=rd();
wt(qry(1,1,n,l,r).h),pc('\n');
}
addtag(1,TAG(0,1,0));
}
return 0;
}
吉司机线段树
不会。
现在会一点了。
来看模板题。
P6242 【模板】线段树 3(区间最值操作、区间历史最值)
-
对区间
, 。 -
对区间
, -
询问
-
询问
-
询问
每次操作之后,
解决操作
先来看
-
时,没用。 -
时, 变小,对 没有影响。 -
时, 变大,对 可能有影响。
回忆一下,我们现在用打标记的方式解决了区间加的问题,那么能否在下传标记时快速更新
我们发现,只有当前区间上打上的标记最大时,最有可能更新
操作
来看操作
我们把区间对
而大于
我们将标记拆成两份,对最大值维护普通加法标记和加法标记历史最大值,对非最大值也维护这两个标记。再对每个区间维护一下区间严格次小值,来保证最大值大于
多递归几次找到符合要求的区间即可。
为了方便维护区间和,还要记最大值的个数。
注意pushdown
的细节,讨论左右儿子是否有区间最大值,如果有,就向那个儿子下传最大值和非最大值的标记,否则只下传非最大值的标记。由于在下传操作之前整个区间的最大值已经修改过了,判断是否有区间最大值的时候要重新开变量记录一下修改前的区间最大值。
整个利用颜色段(似乎?)相关的均摊分析,是
打标记方式实现板子
#include<bits/stdc++.h>
using namespace std;
typedef long long ll;
const int maxn=5e5+10;
const ll inf=1e17+10;
int n,m;
struct TREE{
int l,r;
ll sum,cnt,mxa,mxb,sca,tg1,htg1,tg2,htg2;
}t[maxn<<2];
#define ls(k) (k<<1)
#define rs(k) (k<<1|1)
inline void pushup(int k){
t[k].sum=t[ls(k)].sum+t[rs(k)].sum;
t[k].mxa=max(t[ls(k)].mxa,t[rs(k)].mxa);
t[k].mxb=max(t[ls(k)].mxb,t[rs(k)].mxb);
if(t[ls(k)].mxa>t[rs(k)].mxa){
t[k].cnt=t[ls(k)].cnt;
t[k].sca=max(t[ls(k)].sca,t[rs(k)].mxa);
}
else if(t[ls(k)].mxa<t[rs(k)].mxa){
t[k].cnt=t[rs(k)].cnt;
t[k].sca=max(t[ls(k)].mxa,t[rs(k)].sca);
}
else{
t[k].cnt=t[ls(k)].cnt+t[rs(k)].cnt;
t[k].sca=max(t[ls(k)].sca,t[rs(k)].sca);
}
}
inline void mdf(int k,ll t1,ll ht1,ll t2,ll ht2){
t[k].sum+=t1*t[k].cnt+t2*(t[k].r-t[k].l+1-t[k].cnt);
t[k].mxb=max(t[k].mxb,t[k].mxa+ht1);
t[k].htg1=max(t[k].htg1,t[k].tg1+ht1);
t[k].htg2=max(t[k].htg2,t[k].tg2+ht2);
t[k].mxa+=t1;
t[k].tg1+=t1;
t[k].tg2+=t2;
if(t[k].sca!=-inf) t[k].sca+=t2;
}
void pushdown(int k){
ll mx=max(t[ls(k)].mxa,t[rs(k)].mxa);
if(mx==t[ls(k)].mxa) mdf(ls(k),t[k].tg1,t[k].htg1,t[k].tg2,t[k].htg2);
else mdf(ls(k),t[k].tg2,t[k].htg2,t[k].tg2,t[k].htg2);
if(mx==t[rs(k)].mxa) mdf(rs(k),t[k].tg1,t[k].htg1,t[k].tg2,t[k].htg2);
else mdf(rs(k),t[k].tg2,t[k].htg2,t[k].tg2,t[k].htg2);
t[k].tg1=t[k].htg1=t[k].tg2=t[k].htg2=0;
}
void build(int k,int l,int r){
t[k].l=l,t[k].r=r;
if(l==r){
ll x;
scanf("%lld",&x);
t[k].sum=t[k].mxa=t[k].mxb=x;
t[k].cnt=1;
t[k].sca=-inf;
return;
}
int mid=(l+r)>>1;
build(ls(k),l,mid);
build(rs(k),mid+1,r);
pushup(k);
}
void add(int k,int l,int r,int ql,int qr,ll v){
if(ql<=l&&r<=qr){
t[k].sum+=v*(r-l+1);
t[k].mxa+=v;
t[k].tg1+=v;
t[k].tg2+=v;
t[k].mxb=max(t[k].mxb,t[k].mxa);
t[k].htg1=max(t[k].htg1,t[k].tg1);
t[k].htg2=max(t[k].htg2,t[k].tg2);
if(t[k].sca!=-inf) t[k].sca+=v;
return;
}
pushdown(k);
int mid=(l+r)>>1;
if(ql<=mid) add(ls(k),l,mid,ql,qr,v);
if(mid<qr) add(rs(k),mid+1,r,ql,qr,v);
pushup(k);
}
void updmin(int k,int l,int r,int ql,int qr,ll v){
if(v>=t[k].mxa) return;
if(ql<=l&&r<=qr&&v>t[k].sca){
ll d=t[k].mxa-v;
t[k].sum-=d*t[k].cnt;
t[k].mxa=v;
t[k].tg1-=d;
return;
}
pushdown(k);
int mid=(l+r)>>1;
if(ql<=mid) updmin(ls(k),l,mid,ql,qr,v);
if(mid<qr) updmin(rs(k),mid+1,r,ql,qr,v);
pushup(k);
}
ll qrysuma(int k,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return t[k].sum;
pushdown(k);
int mid=(l+r)>>1;
ll rs=0;
if(ql<=mid) rs+=qrysuma(ls(k),l,mid,ql,qr);
if(mid<qr) rs+=qrysuma(rs(k),mid+1,r,ql,qr);
return rs;
}
ll qrymxa(int k,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return t[k].mxa;
pushdown(k);
int mid=(l+r)>>1;
ll rs=-inf;
if(ql<=mid) rs=max(rs,qrymxa(ls(k),l,mid,ql,qr));
if(mid<qr) rs=max(rs,qrymxa(rs(k),mid+1,r,ql,qr));
return rs;
}
ll qrymxb(int k,int l,int r,int ql,int qr){
if(ql<=l&&r<=qr) return t[k].mxb;
pushdown(k);
int mid=(l+r)>>1;
ll rs=-inf;
if(ql<=mid) rs=max(rs,qrymxb(ls(k),l,mid,ql,qr));
if(mid<qr) rs=max(rs,qrymxb(rs(k),mid+1,r,ql,qr));
return rs;
}
int main(){
scanf("%d%d",&n,&m);
build(1,1,n);
for(int i=1;i<=m;++i){
int op;
scanf("%d",&op);
if(op==1){
int l,r;
ll k;
scanf("%d%d%lld",&l,&r,&k);
add(1,1,n,l,r,k);
}
else if(op==2){
int l,r;
ll k;
scanf("%d%d%lld",&l,&r,&k);
updmin(1,1,n,l,r,k);
}
else if(op==3){
int l,r;
scanf("%d%d",&l,&r);
printf("%lld\n",qrysuma(1,1,n,l,r));
}
else if(op==4){
int l,r;
scanf("%d%d",&l,&r);
printf("%lld\n",qrymxa(1,1,n,l,r));
}
else if(op==5){
int l,r;
scanf("%d%d",&l,&r);
printf("%lld\n",qrymxb(1,1,n,l,r));
}
}
return 0;
}
当然我们并不能局限于板子。历史最值都可以类似维护。
还有从矩阵的角度理解的方式。可以将每个操作理解成
这种理解方式感觉很有拓展性。
矩阵乘法实现方式待补。
猫树
不会。
线段树优化建图
去建图优化相关技术里看。
线段树合并
把两棵线段树合并,就是把对应节点的信息合并起来就好。
启发式合并是两只
节点开满的线段树会使合并操作两只
设现在要把
-
递归到某节点时,若
或 上的对应节点为空,返回另一个。 -
递归到叶子节点后,合并信息。
-
儿子合并好后记得
pushup
。
复杂度就是
个人理解:线段树合并就像做加法,所以也可以做类似前缀和/差分的东西。同时类似并查集进行信息合并时,别忘了把对应的线段树也合并起来。
第一种板子
int merge(int x,int y,int l,int r){
if(!x||!y) return x+y;
if(l==r){
sum[x]+=sum[y];
return x;
}
int mid=(l+r)>>1;
ls[x]=merge(ls[x],ls[y],l,mid);
rs[x]=merge(rs[x],rs[y],mid+1,r);
pushup(x);
return x;
}
话说这里好像有两种写法,一种是上方将一棵树合并到另一棵树上,另一种是新开节点作为合并的结果。感觉上方的写法就够了,避免空间炸掉,暂时似乎没有缺点。
UPD:好吧是有缺点的。
以上做法会使得所有包含结点
当然在一些题目中并不需要可持久化/空间不允许可持久化,两种都要会写。
可持久化的板子
int merge(int x,int y,int l,int r){
if(!x||!y) return x+y;
int z=++sz;
if(l==r){
t[z].mx=t[x].mx+t[y].mx;
t[z].typ=l;
return z;
}
int mid=(l+r)>>1;
t[z].l=merge(t[x].l,t[y].l,l,mid);
t[z].r=merge(t[x].r,t[y].r,mid+1,r);
pushup(z);
return z;
}
空间复杂度
线段树合并的整体DP
去看DP。
线段树分裂
暂时咕咕咕。
会了一点,回来补一下。
线段树分裂和FHQ-Treap很类似。我们可以将其分为按值分裂和按排名分裂。(目前我所见到的都是权值线段树上的分裂,所以以下都按权值线段树来说。)
按值分裂:我们令
那么设当前结点对应区间中点为
-
,那么 的右儿子直接给 (这里可以直接交换二者的右儿子),然后往左儿子递归。 -
,将 的右儿子给 ,然后退出。 -
,往右儿子递归。
分讨结束后记得对pushup
。
按排名分裂是一样的。
常与合并一起用。
据说还能和ODT一起用,但我还不会。
代码
void splitv(int x,int &y,int l,int r,int v){
if(!x) return;
y=++sz;
int mid=(l+r)>>1;
if(v<mid) swap(t[x].rs,t[y].rs),splitv(t[x].ls,t[y].ls,l,mid,v);
else if(v==mid) swap(t[x].rs,t[y].rs);
else splitv(t[x].rs,t[y].rs,mid+1,r,v);
pushup(x);
pushup(y);
}
时间复杂度
可持久化线段树/主席树
可以保存线段树的历史版本。
每次修改把修改后的节点保存下来,单点修改每次修改一条链,长度是
区间修改只能在对应节点上标记永久化,因为下放标记会影响历史版本。
时间复杂度还是单次
初始
可持久化数组
板子,支持历史版本查询,在历史版本上修改等操作。
静态区间第 小
典。对每个前缀建权值线段树。可以理解为不断进行插入操作并保存每个版本,就是可持久化。
查询
板子
#include<bits/stdc++.h>
using namespace std;
const int maxn=2e5+5;
int n,m,tp=0,len,a[maxn],b[maxn],rt[maxn];
struct TREE{
int val,l,r;
}t[maxn*20];
int clone(int k){
int p=++tp;
t[p].l=t[k].l,t[p].r=t[k].r,t[p].val=t[k].val+1;
return p;
}
int build(int k,int l,int r){
k=++tp;
if(l==r) return tp;
int mid=(l+r)>>1;
t[k].l=build(t[k].l,l,mid);
t[k].r=build(t[k].r,mid+1,r);
return k;
}
int update(int k,int l,int r,int x){
k=clone(k);
if(l==r){
return k;
}
int mid=(l+r)>>1;
if(x<=mid) t[k].l=update(t[k].l,l,mid,x);
else t[k].r=update(t[k].r,mid+1,r,x);
return k;
}
int query(int u,int v,int l,int r,int k){
if(l==r) return l;
int mid=(l+r)>>1;
int x=t[t[v].l].val-t[t[u].l].val;
if(k<=x) return query(t[u].l,t[v].l,l,mid,k);
else return query(t[u].r,t[v].r,mid+1,r,k-x);
}
int getid(int x){
return lower_bound(b+1,b+len+1,x)-b;
}
int main(){
scanf("%d%d",&n,&m);
for(int i=1;i<=n;++i){
scanf("%d",&a[i]);
b[i]=a[i];
}
sort(b+1,b+n+1);
len=unique(b+1,b+n+1)-b-1;
rt[0]=build(rt[0],1,len);
for(int i=1;i<=n;++i){
rt[i]=update(rt[i-1],1,len,getid(a[i]));
}
for(int i=1;i<=m;++i){
int l,r,k;
scanf("%d%d%d",&l,&r,&k);
printf("%d\n",b[query(rt[l-1],rt[r],1,len,k)]);
}
return 0;
}
特殊的trick
转二维数点
询问一段区间中有几种颜色。
记
对值域建
询问左端点在
对于中位数,首先想二分,二分出
如何优化第二步?考虑初始每种数值贡献都是
李超线段树
高级的线段树QwQ
要求在平面直角坐标系中维护以下操作:
-
在平面上插入一条线段。
-
给定一个数
,查询与直线 相交的线段中交点纵坐标最大的线段的编号。
用线段树维护每个区间在
现在尝试插入一条线段
考虑某个被
否则,设该区间的原最优线段为
如果
-
若
在左端点比 大,那么 在左半区间有交点,递归到左边。 -
若
在右端点比 大,那么 在右半区间有交点,递归到右边。 -
如果左右端点
都更优,那么 不可能成为答案,不需下传。
最后把
这样修改就做完了。
查询时要把经过的节点的对应点的值都查一下。
时间复杂度:把线段拆到
板子
#include<bits/stdc++.h>
using namespace std;
const int maxn=1e5+10,mo1=39989,mo2=1000000000;
const double eps=1e-9;
int n,las,cnt,s[170000];
struct line{
double k,b;
}q[maxn];
#define ls(k) k<<1
#define rs(k) k<<1|1
void add(int x0,int x,int y0,int y){
++cnt;
if(x0==x) q[cnt].k=0,q[cnt].b=max(y0,y);
else q[cnt].k=1.0*(y0-y)/(x0-x),q[cnt].b=y0-q[cnt].k*x0;
}
double calc(int u,int p){
return q[u].k*p+q[u].b;
}
int cmp(double x,double y){
if(x-y>eps) return 1;
if(y-x>eps) return -1;
return 0;
}
void upd(int k,int l,int r,int u){
int mid=l+r>>1;
int cmid=cmp(calc(u,mid),calc(s[k],mid));
if(cmid==1||(!cmid&&u<s[k])) swap(s[k],u);
int cl=cmp(calc(u,l),calc(s[k],l)),cr=cmp(calc(u,r),calc(s[k],r));
if(cl==1||(!cl&&u<s[k])) upd(ls(k),l,mid,u);
if(cr==1||(!cr&&u<s[k])) upd(rs(k),mid+1,r,u);
}
void update(int k,int l,int r,int ql,int qr,int u){
if(ql<=l&&r<=qr){
upd(k,l,r,u);
return;
}
int mid=l+r>>1;
if(ql<=mid) update(ls(k),l,mid,ql,qr,u);
if(mid<qr) update(rs(k),mid+1,r,ql,qr,u);
}
pair<double,int> pmax(pair<double,int> x,pair<double,int> y){
if(cmp(x.first,y.first)==1) return x;
if(cmp(x.first,y.first)==-1) return y;
return x.second<y.second?x:y;
}
pair<double,int> query(int k,int l,int r,int p){
if(r<p||l>p) return make_pair(0.0,0);
int mid=l+r>>1;
double res=calc(s[k],p);
if(l==r) return make_pair(res,s[k]);
return pmax(make_pair(res,s[k]),pmax(query(ls(k),l,mid,p),query(rs(k),mid+1,r,p)));
}
int main(){
scanf("%d",&n);
for(int i=1,op;i<=n;++i){
scanf("%d",&op);
if(op==0){
int k;
scanf("%d",&k);
k=(k+las-1+mo1)%mo1+1;
las=query(1,1,mo1,k).second;
printf("%d\n",las);
}
else{
int x0,y0,x,y;
scanf("%d%d%d%d",&x0,&y0,&x,&y);
x0=(x0+las-1+mo1)%mo1+1;
x=(x+las-1+mo1)%mo1+1;
y0=((y0+las-1)%mo2+mo2)%mo2+1;
y=((y+las-1)%mo2+mo2)%mo2+1;
if(x0>x) swap(x0,x),swap(y0,y);
add(x0,x,y0,y);
update(1,1,mo1,x0,x,cnt);
}
}
return 0;
}
注意万能头下不能有y1
。
线段树分治
是时间线段树,先离线一下,把一个操作影响的时间区间在线段树上进行操作。可以让难以完成的删除操作变为撤销操作。
然后计算一个时刻的信息就直接查单点,统计经过的节点上带着的信息。查所有时刻就直接DFS。
各种应用比较神秘。
可撤销并查集与线段树分治将删除转为撤销的作用比较搭。
模板题代码
#include<bits/stdc++.h>
using namespace std;
#define gc getchar
#define pc putchar
int rd(){
int f=1,r=0;
char ch=gc();
while(!isdigit(ch)){ if(ch=='-') f=-1;ch=gc();}
while(isdigit(ch)){ r=(r<<1)+(r<<3)+(ch^48);ch=gc();}
return f*r;
}
void wt(int x){
static int stk[30],tp=0;
if(x<0) x=-x,pc('-');
do{
stk[++tp]=x%10,x/=10;
}while(x);
while(tp) pc(stk[tp--]+'0');
}
const int maxn=1e5+10;
int n,m,k;
struct edge{
int u,v;
edge(){}
edge(int x,int y):u(x),v(y){}
};
vector<edge> t[maxn<<2];
int tp,stk[maxn<<2],fa[maxn<<1],sz[maxn<<1];
int findf(int u){
if(u==fa[u]) return u;
return findf(fa[u]);
}
void merge(int u,int v){
u=findf(u),v=findf(v);
if(sz[u]<sz[v]) swap(u,v);
sz[u]+=sz[v];
fa[v]=u,stk[++tp]=v;
}
void undo(){
if(!tp) return;
int x=stk[tp--];
sz[fa[x]]-=sz[x];
fa[x]=x;
}
bool chk(int u){
return findf(u)!=findf(u+n);
}
#define ls(u) (u<<1)
#define rs(u) (u<<1|1)
void update(int u,int l,int r,int ql,int qr,edge k){
if(ql<=l&&r<=qr){
t[u].push_back(k);
return;
}
int mid=(l+r)>>1;
if(ql<=mid) update(ls(u),l,mid,ql,qr,k);
if(mid<qr) update(rs(u),mid+1,r,ql,qr,k);
}
void calc(int p,int l,int r,bool rs){
if(l==r){
bool res=rs;
int cnt=0;
for(int i=0;i<(int)t[p].size();++i){
if(!res) break;
int u=t[p][i].u,v=t[p][i].v;
merge(u,v+n);
merge(v,u+n);
res&=chk(u);
res&=chk(v);
cnt+=2;
}
if(!res) puts("No");
else puts("Yes");
for(int i=1;i<=cnt;++i) undo();
return;
}
bool res=rs;
int mid=(l+r)>>1;
int cnt=0;
for(int i=0;i<(int)t[p].size();++i){
if(!res) break;
int u=t[p][i].u,v=t[p][i].v;
merge(u,v+n);
merge(v,u+n);
res&=chk(u);
res&=chk(v);
cnt+=2;
}
if(!res) for(int i=l;i<=r;++i) puts("No");
else calc(ls(p),l,mid,res),calc(rs(p),mid+1,r,res);
for(int i=1;i<=cnt;++i) undo();
}
int main(){
n=rd(),m=rd(),k=rd();
for(int i=1;i<=n;++i) sz[i]=sz[i+n]=1,fa[i]=i,fa[i+n]=i+n;
for(int i=1;i<=m;++i){
int x=rd(),y=rd(),l=rd(),r=rd();
if(l==r) continue;
update(1,1,k,l+1,r,edge(x,y));
}
// cerr<<1<<endl;
calc(1,1,k,1);
return 0;
}
线段树单侧递归
比较好懂,但不会做题。
单侧递归发生在pushup
时。
由于两个儿子直接合并比较困难,只能先合并一个儿子,然后考虑当前儿子对另一个儿子答案的影响。
以先合并左儿子为例,考虑左儿子对右儿子的影响。这时可以再看右儿子的左右儿子,以上四个结点不妨称之为:左、右、右左、右右。
我们发现在pushup
右的时候,已经计算过了右左对右右的影响。于是可以分类讨论一下:
-
如果右左对右右的影响比左对右右的影响更大,就递归到右左中计算左对右左的影响,然后直接加上被右左影响后的右右的答案,这里需要用右的答案减掉右左的答案。
-
否则,我们要快速计算左对右左的影响,然后递归到右右中计算左对右右的影响。
这样递归pushup
是
区间查询合并答案也是类似的。
例子
我们直接记每个位置的斜率,然后就是统计前缀最大值有多少个。我们同时记一下区间
合并左、右时,显然可以直接把左儿子的贡献加上,然后就是考虑左对右的影响。
-
如果右左的最大值大于左的最大值,那么右左对右右的影响更大,直接加上影响后的右右的答案,然后递归到右左中。
-
否则右左中不会产生前缀最大值,对整个的答案贡献为
,然后递归到右右中。
这样就好了。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· Manus的开源复刻OpenManus初探
· 写一个简单的SQL生成工具
· AI 智能体引爆开源社区「GitHub 热点速览」
· C#/.NET/.NET Core技术前沿周刊 | 第 29 期(2025年3.1-3.9)