zoukankan      html  css  js  c++  java
  • 看到的一个很不错的分析LCA和RMQ的文章(转载,先收着)

    首先请看定义:

    一、最近公共祖先(Least Common Ancestors)
    对于有根树T的两个结点u、v,最近公共祖先LCA(T,u,v)表示一个结点x,满足x是u、v的祖先且x的深度尽可能大。另一种理解方式是把T理解为一个无向无环图,而LCA(T,u,v)即u到v的最短路上深度最小的点。
    这里给出一个LCA的例子:
    例一
    对于T=<V,E>
    V={1,2,3,4,5}
    E={(1,2),(1,3),(3,4),(3,5)}
    则有:
    LCA(T,5,2)=1
    LCA(T,3,4)=3
    LCA(T,4,5)=3

    二、RMQ问题(Range Minimum Query)
    RMQ问题是指:对于长度为n的数列A,回答若干询问RMQ(A,i,j)(i,j<=n),返回数列A中下标在[i,j]里的最小值下标。这时一个RMQ问题的例子:
    例二
    对数列:5,8,1,3,6,4,9,5,7 有:
    RMQ(2,4)=3
    RMQ(6,9)=6

    然后给出两种问题各自的算法和解析

    . RMQ问题的ST算法

    const int MAXN=100000+1;

    const int MAXF=17;

    const int INF=0x7FFFFFFF;

    //可?以?断?言?ceiil(log(MAXN,2))==MAXF

     

    inline int max(int a,int b){return a>b?a:b;}

    inline int min(int a,int b){return a<b?a:b;}

    class{

        int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

        int dp_min[MAXN][MAXF+1];

    public:

        void init(int* a,int n){

            for(int i=0;i<n;i++){

                dp_max[i][0]=a[i];

                dp_min[i][0]=a[i];

            }

            for(int f=1,s=1;s<n;s=(1<<f++)){

                for(int i=0;i+s<n;i++){

                    dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);

                    dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

                }

            }

        }

     

        int query_max(int l,int r){

            if(l>r)return -INF;

            int d=r-l+1;

            int f;

            for(f=0;(1<<f)<=d;f++);

            f--;

            return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);

        }

     

        int query_min(int l,int r){

            if(l>r)return -INF;

            int d=r-l+1;

            int f;

            for(f=0;(1<<f)<=d;f++);

            f--;

            return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

        }

     

    }RMQ;

    来看一下ST算法是怎么实现的(以最小值为例)最小值只需将min换成max即可:

            首先是预处理,用一个DP解决。设a[i]是要求区间最值的数列,f[i,j]表示从第i个数起连续2^j个数中的最小值。例如数列3 2 4 5 6 8 1 2 9 7 ,f[1,0]表示第1个数起,长度为2^0=1的最小值,其实就是3这个数。f[1,2]=5,f[1,3]=8,f[2,0]=2,f[2,1]=4……从这里可以看出f[i,0]其实就等于a[i]。这样,Dp的状态、初值都已经有了,剩下的就是状态转移方程。我们把f[i,j]平均分成两段(因为f[i,j]一定是偶数个数字),从i到i+2^(j-1)-1为一段,i+2^(j-1)到i+2^j-1为一段(长度都为2^(j-1))。用上例说明,当i=1,j=3时就是3,2,4,5 和 6,8,1,2这两段。f[i,j]就是这两段的最小值中的最小值。于是我们得到了动规方程F[i,j]=minF[ij-1],F[i+2^(j-i)j-1].

    接下来是得出最值,也许你想不到计算出f[i,j]有什么用处,一般毛想想计算max还是要O(logn),甚至O(n)。但有一个很好的办法,做到了O(1)。还是分开来。如在上例中我们要求区间[2,8]的最小值,就要把它分成[2,5]和[5,8]两个区间,因为这两个区间的最小值我们可以直接由f[2,2]和f[5,2]得到。扩展到一般情况,就是把区间[l,r]分成两个长度为2^n的区间(保证有f[i,j]对应)。直接给出表达式:

    k:=trunc(l(r-l+1)/ln(2));

    ans:=min(F[lk],F[r-2^k+1,k]);这样就计算了从i开始,长度为2^t次的区间和从r-2^i+1开始长度为2^t的区间的最小值(表达式比较烦琐,细节问题如加1减1需要仔细考虑

    . LCA问题的Tarjan离线算法

    int tree[10001][100],in[10001],p[10001];

    int cas,s,t;

    int n,Q1,Q2;

    bool v[10001];

    void Make_Set(int t)

    {

        p[t]=t;

    }

    int Find_Set(int t)

    {

        if(t!=p[t])

        {

            p[t]=Find_Set(p[t]);

        }

        return p[t];

    }

    void Union(int u,int v)

    {

        p[v]=u;

    }

    int LCA(int u)

    {

        Make_Set(u);

        int i;

        for(i=1;i<=tree[u][0];i++)

        {

            LCA(tree[u][i]);

            Union(u,tree[u][i]);

        }

        v[u]=1;

        if(u==Q1&&v[Q2])

        {

            printf("%d\n",p[Find_Set(Q2)]);

        }

        else if(u==Q2&&v[Q1])

        {

            printf("%d\n",p[Find_Set(Q1)]);

        }

        return 0;

    }

    Tarjan算法基于深度优先搜索的框架,对于新搜索到的一个结点,首先创建由这个结点构成的集合,再对当前结点的每一个子树进行搜索,每搜索完一棵子树,则可确定子树内的LCA询问都已解决。其他的LCA询问的结果必然在这个子树之外,这时把子树所形成的集合与当前结点的集合合并,并将当前结点设为这个集合的祖先。之后继续搜索下一棵子树,直到当前结点的所有子树搜索完。这时把当前结点也设为已被检查过的,同时可以处理有关当前结点的LCA询问,如果有一个从当前结点到结点v的询问,且v已被检查过,则由于进行的是深度优先搜索,当前结点与v的最近公共祖先一定还没有被检查,而这个最近公共祖先的包涵v的子树一定已经搜索过了,那么这个最近公共祖先一定是v所在集合的祖先。

    最后讲解他们的转换关系

    RMQ问题与LCA问题的关系紧密,可以相互转换,相应的求解算法也有异曲同工之妙。下面给出LCA问题向RMQ问题的转化方法。

    对树进行深度优先遍历,每当“进入”或回溯到某个结点时,将这个结点的深度存入数组E最后一位。同时记录结点i在数组中第一次出现的位置(事实上就是进入结点i时记录的位置),记做R[i]。如果结点E[i]的深度记做D[i],易见,这时求LCA(T,u,v),就等价于求E[RMQ(D,R[u],R [v])],(R[u]<R[v])。例如,对于第一节的例一,求解步骤如下:

    数列E[i]为:1,2,1,3,4,3,5,3,1

    R[i]为:1,2,4,5,3

    D[i]为:0,1,0,1,2,1,2,1,0

    于是有:

    LCA(T,5,2) = E[RMQ(D,R[2],R[5])] = E[RMQ(D,2,7)] = E[3] = 1

    LCA(T,3,4) = E[RMQ(D,R[3],R[4])] = E[RMQ(D,4,5)] = E[4] = 3

    LCA(T,4,5) = E[RMQ(D,R[4],R[5])] = E[RMQ(D,5,7)] = E[6] = 3

    易知,转化后得到的数列长度为树的结点数的两倍加一,所以转化后的RMQ问题与LCA问题的规模同次。

    再举一个例子帮助理解:

       (1)

        / \

    (2)   (7)

    / \     \

    (3) (4)   (8)

        /   \

    (5)    (6)

    一个nlogn 预处理,O(1)查询的算法.

    Step 1:

            按先序遍历整棵树,记下两个信息:结点访问顺序和结点深度.

            如上图:

            结点访问顺序是: 1 2 3 2 4 5 4 6 4 2 1 7 8 7 1 //共2n-1个值

            结点对应深度是: 0 1 2 1 2 3 2 3 2 1 0 1 2 1 0

    Step 2:

            如果查询结点3与结点6的公共祖先,则考虑在访问顺序中

            3第一次出现,到6第一次出现的子序列: 3 2 4 5 4 6.

            这显然是由结点3到结点6的一条路径.

            在这条路径中,深度最小的就是最近公共祖先(LCA). 即

            结点2是3和6的LCA.

    Step 3:

            于是问题转化为, 给定一个数组R,及两个数字i,j,如何找出

            数组R中从i位置到j位置的最小值..

            如上例,就是R[]={0,1,2,1,2,3,2,3,2,1,0,1,2,1,0}.

            i=2;j=7;

            接下来就是经典的RMQ问题.

    总结:

    RMQ是给定一列数,动态询问[i,j]区间内的最小(或最大值)。

    LCA是给定一棵树,动态询问u和v的最近公共祖先。

    解决这两种问题都有个很重要的倍增思想(这个思想在后缀数组方面亦有所应用)。

    关键需要记住的是

    在LCA预处理的时候

    p[i,j] 表示i的2^j 倍祖先

    那么就有一个递推式子 p[i,j]=p[p[i,j-1],j-1]

    RMQ和LCA可以相互转化。。   所以只要记住一种就行了。。

    RMQ转LCA的时候是生成一棵类似于堆的递归树;LCA转RMQ的时候用到的是深度优先遍历。

    主要掌握的不在于算法,而是在于倍增思想

    附三份源代码

    //POJ_3264 RMQ ST算?法?

    #include <iostream>

     

    using namespace std;

     

    const int MAXN=100000+1;

    const int MAXF=17;

    const int INF=0x7FFFFFFF;

    //可?以?断?言?ceiil(log(MAXN,2))==MAXF

     

    inline int max(int a,int b){return a>b?a:b;}

    inline int min(int a,int b){return a<b?a:b;}

    class{

        int dp_max[MAXN][MAXF+1];//dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

        int dp_min[MAXN][MAXF+1];

    public:

        void init(int* a,int n){

            for(int i=0;i<n;i++){

                dp_max[i][0]=a[i];

                dp_min[i][0]=a[i];

            }

            for(int f=1,s=1;s<n;s=(1<<f++)){

                for(int i=0;i+s<n;i++){

                    dp_max[i][f]=max(dp_max[i][f-1],dp_max[i+s][f-1]);

                    dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

                }

            }

        }

     

        int query_max(int l,int r){

            if(l>r)return -INF;

            int d=r-l+1;

            int f;

            for(f=0;(1<<f)<=d;f++);

            f--;

            return max(dp_max[l][f],dp_max[r-(1<<f)+1][f]);

        }

     

        int query_min(int l,int r){

            if(l>r)return -INF;

            int d=r-l+1;

            int f;

            for(f=0;(1<<f)<=d;f++);

            f--;

            return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

        }

     

    }RMQ;

     

    int main()

    {

        int n,q;

        int i,j,k;

        int a[50005];

        freopen("input.txt", "r", stdin);

        freopen("output.txt", "w", stdout);

        while (scanf("%d%d", &n, &q) !=EOF)

        {

     

            for (i=0; i<n; i++)

            {

                scanf("%d", &a[i]);

            }

            RMQ.init(a,n);

            for (i=0; i<q; i++)

            {

                scanf("%d%d", &j, &k);

     

                int t = RMQ.query_max(j-1,k-1);

                int s = RMQ.query_min(j-1,k-1);

                printf("%d\n",t-s);

            }

        }

        return 0;

    }

    //POJ_ 1330 lCA转?RMQ

    #include <iostream>

    #include <vector>

    using namespace std;

     

    #define MAXN 20005

     

    //LCA

    int parent[MAXN];

    vector<int> son[MAXN];

    vector<int> E,D;

    int R[MAXN*2];

     

    const int MAXF=17;

    const int INF=0x7FFFFFFF;

    //可?以?断?言?ceiil(log(MAXN,2))==MAXF

     

    inline int min(int a,int b){return a<b?a:b;}

    class{

        int dp_min[MAXN][MAXF+1]; //dp[i][j]表?示?从?a[i]起?连?续?^j次?方?个?数?的?最?大?值?

    public:

        void init(int n){

            for(int i=0;i<n;i++){

                dp_min[i][0]=D[i];

            }

            for(int f=1,s=1;s<n;s=(1<<f++)){

                for(int i=0;i+s<n;i++){

                    dp_min[i][f]=min(dp_min[i][f-1],dp_min[i+s][f-1]);

                }

            }

        }

     

        int query_min(int l,int r){

            if(l>r)return -INF;

            int d=r-l+1;

            int f;

            for(f=0;(1<<f)<=d;f++);

            f--;

            return min(dp_min[l][f],dp_min[r-(1<<f)+1][f]);

        }

     

        int query_min_index(int l, int r)

        {

            int v = query_min(l,r);

            if(v == -INF)return -INF;

            int i;

            for (i=l; i<=r; i++)

            {

                if (D[i] == v) return i;

            }

        }

     

    }RMQ;

     

    void DFS(int root, int deep)

    {

        int i;

        E.push_back(root);

        R[root] = E.size()-1;

        D.push_back(deep);

     

        for (i=0; i<son[root].size(); i++)

        {

            DFS(son[root][i],deep+1);

            E.push_back(root);

            D.push_back(deep);

        }

     

    }

     

    int LCA(int l, int r)

    {

        RMQ.init(D.size());

     

        int x = R[l];

        int y = R[r];

        if (x>y)

        {

            int temp = x;

            x = y;

            y= temp;

        }

        return E[RMQ.query_min_index(x,y)];

    }

     

    int main()

    {

        freopen("input.txt", "r", stdin);

        freopen("output.txt", "w", stdout);

     

        int N;

        int n;

        int i,j,k;

        int a,b;

     

        scanf("%d",&N);

     

        for (i=0; i<N; i++)

        {

            scanf("%d", &n);

            for (j=0; j<=n; j++)

            {

                parent[j] = j;

                son[j].clear();

            }

            E.clear();

            D.clear();

     

            for (j=0; j<n-1; j++)

            {

                scanf("%d%d", &a,&b);

                a--;

                b--;

                parent[b] = a;

                son[a].push_back(b);

            }

            int root = 0;

            while (parent[root] != root) root = parent[root];

     

            DFS(root,0);

            scanf("%d%d", &a,&b);

            a--;

            b--;

            printf("%d\n",LCA(a,b)+1);

        }

     

        return 0;

    }

    //POJ_1330 LCA问?题?的?Tarjan离?线?算?法?

    #include<iostream>

    using namespace std;

    int tree[10001][100],in[10001],p[10001];

    int cas,s,t;

    int n,Q1,Q2;

    bool v[10001];

    void Make_Set(int t)

    {

        p[t]=t;

    }

    int Find_Set(int t)

    {

        if(t!=p[t])

        {

            p[t]=Find_Set(p[t]);

        }

        return p[t];

    }

    void Union(int u,int v)

    {

        p[v]=u;

    }

    int LCA(int u)

    {

        Make_Set(u);

        int i;

        for(i=1;i<=tree[u][0];i++)

        {

            LCA(tree[u][i]);

            Union(u,tree[u][i]);

        }

        v[u]=1;

        if(u==Q1&&v[Q2])

        {

            printf("%d\n",p[Find_Set(Q2)]);

        }

        else if(u==Q2&&v[Q1])

        {

            printf("%d\n",p[Find_Set(Q1)]);

        }

        return 0;

    }

    int main()

    {

        scanf("%d",&cas);

        while(cas--)

        {

            int i;

            memset(tree,0,sizeof(tree));

            memset(in,0,sizeof(in));

            memset(v,0,sizeof(v));

            scanf("%d",&n);

            for(i=1;i<n;i++)

            {

                scanf("%d%d",&s,&t);

                tree[s][++tree[s][0]]=t;

                in[t]++;

            }

            scanf("%d%d",&Q1,&Q2);

            for(i=1;i<=n;i++)

            {

                if(in[i]==0)

                    break;

            }

            LCA(i);

        }

        system("pause");

        return 0;

    }

  • 相关阅读:
    【原创】使用开源libimobiledevice盗取iphone信息
    【原创】Arduino制作Badusb实践
    【原创】Aduino小车玩法全记录
    【原创】Arduino入门基础知识总结
    【原创】Arduino、arm、树莓派与单片机
    【原创】PM3破解IC卡记录
    【转】反编译D-Link路由器固件程序并发现后门
    DDOS分布式拒绝服务
    XSS 初识
    针对企业级别渗透测试流程
  • 原文地址:https://www.cnblogs.com/ACAC/p/1743139.html
Copyright © 2011-2022 走看看