第一节 RMQ、LCA概述

LCA:Lowest Common Ancestor,译为最近公共祖先。其解释就是说:在有根树中,找出树中任意两个节点最近的公共祖先,或者说找到任意两个节点离树根最远的公共祖先。

RMQ:Range Minimum Query,译为区间最小值查询。其解释就是说:对于含有N个元素的数列A,在数列中找到两个指定索引之间的最小值及最小值的位置。

第二节 RMQ Algorithm

首先我们来看RQM算法,我将会根据预处理和查询的速度介绍几种解决该问题的方法。

设有数组A[N],其表示如下:

要求求得区间(2,7)的最小元素,如下图所示:

解法一:直接遍历区间

看到这个问题之后,我们最先想到的就是对区间的这些数进行一次遍历,就可以找到区间的最值,因此查询的时间为O(M)。但是,当数据量非常大并且查询很频繁时,直接遍历序列的效果就不是那么理想了。因为每查询一次就得对序列做一次遍历,对于大数据量这显然不能满足要求了。不过对于小数据量,这种算法倒是不错的选择!

查询:O(M)。

算法的代码如下:

[cpp] view plaincopy
  1. int MaxNum = 0;
  2. for(i = 0; i < range; i++)
  3. {
  4. /**查找最大值**/
  5. if(array[i] > MaxNum)
  6. {
  7. MaxNum = array[i];
  8. }
  9. }

解法二:切割法

解法一中查询的速度为O(M),如果每次查询都这样的话,那真就成了龟速了。于是我们对解法一做了预处理,这就是该节要讲的:切割法。

首先,我们将序列分成sqrt(N)个部分,用数组M[sqrt(N) ]来表示每个部分中最小的值的下标,即这个最小数的位置。对于数组M,我们只需对原序列进行一次遍历就可以得到M。如下图所示:

接下来我们来求RMQ[2,7]。为了得到区间[2,7]的最小值,我们需比较A[2],A[M[1]],A[6],以及A[7],并得到他们中最小值的下标。

分析:其实,这种方法较第一种方法而言并没有实质的改进,甚至还不如方法一。至于为什么这样做,我的解释是:我们是基于查询快慢的角度上来比较的,说白了,就是我们追求的是查询速度,所以说只要查的快了,先做一些预处理也是值得的(解法四正是基于这种思想)现在我们根据上面的例子来看看法二,当做完预处理之后,得到了数组M,此时我们要求区间的最值,那么我们只需将在区间内,包含数组M的值以及包含两个边界的值作比较就行,这样的话,查询的次数:O(M) <= 查询次数 < O(M) + K,其中K < sqrt(N)。

解法三:排序

解法二已经提到我们的目的是查得快,那么我们可对选择区间的这M个数据进行排序,然后就可以直接得到最小值。但是如果做排序的话,会有很大的缺陷。我们来看看。

分析:我们选择快速排序,O(M * LogM),但是快速排序会改变序列中数的相对位置,因此用快排的话,为了保证原数据的顺序不变,我们还得用O(M)的空间来维护原序列,因此这样的消耗是很大的。附注:复杂度为O(M * M)的排序算法在这就不啰嗦了!你懂得!

查询:O(1)。

OK,我们来实现我们的想法,代码如下:

[cpp] view plaincopy
  1. 快速排序
  2. int partition(int *array, int low, int high)
  3. {
  4. int key = array[high];
  5. int i = low;
  6. int j = high;
  7. while(i < j)
  8. {
  9. while(array[i] <= key && i < j)
  10. {
  11. i++;
  12. }
  13. array[j] = array[i];
  14. while(array[j] >= key && i < j)
  15. {
  16. j--;
  17. }
  18. array[i] = array[j];
  19. }
  20. array[i] = key;
  21. return i;
  22. }
  23. void quicksort(int *array, int low, int high)
  24. {
  25. int index;
  26. int i = low;
  27. int j = high;
  28. if(i < j)
  29. {
  30. index = partition(array, low, high);
  31. quicksort(array, low, index - 1);
  32. quicksort(array, index + 1, high);
  33. }
  34. }

排完序之后就可以直接得到最值了!

解法四:Sparse Table(ST) algorithm

ST算法是一种比较高效的在线处理RMQ问题的算法,所谓在线算法,是指每输入一个查询就会马上处理这个查询。ST算法首先会对序列做预处理,完成之后就可以对查询做回答了。

分析:

预处理:O(N * LogN)。

查询:O(1),这样的查询正是我们想要的。

好了,我来详细讲述一下ST算法:

预处理:首先用维护一个数组M[N][LogN],M[i][j]的值是从原序列A的i位置开始,连续2j 个元素的最小值的下标,如下所示:

那么,我们如何计算M[i][j]呢?

我们采用DP的思想将区间分成两部分,即M[i][j - 1]和M[i][2^(j - 1)]。现在我们只需比较这两个子区间就可以得到M[i][j]了。比较规则如下:

[cpp] view plaincopy
  1. void Proprocessing(int M[N][logN], int *A, int N)
  2. {
  3. int i, j;
  4. for(j = 1; (1 << j) < N; j++)
  5. {
  6. for(i = 0; (i + (1 << j) - 1) < N; i++)
  7. {
  8. if(A[ M[i][j - 1] ] < A[ M[i + (1 << (j - 1))][i - 1]])
  9. {
  10. M[i][j] = M[i][j - 1];
  11. }
  12. else
  13. {
  14. M[i][j] = A[ M[i + (1 << (j - 1))][i - 1]];
  15. }
  16. }
  17. }
  18. }

解法五:线段树

[cpp] view plaincopy
  1. void init_tree(int node, int low, int high, int *array, int *M)
  2. {
  3. /***node:表示线段树中的某个节点
  4. ****low :表示低索引
  5. ****high:表示高索引
  6. ****array:表示原数组
  7. ****M:  表示维护下标的数组
  8. ***/
  9. if(low == high) //为叶子节点
  10. {
  11. M[node] = low;
  12. }
  13. else
  14. {
  15. init_tree(2 * node, low, (low + high)/2, array, M);
  16. init_tree(2 * node + 1, (low + high)/2 + 1, high, array, M);
  17. if(array[ M[2 * node] ] <= array[ M[2 * node + 1] ]) //拿到较小值的下标
  18. {
  19. M[node] = M[2 * node];
  20. }
  21. else
  22. {
  23. M[node] = M[2 * node + 1];
  24. }
  25. }
  26. }
通过代码,可得到构造线段树的复杂度为O(N)。
 
       线段树构造成功,接下来就是查询了。我们知道,线段树查询所需的时间为O(LogN)。因为我们在前面已经了解了线段树的几种操作,所以查询在这就不赘述了,直接看代码吧!
[cpp] view plaincopy
  1. int query(int node, int low, int high, int *a, int *b, int i, int j)
  2. {
  3. /***node:表示线段树中的某个节点
  4. ****low :表示低索引
  5. ****high:表示高索引
  6. ****array:表示原数组
  7. ****M:  表示维护下标的数组
  8. ****i, j:表示要查询的区间
  9. ***/
  10. int s, t;
  11. if(i > high || j < low)
  12. return -1;
  13. if(low >= i && high <= j)
  14. return b[node]; //返回最小值的下标
  15. s = query(2 * node, low, (low + high)/2, a, b, i, j);
  16. t = query(2 * node + 1, (low + high)/2 + 1, high, a, b, i, j);
  17. if(s == -1)
  18. return b[node] = t;
  19. if(t == -1)
  20. return b[node] = s;
  21. if(a[s] <= a[t])
  22. return b[node] = s;
  23. else
  24. return b[node] = t;
  25. }

第三节 LCA Algorithm

战前准备:

数组T[i]:表示树中某个节点i的父节点;

数组L[i]:表示树中的某个节点i。

维护数组:P[N][LogN]:其中,P[i][j]表示树中i节点的第j个祖先。

实现的过程如下:

利用二分检索判断节点p和节点q是否在树的同一层:

如果在同一层,那么我们通过DP思想,不断地求LCA(p = P[p][j],q = P[q][j]),一旦 p = q就停止,因为此时p和q的父节点是一样的,也就是说我们找到了最近公共祖先。

如果不在同一层,如果p > q,也就是说p相对与q,p在树的更深层。此时,我们仍然通过DP思想来找到q与p的祖先在同一层的节点,即q = p_祖先。接下来就可按照在同一层的做法做了。

实现就是这么简单。

首先是预处理得到维护数组P[N][LogN]:

[cpp] view plaincopy
  1. void preprocessing(int *t, int n, int p[][max])
  2. {
  3. int i, j;
  4. for(i = 0; i < n; i++)
  5. p[i][0] = t[i];
  6. for(j = 1; (1 << j) <= n; j++)
  7. {
  8. for(i = 0; i < n; i++)
  9. {
  10. if(p[i][j - 1] != -1)
  11. p[i][j] = p[p[i][j - 1]][j - 1];
  12. }
  13. }
  14. }

接下来就是查询了,如下:

[cpp] view plaincopy
  1. int query(int *t, int *l, int s, int t, int n, int p[][max])
  2. {
  3. int tmp, lg, i;
  4. if(l[s] < l[t])
  5. {
  6. tmp = s;s = t;t = tmp;
  7. }
  8. for(lg = 1; (1 << lg) <= l[s]; lg++);
  9. for(i = lg; i >= 0; i--)
  10. {
  11. if((l[s] - (1 << i)) >= l[t])
  12. s = p[s][i];
  13. }
  14. if(s == t)
  15. return s;
  16. for(i = lg; i >= 0; i--)
  17. {
  18. if(p[s][i] != -1 && p[s][i] != p[t][i])
  19. {
  20. s = p[s][i];
  21. t = p[t][i];
  22. }
  23. }
  24. return t[s];
  25. }

上面说的LCA的这种算法应该是最容易想到的,预处理过程O(NLogN),查询O(LogN)。还有一种类似于RMQ分割法德算法,我先就不在这赘述了,以后有时间一定补上。

第四节 结束语

想想、写写、画画.......

后续:本文后半部分拖得周期较长,因此写的比较匆忙。如果本文的内容有任何不妥之处,请指正!

详解RMQ LCA相关推荐

  1. 详解最近公共祖先(LCA)

    看本博客前建议先看一下ST算法解决RMQ问题详解 一,LCA概念 最近公共祖先(Lowest Common Ancestors, LCA)指 有根树中 距离两个 节点最近的 公共祖先. 祖先指 从当前 ...

  2. 五分钟搞懂后缀数组!后缀数组解析以及应用(附详解代码)

    为什么学后缀数组 后缀数组是一个比较强大的处理字符串的算法,是有关字符串的基础算法,所以必须掌握. 学会后缀自动机(SAM)就不用学后缀数组(SA)了?不,虽然SAM看起来更为强大和全面,但是有些SA ...

  3. 线段树扫描线求矩形周长详解

    线段树扫描线求矩形周长详解 原创 wucstdio 最后发布于2018-04-24 16:12:09 阅读数 841 收藏 发布于2018-04-24 16:12:09 版权声明:本文为博主原创文章, ...

  4. [动图演示]Redis 持久化 RDB/AOF 详解与实践

    Redis 是一个开源( BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件.它支持的数据类型很丰富,如字符串.链表.集 合.以及散列等,并且还支持多种排序功能. 什么叫持 ...

  5. 激光雷达与自动驾驶详解

    激光雷达与自动驾驶详解 参考文献链接 https://mp.weixin.qq.com/s/Gk4JJZapKHXZE2AjliR8_A https://mp.weixin.qq.com/s/8xkd ...

  6. Redis持久化RDB/AOF详解与实践

    Redis 是一个开源( BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件.它支持的数据类型很丰富,如字符串.链表.集 合.以及散列等,并且还支持多种排序功能. 什么叫持 ...

  7. [动图演示]Redis 持久化 RDB/AOF 详解与实践 1

    Redis 是一个开源( BSD 许可)的,内存中的数据结构存储系统,它可以用作数据库.缓存和消息中间件.它支持的数据类型很丰富,如字符串.链表.集 合.以及散列等,并且还支持多种排序功能. 什么叫持 ...

  8. 数据结构--树链剖分详解

    数据结构--树链剖分详解 关于模板题---->传送门 题目描述 如题,已知一棵包含N个结点的树(连通且无环),每个节点上包含一个数值,需要支持以下操作: 操作1: 格式: 1 x y z 表示将 ...

  9. 线段树详解 (原理,实现与应用)

    线段树详解 By 岩之痕 目录: 一:综述 二:原理 三:递归实现 四:非递归原理 五:非递归实现 六:线段树解题模型 七:扫描线 八:可持久化 (主席树) 九:练习题 一:综述 假设有编号从1到n的 ...

最新文章

  1. 如何进行机器学习框架选择
  2. 【java基础】POJO和JavaBean的区别
  3. python dlib学习(十):换脸
  4. 【博客话题】技术生涯中的出与入
  5. java原子操作cas_java并发编程系列二:原子操作/CAS
  6. ls 显示目录下的内容和文件相关属性信息
  7. leetcode917
  8. requests中获取请求到文本编码格式
  9. 免费网页模板提供站推荐
  10. Hive的Map Join与Common Join
  11. Dropbox的服务器和网络自动化运维实践
  12. java.util.function包下的四大Function
  13. Access数据库使用DateAdd函数更新日期信息
  14. ssis oracle配置,从SSIS包SQL Server连接Oracle数据库
  15. Vuforia入门之简单图片识别案例(一)
  16. 南京市公安局电子警察系统数据库扩容和异地灾备公开招标采购公告
  17. 使用MODBUS转PROFINET智能网关实现与多个温控器数据读写
  18. 02 C/C++创建tcl自定义命令
  19. 旋转木马图片切换展示js特效
  20. 蝉知CMS7.0.1后台模板Getshell

热门文章

  1. 【错误记录】Google Play 上架报错 ( 我们检测到您的应用程序包含未经认证的广告SDK或未经批准用于儿童导向服务的SDK )
  2. 【错误记录】Android Studio 创建 Flutter 应用被卡住 ( 更新 Flutter 插件 | 命令行创建 | 断网 )
  3. 【OpenGL】十七、OpenGL 绘制四边形 ( 绘制 GL_QUAD_STRIP 模式四边形 )
  4. 【C++ 语言】文件操作 ( fopen | fprintf | fscanf | fgets | fputc | fgetc | ofstream | ifstream )
  5. 【数理逻辑】范式 ( 合取范式 | 析取范式 | 大项 | 小项 | 极大项 | 极小项 | 主合取范式 | 主析取范式 | 等值演算方法求主析/合取范式 | 真值表法求主析/合取范式 )
  6. 【iOS 开发】基本 UI 控件详解 (UIButton | UITextField | UITextView | UISwitch)
  7. [Spring cloud 一步步实现广告系统] 21. 系统错误汇总
  8. HihoCoder#1509 : 异或排序(二进制)
  9. How to Use Git
  10. 数据库复习总结(12)数据检索