Zkw line segment tree: efficient single point/interval modification + query

Preface

Source: Zhang Kunwei, Tsinghua University (zkw)-ppt  "The Power of Statistics"

The heavy-flavored line segment tree is not only faster and smaller than the ordinary line segment tree, but also has a much smaller code size. The loop structure idea is also very clear. It is very suitable for optimizing Dijkstra and nesting on tree sections and tree sets.

principle

There are too many detailed explanations of the heavy flavor line segment tree on the Internet, I will just talk about it briefly, (it should be easy to understand if you have known the ordinary line segment tree)

It first opens an array of MAXN*3, a tree structure, the bottom layer is at least n+2 points, and the point a[i] in the array corresponds to tr[p+i] on the tree.

How to find the constant p :

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

At this time, we get such a tree:

This is a strictly full binary tree, that is to say, the points after tr[p+n] have to be opened (a waste of space? In fact, it is the opposite). At the same time, because there are at least n+2 points in the bottom layer, tr[p+n ] There must be some points in the future (all imaginary points).

The obtained p is actually the number of the first virtual point in the bottom layer, and the function of the virtual point will be mentioned below;

 

Node relationship on the tree :

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]);
//······

At this point, we can read directly into tr[p+i] when inputting, and then update it, without having to build a tree function like in some blogs:

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是最底层,盲访问会出界

 

Update from the leaf node up (the son node updates the father), which saves the time to find the son node from top to bottom compared to ordinary line segment trees

Such as single point modification :

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

Why can't recursive writing be used for heavy flavors? Because the corresponding relationship of each node is strictly regulated, the size of the same depth interval is strictly equal, which is a very standard full binary line segment tree.

As shown in the figure (understand at a glance): Since the child node is updated upwards, the useless point with a larger number will not be visited (unless you write a special judgment when you type the ordinary line segment tree, or dynamically open the point)

This not only ensures the accuracy of searching from the bottom up, but also avoids space waste.

 

The simpler interval modification query (addition and subtraction, maximum and minimum) can be done by difference (I won't need to say more here), but if it is a little more complicated, you can't use difference. Use ordinary line segment tree instead? Do not,

Permanent lazy mark makes up for the shortcomings of the heavy-taste structure that cannot be changed in intervals. Take interval addition and subtraction as an example, lazy[i] represents the value that the interval represented by i needs to be added as a whole, so that the son node can add the lazy value of the ancestor node when traversing upward to get the modified value.

Regarding the limitation of permanent lazy mark, I will talk about it separately later;

 

In interval modification + query, a rule is used: if the interval to be operated is [B+1,C-1], we can start from the interval [A, B], [C, D] (here A=B, C= D, which is separated by letters for the convenience of presentation later) The two leaf nodes are searched upwards,

If [A,B] is the right son, then update to father [A2,B], (A is shifted to the left), if it is the left son, first operate the corresponding right son [B+1,B2], and then update to [A,B2], (B moves to the right)

[C,D] The other way around, the right son will operate the brother's left son,

So when it is updated to [An,Bn], [Cn,Dn] has the same father (Bn==Cn-1), it is certain that the intervals [B+1,Bn] and [Cn,C-1] have been manipulated (Because there is no operation, so A left and D right move regardless of it).

The biggest role of the virtual point here is reflected: when the left end of the interval to be operated is 1, it can be updated from the virtual point 0 upwards to the right; when the right end of the interval is n, it can be updated from the virtual point n+1 Update from top to left.

Due to the need to reserve space for the virtual point, the maximum value of n was opened in theory by 2 times, and the result was 3 times (still 4 times less than the ordinary).

board

Take the maximum value of the interval as an example

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

The above code seems to be relatively long, the code size advantage is not big, but in most cases, the lazy mark is not needed (including only the interval modification single-point query, the original array acts as a lazy mark), so the code size is generally like this

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;
}
//比普通线段树少

Although the heavy taste is searched from bottom to top, based on my experience using heavy taste, when encountering certain problems (as follows, find the first positive number from right to left in the array), you can also go from top to bottom ( In fact, I am too lazy to play the ordinary line segment tree, I will just focus on the taste, I didn't expect it to be right!!!?!? Unlock new usage!)

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

note

Although the heavy-flavored line segment tree is easy to use, it can’t be helped if the lazy tag order cannot be changed (because the permanent lazy tag is operated in descending order of depth, not the order of input)

Are there any restrictions on heavy flavors? No more. From the overall analysis, the zkw line segment tree has one and only one limitation: the lazy mark order must be arbitrarily interchangeable.

Such as  CodeForces 817F

Because of this limitation, many people think that zkw is useless, which is obviously a partial coverage.

Guess you like

Origin blog.csdn.net/weixin_43960287/article/details/108246164