RMQ(Range Minimum Query)
: 给定一个数组,求给定的两个索引(下标)间最小值元素的索引.
符号意义
:
假设一个算法有 f(n)
的预处理时间和g(n)
的查询时间.则这个算法的总的时间复杂度记为
记数组A
在索引i
和j
之间的最小值元素的索引为RMQA
(i, j)
.
例子:A[0,9]
对于给定的数组A[0,N-1]
,N
为元素个数
解法一
:动态规划
对于每对索引(i , j)
存储值RMQA
(i, j)
在数组M[0, N-1][0, N-1]
中
预处理函数如下:
- void process1(int M[MAXN][MAXN], int A[MAXN], int N)
- {
- int i, j;
- for (i =0; i < N; i++)
- M[i][i] = i;
- for (i = 0; i < N; i++)
- for (j = i + 1; j < N; j++)
- if (A[M[i][j - 1]] < A[j])
- M[i][j] = M[i][j - 1];
- else
- M[i][j] = j;
- }
算法时间复杂度为2
) , O(1)>
,空间复杂度为O(N2
)
,对于N比较大时,很耗内存
解法二
:Sparse Table ( 稀疏表 )
一个好的方法是对长度为
2k
的子数组进行预处理。保存一个数组M[0, N-1][0, logN]
,M[i][j]
表示从一个下标从i
开始,长度为2j
的子数组中最小元素的下标
例子如下:
为了计算M[i][j]
,我们必须搜索这个区间的前半部分和后半部分.很明显,每一部分的长度为 2j - 1
。递推式如下:
(原文这里貌似有问题,i+2^(j-1)-1应该改为i+2^(j-1))
预处理函数如下所示:
- void process2(int M[MAXN][LOGMAXN], int A[MAXN], int N)
- {
- int i, j;
-
-
- for (i = 0; i < N; i++)
- M[i][0] = i;
-
- 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];
- }
预处理后,让我们看看怎么计算
RMQA
(i, j)
.
思想就是选择出两个将区间
[i..j]
完全覆盖的小区间(预处理的
M
),然后取其较小值. 取
k = [log(j - i + 1)]
. 为了计算
RMQA
(i, j)
使用如下公式:
此算法总的时间复杂度为
.
解法三
:Segment Tree(线段树)
为了解决RMQ,还可以使用线段树.线段树能在对数时间内在数组区间上进行更新与查询。
我们定义线段树在区间[i, j]
上如下:
- 第一个节点维护着区间 [i, j]
的信息。
- if i
, 那么左孩子维护着区间[i, (i+j)/2]
的信息,右孩子维护着区间[(i+j)/2+1, j]
的信息。
可知
N
个元素的线段树的高度 为
[logN] + 1(只有根节点的树高度为0)
. 下面是区间
[0, 9]
的一个线段树:
线段树和堆有一样的结构, 因此如果一个节点编号为 x
,那么左孩子编号为2*x
右孩子编号为2*x+1
(在这里下标从1开始).
使用线段树解决RMQ问题,我们要维护一个数组M[1, 2 * 2[logN] + 1
]
,
M[i]
维护着被分配给该节点的区间的最小值元素的下标。 该数组初始状态为-1
. 树应该被以下函数初始化 (b
和 e
是当前区间的左右边界):
- void initialize(intnode, int b, int e, int M[MAXIND], int A[MAXN], int N)
- {
- if (b == e)
- M[node] = b;
- else
- {
-
- initialize(2 * node, b, (b + e) / 2, M, A, N);
- initialize(2 * node + 1, (b + e) / 2 + 1, e, M, A, N);
-
-
- if (A[M[2 * node]] <= A[M[2 * node + 1]])
- M[node] = M[2 * node];
- else
- M[node] = M[2 * node + 1];
- }
- }
该函数反映的是树被构建的方法。 我们应该用 node
= 1
, b = 0
和 e = N-1
来调用这个函数
现在开始查询. 如果我们想要找出区间 [i, j]
上的最小值的索引,我们应该使用下面这个简单的函数:
- int query(int node, int b, int e, int M[MAXIND], int A[MAXN], int i, int j)
- {
- int p1, p2;
-
-
-
-
- if (i > e || j < b)
- return -1;
-
-
-
- if (b >= i && e <= j)
- return M[node];
-
-
-
- p1 = query(2 * node, b, (b + e) / 2, M, A, i, j);
- p2 = query(2 * node + 1, (b + e) / 2 + 1, e, M, A, i, j);
-
-
-
- if (p1 == -1)
- return M[node] = p2;
- if (p2 == -1)
- return M[node] = p1;
- if (A[p1] <= A[p2])
- return M[node] = p1;
- return M[node] = p2;
-
- }
我们可以用 node = 1
, b = 0
和 e = N - 1
来调用这个函数。 因为第一个节点的区间是 [0, N-1]
.
很容易看出任何查询都能在时间 O(log N)
内完成。
使用线段树得到了一个
的算法.
线段树非常有用,不仅仅因为它可以用在解决RMQ问题上。它是一个非常灵活的数据结构,
在范围搜索问题上有很多应用.
阅读(1200) | 评论(0) | 转发(0) |