引入
关于问题(静态区间最值查询),我们一般用的表,但是还有很多其他用法与用途。
静态区间最值
也就是对于一个序列,我们每次要查询一个区间中的
其实一般用树状数组或者线段树可以做到的复杂度为询问数,但是因为是静态的,我们可以用表做到
其实思路是这样的,类似于倍增:
这样其实也类似于线段树对于一个区间的管理,但是由于是静态的,不涉及修改,所以我们可以用数组代替记录下来,然后直接查询。(查询每次访问两个数组的值是的)
代码实现大概这样:
Luogu模板
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=2e5+10,Log=19;
int n,m;
int maxv[Log][M],lg[Log<<1],ref[M],cnt;
void init(){
lg[0]=1;for(cnt=1;;cnt++)
{lg[cnt]=(lg[cnt-1]<<1);if(lg[cnt]>n) break;}
ref[2]=1;for(int i=3;i<=n;i++)ref[i]=ref[i>>1]+1;
for(int i=1;i<=n;i++)scanf("%d",&maxv[0][i]);
for(int i=1;i<=cnt;i++){
for(int j=1,up=n-lg[i]+1;j<=up;j++){
maxv[i][j]=max(maxv[i-1][j],maxv[i-1][j+lg[i-1]]);
}
}
}
int query(int a,int b){
if(a>b)swap(a,b);
int k=0,len=b-a+1;
k=ref[len-1];//预处理这个后才是真正的O(1)
return max(maxv[k][a],maxv[k][b-lg[k]+1]);//查询最新只需max换成min即可
}
int L,R;
int main(){
scanf("%d%d",&n,&m);
init();
for(int i=1;i<=m;i++){
scanf("%d%d",&L,&R);
printf("%d
",query(L,R));
}
return 0;
}
- 类似用法
那么还可以用来求取静态区间,合并方式只不过将改成了。
树上(最近公共祖先)
我们可以用静态树的在线算法:倍增,树链剖分。
也可以用动态树的在线算法:LCT维护
还可以使用静态树的离线算法:Trajan
其实,如果询问量较多,可以使用来实现查询。
我们如果求出一棵树的欧拉序,我们来看看,如下图:
欧拉序:就是在深搜的过程中进入时加一次退出时也加一次,简单点就是每次访问时都加一次
我们对其求出的欧拉序为:
每个点的深度为:
然后我们来看,先令为号点最开始出现的位置,对于,我们就只需查询欧拉序中的深度最小的那个点的编号即可。
我们模拟一下:
对于上述图中的,我们相当于查询,那么这里面最小的深度的点就是中的,而也确实是它们的
其实正确性是这样的,对于欧拉序中的两个开始位置直之间的点,肯定包含完了这个两个点的路径上的所有点,而肯定在路径上,并且深度是最小的,所以这样就可以求出。
转欧拉序后长度是,所以复杂度最后为的,其中,所以就是的。
代码:
#include<cstdio>
#include<cstring>
#include<algorithm>
using namespace std;
const int M=6e5+10,Log=22;
int n,m,lg[M<<1],s;
struct node{
int p,dep;
node(){}
node(int a,int b):p(a),dep(b){}
bool operator <(const node &a)const{return dep<a.dep;}
}maxv[Log][M<<1];
struct ss{
int to,last;
ss(){}
ss(int a,int b):to(a),last(b){}
}g[M<<1];
int head[M],cnt;
void add(int a,int b){
g[++cnt]=ss(b,head[a]);head[a]=cnt;
g[++cnt]=ss(a,head[b]);head[b]=cnt;
}
int dep[M],pos[M],tot;
void dfs(int a,int b){
dep[a]=dep[b]+1;maxv[0][pos[a]=++tot]=node(a,dep[a]);
for(int i=head[a];i;i=g[i].last){
if(g[i].to==b) continue;
dfs(g[i].to,a);
maxv[0][++tot]=node(a,dep[a]);
}
}
void init(){
lg[2]=lg[3]=1;
for(int i=4;i<=tot;i++)lg[i]=lg[i>>1]+1;
for(int i=1;(1ll<<i)<=tot;i++){
for(int j=1;j<=tot;j++){
maxv[i][j]=min(maxv[i-1][j],maxv[i-1][j+(1<<(i-1))]);
}
}
}
int getlca(int a,int b){
if(a>b)swap(a,b);
int k=lg[b-a+1];
return min(maxv[k][a],maxv[k][b-(1<<k)+1]).p;
}
int a,b;
int main(){
scanf("%d%d%d",&n,&m,&s);
for(int i=1;i<n;i++){
scanf("%d%d",&a,&b);
add(a,b);
}
dfs(s,0);
init();
for(int i=1;i<=m;i++){
scanf("%d%d",&a,&b);
printf("%d
",getlca(pos[a],pos[b]));
}
return 0;
}
拓展
我们能不能做到和离线的Tarjan同样优秀的复杂度呢?,其实是可以的。
我们观察一个性质,就是欧拉序里面的相邻两点的差不超过,所以可以使用
其实这种网上很少讲,虽然有,但是不清楚,所以博主自己了几种方法。
对于的预处理,这是主要要解决的问题,查询已经非常优秀了。
所以我们考虑分块,对于每一块我们做一次,对于分出来的所有块我们再做一次,块的大小大概是的大小,总共分成块。
对于每一块,先内部求,那么复杂度为,所以复杂度为
然后知道每一块的最值,我们再对块求一个,那么复杂度为,算下来不到。
所以总的复杂度为。
每次查询则分为三部分,两个块内和一个块间,所以复杂度还是的。
但是这个根本没用到相邻的相差的性质。
所以我们再来看,同样分块,将的变化看作,我们将,然后对于一块只有种不同的情况。
所以我们枚举这些不同情况(用二进制枚举的方式)
类似于这种:
int S=(1<<int(log2(n)+1))>>1;
for(int i=0;i<=S;i++)work();
然后处理这些情况下,从左往右的前缀最小(大),(应该是处理区间和的最值,也就是偏移量,但是这里实际上的实现似乎有点小问题)。
如:
则表示的是。
然后我们维护的其实是的前缀和的。
那么对应到实际上的序列,我们只需知道左端点的值就能快速算出真正最小的值。
那么将每种区间的情况对应上去,每次查询只需加上偏移值即可(也就是左端点值,如果你开始设置的最左边的一个差为的话你要减去,否则加上)。
那么复杂度为
块间的处理还是用原来的的方式,复杂度为,所以最后还是的。
具体来说,在的时候,复杂度才只有不到。
其实计算来就是
而在的时候就只有:
当的时候只有:
所以常数大概是在之间,比的小的多了,况且询问是。
那么对于边界块的特殊处理:
对于不满长度的块,暴力即可,复杂度为常数。
代码实现,目前不太好写,博主就没有写,而且没找到卡的的题,QWQ。
最终拓展
其实对于所有的一般的序列,不满足的性质的,我们可以将其转化为笛卡尔树,然后区间最值问题就转化为了树上求的问题,就可以用了,于是就可以做到。
End
讲解中也许有很多问题,如果有会的大佬觉得有问题的话,提出并联系博主。