是的,每个查询都可以得到O(log N),但是解释会有点长……
这个问题可以描述为“唯一范围产品查询”。这类似于“唯一范围求和查询”问题,您需要求和,而不是乘积或唯一元素。
唯一范围求和查询类似于“范围内不同元素的计数”问题,解决方案为described here。这篇文章不太容易理解(特别是如果你没有参加过很多高水平的算法编程比赛),所以我将在这里更详细地解释“范围内不同元素的计数”的解决方案。
计算范围内的不同元素
假设你有values = [1, 2, 3, 1, 2, 1, 3]。对于每个值,您
想找到具有相同值的下一个位置。这是
nextposes = [4, 5, 7, 6, inf, inf, inf]。假设你有
query = [2, 6],nextposes = [5, 6, 7, inf, inf],其中 3 个 > 6,所以
答案是 3。
这是我从帖子中复制示例的简单部分。这给出了我们想要解决的问题的想法,但不是以一种易于理解的方式。下一个想法是将其放入 2D 树中。这篇文章没有解释它是如何工作的,所以我会在这里解释一下。
2D 树是段树的段树。因此,您需要构建一个线段树,并在第一个线段树的每个顶点上,构建另一个线段树。您还必须确定在段树中放入什么。从上面的例子中,2D segtree 描述了一个 2D 平面。您在平面上有很多点,您想将它们插入到结构中。示例中的点是(1, 4), (2, 5), (3, 7), (4, 6), (5, inf), (6, inf), (7, inf)。那是 (x,y) = (array position, next array position) 。如果将这些绘制在平面上,您将意识到对于每个查询,您必须计算矩形区域中的点数。在给出的示例中,您想要计算2 <= x <= 6, y > 6 所在的点数。如果你遍历你的段树,它会花费O(log^2 N) 来做这件事。更新树,还要遍历段树,代价是一样的。
优化
如果你做一个简单的 2D 段树,它将花费你 O(N*max(V)) 内存(其中 max(V) 是最大可能的整数值),并且可能与获取此内存的时间相同。事实上,你不需要那么多内存。您可以构建一个延迟初始化的 2D 线段树。这意味着您从树顶部的单个节点开始代表整个范围,并且仅在需要时才拆分它(因为您在树中插入一些东西)。这会将内存成本降低到O(N log^2 (N))。
唯一范围求和查询
要获得总和,您可以对插入到 2D 树中的每个顶点进行加权。不要认为您需要帖子中提到的陷阱。
唯一范围产品查询
不用加法,每次你想加法,就用乘法代替。
最后,算法复杂度为O((N + Q) log^2 N)。在 O(log^2 N) 的每个查询中,这不如 log(N) 您要求的每个查询那么好。我现在将解释如何通过查询获得O(log N)。
O(log N) 每个查询
为了实现这一点,我们可以使用持久数据结构。持久性数据结构是您可以对其进行修改(即进行更新)的结构,并且您将获得更新后的新结构,并且您还可以在更新之前保留旧结构(有点像不可变字符串)。然而,持久结构以优化的方式做到这一点,因为新旧数据结构将共享它们的大部分内容。对于分段树,假设您要更新某个位置。您只需要在通往新位置的路径上创建新节点(即需要重新创建 O(log N) 个节点)。其余节点可以与旧数据结构中的相同。
现在我们知道了持久数据结构是什么,想法是采用我们之前插入到 2D 树中的 (x,y) 坐标。这一次,我们要按 y 坐标降序对它们进行排序。然后我们按此顺序将这些点插入到一维持久线段树中。每次插入的费用为O(log N)。
我们在每次插入后保留一个指向段树根节点的指针数组。因此,对于 y > some number 的每个约束,我们都有 1 个分段树。
现在像以前一样查询矩形区域2 <= x <= 6, y > 6,我们首先找到代表y > 6的段树(就在我们插入点(4,6)之前)。然后,我们对范围[2,6] 进行范围乘积查询,花费O(log N) 时间。
由于构建N 持久段树的成本为O(N log N),并且每个查询为O(log N),因此总时间复杂度为O((N + Q) log N)!
这个解决方案的实现非常复杂,因为您需要创建一个持久的、延迟初始化的分段树。但是,我确实认为编写一维持久分段树比编写二维分段树更容易。