Zkw線分ツリー:効率的な単一点/間隔の変更+クエリ

序文

出典:清華大学(zkw)のZhang Kunwei-ppt  "The Power of Statistics"

濃厚な線分ツリーは、通常の線分ツリーよりも高速で小さいだけでなく、コードサイズもはるかに小さくなります。ループ構造の考え方も非常に明確です。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で表される間隔を全体として加算する必要がある値を表します。これにより、息子ノードは、上にトラバースして変更された値。

パーマネントレイジーマークの制限については、後で別途説明します。

 

間隔変更+クエリでは、ルールが使用されます。操作される間隔が[B + 1、C-1]の場合、間隔[A、B]、[C、D](ここではA = B)から開始できます。 、C = D、後で表示するために文字で区切られています)2つのリーフノードが上向きに検索されます。

[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から更新されます。上から左に更新されます。

仮想点用のスペースを確保する必要があるため、理論上、nの最大値は2倍に開かれ、結果は3倍になりました(通常の4分の1)。

ボード

例として間隔の最大値を取り上げます

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