前言
出处:清华大学 张昆玮(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线段树有且仅有这一个限制:懒标记顺序必须可任意调换。
因为这个限制,很多人觉得zkw没什么用,这显然是以偏盖全了。