博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
看到的一个很不错的分析LCA和RMQ的文章(转载,先收着)
阅读量:4943 次
发布时间:2019-06-11

本文共 8114 字,大约阅读时间需要 27 分钟。

首先请看定义:

一、最近公共祖先(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;

}

转载于:https://www.cnblogs.com/ACAC/archive/2010/05/24/1743139.html

你可能感兴趣的文章