zoukankan      html  css  js  c++  java
  • 关于RMQ的一些拓展

    引入

    关于RMQRMQ问题(静态区间最值查询),我们一般用的STST表,但是还有很多其他用法与用途。


    静态区间最值

    也就是对于一个序列AA,我们每次要查询一个区间lrlsim r中的min/max{Ai}min/max{A_i}

    其实一般用树状数组或者线段树可以做到nlogn+Qlognnlogn+Qlogn的复杂度QQ为询问数,但是因为是静态的,我们可以用STST表做到nlogn+Qnlogn+Q

    其实思路是这样的,类似于倍增:
    eg

    这样其实也类似于线段树对于一个区间的管理,但是由于是静态的,不涉及修改,所以我们可以用数组代替记录下来,然后直接查询。(查询每次访问两个数组的值是O(1)O(1)的)

    代码实现大概这样:

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

    • 类似用法

    那么RMQRMQ还可以用来求取静态区间gcdgcd,合并方式只不过将max/minmax/min改成了gcdgcd


    树上LCALCA(最近公共祖先)

    我们可以用静态树的在线算法:倍增O(nlogn+Qlogn)O(nlogn+Qlogn),树链剖分O(n+Qlogn)O(n+Qlogn)
    也可以用动态树的在线算法:LCT维护O(nlogn+Qlogn+大常数)O(nlogn+Qlogn+ ext{大常数})
    还可以使用静态树的离线算法:TrajanO(n+m+Q+并查集)O(n+m+Q+ ext{并查集})

    其实,如果询问量较多,可以使用RMQRMQ来实现查询LCALCA

    我们如果求出一棵树的欧拉序,我们来看看,如下图:

    欧拉序:就是在深搜的过程中进入时加一次退出时也加一次,简单点就是每次访问时都加一次

    eg

    我们对其求出的欧拉序为:

    1,2,3,2,4,2,1,5,6,5,7,5,11,2,3,2,4,2,1,5,6,5,7,5,1

    每个点的深度为:
    dep[1]=1dep[1]=1
    dep[2]=2dep[2]=2
    dep[3]=3dep[3]=3
    dep[4]=3dep[4]=3
    dep[5]=2dep[5]=2
    dep[6]=3dep[6]=3
    dep[7]=3dep[7]=3

    然后我们来看,先令st[i]st[i]ii号点最开始出现的位置,对于lca(a,b)lca(a,b),我们就只需查询欧拉序中的st[a]st[b](st[a]st[b])st[a]sim st[b](st[a]leq st[b])深度最小的那个点的编号即可。

    我们模拟一下:
    对于上述图中的lca(3,5)lca(3,5),我们相当于查询st[3]st[5]st[3]sim st[5],那么这里面最小的深度的点就是3,2,4,3,1,53,2,4,3,1,5中的11,而11也确实是它们的lcalca

    其实正确性是这样的,对于欧拉序中的两个开始位置直之间的点,肯定包含完了这个两个点的路径上的所有点,而lcalca肯定在路径上,并且深度是最小的,所以这样就可以求出。

    转欧拉序后长度是n+mn+m,所以复杂度最后为O((n+m)log(n+m)+Q)O((n+m)log(n+m)+Q)的,其中m=n1m=n-1,所以就是O(nlogn+Q)O(nlogn+Q)的。

    代码:

    #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同样优秀的复杂度呢?O(n+Q)O(n+Q),其实是可以的。

    我们观察一个性质,就是欧拉序里面的相邻两点的depdep差不超过11,所以可以使用±1RMQpm 1RMQ

    其实这种RMQRMQ网上很少讲,虽然有,但是不清楚,所以博主自己yyyy了几种方法。


    对于O(nlogn)O(nlogn)的预处理,这是主要要解决的问题,查询O(1)O(1)已经非常优秀了。

    所以我们考虑分块,对于每一块我们做一次RMQRMQ,对于分出来的所有块我们再做一次RMQRMQ,块的大小大概是lognlogn的大小,总共分成nlognlceilfrac{n}{logn} ceil块。

    对于每一块,先内部求RMQRMQ,那么复杂度为nlogn×logn×log(logn)lceilfrac{n}{logn} ceil imes logn imes log(logn),所以复杂度为nloglognnloglogn

    然后知道每一块的最值,我们再对nlognlceilfrac{n}{logn} ceil块求一个RMQRMQ,那么复杂度为nlognlognlognlceilfrac{n}{logn} ceil loglceilfrac{n}{logn} ceil,算下来不到O(n)O(n)

    所以总的复杂度为O(nloglogn)O(nloglogn)

    每次查询则分为三部分,两个块内和一个块间,所以复杂度还是O(1)O(1)的。


    但是这个根本没用到相邻的相差11的性质。
    所以我们再来看,同样分块,将+1,1+1,-1的变化看作0,10,1,我们将,然后对于一块只有2logn2=2logn=n2^{frac{logn}{2}}=sqrt{2^{logn}}=sqrt{n}种不同的情况。

    所以我们枚举这些不同情况(用二进制枚举的方式)
    类似于这种:

    int S=(1<<int(log2(n)+1))>>1;
    for(int i=0;i<=S;i++)work();
    

    然后处理这些情况下,从左往右的前缀最小(大),(应该是处理区间和的最值,也就是偏移量,但是这里实际上的实现似乎有点小问题)。
    如:01010101
    则表示的是1,+1,1,+1-1,+1,-1,+1
    然后我们维护的其实是01010101的前缀和的RMQRMQ

    那么对应到实际上的序列,我们只需知道左端点的值就能快速算出真正最小的值。

    那么将每种区间的情况对应上去,每次查询只需加上偏移值即可(也就是左端点值,如果你开始设置的最左边的一个差为11的话你要减去11,否则加上11)。

    那么复杂度为O(nlogn2loglogn2+nlogn2)O(sqrt{n}frac{logn}{2}logfrac{logn }{2}+lceil{frac{n}{frac{logn}{2}}} ceil)

    块间的处理还是用原来的RMQRMQ的方式,复杂度为O(nlogn2lognlogn2)O(lceilfrac{n}{frac{logn}{2}} ceil loglceilfrac{n}{frac{logn}{2}} ceil),所以最后还是O(n)O(n)的。
    具体来说,在n=1e8n=1e8的时候,复杂度才只有不到6e86e8
    其实计算来就是1e4×13×4+1e8×4+7692308×23=5774430841e4 imes13 imes4+1e8 imes 4+7692308 imes 23=577443084
    而在n=1e7n=1e7的时候就只有:
    3162×12×4+1e7×4+16666667=568184433162 imes 12 imes 4+1e7 imes 4+16666667=56818443
    n=1e6n=1e6的时候只有:
    1000×10×4+1e6×4+1700000=57400001000 imes 10 imes 4+1e6 imes 4+1700000=5740000
    所以常数大概是在565sim 6之间,比nlognnlognlognlogn小的多了,况且询问是O(1)O(1)

    那么对于边界块的特殊处理:
    对于不满长度logn2frac{logn}{2}的块,暴力RMQRMQ即可,复杂度为常数。

    代码实现,目前不太好写,博主就没有写,而且没找到卡nlognnlognLCALCA的题,QWQ。


    最终拓展

    其实对于所有的一般的序列,不满足±1pm1的性质的,我们可以将其转化为笛卡尔树,然后区间最值问题就转化为了树上求LCALCA的问题,就可以用±1RMQpm1RMQ了,于是就可以做到O(n)O(n)

    End

    讲解中也许有很多问题,如果有会±1RMQpm 1RMQ的大佬觉得有问题的话,提出并联系博主。

  • 相关阅读:
    推荐一个博客,或许给技术流的自己一些启示
    Boost多线程-替换MFC线程
    Python:Matplotlib 画曲线和柱状图(Code)
    AI:机器人与关键技术--总是被科普
    OnLineML一:关于Jubatus 的简介...
    使用PCL::GPU::遇到问题
    dll文件:关于MFC程序不能定位输入点
    实践:使用FLANN.LSH进行检索
    模式识别两种方法:知识和数据
    几个方便编程的C++特性
  • 原文地址:https://www.cnblogs.com/VictoryCzt/p/10053400.html
Copyright © 2011-2022 走看看