【算法笔记】【专题】RMQ 问题:ST表/树状数组/线段树

news2024/11/29 0:44:17

0. 前言

好久没更算法笔记专栏了,正好学了新算法来更新……
这也是本专栏的第一个专题问题,涉及到三种数据结构,如果写得有问题请各位大佬多多指教,谢谢!

1. 关于 RMQ 问题

RMQ 的全称是 Range Minimum/Maximum Query,即区间最大/最小值问题。

本文中,我们的算法以求最大值为例(最小值基本一致)。题面如下:

给定一个长为 N N N的序列 A = ( A 1 , A 2 , … , A N ) A=(A_1,A_2,\dots,A_N) A=(A1,A2,,AN)
Q Q Q个询问,第 i i i个询问如下:
→   \to~  给定 1 ≤ l i ≤ r i ≤ N 1\le l_i\le r_i\le N 1liriN,求区间 [ l i , r i ] [l_i,r_i] [li,ri]的最大值,即 max ⁡ j = l i r i A j \displaystyle\max_{j=l_i}^{r_i} A_j j=limaxriAj

下面,我们将从暴力算法开始,逐步讲解 RMQ 问题的常用解法。

通用的 RMQ 问题(除暴力外所有算法都能通过):

  • 洛谷 P1816 忠诚(标准的RMQ,没有任何变动,全篇通用例题)
  • 洛谷 P2880 [USACO07JAN] Balanced Lineup G
  • 洛谷 P2251 质量检测
  • 洛谷 P8818 [CSP-S 2022] 策略游戏(有一定思维难度)

2. 解法

2.1 暴力法

我们先读取序列 A A A,再逐个读取询问,对于每个询问直接遍历 A l … A r A_l\dots A_r AlAr,最终输出结果。
总时间复杂度为 O ( ∑ r i − l i ) = O ( N Q ) \mathcal O(\sum r_i-l_i)=\mathcal O(NQ) O(rili)=O(NQ)

#include <cstdio>
#define maxn 100005
using namespace std;

int a[maxn];

int main()
{
	int n, q;
	scanf("%d%d", &n, &q);
	for(int i=0; i<n; i++)
		scanf("%d", a + i);
	while(q--)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		l --, r --;
		int res = a[l];
		for(int i=l+1; i<=r; i++)
			if(a[i] < res)
				res = a[i];
		printf("%d ", res);
	}
	return 0;
}

然而,当你提交到洛谷 P1816时……

TLE一个点

肯定还是时间复杂度的锅,算法需要进一步优化

2.2 Sparse Table

Sparse Table(以下称ST表)是用于静态求解 RMQ 问题的数据结构。

静态求解是指在原始序列不改变的情况下求解问题。或者说,ST表不能直接进行修改操作

ST表的初始化时间复杂度为 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN),单次查询时间复杂度为 O ( 1 ) \mathcal O(1) O(1)

2.2.1 存储结构

ST表的本质是一个 N × ⌈ log ⁡ N ⌉ N\times\lceil\log N\rceil N×logN的二维数组,其定义如下(令 A A A表示原数组,求最小值同理):
s t [ i ] [ j ] = max ⁡ { A i , A i + 1 , … , A i + 2 j − 1 } st[i][j]=\max\{A_i,A_{i+1},\dots,A_{i+2^j-1}\} st[i][j]=max{Ai,Ai+1,,Ai+2j1}
也就是说, s t [ i ] [ j ] st[i][j] st[i][j]表示从 A i A_i Ai开始, 2 j 2^j 2j个元素中的最大值。这运用了倍增的思想。

下面考虑如何快速初始化整个数组。

2.2.2 初始化

根据倍增的常用算法,使用类似于DP的方式填满整个表:
s t [ i ] [ j ] = max ⁡ { s t [ i ] [ j − 1 ] , s t [ i + 2 j − 1 ] [ j − 1 ] } st[i][j]=\max\{st[i][j-1],st[i+2^{j-1}][j-1]\} st[i][j]=max{st[i][j1],st[i+2j1][j1]}
填表时,先枚举 j j j,再枚举 i i i。由于整个表共有大约 N log ⁡ N N\log N NlogN个状态,而计算一个状态值的时间复杂度为 O ( 1 ) \mathcal O(1) O(1),所以初始化的总时间复杂度为 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN)

伪代码如下:

function init() {
	for i = 1 to N
		st[i][0] = A[i]
	for j = 1 to log2(N)
		for i = 1 to (N + 1 - 2^j)
			st[i][j] = max(st[i][j - 1], st[i + 2^(j-1)][j - 1])
}

C++ 实现:

void init()
{
	for(int i=0; i<n; i++)
		st[i][0] = A[i];
	for(int j=1; j<=log2(n); j++)
		for(int i=0; i+(1<<j)<=n; i++) // 注意必须是<=n,1<<j即为2^j
			st[i][j] = max(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}

2.2.3 查询

对于 [ l , r ) [l,r) [l,r)区间的 RMQ 查询,根据ST表的原理,我们要找到两个区间 [ l , a ) [l,a) [l,a) [ b , r ) [b,r) [b,r),使得它们的并集正好为 [ l , r ) [l,r) [l,r)。即: l ≤ b ≤ a ≤ r l\le b\le a\le r lbar

为什么是并集?
→   \to~  因为 max ⁡ ( a , a ) = a \max(a,a)=a max(a,a)=a,所以重复的不影响结果。
→   \to~  如果出现遗漏,且遗漏的正好为最大/最小值,那会影响最终结果,所以不能遗漏。
→   \to~  如果检查到了多余的元素,且多余的正好大于原区间的最大值,会使查询结果变大,所以不能有多余。
综上,必须满足 [ l , a ) [l,a) [l,a) [ b , r ) [b,r) [b,r)并集正好为 [ l , r ) [l,r) [l,r)才能查询。

要满足上述条件,我们可以 a a a尽可能靠近 r r r,让 b b b尽可能靠近 l l l,来达到这样的效果。
此时,我们还需要满足并集的条件 l ≤ a , b ≤ r l\le a,b\le r la,br,因此我们需要找到最大的 k k k,使得 a = l + 2 k a=l+2^k a=l+2k b = r − 2 k b=r-2^k b=r2k

则有
{ l + 2 k ≤ r r − 2 k ≥ l    →    2 k ≤ r − l   →   k ≤ log ⁡ 2 ( r − l ) \left\{ \begin{array}{c} l+2^k\le r \\ r-2^k\ge l \end{array} \right.~~\to~~ 2^k\le r-l\\ ~\\ \to~k\le \log_2(r-l) {l+2krr2kl    2krl  klog2(rl)
又因为 k k k必须是整数,所以取 k = ⌊ log ⁡ 2 ( r − l ) ⌋ k=\lfloor\log_2(r-l)\rfloor k=log2(rl)⌋即可。

// query(l, r) = max(A[l], ..., A[r - 1])
inline int query(int l, int r)
{
	int k = log2(r - l);
	return max(st[l][k], st[r - (1 << k)][k]);
}

2.2.4 完整实现

下面给出用Sparse Table解决例题的完整代码。总时间复杂度为 O ( Q + N log ⁡ N ) \mathcal O(Q+N\log N) O(Q+NlogN)

#include <cstdio>
#include <cmath>
#include <algorithm>
#define maxn 100005
using namespace std;

int st[maxn][17]; // 2^17=131072

void init(int n)
{
	for(int j=1, t=log2(n); j<=t; j++)
		for(int i=0; i+(1<<j)<=n; i++)
			st[i][j] = min(st[i][j - 1], st[i + (1 << j - 1)][j - 1]);
}

inline int query(int l, int r)
{
	int k = log2(r - l);
	return min(st[l][k], st[r - (1 << k)][k]); // 注意此题为min,不是求max
}

int main()
{
	int n, q;
	scanf("%d%d", &n, &q);
	for(int i=0; i<n; i++)
		scanf("%d", st[i]); // 直接读入到ST表中,节约时间和空间
	init(n);
	while(q--)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		// 此处注意因为是左闭右开区间[l,r),所以只有l需要-1
		printf("%d ", query(--l, r));
	}
	return 0;
}

AC
运行时间: 128 m s 128\mathrm{ms} 128ms
使用内存: 6.90 M B 6.90\mathrm{MB} 6.90MB

2.3 树状数组

关于树状数组的原理我已经在这篇文章中讲过,这里不再多说了。下面我们考虑如何应用树状数组解决 RMQ 问题。

2.3.1 原算法

树状数组可以用lowbit操作实现prefixSum(前缀和)以及update(更新)操作,时间复杂度均为 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN)。不仅是加法,对于任意满足结合律的运算这两种操作都有效。

我们来简单实现一下支持prefixMaxupdate操作的树状数组:

#define INF 2147483647
#define lowbit(x) ((x) & -(x))
inline void setmax(int& x, int y) { if(y > x) x = y; }

int n, A[N], bit[N];

// max(A[1], ..., A[i])
inline int prefixMax(int i)
{
	int res = -INF;
	for(; i>0; i-=lowbit(i))
		setmax(res, bit[i]);
	return res;
}

// A[i] = max(A[i], val)
inline void update(int i, int val)
{
	for(; i<=n; i+=lowbit(i))
		setmax(bit[i], val);
}

若要初始化树状数组,可以利用update操作进行 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN)的初始化:

inline void init()
{
	for(int i=1; i<=n; i++)
		bit[i] = -INF; // 这一段不要忘!
	for(int i=1; i<=n; i++)
		update(i, A[i]);
}

另外,我们也可以用子节点直接更新父节点,达到 O ( N ) \mathcal O(N) O(N)建树的效果:

inline void init()
{
	for(int i=1; i<=n; i++)
		bit[i] = A[i];
	for(int i=1; i<=n; i++)
	{
		int j = i + lowbit(i);
		if(j <= n) setmax(bit[j], bit[i]);
	}
}

考虑加法时我们计算rangeSum(区间和)的算法:

inline int rangeSum(int l, int r)
{
	return prefixSum(r) - prefixSum(l - 1);
}

也就是用 ( A 1 + A 2 + ⋯ + A r ) − ( A 1 + A 2 + ⋯ + A l − 1 ) = A l + ⋯ + A r (A_1+A_2+\dots+A_r)-(A_1+A_2+\dots+A_{l-1})=A_l+\dots+A_r (A1+A2++Ar)(A1+A2++Al1)=Al++Ar

现在回过来考虑 RMQ 的查询,Min/Max运算不可逆,所以很明显不能用这种计算方式。
下面我们来介绍针对 RMQ 的树状数组设计。

2.3.2 RMQ 树状数组

我们令 f ( l , r ) f(l,r) f(l,r)表示rangeMax(l, r),即 [ l , r ] [l,r] [l,r]的区间最大值。
t = r − l o w b i t ( r ) t=r-\mathrm{lowbit}(r) t=rlowbit(r) A A A表示原序列, B B B表示树状数组,考虑如下递推式:
f ( l , r ) = { max ⁡ { B r , f ( l , t ) } ( t ≥ l ) max ⁡ { A r , f ( l , r − 1 ) } ( t < l ) − ∞ ( l < r ) f(l,r)=\begin{cases} \max\{B_r,f(l,t)\} & (t\ge l)\\ \max\{A_r,f(l,r-1)\} & (t<l)\\ -\infty & (l < r) \end{cases} f(l,r)= max{Br,f(l,t)}max{Ar,f(l,r1)}(tl)(t<l)(l<r)

等式 1 1 1 证明
根据树状数组的定义, B i = max ⁡ { A i − l o w b i t ( i ) + 1 , … , A i } = f ( i − l o w b i t ( i ) + 1 , i ) B_i=\max\{A_{i-\mathrm{lowbit}(i)+1},\dots,A_i\}=f(i-\mathrm{lowbit}(i)+1,i) Bi=max{Ailowbit(i)+1,,Ai}=f(ilowbit(i)+1,i)
又有 f ( l , r ) = max ⁡ { f ( l , t ) , f ( t + 1 , r ) } f(l,r)=\max\{f(l,t),f(t+1,r)\} f(l,r)=max{f(l,t),f(t+1,r)},由于 f ( t + 1 , r ) = f ( r − l o w b i t ( r ) + 1 , r ) = B r f(t+1,r)=f(r-\mathrm{lowbit}(r)+1,r)=B_r f(t+1,r)=f(rlowbit(r)+1,r)=Br,所以当 t ≥ l t\ge l tl时, f ( l , r ) = max ⁡ { B r , f ( l , t ) } f(l,r)=\max\{B_r,f(l,t)\} f(l,r)=max{Br,f(l,t)}

等式 2 2 2 证明
根据 max ⁡ \max max操作的结合律可得: f ( l , r ) = max ⁡ { f ( l , r − 1 ) , f ( r , r ) } = max ⁡ { f ( l , r − 1 ) , A r } f(l,r)=\max\{f(l,r-1),f(r,r)\}=\max\{f(l,r-1),A_r\} f(l,r)=max{f(l,r1),f(r,r)}=max{f(l,r1),Ar}
这个等式对于任意 r r r都成立,但出于时间考虑,我们尽可能使用等式 1 1 1(如果全用等式 2 2 2就退化成了 O ( N ) \mathcal O(N) O(N)的暴力)。

代码实现:

int rangeMax(int l, int r)
{
	if(l == r) return A[l];
	int t = r - lowbit(r);
	return t < l?
		max(A[r], rangeMax(l, r - 1)):
		max(bit[r], rangeMax(l, t));
}

这种查询方式的时间复杂度不好估算,可粗略地记为 O ( log ⁡ N ) \mathcal O(\log N) O(logN)。实际情况下,运行时间可能稍大于这个值。
另外,此算法对任意满足结合律的运算(如gcdlcm)都有效。

2.3.3 完整实现

下面给出用树状数组解决例题的完整代码。总时间复杂度为 O ( N + Q log ⁡ N ) \mathcal O(N+Q\log N) O(N+QlogN)1

#include <cstdio>
#include <algorithm>
#define maxn 100005
using namespace std;

#define lowbit(x) ((x) & -(x))

int a[maxn], bit[maxn];

int rangeMin(int l, int r)
{
	if(l == r) return a[l];
	int t = r - lowbit(r);
	return t < l?
		min(a[r], rangeMin(l, r - 1)):
		min(bit[r], rangeMin(l, t));
}

inline void init(int n)
{
	for(int i=1; i<=n; i++)
		bit[i] = a[i];
	for(int i=1; i<=n; i++)
	{
		int j = i + lowbit(i);
		if(j <= n)
			bit[j] = min(bit[j], bit[i]);
	}
}

int main()
{
	int n, q;
	scanf("%d%d", &n, &q);
	for(int i=1; i<=n; i++)
		scanf("%d", a + i);
	init(n);
	while(q--)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		printf("%d ", rangeMin(l, r));
	}
	return 0;
}

AC
运行时间: 207 m s 207\mathrm{ms} 207ms
使用内存: 1.10 M B 1.10\mathrm{MB} 1.10MB

另外,我们还可以把rangeMin写成非递归的形式,以进一步节省运行时间:

#include <cstdio>
#define maxn 100005
using namespace std;

#define INF 2147483647
#define lowbit(x) ((x) & -(x))
inline void setmin(int& x, int y) { if(y < x) x = y; }

int a[maxn], bit[maxn];

int rangeMin(int l, int r)
{
	int res = INF;
	while(l <= r)
	{
		int t = r - lowbit(r);
		if(t < l) setmin(res, a[r--]);
		else setmin(res, bit[r]), r = t;
	}
	return res;
}

inline void init(int n)
{
	for(int i=1; i<=n; i++)
		bit[i] = a[i];
	for(int i=1; i<=n; i++)
	{
		int j = i + lowbit(i);
		if(j <= n) setmin(bit[j], bit[i]);
	}
}

int main()
{
	int n, q;
	scanf("%d%d", &n, &q);
	for(int i=1; i<=n; i++)
		scanf("%d", a + i);
	init(n);
	while(q--)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		printf("%d ", rangeMin(l, r));
	}
	return 0;
}

AC

运行时间: 135 m s 135\mathrm{ms} 135ms
使用内存: 1.14 M B 1.14\mathrm{MB} 1.14MB

2.4 线段树

线段树和树状数组一样,都是解决区间问题的树状结构。不过线段树的应用范围更加广泛,时间复杂度与树状数组基本一致,但每种操作都有一个 3 ∼ 4 3\thicksim4 34之间的常数。线段树建树(初始化)的时间复杂度为 O ( N ) \mathcal O(N) O(N),单次区间查询的时间复杂度为 O ( log ⁡ N ) \mathcal O(\log N) O(logN)

RMQ 问题不涉及修改操作,因此我们暂时不考虑这种操作。

线段树

如图即为 N = 10 N=10 N=10的一棵线段树。可以发现,线段树本质上是一棵二叉树,每个结点代表一个区间,其存储的值为这个区间的区间和(在 RMQ 问题中为区间最大/最小值)。一个结点的左儿子结点和右儿子结点对应区间的并集正好为这个结点对应的区间(叶子结点除外),且左右两区间的长度的差值不超过 1 1 1

从树的角度考虑, n 1 = 0 n_1=0 n1=0,即没有子结点数量为 1 1 1的结点。

一般的,若一个结点对应的区间为 [ l , r ] [l,r] [l,r]

  • l = r l=r l=r:此结点为叶子结点。
  • l < r l<r l<r,令 m = ⌊ l + r 2 ⌋ m=\lfloor\frac{l+r}2\rfloor m=2l+r
    • 左子结点对应的区间为 [ l , m ] [l,m] [l,m]
    • 右子结点对应的区间为 [ m + 1 , r ] [m+1,r] [m+1,r]

顺便纠正一下线段树的几个误区:
线段树是一棵完全二叉树。
请仔细看看图。
线段树是一棵二叉搜索树。
反正我是没找到哪里这样定义的。百度百科 OI Wiki
线段树上同一深度的结点所对应的区间长度一定相等。
看看图, [ 1 , 3 ] [1,3] [1,3] [ 4 , 5 ] [4,5] [4,5]两个区间,长度明显不相等。
特例:当 N N N 2 2 2的整数次幂时,这句话一定成立。

2.4.1 存储结构

线段树采用堆式储存法,根结点为 1 1 1,结点 u u u的父亲为 ⌊ u 2 ⌋ \lfloor\frac u2\rfloor 2u,左子结点为 2 u 2u 2u,右子结点为 2 u + 1 2u+1 2u+1

可以用位运算优化:

  • u u u的父亲: ⌊ u 2 ⌋ =   \lfloor\frac u2\rfloor=~ 2u= u >> 1
  • u u u的左儿子: 2 u =   2u=~ 2u= u << 1
  • u u u的右儿子: 2 u + 1 =   2u+1=~ 2u+1= u << 1 | 1(或u << 1 ^ 1
// 数组定义
int a[N], c[4 * N];

// 宏定义
#define INF 2147483647
#define ls(x) (x << 1) // 左儿子结点
#define rs(x) (x << 1 | 1) // 右儿子结点
#define par(x) (x >> 1) // 父亲结点

下文中,我们令 N N N表示元素个数, A A A表示原数组, C C C表示线段树(数组形式存储)。
可以证明,线段树的结点个数不会超过 4 N − 5 4N-5 4N5,所以我们可以把 C C C的长度设为 4 N 4N 4N

2.4.2 建树(初始化)

对于一个结点数据的计算,我们可以先递归地初始化其左子树,再到右子树,最后两儿子的数据取 max ⁡ \max max即可。

代码实现:

// 结点p, 区间[l, r]建树
void build(int l, int r, int p)
{
	if(l == r)
	{
		c[p] = a[l];
		return;
	}
	int m = l + r >> 1;
	build(l, m, ls(p));
	build(m + 1, r, rs(p));
	c[p] = max(c[ls(p)], c[rs(p)]);
}

2.4.3 区间查询

同样采用递归的方式,设 p p p为当前结点, [ l , r ] [l,r] [l,r]为当前结点区间, [ a , b ] [a,b] [a,b]为当前查询区间,函数返回 [ l , r ] [l,r] [l,r] [ a , b ] [a,b] [a,b]交集区间和

  • 如果 [ l , r ] [l,r] [l,r] [ a , b ] [a,b] [a,b]的子集( a ≤ l ≤ r ≤ b a\le l\le r\le b alrb),直接返回当前结点对应的区间和。
  • 否则,递归查询左右子树,如果没有交集则不查询。返回查询的子树的最大值。

如果上面不是很好理解,可以直接看代码实现:

// 结点p, 查询区间[a, b], 当前区间[l, r]
int query(int l, int r, int a, int b, int p)
{
	if(a <= l && r <= b) return c[p];
	int m = l + r >> 1, res = -INF;
	if(m >= a) res = max(res, query(l, m, a, b, ls(p)));
	// m + 1 <= b 即为 m < b
	if(m < b) res = max(res, query(m + 1, r, a, b, rs(p)));
	return res;
}

2.4.4 完整实现

下面给出用线段树解决例题的完整代码。总时间复杂度为 O ( N + Q log ⁡ N ) \mathcal O(N+Q\log N) O(N+QlogN)

#include <cstdio>
#include <algorithm>
#define maxn 100005
using namespace std;

#define INF 2147483647
#define ls(x) (x << 1)
#define rs(x) (x << 1 | 1)
#define par(x) (x >> 1)

int a[maxn], c[maxn << 2];

void build(int l, int r, int p)
{
	if(l == r)
	{
		c[p] = a[l];
		return;
	}
	int m = l + r >> 1;
	build(l, m, ls(p));
	build(m + 1, r, rs(p));
	c[p] = min(c[ls(p)], c[rs(p)]);
}

int query(int l, int r, int a, int b, int p)
{
	if(a <= l && r <= b) return c[p];
	int m = l + r >> 1, res = INF;
	if(m >= a) res = min(res, query(l, m, a, b, ls(p)));
	if(m < b) res = min(res, query(m + 1, r, a, b, rs(p)));
	return res;
}

int main()
{
	int n, q;
	scanf("%d%d", &n, &q);
	for(int i=0; i<n; i++)
		scanf("%d", a + i);
	build(0, n - 1, 1);
	while(q--)
	{
		int l, r;
		scanf("%d%d", &l, &r);
		printf("%d ", query(0, n - 1, --l, --r, 1));
	}
	return 0;
}

AC
运行时间: 163 m s 163\mathrm{ms} 163ms
使用内存: 1.78 M B 1.78\mathrm{MB} 1.78MB

3. 总结

我们来对比一下四种算法,从理论的角度:

算法预处理时间复杂度单次查询时间复杂度空间复杂度符合题目要求?2
暴力 O ( 1 ) \mathcal O(1) O(1) O ( N ) \mathcal O(N) O(N) O ( N ) \mathcal O(N) O(N)
ST表 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN) O ( 1 ) \mathcal O(1) O(1) O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN)✔️
树状数组 O ( N ) \mathcal O(N) O(N) O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN) O ( log ⁡ N ) \mathcal O(\log N) O(logN) O ( N ) \mathcal O(N) O(N)✔️
线段树 O ( N ) \mathcal O(N) O(N) O ( log ⁡ N ) \mathcal O(\log N) O(logN) O ( N ) \mathcal O(N) O(N)✔️

从洛谷 P1816上实际运行情况的角度(暴力TLE,不考虑):

算法运行时间内存占用代码长度
ST表 128 m s 128\mathrm{ms} 128ms 6.90 M B 6.90\mathrm{MB} 6.90MB 625 B 625\mathrm B 625B
树状数组(递归)3 207 m s 207\mathrm{ms} 207ms 1.10 M B 1.10\mathrm{MB} 1.10MB 720 B 720\mathrm B 720B
树状数组(非递归)3 135 m s 135\mathrm{ms} 135ms 1.14 M B 1.14\mathrm{MB} 1.14MB 786 B 786\mathrm B 786B
线段树 163 m s 163\mathrm{ms} 163ms 1.78 M B 1.78\mathrm{MB} 1.78MB 905 B 905\mathrm B 905B

可以看出,ST表写起来简单省事,运行时间也是最快,不过内存占用稍高,毕竟空间复杂度是 O ( N log ⁡ N ) \mathcal O(N\log N) O(NlogN)
树状数组可以说是均衡了时间与空间,尽量使用非递归查询,比递归速度快 53 % 53\% 53%,当然如果想省事也可以使用递归式查询;
线段树可以说是完全输给了树状数组(非递归),不过线段树功能比较多,用来做 RMQ 可以说是大材小用。所以线段树在 RMQ 问题中没什么优势,有一些缺点还是可以理解的。


本文到此结束,希望大家给个三连!
这也是我在 2023 2023 2023年写的第一篇文章(也是我的第一篇万字长文),祝大家新年快乐!


  1. 直接调用update建树的方法总时间复杂度为 O ( ( N + Q ) log ⁡ N ) \mathcal O((N+Q)\log N) O((N+Q)logN),这里采用的是前面说的 O ( N ) \mathcal O(N) O(N)快速建树。在此问题中,由于不需要修改,使用 O ( N ) \mathcal O(N) O(N)建树可省去update方法。 ↩︎

  2. 以洛谷 P1816为准, N ≤ 1 0 5 N\le 10^5 N105,所以单次查询时间复杂度不能超过 O ( N ) \mathcal O(\sqrt N) O(N )。 ↩︎

  3. 此处指递归/非递归的单次查询实现,两者其他操作完全一致。 ↩︎ ↩︎

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/142576.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

《Linux运维实战:Centos7.6基于docker-compose一键离线部署单节点redis6.2.8 》

一、部署背景 由于业务系统的特殊性&#xff0c;我们需要面向不通的客户安装我们的业务系统&#xff0c;而作为基础组件中的redis针对不同的客户环境需要多次部署&#xff0c;作为一个运维工程师&#xff0c;提升工作效率也是工作中的重要一环。所以我觉得有必要针对redis6.2.8…

使用 .NET 标记游戏地图关键坐标点

本文以天涯明月刀 OL 游戏的云上之城探索玩法为例&#xff0c;介绍如何使用 .NET 在游戏地图中标记大量关键坐标点。 1. 背景 大概很多程序员都是喜欢玩游戏的吧&#xff0c;我也不例外。我们经常会看到电视剧中的各路游戏大神&#xff0c;要么是有只有他一个人会的骚操作&…

Linux--信号--信号的产生方式--核心转储--0104

1. 什么是信号 生活中的信号&#xff1a;红绿灯&#xff0c;狼烟&#xff0c;撤退、集合...。 我们认识这些信号&#xff0c;首先是因为自己记住了对应场景下的信号后续需要执行的动作。如果信号没有产生&#xff0c;我们依旧知道如何处理这个信号。收到信号&#xff0c;我们…

springboot学习(七十八) springboot中通过自定义注解实现数据脱敏的功能

文章目录前言一、引入hutools工具类二、定义常用需要脱敏的数据类型的枚举三、定义脱敏方式枚举四、自定义脱敏的注解五、自定义Jackson的序列化方式六、使用七、脱敏效果前言 对于某些接口返回的信息&#xff0c;涉及到敏感数据的必须进行脱敏操作&#xff0c;例如银行卡号、…

带你了解ssh服务过程

远程连接服务 1、什么是远程连接服务器 远程连接服务器通过文字或图形接口方式来远程登录系统&#xff0c;让你在远程终端前登录linux主机以取得可操作主机接口&#xff08;shell&#xff09;&#xff0c;而登录后的操作感觉就像是坐在系统前面一样。 2、远程连接服务器的功…

【C++】函数重载的使用及原理

概述 在学校里&#xff0c;我们都会有班里同学被起外号的经历&#xff0c;而且同一个人可能还会有好几个外号。 在自然语言中&#xff0c;一个词可以有多重含义&#xff0c;人们可以通过上下文来判断该词真实的含义&#xff0c;即该词被重载了。 目录 概述 什么是函数重载 …

项目管理:如何制作项目进度计划表?

项目进度管理是根据项目目标&#xff0c;编制合理的进度计划&#xff0c;并在项目推进过程中随时检查项目执行情况。 项目进度管理的目的就是为了实现最优工期&#xff0c;多快好省地完成任务。 而甘特图&#xff0c;就是用表格图形的方式来展示项目的进展&#xff0c;是一个比…

赛狐ERP:优秀的亚马逊运营具备的五项能力!

我们都知道&#xff0c;亚马逊运营是整个店铺的主导&#xff0c;很大程度上会影响着一个店铺经营的好坏&#xff0c;那么一个好的亚马逊运营&#xff0c;应该具备哪些能力呢&#xff1f;今天赛狐ERP就来给和大家聊一聊&#xff0c;希望对各位亚马逊运营们会有启发&#xff01;1…

ORB-SLAM2 --- LocalMapping::Run 局部建图线程解析

目录 一、线程作用 二、局部建图线程主要流程 三、局部建图线程主函数 四、调用函数解析 4.1 设置"允许接受关键帧"的状态标志LocalMapping::SetAcceptKeyFrames函数解析 4.2 查看列表中是否有等待被插入的关键帧LocalMapping::CheckNewKeyFrames函数 4.3 …

十分钟学会在linux上部署chrony服务器(再见 NTP,是时候拥抱下一代时间同步服务 Chrony 了)

chrony服务器 Chrony 相较于 NTPD 服务的优势 安装与配置&#xff08;Chrony的配置文件是/etc/chrony.conf&#xff09; 同步网络时间服务器 设置开机启动&#xff0c;重启服务 chronyc sources 输出结果解析 练习 实验模型图如下 实验a如下 实验b如下 再见 NTP&#x…

中国手机市场全面衰退,连苹果也未能幸免,大跌近三成

CINNO公布了11月份国内手机市场的数据&#xff0c;数据显示2022年11月份中国市场的手机出货量同比下滑21.7%&#xff0c;在整体大环境出现销量下滑的情况下&#xff0c;此前曾持续逆势增长的苹果也顶不住了&#xff0c;苹果在中国市场的出货量也出现了下滑的势头。数据显示2022…

06-Alibaba Nacos注册中心源码剖析

Nacos&Ribbon&Feign核心微服务架构图 架构原理 1、微服务系统在启动时将自己注册到服务注册中心&#xff0c;同时外发布 Http 接口供其它系统调用(一般都是基于SpringMVC) 2、服务消费者基于 Feign 调用服务提供者对外发布的接口&#xff0c;先对调用的本地接口加上注…

JS继承有哪些,你能否手写其中一两种呢?

引言 JS系列暂定 27 篇&#xff0c;从基础&#xff0c;到原型&#xff0c;到异步&#xff0c;到设计模式&#xff0c;到架构模式等&#xff0c; 本篇是 JS系列中第 3 篇&#xff0c;文章主讲 JS 继承&#xff0c;包括原型链继承、构造函数继承、组合继承、寄生组合继承、原型…

前端vue项目发送请求不携带cookie(vue.config.js和nginx反向代理)

一、本地环境——使用vue.config.js配置了跨域代理本来发现问题&#xff0c;是因为后台记录到接收到的sessionId一直在变化&#xff0c;导致需要在同一个sessionId下处理的逻辑无法实现。一开始以为是前后端分离跨域导致的&#xff0c;网上给出了解决方案&#xff1a;main.js中…

线程同步的实现

线程同步 同步就是协同步调&#xff0c;按预定的先后次序进行运行。如:你说完&#xff0c;我再说。 "同"字从字面上容易理解为一起动作 其实不是&#xff0c;"同"字应是指协同、协助、互相配合。 如进程、线程同步&#xff0c;可理解为进程或线程A和B一…

USB子系统简述

引子&#xff1a;关于 lsusb 命令 lsusb 列出系统中所有的USB设备&#xff1a; Bus 004 Device 001: ID 1d6b:0003 Linux Foundation 3.0 root hubBus 004 &#xff1a;表示第四个 usb 主控制器&#xff08;机器上总共有四个 usb 主控制器&#xff0c;可以通过命令 lspci | g…

看完这篇文章终于弄明白了什么是 RocketMQ 的存储模型

RocketMQ 优异的性能表现&#xff0c;必然绕不开其优秀的存储模型 。这篇文章&#xff0c;笔者按照自己的理解 , 尝试分析 RocketMQ 的存储模型&#xff0c;希望对大家有所启发。1 整体概览首先温习下 RocketMQ 架构。整体架构中包含四种角色 :Producer &#xff1a;消息发布的…

基于Python深度学习的垃圾分类代码,用深度残差网络构建

垃圾分类 完整代码下载地址&#xff1a;基于Python深度学习的垃圾分类代码 介绍 这是一个基于深度学习的垃圾分类小工程&#xff0c;用深度残差网络构建 软件架构 使用深度残差网络resnet50作为基石&#xff0c;在后续添加需要的层以适应不同的分类任务模型的训练需要用生…

Qt扫盲-QSerialPort理论总结

QSerialPort理论总结一、概述二、使用流程1. 错误处理2. 阻塞串行端口编程3. 非阻塞串行端口编程三、信号四、注意事项一、概述 QSerialPort 类其实就是一个打开串口&#xff0c;进行串口通信传输数据的功能类。我们可以使用QSerialPortInfo帮助类获取有关可用串行端口的信息&…

JavaEE高阶---Spring AOP

一&#xff1a;什么是Spring AOP&#xff1f; 首先&#xff0c;AOP是一种思想&#xff0c;它是对某一类事情的集中处理。 如用户登录权限的效验&#xff0c;没学 AOP 之前&#xff0c;我们所有需要判断用户登录的页面&#xff0c;都要各自实现或调用验证的方法。然后有了 AOP …