RMQ与LCA(From TopCoder Algorithm Tutorials)

原帖地址:

http://www.topcoder.com/tc?module=Static&d1=tutorials&d2=lowestCommonAncestor#Range_Minimum_Query_%28RMQ%29

RMQ(Range Minimal Query)问题

 

给定一个数组A[1..N],RMQ(A,i,j)就是A[i..j]的最小值的下标。

 

 

From TopCoder

 

朴素的RMQ在线算法O(N2)-O(1)

 

逐一计算出每一对(i,j)的值,存储在NXN的数组M中,算法复杂度为N*N2=N3。利用动态规划,可以降低到O(N2)。

 

[cpp]  view plain  copy
  1. void  
  2. rmq_dp(int a[],int m[][MAXN],int n)  
  3. {  
  4.     int i,j;  
  5.     for(i=0;i<n;i++)  
  6.         m[i][i]=a[i];  
  7.     for(i=0;i<n;i++)  
  8.         for(j=i+1;j<n;j++)  
  9.             m[i][j]=a[m[i][j-1]]>a[j]?j:m[i][j-1];  
  10. }  

 

时空复杂度均为O(N2)。

 

O(N)-O(sqrt(N))的一种解法

 

将数组按照每段的大小为sqrt(N)(向下取整)进行分段,则一共有sqrt(N)(向上取整)段

 

From TopCoder

用M[0..sqrt(N)]保存每一段的最小值,复杂度为O(N)。

查询的过程先求出i,j所在段x,y

RMQ(i,j)=M[x]                    x==y

RMQ(i,j)=Min{A[i..sqrt(N)+i-1],M[x+1..y-1],A[y*sqrt(N)..j]}                        x!=y

元素个数最多不超过3sqrt(N)

 

Sparse Table algorithm(ST算法)O(N*logN)-O(1)

 

实际上也是一种DP算法,用m[i][j]来表示从下标i开始,长度为2j的子数组的最小值,数组大小只需要m[0..N-1][0..logN]

 

DP状态转移方程为

M[i][j]=A[i]                                                                       j=0

M[i][j]=Min{A[M[i][j-1]],A[M[i+2j-1][j-1]]}                        j>0

[cpp]  view plain  copy
  1. void  
  2. rmq_st(int a[],int m[][LOGMAXN],int n)  
  3. {  
  4.     int i,j;  
  5.   
  6.     for(i=0;i<n;i++)  
  7.         m[i][0]=a[i];  
  8.   
  9.     for(i=0;i<n;i++)  
  10.         for(j=1;i+(1<<j)-1<n;j++)  
  11.             m[i][j]=a[m[i][j-1]]<a[m[i+(1<<(j-1))][j-1]]?m[i][j-1]:m[i+(1<<(j-1))][j-1];  
  12. }  

 

查询的过程需要找到能够将区间[i,j]覆盖但是又不能越界的两个m区间,采用的方法是

m[i][k]为从i开始长度为2k的区间,m[j-2k+1][j]为以j结束长度为2k的区间

需要的条件是j>=i+2k-1>=j-2k+1

故取k=log(j-i+1)

取a[m[i][k]]和a[m[j-2k+1][k]]中的较小的值的下标即为RMQ(i,j)

 

Segment trees 线段树 O(N)-O(logN)

解决RMQ问题也可以用Segment trees,Segment trees是一种类似堆的数据结构,定义如下

 

 

 

[0,9]的segment trees如下图所示

segment trees可以采用heap的数据存储结构,节点node的左儿子为节点2*node,右儿子为2*node+1

类似堆,可以用数组M[1..2*2logN+1]来表示,对于RMQ问题,存储的区间信息就是该区间的最小值

segment trees的一个建立过程:初始调用应该是 segtree_init(1,0,N-1,m,a)

 

[cpp]  view plain  copy
  1. void  
  2. segtree_init(int node,int b,int e,int m[],int a[])  
  3. {  
  4.     if(b==e)  
  5.         m[node]=b;  
  6.     else{  
  7.         segtree_init(node*2,b,(b+e)/2,m,a);  
  8.         segtree_init(node*2+1,(b+e)/2+1,e,m,a);  
  9.   
  10.         m[node]=a[m[node*2]]<a[m[node*2+1]]?m[node*2]:m[node*2+1];  
  11.     }  
  12.   
  13. }  

 

查询过程

 

[cpp]  view plain  copy
  1. int  
  2. seg_query(int node,int b,int e,int i,int j,int m[],int a[])  
  3. {  
  4.     int p1,p2;  
  5.     if(i>e || j<b)  
  6.         return -1;  
  7.     if(i<=b && j>=e)  
  8.         return m[node];  
  9.   
  10.     p1=seg_query(node*2,b,(b+e)/2,i,j,m,a);  
  11.     p2=seg_query(node*2+1,(b+e)/2+1,e,i,j,m,a);  
  12.   
  13.     if(p1==-1)  
  14.         return p2;  
  15.     if(p2==-1)  
  16.         return p1;  
  17.     return a[p1]<a[p2]?p1:p2;  
  18.   
  19. }  

比较次数为O(logN)

 

Lowest Common Ancestor (LCA问题) 这里讨论的都是在线算法

 

 一个分段解法 O(N)-O(sqrt(N))

 以sqrt(H)为大小进行分段,H为树的高度,例如

 

用一个深度优先遍历的顺序来预处理每个节点i

对于第一段所有的节点i,祖先是节点1,置P[i]=1

后面的每一段的最上层的节点,即深度最低的节点i,P[i]=father[i],其他层次的节点i,P[i]=P[father[i]]

这样预处理后,每个节点都指向上一段的最低一层,即深度最高的祖先节点,对于P[i]=P[j],节点i,j的LCS一定是P[i]或P[i]的子节点

 

 

这样,如果要查询的两个节点 (x,y) 如果P[x]=P[y] ,那么最多进行O(sqrt(H))次查询即可得到

[c-sharp]  view plain  copy
  1. while(x != y)  
  2.     if (L[x] > L[y])  
  3.        x = T[x];  
  4.     else  
  5.        y = T[y];  
  6. return x;  

对于P[i]!=P[j]的,进行O(sqrt(H))次处理,使得P[x]=P[y] ,继续用上面的代码进行查询

[cpp]  view plain  copy
  1. while (P[x] != P[y])  
  2.     if (L[x] > L[y])  
  3.          x = P[x];  
  4.     else  
  5.        y = P[y];  

 

一共需要2*sqrt(H)次操作,最坏情况下H=N,算法最坏情况下的复杂度为O(sqrt(N))

 

另外一种简单的解法 O(NlogN)-O(logN)

 

 

仍然是DP,设P[i][j]为节点i的第 2个祖先,那么有状态转移方程

P[i][0]=father[i]

P[i][j]=P[P[i][j-1]][j-1]     j>0

用2进制的思想,先使深度较高的节点2进制的阶段上升,使两个节点在同一深度上,设p,q为两个待查节点,且L(p)>L(q),L表示p,q的深度,令log=log(L(p)),那么用log位2进制数就可以表示p的深度,从2log开始检查,如果L(p)-2log仍然>=L(q),那么p=P[p][log],上升到相应的祖先节点,log--,重复检查直到log=0。当两个节点在同一深度上,如果两个节点相等,那么直接返回,如果不相等,同样利用二进制思想,从log开始检查,如果P[p][log]!=P[q][log],分别上升至P[p][log]和P[q][log],循环到0的时候,必然有LCA(p,q)=father(p)

预处理复杂度为NlogN,查询复杂度为logH,最坏情况下H=N,为logN

 

从LCA到RMQ

 

 

LCA问题可以在线性时间内转化成RMQ问题,所有解决RMQ问题的方法都适用。

 

 

 

 

深度优先遍历树,并且记录每次访问边的端点,首先记录下根节点,那么一共有2N+1个节点被记录,用数组E[1..2N+1]来存储节点,并且用L[1..2N+1]来存储相应的节点的深度。R[1..N]来表示第一次访问节点N的数组E的下标,例如对于上图,R[9]=10,R[12]=15,而LCA(9,12)=E[RMQ(L,10,15)]。

同时L数组有个特点,相邻元素的差为+-1,一般叫做+-1RMQ问题。

 

从RMQ到LCA

 

RMQ问题同样可以转换到LCA问题。这意味着一般的RMQ问题可以转换成特殊的+-1RMQ问题。我们需要用到cartesian trees 笛卡尔树。

数组A[0..N-1]的笛卡尔树是一个以数组最小值为根的二叉树C(A),以该最小值的下标i来标识根节点。如果i>0那么该节点的左儿子是数组A[0..i-1]的笛卡尔树,否则就没左儿子。右儿子是数组A[i+1,N-1]的笛卡尔树。如果数组里有相同的元素,该数组的笛卡尔树不是唯一的,这里相同的元素取第一个出现的,所以这里的笛卡尔树是唯一的。显然,RMQ(A,i,j)=LCA(C,i,j),示例如下

 

 

 

可以用栈来完成RMQ到LCA的线性时间的转化。初始栈为空,数组元素依次入栈,入栈的元素从栈底到栈顶是递增的顺序,即,如果入栈的元素i小于栈顶元素,那么栈顶元素出栈,直到栈顶元素<=当前要入栈的元素i为止,接着完成i入栈,并且i作为入栈前栈顶的右儿子,而最后出栈的元素作为i的左儿子。如此循环所有元素,最后将栈排空,最后出栈的就是根节点。

 

步  栈  修改树
0 0 0是树中唯一的节点。
1 0 1 A[1]>A[0],故1入栈,1为0的右儿子。
2 0 2 A[2]<A[1],1出栈,2入栈,2为0的右儿子,1为2的左儿子。
3 3 A[3]是数组中最小的元素,故所有元素都出栈,3为树的根。0为3的左儿子。
4 3 4 4入栈,4为3的右儿子。
5 3 4 5 5入栈,5为4的右儿子。
6 3 4 5 6 6入栈,6为5的右儿子。
7 3 4 5 6 7 7入栈,7为6的右儿子。
8 3 8 8也是最小的元素,除了3所有的元素出栈,8为3的右儿子,最后出栈的4为8的左儿子。
9 3 8 9 9入栈,9为8的右儿子。
T[i]表示i的父亲节点。st[]实现栈。
[cpp]  view plain  copy
  1. int  
  2. rmq2lca(int *a,int n)  
  3. {  
  4.     int i,k,top;  
  5.     int st[MAXN];  
  6.   
  7.     top=-1;  
  8.     k=top;  
  9.     for(i=0;i<n;i++){  
  10.         while(k>=0 && a[i]<a[st[k]])  
  11.             k--;  
  12.         if(k!=-1)  
  13.             T[i]=st[k];  
  14.         if(k<top)  
  15.             T[st[k+1]]=i;  
  16.       
  17.         st[++k]=i;  
  18.         top=k;  
  19.     }  
  20.   
  21.     T[st[0]]=-1;  
  22. }  
+-1RMQ问题的算法 O(N)-O(1)

 

现在我们知道一般的RMQ问题可以通过LCA来转化成+-1RMQ问题。我们可以利用+-1的性质来找到一个O(N)-O(1)算法。

对于数组A[1..N],相邻元素之差为+-1,那么对于所有的i,j的RMQ(A,i,j)的集合就只有 2种可能性。

将问题按照大小为 I=(logN)/2 分组,故共有 N/I 组,数组 A‘ 存储每一个分块的最小值,数组 存储其下标,时间复杂度为O(N)。数组A'和B的大小都为N/I。现在我们用ST算法来预处理A’,时空开销都为 O(N/l*log(N/l))=O(N) 。每一个分块的大小为I,故每一个分块的RMQ就只有2I=sqrt(N) 种可能性。计算出每一种可能性并存储在表P中,时空复杂度为O(sqrt(N)*I2)=O(N),计算出每一个小块所属于的数组,数组类型可以用10序列来表示,即计算两个元素的差,然后用0来替换-1,复杂度为O(N)

 

对于查询RMQ(i,j),计算出i,j所在块x,y和块内下标a,b

 

如果x==y,则进行块内搜索,查出块x的数组类型,用a,b查询表P即可。

如果x!=y,则i,j区间分成三块,a到x块末尾,x+1到y-1块,y块开头到b,每一块的查询都是O(1)的复杂度,求出最小值即可。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值