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}(nlog _ 2n)) | (mathcal{O}(log_2 n)) |
离线(mathrm{Tarjan}) | (-) | (mathcal{O}(nalpha(n)+q)) |
(mathrm{dfs})序转化的(mathrm{Spare Table})算法 | (mathcal{O}(nlog_2 n)) | (mathcal{O}(1)) |
(mathrm{RMQ})算法 | 预处理复杂度 | 询问复杂度 |
---|---|---|
线段树 | (mathcal{O}(n)) | (mathcal{O}(log_2 n)) |
(mathrm{Spare Table})算法 | (mathcal{O}(nlog _ 2n)) | (mathcal{O}(1)) |
(mathrm{Four Russian})算法 | (mathcal{O}(nlog_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 nlog ^2 n))时间预处理,(mathcal{O}(1))回答的算法。
缺点在于,上述算法实现难度太大,转化太多,实用性不大。
我们有更简单的解决方案,我们可以暴力处理块内询问,时间复杂度最差为(mathrm{O}(n+qlog_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)),由于博主没有写过,就不详细讲了。