『LCA和RMQ问题杂谈』
LCA问题和RMQ问题的转化
首先\(\mathrm{LCA}\)问题指的是求解树上两点的最近公共祖先,\(\mathrm{RMQ}\)问题指的是求解数列区间最值。
LCA转RMQ
\(\mathrm{LCA}\)问题转\(\mathrm{RMQ}\)问题应该是人尽皆知了,我们可以先跑出树的\(\mathrm{dfs}\)序,使用每次进入或回到节点都记录一次的那种\(\mathrm{dfs}\)序,那么只需记录每个节点第一次出现位置就可以查询了。
具体来说,我们找到两个点分别在\(\mathrm{dfs}\)序中第一次出现的位置,那么容易得知它们的\(\mathrm{LCA}\)就是这段序列区间内深度最浅的点,那么就把问题转换成了寻找区间最小值。
RMQ转LCA
这个可能稍微高级一点。首先我们要知道笛卡尔树,就是把\(n\)个二元组\((x,y)\)建成一棵树,使得\(x\)这一维是二叉搜索树,\(y\)这一维是堆,当然大根堆小根堆都可。可想而知,\(\mathrm{Treap}\)就是第二维随机的笛卡尔树。
那么根据笛卡尔树的定义可知,一个区间的最大\(/\)最小值就是这两点在笛卡尔树上的\(\mathrm{LCA}\),因为深度越浅的节点优先级越高,并且它们的\(\mathrm{LCA}\)一定被包括在序列区间内,这样就把问题转换成求\(\mathrm{LCA}\)了。
转化算法
首先\(\mathrm{LCA}\)转\(\mathrm{RMQ}\)不用多说,\(\mathrm{dfs}\)是\(\mathcal{O}(n)\)的,那么我们需要考虑一下如何根据序列构造笛卡尔树。
\(\mathrm{naive}\)的方法就是用数据结构找区间最值,递归建树,不过这样你都会找区间最值了,那还有什么好转的呢?
其实笛卡尔树有\(\mathcal{O}(n)\)的构建方法,只需要每次维护前缀笛卡尔树右链上的节点就可以了,小根堆笛卡尔树参考代码如下:
for (int i = 1 , k; i <= n; i++)
{
for (k = top; k && h[st[k]] > h[i]; k--);
if ( k != 0 ) son[st[k]][1] = i;
if ( k < top ) son[i][0] = st[k+1];
st[++k] = i , top = k;
}
LCA问题和RMQ问题的解决算法
经典算法
这个应该不用多说,网络上资料很多,我们对比一下即可。
\(\mathrm{LCA}\)算法 | 预处理复杂度 | 询问复杂度 |
---|---|---|
树链剖分 | \(\mathcal{O}(n)\) | \(\mathcal{O}(\log_2 n)\) |
树上倍增 | \(\mathcal{O}(n\log _ 2n)\) | \(\mathcal{O}(\log_2 n)\) |
离线\(\mathrm{Tarjan}\) | \(-\) | \(\mathcal{O}(n\alpha(n)+q)\) |
\(\mathrm{dfs}\)序转化的\(\mathrm{Spare\ Table}\)算法 | \(\mathcal{O}(n\log_2 n)\) | \(\mathcal{O}(1)\) |
\(\mathrm{RMQ}\)算法 | 预处理复杂度 | 询问复杂度 |
---|---|---|
线段树 | \(\mathcal{O}(n)\) | \(\mathcal{O}(\log_2 n)\) |
\(\mathrm{Spare\ Table}\)算法 | \(\mathcal{O}(n\log _ 2n)\) | \(\mathcal{O}(1)\) |
\(\mathrm{Four\ Russian}\)算法 | \(\mathcal{O}(n\log_2 \log_2 n)\) | \(\mathcal{O}(1)\) |
- \(\mathrm{Four\ Russian}\)算法指的是将序列分为\(\log_2 n\)块,块间和块内分别处理\(\mathrm{Spare\ Table}\)的\(\mathrm{RMQ}\)算法。
更高效的算法
然而,毒瘤们肯定不会满足于上面这些简单经典算法的时间复杂度。
首先对于\(\mathrm{LCA}\)问题,我们可以跑\(\mathrm{dfs}\)序\(\mathcal{O}(n)\)转化为\(\mathrm{RMQ}\)问题,而我们注意到\(\mathrm{dfs}\)序中相邻两个元素差的绝对值不超过\(1\),我们称之为\(\mathrm{In-RMQ}\),可以利用这个性质优化算法。
当然对于一般的\(\mathrm{RMQ}\),可以多一步笛卡尔树的转化,再跑\(\mathrm{dfs}\)序,同样可以转化为\(\mathrm{In-RMQ}\)问题。
考虑把序列分成\(x\)块,每块处理最值,然后对块之间处理\(\mathrm{Spare\ Table}\)。当\(x\)取\(\log_2n\)时,预处理时间复杂度不大于\(O(n)\)。
然后我们预处理每块的前缀后缀最值,这样就可以\(\mathcal{O}(1)\)回答跨越两个块的询问了。
那么我们现在要做的就是想办法快速处理同一个块内的询问。首先我们注意到对于\(\pm 1\)序列相同的数列,其\(\mathrm{In-RMQ}\)问题的解都相同,现在我们只要把序列分成大小为\(\frac{\log_2 n}{2}\)的块,那么本质不同的块就只有\(n^{0.5}\)种。对于每一个本质不同的块,直接\(\log^2n\)处理答案,那么就可以得到一个\(\mathcal{O}(\sqrt n\log ^2 n)\)时间预处理,\(\mathcal{O}(1)\)回答的算法。
缺点在于,上述算法实现难度太大,转化太多,实用性不大。
我们有更简单的解决方案,我们可以暴力处理块内询问,时间复杂度最差为\(\mathrm{O}(n+q\log_2n)\)。但是,由于绝大多数询问都是\(\mathcal{O}(1)\)回答的,所以常数极小。并且,在数据随机的情况下,可以直接认为其回答一次询问的期望复杂度为\(\mathcal{O}(1)\)。由于我们可以微调块大小,所以此算法几乎不可卡满。 更大的好处是,代码量减小了,甚至不需要笛卡尔树的转化。
这里提供一份参考代码:
const int N = 2e7 + 2 , LogN = 26;
int n,m,s,Size,T,a[N],Log[N/24],pre[N],suf[N],f[N/24][LogN];
#define Lborder(x) ( (x-1) * Size + 1 )
#define Rborder(x) ( x == T ? n : Size * x )
#define Belong(x) ( ( x % Size == 0 ) ? ( x / Size ) : ( x / Size + 1 ) )
inline void Setblocks(void)
{
Size = log(n) / log(2) /*sqrt(n)*/ , T = n / Size;
if ( T * Size < n ) ++T; Log[1] = 0;
for (register int i = 2; i <= T; i++) Log[i] = Log[i>>1] + 1;
for (register int i = 1; i <= T; ++i)
{
int Max = 0 , L = Lborder(i) , R = Rborder(i);
for (register int j = L; j <= R; ++j) Max = max( Max , a[j] );
f[i][0] = Max , pre[L] = a[L] , suf[R] = a[R];
for (register int j = L + 1; j <= R; j++) pre[j] = max( pre[j-1] , a[j] );
for (register int j = R - 1; j >= L; j--) suf[j] = max( suf[j+1] , a[j] );
}
for (register int k = 1; (1<<k) <= T; k++)
for (register int i = 1; i + (1<<k) - 1 <= T; i++)
f[i][k] = max( f[i][k-1] , f[ i + (1<<k-1) ][k-1] );
}
inline int Query(int l,int r)
{
int L = Belong(l) , R = Belong(r) , Ans = 0;
if ( L + 1 == R ) return max( suf[l] , pre[r] );
else if ( L + 1 < R ) {
int k = Log[R-L-1] , res = max( suf[l] , pre[r] );
return max( res , max( f[L+1][k] , f[R-(1<<k)][k] ) );
}
for (register int i = l; i <= r; i++) Ans = max( Ans , a[i] );
return Ans;
}
该算法还可以使用根据巧妙的分块大小优化,使其时间复杂度达到严格\(\mathcal{O}((n+q)\sqrt{\log_2 n})\)
神奇的是,我们还可以换一种思路:针对块内询问,我们状压以每个点为左端点开始的单调队列,使用位运算技巧可以直接得到答案,时间复杂度严格\(O(n+q)\),由于博主没有写过,就不详细讲了。