线段树和树状数组总结

一大波线段树和树状数组预警。

树状数组基础部分

Binary Indexed Tree.BIT

线段树可以实现树状数组所有功能,但树状数组简单易写常数小。

树状数组利用了二进制数的特性,很精巧。

注意到一件事,若将当前点下标从低到高第一个1记作k,那么当前点的范围即包含当前点的前面长度为k的区间。

考虑任意正整数x只有唯一的二进制表示方式,设x=2k1+2k2++2km,其中k1<k2<<km,那么就可以把[1,x]划分为长度为2k1,2k2,,2km1,2km的互不相交的区间,就得到了上面的结论。感性理解一下。

lowbit利用了补码的特性,xx的每一位取反再加一,使得最低位上的1保留下来,其他的1都扔掉了。

加上lowbit相当于跳向父亲,减去lowbit相当于向前跳。

单次时间复杂度显然O(logn)

树状数组只能维护前缀查询,不能维护没有可减性的信息,有局限性,但好写。

当然与值域线段树相对应有值域树状数组。

板子:
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]记录以(i,j)为右下角,宽为lowbit(i),长为lowbit(j)的矩形的信息。

于是其他操作就和一维的树状数组很像了,支持单点修改,前缀的一个矩形的查询。

单点修改,矩形查询

就是上面说的。

矩形修改,单点查询

考虑差分。

我们并不能直观地知道二维差分是一个什么东西,但是我们知道二维前缀和,以及前缀和与差分互逆。

已知:

sumi,j=sumi1,j+sumi,j1+ai,jsumi1,j1

我们把sum换成aa换成d,于是得到了二维差分的关系式:

ai,j=ai1,j+ai,j1+di,jai1,j1

稍作整理:

di,j=ai,jai1,jai,j1+ai1,j1

于是可以差分后将操作转化为单点修改,矩形查询,做完了。

矩形修改,矩形查询

我们需要推一下式子。

对于矩形修改,我们还是选择差分。

查询显然可以用前缀相减,于是考虑如何查前缀信息。

要求的显然是:

i=1xj=1yh=1ik=1jdh,k

推一下:

i=1xj=1yh=1ik=1jdh,k=h=1xk=1ydh,ki=hxj=ky1=h=1xk=1y(xh+1)(yk+1)dh,k=h=1xk=1y(x+1)(y+1)dh,k(y+1)hdh,k(x+1)kdh,k+hkdh,k

于是维护四个树状数组di,j,idi,j,jdi,j,ijdi,j即可。

P4514 上帝造题的七分钟

板子
#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;
} 

线段树基础部分

常用的维护区间信息的数据结构。

常见的区间和等东西以及一些神秘的信息都可以尝试用线段树。

通常用pushuppushdown上传信息和下放标记。

通常用堆式储存,ls=k<<1,rs=k<<1|1

区间修改通常使用懒标记,查询或修改到后代时再下放标记。

也有值域线段树,可以支持部分平衡树的操作。

技巧:

  • 动态开点:降低空间复杂度,据说可以只开2n的空间。

  • 标记永久化:若确定一个点上的标记不会爆范围,就可以永久化它,不下放,而是在经过的时候累计贡献。

抽象线段树

对线段树深刻理解一下。

线段树上维护的东西分成两类:信息和标记。这可以写两个结构体。

维护线段树时,首先是设计信息和标记,接下来要考虑的就是信息和标记之间的合并。

分为三类来讨论:标记与标记合并,信息和信息合并,信息和标记合并(标记作用在信息上)。

这样来考虑问题会更有条理一点。

我们按以下步骤来设计:

  • 设计信息,同时得到了信息如何合并。

  • 设计标记,同时得到标记如何作用在信息上。

  • 实现标记之间的合并。这一步可以讨论两个具有先后关系的标记应当如何合并。(或许相当于合并两个操作序列)

维护区间加,区间乘

板子。

维护区间max,区间赋值,区间加

luogu扶苏的问题

赋值的优先级高于加法。

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;
}

标记其实是好维护的。

维护网格图最短路

CF Maze-2D

2×n的迷宫,有障碍,维护其中两点间的最短路。

Sol:

对于一段区间[l,r],维护左上到右上,左上到右下,左下到右上,左下到右下(分别记作d1,d2,d3,d4)四条最短路,不可达就记作INF

考虑合并两个区间,以新区间左上到右下的最短路为例,可能不可达(INF),可能从左区间左上到右上再从右区间左上到右下转移过来,可能从左区间左上到右下再从右区间左下到右下转移过来,注意从左区间走到右区间还要一步。

于是d2=min{INF,ls.d1+rs.d2+1,ls.d2+rs.d4+1}

其他一样讨论即可。

代码
#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;
}

维护最大子段和

luogu 小白逛公园

单点修改,查询区间最大子段和。

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;
}

区间加,区间gcd

由更相减损术gcd(a,b)=gcd(a,ba)得,序列的gcd等于该序列差分后的gcd。但注意左端点特殊处理。

差分后区间加变成单点修改。

于是做完了。

维护区间Hash/或者其他字符串相关的东西

Hash只需要改一下pushuppushdown就好,但是很难写,不带修的话最好写倍增而不是上数据结构。

维护其他字符串的东西只是把线段树当成一个工具就行。

历史和线段树

配合扫描线之区间子区间问题食用。

区间加,区间历史版本和。

使用抽象线段树的思想,来设计一下信息和标记:

信息需要维护区间历史和h,区间和s,区间长度len。标记暂时需要维护每个位置加上的数ds,求历史和轮数t,现在来推hstds如何决定的式子。

设每次求历史和之前打上的dsdsi,那么历史和新增i=1t(s+lendsi)=st+leni=1tdsi,于是还要维护标记hs=i=1tdsi

标记合并:hs=hsl+hsr+dsltrl指靠前的标记,r指靠后的标记。

标记作用在信息上:hh+st+lenhs,注意这里要先更新h

信息合并:s=sl+sr,h=hl+hr

按抽象线段树的思想写的
#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(区间最值操作、区间历史最值)

  1. 对区间[l,r]aiai+k

  2. 对区间[l,r]aimin(ai,k)

  3. 询问i=lrai

  4. 询问maxi=lrai

  5. 询问maxi=lrbi

每次操作之后,bimax(bi,ai)

解决操作1,3,4是trivial的。来看2,5

先来看5吧。现在的问题就在于无法快速更新bi。我们来观察一下,在区间加k操作下,bi会如何变化。

  • k=0时,没用。

  • k<0时,ai变小,对bi没有影响。

  • k>0时,ai变大,对bi可能有影响。

回忆一下,我们现在用打标记的方式解决了区间加的问题,那么能否在下传标记时快速更新maxbi呢?

我们发现,只有当前区间上打上的标记最大时,最有可能更新bi。所以开两个标记,一个是普通的加法标记,另一个记加法标记的历史最大值。这样在下传的时候就能顺带更新maxbi了。

操作5结束了。

来看操作2。我们发现区间取min操作完全不能直接在复杂度正确的时间内维护,考虑转化一下:

我们把区间对kmin转化为区间内大于k的数都去一些东西变成k。而区间内进行减法与我们的加法标记是相适应的。

而大于k的数需要减去的东西不一样,那么我们退一步,如果区间内只有最大值大于k,其他数都k,怎么维护?

我们将标记拆成两份,对最大值维护普通加法标记和加法标记历史最大值,对非最大值也维护这两个标记。再对每个区间维护一下区间严格次小值,来保证最大值大于k,严格次小值小于k(这里严格小于是为了防止在取min后最大值等于严格次小值)。

多递归几次找到符合要求的区间即可。

为了方便维护区间和,还要记最大值的个数。

注意pushdown的细节,讨论左右儿子是否有区间最大值,如果有,就向那个儿子下传最大值和非最大值的标记,否则只下传非最大值的标记。由于在下传操作之前整个区间的最大值已经修改过了,判断是否有区间最大值的时候要重新开变量记录一下修改前的区间最大值。

整个利用颜色段(似乎?)相关的均摊分析,是O(log2n)的。

打标记方式实现板子
#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;
} 

当然我们并不能局限于板子。历史最值都可以类似维护。

还有从矩阵的角度理解的方式。可以将每个操作理解成(+,max)矩阵乘法,当然这样直接维护区间和不太自然,但是可以发扬人类智慧将区间和算出来。尝试找一下转移矩阵,我们可以发现,矩阵中特殊的(不是01的部分)就是我们以上维护的标记。

这种理解方式感觉很有拓展性。

矩阵乘法实现方式待补。

猫树

不会。

线段树优化建图

建图优化相关技术里看。

线段树合并

把两棵线段树合并,就是把对应节点的信息合并起来就好。

启发式合并是两只log的,但线段树合并可以做到单log

节点开满的线段树会使合并操作两只log,因此要用动态开点。

设现在要把y合并到x上。

  1. 递归到某节点时,若xy上的对应节点为空,返回另一个。

  2. 递归到叶子节点后,合并信息。

  3. 儿子合并好后记得pushup

复杂度就是O(nlogn)的。证明先咕了。

个人理解:线段树合并就像做加法,所以也可以做类似前缀和/差分的东西。同时类似并查集进行信息合并时,别忘了把对应的线段树也合并起来。

第一种板子
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:好吧是有缺点的。

以上做法会使得所有包含结点x的线段树的信息发生变化,破坏了原来的线段树的结构。但是我们只希望更改当前线段树上的信息,所以需要新开结点,类似可持久化的思想。或许可以称之为可持久化线段树合并

当然在一些题目中并不需要可持久化/空间不允许可持久化,两种都要会写。

可持久化的板子
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;
}

空间复杂度O(nlogn),可能带2倍常数。

线段树合并的整体DP

去看DP。

线段树分裂

暂时咕咕咕。

会了一点,回来补一下。

线段树分裂和FHQ-Treap很类似。我们可以将其分为按值分裂和按排名分裂。(目前我所见到的都是权值线段树上的分裂,所以以下都按权值线段树来说。)

按值分裂:我们令splitv(x,y,l,r,v)表示将对应区间为[l,r]x结点分裂,保留值v的部分,值>v的部分分裂到y上。

那么设当前结点对应区间中点为mid,分类讨论一下:

  1. v<mid,那么x的右儿子直接给y(这里可以直接交换二者的右儿子),然后往左儿子递归。

  2. v=mid,将x的右儿子给y,然后退出。

  3. v>mid,往右儿子递归。

分讨结束后记得对xypushup

按排名分裂是一样的。

常与合并一起用。

据说还能和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);
}

时间复杂度O(logn)。空间尽量开大些,空间复杂度大概为O(qlogn),但是往上乘了常数,可以往50倍开。

可持久化线段树/主席树

可以保存线段树的历史版本。

每次修改把修改后的节点保存下来,单点修改每次修改一条链,长度是logn的。

区间修改只能在对应节点上标记永久化,因为下放标记会影响历史版本。

时间复杂度还是单次O(logn)的,空间复杂度要具体分析。

初始2n个节点(动态开点嘛),m次操作,每次修改长为logn的一条链,所以大概要2n+mlogn的数组,尽量开大点。

可持久化数组

板子,支持历史版本查询,在历史版本上修改等操作。

静态区间第k

典。对每个前缀建权值线段树。可以理解为不断进行插入操作并保存每个版本,就是可持久化。

查询[l,r]的第k小,考虑取出[l,r]的信息,其实就是第r棵树的信息减去第l1棵树的信息。在权值线段树上类似二分地找第k小即可。

板子
#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

转二维数点

luogu HH的项链

询问一段区间中有几种颜色。

prei表示位置i上的颜色上次出现的位置。那么对于区间[l,r],要数的就是prei<l的个数。于是用主席树。

对值域建

luogu middle

询问左端点在[a,b],右端点在[c,d]的区间的最大中位数。

对于中位数,首先想二分,二分出mid,小于它的设为1,大于它的设为1

[a,b]查询最大后缀和,[c,d]查询最大前缀和,[b+1,c1]查询和,求总和是否大于0,若大于0则中位数还可以再增大。

如何优化第二步?考虑初始每种数值贡献都是1,在第x棵树上将小于x的数的贡献赋值成1,这是一个类似于递推的过程,预处理时额外保存每种数值的出现位置即可。每个历史版本都要保存,于是主席树。

李超线段树

高级的线段树QwQ

要求在平面直角坐标系中维护以下操作:

  • 在平面上插入一条线段

  • 给定一个数k,查询与直线x=k相交的线段中交点纵坐标最大的线段的编号。

Sol

用线段树维护每个区间在x=mid处纵坐标最大的直线的信息。

现在尝试插入一条线段f,首先在线段树上取出完全被f覆盖的log个区间,然后对每个拆出来的区间考虑更新它和它的子区间。

考虑某个被f完全覆盖的区间,若该区间原来没有最优的线段,那么f直接成为该区间的最优线段。

否则,设该区间的原最优线段为g,我们比较在x=midfg的大小。

如果f更优,直接交换fg,现在讨论f不比g优的情况:

  • f在左端点比g大,那么f,g在左半区间有交点,递归到左边。

  • f在右端点比g大,那么f,g在右半区间有交点,递归到右边。

  • 如果左右端点g都更优,那么f不可能成为答案,不需下传。

最后把g挂在该区间上就好。注意g不等同于在x=mid上取值最大的线段,容易举出反例。

这样修改就做完了。

查询时要把经过的节点的对应点的值都查一下。

时间复杂度:把线段拆到log个区间是O(logn),每个区间更新也是O(logn)的,所以一次修改的复杂度是O(log2n)。一次查询显然O(logn)。注意,插入一条线段需要拆分,于是多带一个log,但是全局插入一条直线是不用拆分的,于是O(logn)

板子
#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右的时候,已经计算过了右左对右右的影响。于是可以分类讨论一下:

  • 如果右左对右右的影响比左对右右的影响更大,就递归到右左中计算左对右左的影响,然后直接加上被右左影响后的右右的答案,这里需要用右的答案减掉右左的答案。

  • 否则,我们要快速计算左对右左的影响,然后递归到右右中计算左对右右的影响。

这样递归O(logn)层,于是pushupO(logn)的,于是整个修改是O(log2n)的。

区间查询合并答案也是类似的。

例子

link 楼房重建

我们直接记每个位置的斜率,然后就是统计前缀最大值有多少个。我们同时记一下区间max

合并左、右时,显然可以直接把左儿子的贡献加上,然后就是考虑左对右的影响。

  • 如果右左的最大值大于左的最大值,那么右左对右右的影响更大,直接加上影响后的右右的答案,然后递归到右左中。

  • 否则右左中不会产生前缀最大值,对整个的答案贡献为0,然后递归到右右中。

这样就好了。

posted @   RandomShuffle  阅读(13)  评论(0编辑  收藏  举报
编辑推荐:
· 从 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)
点击右上角即可分享
微信分享提示