zkw线段树:高效的单点/区间修改+查询

前言

出处:清华大学 张昆玮(zkw) - ppt 《统计的力量》

重口味线段树不仅比普通线段树速度快、空间小,而且码量小得多,循环结构思路也很清晰,很适合用来优化Dijkstra和套在树剖以及树套树上。

原理

重口味线段树的详细解说网上太多了,我只简单说一下,(了解过普通线段树的应该很容易懂)

它先开一个 MAXN*3 的数组,树形结构,底层为至少 n+2 个点,数组中点 a[i] 在树上对应节点为 tr[p+i] ,

常数 p 求法

for(p=1;p<n+2;p<<=1);

此时得到这样一棵树:

这是个严格的满二叉树,也就是说,tr[p+n]以后的点要开完(浪费空间?其实恰恰相反),同时由于底层至少n+2个点,满二叉树中 tr[p+n] 以后必然还有点(都为虚点)。

得到的p其实就是底层第一个虚点的编号,虚点作用会在下面提及;

树上节点关系

tr[i]=tr[i<<1]+tr[i<<1|1];
tr[i]=max(tr[i<<1],tr[i<<1|1]);
tr[i]=min(tr[i<<1],tr[i<<1|1]);
//······

此时我们可以在输入的时候直接往 tr[p+i] 里面读入,然后往上更新,不用像某些博客里再搞个建树函数:

for(p=1;p<n+2;p<<=1);
for(int i=1;i<=n;i++)tr[p+i]=read();
for(int i=p-1;i>0;i--)tr[i]=tr[i<<1]+tr[i<<1|1];//例如区间加
//不能从p开始更新,因为p是最底层,盲访问会出界

从叶节点往上更新(儿子节点更新父亲),相比普通线段树,省掉了从上往下找儿子节点的时间

单点修改

inline void change(int x,int d){
	for(tr[p+x]=d,x=p+x>>1;x>0;x>>=1)
        tr[x]=max(tr[x<<1],tr[x<<1|1]);
}

为什么重口味可以不用递归写?因为它的每个节点对应关系是严格规定的,同一深度区间大小严格相等,是一种极规范的满二叉线段树。

如图(一眼明白):由于从子节点往上更新,不会访问到编号更大的没用的点(除非你打普通线段树时写特判,或者动态开点)

这样既保证了从下往上搜的准确性,又避免了空间浪费。

较简单的区间修改查询(加减、最大最小)可以用差分来做(这里就不用我多说了),但是稍微复杂一点就不能用差分了,改用普通线段树?不,

永久化懒标记,弥补了重口味结构原本无法区间改查的缺点。拿区间加减来说,lazy[i] 就表示 i 代表的区间需要整体加上的值,这样儿子节点就可以在往上遍历时把祖先节点的 lazy 值加上从而得到修改后的值。

关于永久化懒标记的限制,我会在后面单独说;

区间修改+查询时,利用到了一个规律:若需要操作的区间为 [B+1,C-1],我们可以从区间 [A,B],[C,D] (这里A=B,C=D,只是为了后面方便表示而用字母区分开)两个叶节点往上搜,

若 [A,B] 为右儿子,则更新为父亲 [A2,B],(A左移),若为左儿子,则先把对应右儿子 [B+1,B2] 操作了,然后更新为 [A,B2],(B右移)

[C,D] 则反过来,为右儿子则操作兄弟左儿子,

这样当更新到 [An,Bn],[Cn,Dn]有同一父亲时(Bn==Cn-1),肯定区间 [B+1,Bn] 和 [Cn,C-1] 都被操作过了(因为没操作,所以A左移和D右移不管它)。

这里虚点的最大作用就体现出来了:当待操作的区间左端点为1时,可以从虚点0处往上向右更新;区间右端点为n时,可以从虚点n+1处往上向左更新。

由于要给虚点预留空间,所以本来理论上是开2倍n的最大值,结果要开3倍(还是比普通的4倍少)。

板子

以区间最大值为例

inline void add(int l,int r,int d){//区间修改
	for(l=p+l-1,r=p+r+1;(l^1)!=r;){
		if((l&1)^1)tr[l^1]+=d,lazy[l^1]+=d;//改值并搭懒标记
		if(r&1)tr[r^1]+=d,lazy[r^1]+=d;
		l>>=1,r>>=1,tr[l]=max(tr[l<<1],tr[l<<1|1])+lazy[l];//边修改边更新
		tr[r]=max(tr[r<<1],tr[r<<1|1])+lazy[r];
	}
	for(l>>=1;l>0;l>>=1)tr[l]=max(tr[l<<1],tr[l<<1|1])+lazy[l];//最后更新到根
}
inline int sch(int l,int r){//区间查询
	int resl=0,resr=0;//左右两边分别记录,因为两边各自遇到的懒标记不一样
	for(l=p+l-1,r=p+r+1;(l^1)!=r;){
		if((l&1)^1)resl=max(resl,tr[l^1]);
		if(r&1)resr=max(resr,tr[r^1]);
		l>>=1,r>>=1,resl+=lazy[l],resr+=lazy[r];
	}resl=max(resl,resr);
	for(l>>=1;l>0;l>>=1)resl+=lazy[l];//与根节点之间的懒标记也要算上
	return resl;
}

上述代码看起来好像比较长,码量优势不大,但是大多数情况下用不着懒标记(包括只有区间修改单点查询的情况,原数组充当懒标记),所以码量一般是这样

inline void add(int l,int r,int d){//区间加
	for(l=p+l-1,r=p+r+1;(l^1)!=r;l>>=1,r>>=1){
		if((l&1)^1)tr[l^1]+=d;
		if(r&1)tr[r^1]+=d;
	}
}
inline int schp(int x){//单点查
	int res=0;
	for(x=p+x;x>0;x>>=1)res+=tr[x];
	return res;
}
//比普通线段树少

虽然重口味是从下往上搜,但是依据我使用重口味的经验,当遇到某些问题(如下,求数组中从右往左的第一个正数),也可以从上往下(其实是我懒得打普通线段树了,将就一下重口味,没想到对了!!??!?解锁新用法!)

inline int sch(){
	for(int x=1,lz=0;x<=p+n;){//线段树存区间最大值 
		if(x>p)return tr[x]+lz>0?x-p:-1;//到底层返回
		lz+=lazy[x];     //累加懒标记 
		if(tr[x<<1|1]+lz>0)x=(x<<1|1);
		else x<<=1;
	}
}

注意

重口味线段树虽然好用,但是遇到懒标记顺序不可调换的题就没办法(因为永久化懒标记是按深度从大到小依次操作,而非输入顺序)

重口味还有什么限制?没有了。从整体分析,zkw线段树有且仅有这一个限制:懒标记顺序必须可任意调换。

如 CodeForces 817F

因为这个限制,很多人觉得zkw没什么用,这显然是以偏盖全了。

猜你喜欢

转载自blog.csdn.net/weixin_43960287/article/details/108246164