zoukankan      html  css  js  c++  java
  • sss

    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)),由于博主没有写过,就不详细讲了。

  • 相关阅读:
    OpenRisc-52-run openrisc&orpmon on ml501 board
    PHP之APC缓存详细介绍(转)
    ios 使用GCD 多线程 教程
    poj2454
    尝鲜delphi开发android/ios_环境搭建
    HDU 3308 线段树 最长连续上升子序列 单点更新 区间查询
    jQuery 表格排序插件 Tablesorter 使用
    Oracle 常见错误
    安卓开发44:解决 INSTALL_FAILED_UID_CHANGED 等问题
    Java的native方法
  • 原文地址:https://www.cnblogs.com/Parsnip/p/12660750.html
Copyright © 2011-2022 走看看