在O(lg N)时间内查询一个范围内不同整数的数量,是否可能?

23

我已经阅读了一些关于两种常见数据结构的教程,它们可以在O(lg N)的时间内实现区间更新和查询:线段树二叉索引树(BIT / Fenwick Tree)。

我找到的大多数示例都涉及到某些类似“范围内整数求和”、“范围内整数异或”等可交换和可结合运算。

我想知道这两个数据结构(或者任何其他数据结构/算法,请提议)是否可以在O(lg N)的时间内实现以下查询?(如果不行,那么O(sqrt N)呢)

给定一个整数数组A,在区间[l,r]中查询不同整数的数量

PS:假设可用整数的数量约为10^5,因此used [color] = true或位掩码不可能。

例如:A =[1,2,3,2,4,3,1],query([2,5]) = 3,其中范围索引从0开始。


1
如果A中没有任何结构(例如排序),则O(n)是下限,因为您需要查看每个元素。 - sascha
@sascha 我明白你的意思,但是你是否意味着如果A已经排序,那么查询可以在O(lg N)时间内完成?因为在排序之前追踪每个元素的原始索引似乎非常困难... - shole
我不确定,但是上限当然被减少到O(r-l) - sascha
@sascha 这个参数产生了预处理+查询时间的下限,但 OP 对查询时间感兴趣。这类似于如何在次线性查询时间内完成范围求和。 - xuanji
6个回答

43
是的,即使您需要在线回答查询,也可以在O(log n)中完成此操作。但是,这需要一些相当复杂的技术。
首先,让我们解决以下问题:给定一个数组,回答形式为“在[l,r]范围内有多少个数字<= x”的查询。这可以通过类似于段树的结构来完成,有时称为Merge Sort Tree。它基本上是一个段树,其中每个节点存储一个排序的子数组。该结构需要O(n log n)的内存(因为有log n层,每层都需要存储n个数字)。它也可以在O(n log n)中构建:您只需自下而上地遍历,并为每个内部顶点合并其子代的排序列表。
这是一个例子。假设1 5 2 6 8 4 7 1是原始数组。
|1 1 2 4 5 6 7 8|
|1 2 5 6|1 4 7 8|
|1 5|2 6|4 8|1 7|
|1|5|2|6|8|4|7|1|

现在你可以在O(log^2 n时间)内回答这些查询:只需对段树进行正常查询(遍历O(log n)个节点),然后进行二分查找,以了解该节点中有多少个数字≤ x(从这里开始另外需要O(log n)的时间)。
使用Fractional Cascading技术可以将其加速到O(log n),它基本上允许您在根节点而不是每个节点中执行二分查找。但是它足够复杂,需要在文章中进行描述。
现在我们回到原始问题。假设您有一个数组a_1,...,a_n。构建另一个数组b_1,...,b_n,其中b_i = a_i在数组中下一个出现的索引,如果它是最后一次出现,则为∞。
例(从1开始索引):
a = 1 3 1 2 2 1 4 1
b = 36 58 ∞ ∞

现在让我们在[l, r]中计算数字。对于每个唯一的数字,我们将计算其在段中的最后出现。通过b_i的概念,您可以看到数字的出现仅当b_i > r时才是最后的。因此,问题归结为“在段[l,r]中有多少个数字>r”,这显然可以简化为我上面描述的内容。
希望能对您有所帮助。

好的回答。也许你可以回答这个问题:http://cs.stackexchange.com/questions/64137/how-many-range-are-completely-inside-a-given-range - Saeid
@Tempux 我读到这个问题时已经有人回答了,尽管我试图展示如何使用相同的结构来满足你的需求。 - Ivan Smirnov
我们可以在每个线段树节点上维护一个集合,而不是维护向量。在返回查询结果时,我们可以返回该节点维护的集合,并最终返回集合的大小。 - Ajay Kumar

7
如果您愿意离线回答查询,则普通的Segment Trees/BIT仍然有用。
请按r值对查询进行排序。
为区间求和查询[0,n]创建一个Segment Tree。
对于从左到右的输入数组中的每个值:
在段树中当前索引i处增加1。
对于当前元素,如果之前已经见过,则在它的上一个位置处将其在段树中减少1。
通过查询[l,r==i]范围内的总和来回答以当前索引i结尾的查询。
简而言之,该想法是保持向右标记索引,每个单独元素的最新出现并将先前出现的元素设置为0。范围的总和将给出唯一元素的计数。
总体时间复杂度仍然为nLogn。

3

有一种著名的离线方法可以解决这个问题。如果你有一个大小为n的数组和q个查询,在每个查询中,你需要知道该范围内不同数字的计数,那么你可以在O(n log n + q log n)的时间复杂度内解决整个问题。这类似于在O(log n)的时间内解决每个查询。

让我们使用RSQ(区间和查询)技术来解决这个问题。对于RSQ技术,您可以使用线段树或BIT。让我们讨论线段树技术。

要解决此问题,您需要一个离线技术和一个线段树。现在,什么是离线技术??离线技术是在离线状态下执行某些操作。在问题求解中,离线技术的一个例子是,您首先输入所有查询,然后以一种方式重新排序它们,使您可以正确且轻松地回答它们,最后按给定的输入顺序输出答案。

解决思路:

首先,输入一个测试用例,并将给定的n个数字存储在数组中。假设数组名称为array [],并输入q个查询并将它们存储在向量v中。其中v的每个元素都包含三个字段-l,r,idx。其中l是查询的起点,r是查询的终点,而idx是查询的数量。例如:这是第n个查询。现在,根据查询的终点对向量v进行排序。假设我们有一个可以存储至少10 ^ 5个元素信息的线段树。我们还有一个名为last [100005]的数组,它存储数字在array []中的最后位置。

初始时,树的所有元素都为零,而上述所有元素均为-1。现在在数组[]上运行循环。现在,在循环内部,您需要检查每个数组[]索引的以下内容。

last [array [i]]是否为-1?如果是-1,则写入last [array [i]] = i,并调用update()函数,该函数将在segment tree的last [array [i]] th位置加上+1。如果last [array [i]]不是-1,则调用segment tree的update()函数,该函数将在segment tree的last [array [i]] th位置减去1或添加-1。现在,您需要将当前位置存储为将来的最后位置。因此,您需要编写last [array [i]] = i,并调用update()函数,该函数将在segment tree的last [array [i]] th位置添加+1。

现在,您需要检查查询是否在当前索引完成。也就是说,如果(v [current]。r == i)。如果这是真的,那么调用segment tree的query()函数,该函数将返回范围v [current]。l到v [current]。r和将结果存储在答案[]数组的v [current]。idx ^ th索引中。您还需要将当前值增加1。6.现在打印包含您最终答案的答案[]数组,按给定的输入顺序。

算法的复杂度为O(n log n)。


2
给定的问题也可以使用Mo算法(离线)解决,也称为平方根分解算法。
总时间复杂度为O(N*SQRT(N))。
有关详细说明,请参见mos-algorithm,它甚至具有复杂性分析和可以使用此方法解决的SPOJ问题。

1

kd树提供O(logn)的范围查询,其中n是点的数量。

如果您想要比kd树更快的查询,并且愿意支付内存成本,那么范围树就是您的好朋友,提供以下查询:

O(logdn + k)

其中n是树中存储的点数,d是每个点的维度,k是给定查询报告的点数。


在这个领域,Bentley 是一个重要的名字。 :)


1
对我来说一下子理解有点困难,但我感觉这可能是正确的方向...先放一下,让我花些时间阅读和测试,然后再接受吧 :) - shole
1
@shole,请原谅我的无知,UV是什么? - Liviu
kd树提供O(log n)点查询,但我认为枚举点(例如计算不同整数的数量)在输出大小上是线性的,使它们对于这个问题是超线性的。 - xuanji

0
在Ivan Smirnov的最佳答案基础上进行改进,这里是用Python编写的原型实现。
from bisect import bisect_left
import random
import time

def build_index(input):
    n = len(input)
    last = {}
    next = []
    for j in range(n):
        i = n - j - 1
        if input[i] in last:
            next.append(last[input[i]])
        else:
            next.append(n)
        last[input[i]] = i
    next.reverse()
    return next

def merge(left, right):
    answer = []
    p = 0
    q = 0
    while True:
        if p == len(left):
            if q == len(right):
                return answer
            else:
                answer.append(right[q])
                q += 1
        elif q == len(right):
            answer.append(left[p])
            p += 1
        elif left[p] < right[q]:
            answer.append(left[p])
            p += 1
        else:
            answer.append(right[q])
            q += 1

def build_tree(input, l, r):
    if r - l == 1:
        return (input[l:r], None, None)
    else:
        mid = (l + r) // 2
        left = build_tree(input, l, mid)
        right = build_tree(input, mid, r)
        merged = merge(left[0], right[0])
        return (merged, left, right)
    
def query(index, tree_left, tree_right, query_l, query_r):
    tree_width = tree_right - tree_left
    if tree_width == 1:
        if index[0][0] >= query_r:
            return 1
        else:
            return 0
    else:
        mid = (tree_left + tree_right) // 2
        if query_r <= mid:
            return query(index[1], tree_left, mid, query_l, query_r)
        elif query_l >= mid:
            return query(index[2], mid, tree_right, query_l, query_r)
        elif query_l <= tree_left and query_r >= tree_right:
            arr = index[0]
            return tree_width - bisect_left(arr, query_r)
        else:
            return query(index[1], tree_left, mid, query_l, query_r) + query(index[2], mid, tree_right, query_l, query_r)

def brute(input, i, j):
    seen = {}
    for k in range(i, j):
        seen[input[k]] = True
    return len(seen)

input = random.choices(list(range(8)), k=100000)
index = build_index(input)
tree = build_tree(index, 0, len(index))
n = len(input)
i = random.randint(0, n)
j = random.randint(i + 1, n)

t1 = time.time()
q = query(tree, 0, len(index), i, j)
t2 = time.time()
b = brute(input, i, j)
t3 = time.time()

print(q == b)
print(t2 - t1)
print(t3 - t2)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接