Day4 RMQ,LCA

文章目录

展开

0.前言

本章节将讲述RMQ(区间最值问题),ST表,LCA(最近公共祖先)以及树链剖分相关知识。

以及会针对具体问题讨论树上差分。

废话不多说,让我们开始本章的内容吧。

1.RMQ

1.1 问题描述

RMQ的模板问题是指:给你一个数组,其中有N(1 \leq N \leq 50000)个数字,现在给你Q(1 \leq Q \leq 200000)次询问,每次询问区间[l,r]内的最大值。

1.2 问题分析

最朴素的办法是暴力,但是暴力的时间复杂度是O(NQ),很显然会TLE

1.2.1 预处理

那么我们来考虑一种DP预处理算法。

a[i]是给定的数组,f[i,j]表示从第i个数起连续2^j个数中的最大值(注意我们的状态不是表示区间[i,j]的最大值,因为这个状态的复杂度是O(n^2)的,也不足够优秀)。

我们举个例子:数列a{6,1,8,3,5,9,2,4,10,7}

那么f[1,0]=max{6}=6f[1,1]=max{6,1}=6f[1,2]=max{6,1,8,3}=8f[1,3]=max{6,1,8,3,5,9,2,4}=9。这样我们就可以理解状态的含义了。

DP三要素:状态、初态、转移方程。

我们来考虑初态,即f[i,0]=max{a[i]}=a[i](0\leq i< n)

最后是转移方程:

对于f[i,j],当j>0时(此时表示的区间内一定有偶数个数字),我们将其均分成两段,分别是f[i,j-1]f[i+2^{(j-1)},j-1]。因此我们得到状态转移方程f[i,j]=max(f[i,j-1],f[i+2^{(j-1)},j-1])

这样我们可以通过O(nlogn)的时间将所有区间的最大值初始化出来。

1.2.2 查询

要想查询区间[l,r]的最大值,那么我们就要找到能覆盖这个区间的幂次。

也就是说,我们要找到一个k,使得区间[l,l+2^k-1]和区间[r-2^k+1,r]有交集。

因此我们要满足如下条件:l\leq r-2^k+1l+2^k-1 \leq rr-2^k+1 \leq l+2^k-1

解得:k \leq log_2(r-l+1)k \geq log_2(\frac{r-l}{2}+1)

可证,取k=\lfloor log_2(r-l+1) \rfloor满足上式。

因此,查询区间[l,r]的最大值,即为max(f[l,k],f[r-2^k+1][k])

单次查询时间复杂度为O(1)

1.3 例题

1.3.1 洛谷P3865 【模板】ST 表

题目链接

这是一道RMQ的模板题,我们可以用这道题具体来实现上述讲解。

ST表代码:

在我们具体实现时,要注意ki的枚举顺序。应该是先枚举k后枚举i,因为我们要先修改小区间才能逐步扩展到大区间。还需注意ki的枚举范围,不要超出数组长度。

1.3.2 洛谷P1440 求m区间内的最小值

题目链接

这道题有很多种解法,我们就只考虑用RMQ解,查询是区间[i-m,i-1](1 \leq i \leq n)

RMQ代码:

大家按照上面的代码写完后,会发现只能得80分,有两个点MLE了。

所以我们要考虑优化空间,DP中优化空间的方法就是滚动数组了。

有关滚动数组的知识会在后续DP专题中介绍,先贴一个博客,大家可以进行参考。

RMQ+滚动数组的代码如下:

对于这道题,大家只需要暂时掌握第一种写法,即只用RMQ的写法。至于滚动数组,可以等以后具体学习后再来完善。

1.4 总结

我们再来重新考虑这道题,发现区间求值类的问题好像线段树也能求解,但是线段树的区间修改区间查询都是O(logn)的,当查询次数m极高时,会出现复杂度O(mlogn)爆时间的情况。而RMQ仅需要O(nlogn)的复杂度初始化,然后O(1)查询即可,优化了时间。

上面我们构造的状态f[i][j]就是ST表。所以我们可以将ST表视作解决RMQ问题的工具。具体区分就是ST表示方法,而RMQ是问题。

2. LCA(Least Common Ancestors)

2.1 问题描述

洛谷P3379 【模板】最近公共祖先(LCA)题目链接

给定一颗有根多叉树,求出指定两个点的最近公共祖先。

最近公共祖先:层数最大(深度最深)的公共祖先。

一般会有多组提问,即M组提问,每组提问给定两个点,求这两个点的最近公共祖先。

LCA问题有很多种解法,我们一个一个来分析。

2.2 离线Tarjan算法

离线算法指先存下所有的询问,然后将这些询问按处理顺序解答,最后统一输出答案的方法。这种算法需要我们事先知道问题,对于动态给出的问题便不适用。

本算法利用了并查集技术和树的后序遍历技术。

我们每遍历一个结点,首先遍历这个结点的所有子树。当存在某次询问的两个结点都在以本结点为根的子树内时,这次询问的答案就是本节点。

2.3 转化为RMQ解决LCA

RMQ问题已经讲过,如何将LCA转换成RMQ是我们要思考的。

树上的任意两点间都有且仅有一条路,而两点的最近公共祖先是路上深度最小的点。

对于LCA转化为RMQ,我们需要记录欧拉序列,深度序列和每个点在欧拉序中第一次出现的位置序列。

树的欧拉序列是在对树进行DFS时,记录经过的每一个结点。

我们给出一个树的例子:

这棵树的欧拉序如下图第一行:

那么上图第二行是什么呢?是对应结点的深度。

这样,我们只需要最后记录任意两个点之间的路径怎么找就可以了。

我们记录每个点第一次被访问的次序(这个辅助序列只是方便我们快速查询到某个节点在欧拉序里的位置):

这样,我们需要找到对应结点间欧拉序列的点的深度的最小值。

虽然欧拉序列中很多点会出现多次,或者出现子树等等,但这对我们找深度最小的点是没有影响的。

比如上图的811,中间的跨度从欧拉序下标8到欧拉序下标20,虽然理论上两个点的路径为8-6-3-7-11,但是其他若干个点的深度都比两者LCA的深度要大。因此,我们可以直接求这个区间内的深度最小的点。

这种算法的理解还算简单,但是代码实现起来会遇到很多问题,如果RMQ模板不熟练的话建议先多敲几遍RMQ的模板题,尽量做到“肌肉记忆”。这种算法的实现难度还是很大的,相当于将求树的欧拉序和RMQ两道问题进行合并。但还是需要掌握的。

2.4 倍增LCA

我们先考虑一种朴素的求解方法:我要一步一步往上爬

我们先把xy弄到同一深度,然后同时一步一步往上找,直到找到共同的人,它就是xyLCA

但是这种方法时间复杂度极其极其高,毕竟是蜗牛嘛

所以我们考虑倍增。

何为倍增,就是翻倍增加。你一次向上找一个慢,那我就一次向上找到2^i

我们首先预处理出表f[i][j],表示结点i向上走2^j步的结点。易得f[i][0]为结点i的父亲。

预处理的方法也很简单,f[i][j]=f[f[i][j-1]][j-1]

当然我们还需要预处理出每个结点的深度记在数组depth中。

我们爬的时候是从大距离到小距离爬,因为如果从小到大爬,假设爬五层遇到了LCA,但是你却是爬了1+2+4,总是遇不到LCA的,而从大向小爬就能爬到4+1。这里的表述不是很清晰,大家可以自己爬一爬看看。

我们真正求解是碰到爬完后遇到的人相等的时候先不管,如果两个人都跳了8个距离,遇到的人相等,并不能确定下面有没有人还满足LCA,所以只能求出CA

当然,写法可能千奇百怪,但是思想是共同的。

2.5 树剖LCA

挖坑待填。

3.树上差分

挖坑待填。

发表回复

您的电子邮箱地址不会被公开。 必填项已用*标注