博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
RMQ与LCA
阅读量:5125 次
发布时间:2019-06-13

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

Rmq问题,所谓rmq问题,就是(range max/min query),就是区间最大值,最小值的查询。

最常规的方法想到的是直接的查询,就是从扫面一边数组中的相关位置,然后找到最值。时间复杂度为O(n), 但是这样的算法,如果一旦查询的次数很多,查询的量很大的话,那么会变得效率不是很高。

这篇将rmq的博文,大致分两部分,第一部分讲一下一个在线的rmq算法,叫做ST(Sparse Table)算法,是采用动态规划的思想。第二部分呢,是说一下,LCA与RMQ问题的转化,LCA问题可以经过DFS+st变成rmq(min)的解法,而且求解的时间复杂度规模是一样的。那么下面先来开始第一部分:

St算法,st算法是采用动态规划的思想。我们以最小值为例,现在要求RMQ(A,i,j),数组A就是我们要求区间最值的数组,我们用M[i][j]表示从i开始,长度为2^j的数组长度内最小值的数组元素的下标。如果我们要这个区间内的最小值,我们可以将其转化为寻找M[i][j-1]与M[i+2^(j-1)-1][j-1]的最小值,因为这两个区间刚好是上述区间的一半。这样我们就得到了状态转移方程,

M[i][j]=M[i][j-1] (A[M[i][j-1]]<=A[M[i+2^(j-1)][j-1]])

Else M[i][j] = M[i+2^(j-1)][j-1]

Dp不仅需要状态转移方程,并且还需要初始值,那么我们的初始值该如何得出呢。其实我们不难看出M[i][0] = i;

那么这样,我们就轻松得求出了一段代码了。

void process2(int M[MAXN][LOGMAXN], int A[MAXN], int N)

  {

      int i, j;

     //initialize M for the intervals with length 1

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

          M[i][0] = i;

     //compute values from smaller to bigger intervals

      for (j = 1; 1 << j <= N; j++)

          for (i = 0; i + (1 << j) - 1 < N; i++)

              if (A[M[i][j - 1]] < A[M[i + (1 << (j - 1))][j - 1]])

                  M[i][j] = M[i][j - 1];

              else

                  M[i][j] = M[i + (1 << (j - 1))][j - 1];

  } 

接下来我们要进行查询了。那么我们的想法是找到一个是找到两块空间,使其可以完全得覆盖i到j。我们找了k=[log2(j-i+1)],中括号是取整的意思。

那么我们就可以得出结论:

RMQ(A,i,j) = M[i][k] (A[M[i][k]]<A[M[j-2^k+1][k]])

else RMQ(A,i,j) = M[A[j-2^k+1][k]];

第二部分,按照计划,讨论一下LCA与RMQ最小值问题的相互转化

算法的过程是:比方说,现在我我们有一棵树,我们使用dfs对这个树来进行访问,在这个树访问的过程中,我们需要记录几个量。访问时每访问到一个节点(无论是初次访问还是回溯时访问的),首先我们要记录下这个点所在的层数,也就是说是这个点在数中的深度,有这样的话,我们会访问到2n-1(加入树的节点个数为n的话)个节点,那么我们新建立一个数组叫做E,大小为2n-1.然后我们还要开一个数组R,大小也为2n-1.犹豫dfs访问我们会记录一个顺序,那么比方说我们的dfs顺序为A->B->A->c->A( 这个就是A为根,并且两个节点为B,C的简单的树),那么R[i]记录的就是第i个点在序列中第一次出现的位置。然后用D[2n-1]记录对应访问顺序的节点的所在的层数,下面从别人的博客中截取一个小的例子做示范一下:

  (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:

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

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

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

        i=2;j=7;

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

弄明白了这些问题后,下面我们来研究一下RMQ的算法的复杂度的问题,可以看到在第一部分代码的双重循环中,最外一层循环是移位操作。所以其复杂度应该是logN,而里面一层的复杂度是N,所以总的算法的复杂度是N*logN。并且这个复杂度只是进行初始化时的操作,在初始化结束之后,每次询问只要O(1)的复杂度。

在来看一下,LCA转化后的算法,一次dfs,应该是logN的复杂度,然后生成了一个2*N-1的数组,然后运用RMQ st的算法,复杂度变为(2N-1)*log(2N-1),    其实最终还是N*logN的。而相比之下,上一篇文章中所提到的Tarjan算法的复杂度为O(n+Q),其中Q为查询的次数。

 

 

    很想鄙视下这两个算法,头疼头疼。

 

转载于:https://www.cnblogs.com/Lawlight/archive/2012/11/03/2752902.html

你可能感兴趣的文章
【架构】Linux的架构(architecture)
查看>>
ASM 图解
查看>>
Date Picker控件:
查看>>
svn在linux下的使用(svn命令行)ubuntu 删除 新增 添加 提交 状态查询 恢复
查看>>
java处理url中的特殊字符%等
查看>>
你的第一个Django程序
查看>>
Tomcat免安装版的环境变量配置以及Eclipse下的Tomcat配置和测试
查看>>
Unity3D性能优化之Draw Call Batching
查看>>
grafana授权公司内部邮箱登录 ldap配置
查看>>
treegrid.bootstrap使用说明
查看>>
[Docker]Docker拉取,上传镜像到Harbor仓库
查看>>
javascript 浏览器类型检测
查看>>
基于easyX的<颜色侵略>小游戏
查看>>
nginx 不带www到www域名的重定向
查看>>
CruiseControl.NET ----- mail 配置
查看>>
记录:Android中StackOverflow的问题
查看>>
导航,头部,CSS基础
查看>>
[草稿]挂载新硬盘
查看>>
[USACO 2017 Feb Gold] Tutorial
查看>>
关于mysql中GROUP_CONCAT函数的使用
查看>>