ST表

本文仅发布于此博客和作者的洛谷博客,不允许任何人以任何形式转载,无论是否标明出处及作者。


0x00 概述

ST表(Sparse Table)是一种基于倍增思想的数据结构,可用于区间查询

0x10 ST表

0x11 模板

给定一个长度为 n 的数列,和 q 次询问,求出每一次询问的区间内数字的最大值。

对于 100% 的数据,满足 1n1051q2×106

可以发现,询问的次数明显大于数列长度,线段树每次查询是O(logn)的,TLE,无法通过。

0x12 正解: ST表。

我们可以维护这样一个数据结构:

  1. 建立:设 n 为数列长度,原数列为a[i]m=log2n.建立一个表格g[n][m],g[i][j]表示区间 [i,i+2j1] 中的最大值,即 max{a[x]}(x[i,i+2j1]).

  2. 初始化:显然,当 j=0 时,g[i][j]=a[i]j0 时,g[i][j]=max(g[i][j-1],g[i+pow(2,j-1)][j-1])

    如图。

  3. 查询:设查询区间左右端点为 l,r ,长度 len=rl+1 。我们在表里找到两个区间 t1,t2 ,满足:可重但是不能漏地覆盖 [l,r]

    显然, t1 的左端点是 lt2 的右端点是 r

    两个区间的一个端点已经分别确定,只需要一个合适的区间长度m

    设区间长度 len=rl+1 ,则区间长度m显然必须满足这些条件:

    1. 2mlen,不然没有办法全部覆盖。
    1. mlen,不然 t1,t2 会超过查询区间的边界。
    1. m=2x,其中 xN ,不然 t1,t2 在表格里面找不到。
      并且两个区间的长度都为 2log2 时,可以满足上面的条件。

    x=log2len时,符合条件。

    此时, t1=[l,l+2x1],t2=[r2x+1,r] ,在g[n][m]里就分别是t1=g[l][x]t2=g[r-pow(2,x)+1][x]

    最后的结果就是max(t1,t2)

0x13 复杂度分析

空间复杂度:一个 lognn列的表,nlogn

时间复杂度:初始化时每次O(1)填入一个g[i][j],表格里有 nlogn 个数需要计算,所以是 O(nlogn) 的。查询时O(1)得到答案。总复杂度是 O(nlogn+q)

可以通过模板题。

0x14 代码

题目:link

#include<bits/stdc++.h>
using namespace std;
int g[100005][17];//log 1e5=16.6
int main(){
	int n,m,q;
	cin>>n>>q;
	m=__lg(n)+1;//st表的行数,__lg(x)可以在O(1)内返回floor(log2(x)).
	for(int i=1;i<=n;i++){
		cin>>g[i][0];
	}
	for(int j=1;j<=m;j++){
		for(int i=1;i+(1<<j)-1<=n;i++){//区间不能超
			g[i][j]=max(g[i][j-1],g[i+(int)pow(2,j-1)][j-1]);
		}
	}
	for(int i=1;i<=q;i++){
		int l,r;
		cin>>l>>r;
		int len=r-l+1;
		int x=__lg(len);
		int ans=max(g[l][x],g[r-(int)pow(2,x)+1][x]);
		cout<<ans<<endl;
	}
	return 0;
}

轻微卡常(快读,\npow改成位运算等)即可通过。


注意:这里初始化最好就这么写,最好不要进行什么额外的操作,不然容易出锅。

0x20 优缺点分析

我们把它和也可以完成区间RMQ的线段树相比。

0x21 优点

  1. 显然,三个数据结构中,只有ST表能做到O(1)查询,适合在q很大的时候使用。

  2. 而且,ST表的码量比线段树少很多,甚至比树状数组还小一点。

0x22 缺点

  1. 只能计算可重复贡献问题,比如说像最大值这种,max{[1,6]}=max{max{[1,4]},max{[3,6]}}3,4 被重复计算但是对结果没有影响。可重复贡献问题还有区间按位与,按位或,GCD等。连一个区间求和都做不成

  2. 不能做带修的。

0x30 习题

0x31 Problem 1

linkP7333 [JRKSJ R1]JFCA 名字好评



Solution

首先进行一个破环为链,变成一个长度为2n的链(经典操作),只存 aibi 不管。

显然,区间 [k,k+x] 的最大值在 x 递增时,是单调递增的,于是我们可以有以下操作:

对于每一个点 i ,如果链上的区间 [i+1,i+n/2] 的最大值 bi (等价于“存在点 j 满足 j[i+1,i+n/2]ajbi ”),那么就在此区间上二分,找到一个满足 [i+1,j] 的最大值 bi 且编号尽可能最小的点 j ,算一下 ij 的距离即可。

对于区间 [in/2,i1] 进行相似的操作,所得结果和上面那个区间的结果取个 min 就好。(注意进行无解的判断)

区间最大值显然直接使用ST表,O(1)解决。

另外, in/2 可能是负数,遇到负数直接把区间往右平移 n 就可以。

#include<bits/stdc++.h>
using namespace std;
struct pt{
	int a;
	int b;
}a[100007];
int n;
int chain[200014];//链
int st[200014][18];
int query(int l,int r){//查询,l可以不>r,也可以是负数
	if(l>r){
		swap(l,r);
	}
	int len=r-l+1;
	if(l<=0){
		l+=n;
		r+=n;
	}
	int x=__lg(len);
	return max(st[l][x],st[r-(1<<x)+1][x]);//经典公式
}
int bs(int k,int l,int r){//[i+1,i+n/2]的二分
	if(l==r){
		return r-k;
	}
	int mid=(l+r)/2;
	if(query(k+1,mid)<a[k].b){//查询[k+1,mid]有没有解
		return bs(k,mid+1,r);
	}else{
		return bs(k,l,mid);
	}
}
int rev_bs(int k,int l,int r){//[i-n/2,i-1]的二分
	if(l==r){
		return k-r;
	}
	int mid=(l+r+1)/2;//不+1有死循环(其他方式避免也可以
	if(query(k-1,mid)<a[k].b){
		return rev_bs(k,l,mid-1);
	}else{
		return rev_bs(k,mid,r);
	}
}
int main(){
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i].a;
	}
	for(int i=1;i<=n;i++){
		cin>>a[i].b;
	}
	if(n==1){//n=1要特判
		cout<<-1;
		return 0;
	}
	for(int i=1;i<=n;i++){
		chain[i]=a[i].a;
	}
	for(int i=1;i<=n;i++){//再复制一遍,长度变成2n
		chain[i+n]=a[i].a;
	}
	for(int i=1;i<=2*n;i++){
		st[i][0]=chain[i];
	}
	for(int j=1;j<=18;j++){//初始化
		for(int i=1;i+(1<<j)-1<=2*n;i++){
			st[i][j]=max(st[i][j-1],st[i+(1<<(j-1))][j-1]);
		}
	}
	for(int i=1;i<=n;i++){
		int ans=min((query(i+1,i+n/2)<a[i].b?0x3f3f3f3f:bs(i,i+1,i+n/2)),(query(i-1,i-n/2)<a[i].b?0x3f3f3f3f:rev_bs(i,i-n/2,i-1)));
		//重点长难句 包括对区间里有没有解的预先判断,两个解取min等
		cout<<(ans==0x3f3f3f3f?-1:ans)<<' ';//无解判断
	}
}

0x32 Problem 2

linkP8818 [CSP-S 2022] 策略游戏 进行鞭尸



Solution:我觉得这道题应该不需要Solution了。

#include<bits/stdc++.h>
using namespace std;
int a_max[100005][17];
int a_min[100005][17];
int b_max[100005][17];
int b_min[100005][17];
int a_pos[100005][17];
int a_neg[100005][17];
int a[100005];
int b[100005];
int main(){
	int n,m,q;
	cin>>n>>m>>q;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=m;i++){
		cin>>b[i];
	}
	for(int i=1;i<=n;i++){
		a_max[i][0]=a[i];
		a_min[i][0]=a[i];
		if(a[i]>=0){
			a_pos[i][0]=a[i];
		}else{
			a_pos[i][0]=0x3f3f3f3f;
		}
		if(a[i]<=0){
			a_neg[i][0]=a[i];
		}else{
			a_neg[i][0]=-0x3f3f3f3f;
		}
	}
	for(int i=1;i<=n;i++){
		b_max[i][0]=b[i];
		b_min[i][0]=b[i];
	}
	for(int j=1;j<=17;j++){
		for(int i=1;i+(1<<j)-1<=n;i++){
			a_max[i][j]=max(a_max[i][j-1],a_max[i+(1<<(j-1))][j-1]);
			a_min[i][j]=min(a_min[i][j-1],a_min[i+(1<<(j-1))][j-1]);
			b_max[i][j]=max(b_max[i][j-1],b_max[i+(1<<(j-1))][j-1]);
			b_min[i][j]=min(b_min[i][j-1],b_min[i+(1<<(j-1))][j-1]);
			a_pos[i][j]=min(a_pos[i][j-1],a_pos[i+(1<<(j-1))][j-1]);
			a_neg[i][j]=max(a_neg[i][j-1],a_neg[i+(1<<(j-1))][j-1]);
		}
	}
	#define int long long
	for(int i=1;i<=q;i++){
		int l1,r1,l2,r2;
		cin>>l1>>r1>>l2>>r2;
		int amax=max(a_max[l1][__lg(r1-l1+1)],a_max[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
		int amin=min(a_min[l1][__lg(r1-l1+1)],a_min[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
		int bmax=max(b_max[l2][__lg(r2-l2+1)],b_max[r2-(1<<(__lg(r2-l2+1)))+1][__lg(r2-l2+1)]);
		int bmin=min(b_min[l2][__lg(r2-l2+1)],b_min[r2-(1<<(__lg(r2-l2+1)))+1][__lg(r2-l2+1)]);
		int apos=min(a_pos[l1][__lg(r1-l1+1)],a_pos[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
		int aneg=max(a_neg[l1][__lg(r1-l1+1)],a_neg[r1-(1<<(__lg(r1-l1+1)))+1][__lg(r1-l1+1)]);
		if(amin>=0&&bmin>=0){
			cout<<amax*bmin<<endl;
			continue;
		}
		if(amin>=0&&bmin<=0){
			cout<<amin*bmin<<endl;
			continue;
		}
		if(amax<=0&&bmax<=0){
			cout<<amin*bmax<<endl;
			continue;
		}
		if(amax<=0&&bmax>=0){
			cout<<amax*bmax<<endl;
			continue;
		}
		if(bmin>=0){
			cout<<amax*bmin<<endl;
			continue;
		}
		if(bmax<=0){
			cout<<amin*bmax<<endl;
			continue;
		}
		cout<<max(apos*bmin,aneg*bmax)<<endl;
		continue;
	}
}

0x33 Problem 3

linkP7974 [KSN2021] Delivering Balls 阴间的

不要看他的题面,翻译的很不好。 可以看这个



Solution:实为阴间题。

我们把题目里面的蓝线叫做山(很形象对不对),起点设为l,终点设为r

首先我们先进行一个贪心

  1. 绝不会出现掉头(往远离终点的方向走)的情况:显然。

  2. 能斜着走就不直上直下:看图,黑字是每一段消耗的体力。

  1. 如图红线和绿线的花费是一样的,所以不如直接找到使得 HiHl(li) 最大的 i ,在 l 上原地升天 HiHl(li) 这么多。(不知道这个柿子是什么意思?就是相当于一个“经过 Hi 的斜率为1的直线(在图中用黑线表示)在 l 上的截距”,就是红线和绿线在起点直上直下的部分)。

  1. 如图,绿线顶个尖尖出来完全没有必要,路径一定是尽可能地“平”。

经过这一番贪心,我们可以得出最终路径的大概模样:

设“使得 HiHl(li) 最大的 i ”为 lhigh ,“使得 HiHr(ir) 最大的 i ”为 rhigh

第一部分: llhigh 。(图中为 12

第二部分: lhighrhigh 。(图中为 28

第三部分: rhighr 。(图中为 88 ,不存在)

我们算一下每一部分的体力消耗。

第一部分:显然就是 4(HlhighHl) (四倍高度差)。

第二部分:设 [lhigh,rhigh] 中的最高峰为 nhigh

第二部分可以再分成三小段:上升段,平行段,下降段。

上升段为 4(HnhighHlhigh) (也是四倍高度差)

下降段为 HnhighHrhigh (一倍高度差)

平行段为 2((rhighlhigh)(HnhighHlhigh)(HnhighHrhigh)) (总长度减去上升段和下降段就是平行段)

第三部分: HrhighHr (一倍高度差)

最后相加就是总体力消耗了。

下面,考虑一下 lhigh,rhigh,nhigh 怎么求。

nhigh 不必多说。ST表,在初始化的时候顺带维护最大值的位置即可。

至于 lhigh ,看一下定义: HiHl(li) 最大的 iHll 是定值,对于每个山都一样。把这两个东西从柿子里扔掉,我们发现,第 i 个山获得了 i 的加成。所以。对于所有 Hi ,我们直接把 Hi=i ,此时 lhigh 就是区间 [l,r] 的最大值。ST表维护。

rhigh 区别不大,改成 Hi=(Ni+1) 即可。

最后,我们就完成了这道题。

还有一些细节:

  1. 有可能 l>rswap一下然后把四倍高度差一倍高度差对调一下就行。

  2. 山的高度 <1e9 ,会爆int,开long long

阴间代码警告!!

#include<bits/stdc++.h>
#define int long long
using namespace std;
struct ST{
	int val;
	int pos;//最大值的位置也需要维护
};
int a[200005];
int l_a[200005];
int r_a[200005];
ST st_l[200005][19];
ST st_r[200005][19];
ST st_n[200005][19];
signed main(){
	int n,q;
	cin>>n;
	for(int i=1;i<=n;i++){
		cin>>a[i];
	}
	for(int i=1;i<=n;i++){
		l_a[i]=a[i]-i;//为了维护lhigh,处理原数据
		r_a[i]=a[i]-(n-i+1);//rhigh
	}
	for(int i=1;i<=n;i++){
		st_l[i][0].val=l_a[i];
		st_r[i][0].val=r_a[i];
		st_n[i][0].val=a[i];
		st_l[i][0].pos=i;
		st_r[i][0].pos=i;
		st_n[i][0].pos=i;
	}
	for(int j=1;j<=19;j++){//大型初始化现场
		for(int i=1;i+(1<<j)-1<=n;i++){
			if(st_l[i][j-1].val>st_l[i+(1<<(j-1))][j-1].val){
				st_l[i][j].val=st_l[i][j-1].val;
				st_l[i][j].pos=st_l[i][j-1].pos;
			}else{
				st_l[i][j].val=st_l[i+(1<<(j-1))][j-1].val;
				st_l[i][j].pos=st_l[i+(1<<(j-1))][j-1].pos;
			}
		
			if(st_r[i][j-1].val>st_r[i+(1<<(j-1))][j-1].val){
				st_r[i][j].val=st_r[i][j-1].val;
				st_r[i][j].pos=st_r[i][j-1].pos;
			}else{
				st_r[i][j].val=st_r[i+(1<<(j-1))][j-1].val;
				st_r[i][j].pos=st_r[i+(1<<(j-1))][j-1].pos;
			}
		
			if(st_n[i][j-1].val>st_n[i+(1<<(j-1))][j-1].val){
				st_n[i][j].val=st_n[i][j-1].val;
				st_n[i][j].pos=st_n[i][j-1].pos;
			}else{
				st_n[i][j].val=st_n[i+(1<<(j-1))][j-1].val;
				st_n[i][j].pos=st_n[i+(1<<(j-1))][j-1].pos;
			}
		}
	}
	cin>>q;
	for(int i=1;i<=q;i++){
		bool rev=false;
		int l,r;
		cin>>l>>r;
		if(l>r){//如果l>r,交换,标记reverse用来调整体力倍率
			rev=true;
			swap(l,r);
		}
		ST lhigh=(st_l[l][__lg(r-l+1)].val>st_l[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
				 st_l[l][__lg(r-l+1)]:st_l[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
		ST rhigh=(st_r[l][__lg(r-l+1)].val>st_r[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
				 st_r[l][__lg(r-l+1)]:st_r[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
		ST nhigh=(st_n[l][__lg(r-l+1)].val>st_n[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)].val?
				 st_n[l][__lg(r-l+1)]:st_n[r-(1<<(__lg(r-l+1)))+1][__lg(r-l+1)]);
		//阴间的......
		cout<<
			  (a[lhigh.pos]-a[l])*(rev?1:4)
			+ (a[nhigh.pos]-a[lhigh.pos])*(rev?1:4)
			+ (rhigh.pos-lhigh.pos-(a[nhigh.pos]-a[lhigh.pos])-(a[nhigh.pos]-a[rhigh.pos]))*2
			+ (a[nhigh.pos]-a[rhigh.pos])*(rev?4:1)
			+ (a[rhigh.pos]-a[r])*(rev?4:1)
		<<endl;
	}
}

闲话:其实这道题除了代码毒瘤一点还是很可以的,把题目中“行变换计算体力”改成“列变换计算体力”也能做思路差不多而且会简单很多(不要问我为什么知道 问就是读错题了...)。另外,用线段树做一个带修的版本也很好,甚至可以再加上山的删除(类似链表那种删除,删掉以后位置也被挤掉那种),用线段树应该也可以进行解决(没仔细想,应该没问题),再毒瘤一点可以再塞一个在末尾增加山,好像也能做......总之扩展一下还能玩出很多新花样,然后在毒瘤的路上越走越远[doge]

0x40 额外题单

P2880 [USACO07JAN] Balanced Lineup G:板子。

P7809 [JRKSJ R2] 01 序列:维护的东西很有意思

posted @   Cerebral_fissure  阅读(65)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 地球OL攻略 —— 某应届生求职总结
· 周边上新:园子的第一款马克杯温暖上架
· Open-Sora 2.0 重磅开源!
· 提示词工程——AI应用必不可少的技术
· .NET周刊【3月第1期 2025-03-02】
点击右上角即可分享
微信分享提示