跳表
倍增
倍增是一种基于二进制待定系数的二分思路,它在一定程度上比l,r,mid的形式需要考虑的边界问题更少,但是它的常数更大。
int x=-1;
for(int i=16;~i;i--)if(check(x+(1<<i)))x+=(1<<i);
倍增在树上更常见一点。或者说,在链式结构上更常见。
数组上可以用l,r,mid这种形式的二分查找,从本质来讲是因为数组在空间上是连续的。
而链表结构在空间上不连续,所以没办法用这种形式二分。
跳表
跳表又叫ST算法,它的本质是一种倍增算法。
跳表是为了处理静态查询问题,绝大部分问题都是动态的,而且即使是静态查询问题,跳表也不比线段树更有优势。
跳表真正的优势在于,预处理时间和查询时间的不对等(预处理Nlog,查询O(1))
当数据量只有1e5,而查询数达到了1e7并且强制在线。
这个时候跳表以绝对优势暴打了绝大部分算法。
跳表有两种
步长为根号和二进制倍增跳表
不同种类有各自使用场景
先给个跳表的老本行RMQ(区间查询极大/小值)问题。
给一个长度大小为N的数组,进行若干次查询。
如果这个“若干次”=N,那它可能打不过线段树或者和线段树打平。
如果这个“若干次”=10N,那么倍增跳表将很有很大优势
如果这个“若干次”=100N,那么根号跳表开始具有优势。
这里的优势是指与线段树相比。
根号跳表
先开一个叫做\(ST_{small}[N][ \sqrt N ]\)的二维数组。
\(ST_{small}[i][j]\)表示以i为起点,向后延伸长度为j的区间信息。
接下来开一个叫做\(ST_{big}[N][\sqrt N]\)的二维数组。
\(ST_{big}[i][j]\)表示以i为起点,向后延伸长度为\(j*\sqrt N\)的区间信息。
显然\(ST_{big}[i][1]=ST_{small}[i][\sqrt N]\);
然后对于任意一段查询\(max{l,r}\)
转化成\(max(ST_{big}[l][len/ \sqrt N],ST_{small}[l+len/ \sqrt N *\sqrt N ][len-len/ \sqrt N *\sqrt N])\)
虽然很难看,但是它也是一种O(1)查询的算法。
sqrt(N)一般用int block=sqrt(N)声明,以防len/sqrt(N)有精度问题。
这种思想有的地方会叫做“大小分治”或者“大步小步”。
总之就是利用N=isqrt(N)+j这个等式使得i,j都在sqrt(N)的范围内导致预处理成为可能。
还有一个典型的例子就是光速幂。
光速幂大概就是求斐波那契数列第n项\((n\leq 10^9)\),q次询问\((q \leq 10^7)\)
令\(\sqrt 10^9\)约为31622,A为矩阵
\(ST_{small}存A^0,A^1,···,A^{31622}\)
\(ST_{big}存A^{31622},A^{31622*2},····,A^{10^9}\)
这样就能O(1)求出特定项
倍增跳表
如果是朴素的查询RMQ,或者其他可重复合并信息,举个例子,区间gcd。也就是多次重复合并不影响结果的情况下,千万别写根号跳表,写倍增跳表。
倍增跳表是这样被定义的,首先开一个叫做\(ST[N][log(N)]\)。
ST[i][j]表示以i为起点,向后延伸\(2^j\)的区间信息。
显然存在dp转移方程
\(ST[i][j]=max(ST[i][j-1],ST[i+(1<<(j-1))][j-1]);\)
\(k=log_2(r-l+1)\)
\(max(ST[l][k],ST[r-(1<<k)+1][k])\)
这里有个强烈建议,你的log2不要调用系统函数。
众所周知,在处理整数问题上,pow,log,sqrt等函数都比较有问题。
第一是精度问题,第二是时间常数问题。
有的时候如果需要“判断是否为k的幂”,“平方/立方数判断”,“取以二为底的对数”。这种如果范围不大,我都建议你直接暴力跑一个预处理。
这里的话其实就是
LG2[1]=0
for(int i=2;i<N;++i)LG[i]=LG[i/2]+1;