[英]Find the number of elements greater than x in a given range
给定一个包含 n 个元素的数组,如何以 O(log n) 复杂度在给定范围索引 i 到索引 j 中找到大于或等于给定值 (x) 的元素数量?
查询的形式为 (i, j, x),这意味着从数组中的第 i 到第 j 个元素中查找大于 x 的元素数
数组未排序。 i, j & x 对于不同的查询是不同的。 数组的元素是静态的。 编辑:对于不同的查询,i、j、x 都可以不同!
如果我们事先知道所有查询,我们可以通过使用Fenwick 树来解决这个问题。
首先,我们需要根据它们的值对数组和查询中的所有元素进行排序。
因此,假设我们有数组 [5, 4, 2, 1, 3] 和查询 (0, 1, 6) 和 (2, 5, 2),我们将在排序后得到以下结果: [1, 2, 2 , 3 , 4 , 5, 6]
现在,我们需要按降序处理每个元素:
如果我们遇到一个来自数组的元素,我们将更新它在 Fenwick 树中的索引,这需要 O(log n)
如果遇到查询,我们需要检查在查询的这个范围内,树中添加了多少元素,需要 O(log n)。
对于上面的例子,过程将是:
1st element is a query for value 6, as Fenwick tree is empty -> result is 0
2nd is element 5 -> add index 0 into Fenwick tree
3rd element is 4 -> add index 1 into tree.
4th element is 3 -> add index 4 into tree.
5th element is 2 -> add index 2 into tree.
6th element is query for range (2, 5), we query the tree and get answer 2.
7th element is 1 -> add index 3 into tree.
Finish.
因此,总的来说,我们的解决方案的时间复杂度为 O((m + n) log(m + n)),其中 m 和 n 分别是来自输入数组的查询数和元素数。
只有当您对数组进行排序时,这才是可能的。 在这种情况下,二进制搜索通过您的条件的最小值并通过将您的索引范围除以其找到的位置细分为两个间隔来计算计数。 然后只需计算通过您的条件的间隔的长度。
如果数组未排序并且您需要保留其顺序,则可以使用索引 sort 。 放在一起时:
定义
让<i0,i1>
是您使用的索引范围, x
是您的值。
索引排序数组部分<i0,i1>
所以创建大小为m=i1-i0+1
数组并对其进行索引排序。 这个任务是O(m.log(m))
其中m<=n
。
二分查找x
在索引数组中的位置
这个任务是O(log(m))
并且你想要索引j = <0,m)
其中array[index[j]]<=x
是最小值<=x
计算计数
简单地计算j
到m
之后有多少个索引
count = mj;
如您所见,如果数组已排序,您的复杂度为O(log(m))
但如果不是,则您需要对O(m.log(m))
进行排序,这比简单的方法O(m)
更糟糕,这应该是仅在数组经常更改且无法直接排序时使用。
[Edit1] 我所说的索引排序是什么意思
通过索引排序,我的意思是:让数组a
a[] = { 4,6,2,9,6,3,5,1 }
索引排序意味着您按排序顺序创建索引的新数组ix
因此例如升序索引排序意味着:
a[ix[i]]<=a[ix[i+1]]
在我们的例子中,索引冒泡排序是这样的:
// init indexes
a[ix[i]]= { 4,6,2,9,6,3,5,1 }
ix[] = { 0,1,2,3,4,5,6,7 }
// bubble sort 1st iteration
a[ix[i]]= { 4,2,6,6,3,5,1,9 }
ix[] = { 0,2,1,4,5,6,7,3 }
// bubble sort 2nd iteration
a[ix[i]]= { 2,4,6,3,5,1,6,9 }
ix[] = { 2,0,1,5,6,7,4,3 }
// bubble sort 3th iteration
a[ix[i]]= { 2,4,3,5,1,6,6,9 }
ix[] = { 2,0,5,6,7,1,4,3 }
// bubble sort 4th iteration
a[ix[i]]= { 2,3,4,1,5,6,6,9 }
ix[] = { 2,5,0,7,6,1,4,3 }
// bubble sort 5th iteration
a[ix[i]]= { 2,3,1,4,5,6,6,9 }
ix[] = { 2,5,7,0,6,1,4,3 }
// bubble sort 6th iteration
a[ix[i]]= { 2,1,3,4,5,6,6,9 }
ix[] = { 2,7,5,0,6,1,4,3 }
// bubble sort 7th iteration
a[ix[i]]= { 1,2,3,4,5,6,6,9 }
ix[] = { 7,2,5,0,6,1,4,3 }
所以升序索引排序的结果是这样的:
// ix: 0 1 2 3 4 5 6 7
a[] = { 4,6,2,9,6,3,5,1 }
ix[] = { 7,2,5,0,6,1,4,3 }
原始数组保持不变,只是索引数组发生了变化。 项目a[ix[i]]
其中i=0,1,2,3...
按升序排序。
所以现在如果x=4
在这个区间你需要找到(bin search) i
最小但仍然a[ix[i]]>=x
所以:
// ix: 0 1 2 3 4 5 6 7
a[] = { 4,6,2,9,6,3,5,1 }
ix[] = { 7,2,5,0,6,1,4,3 }
a[ix[i]]= { 1,2,3,4,5,6,6,9 }
// *
i = 3; m=8; count = m-i = 8-3 = 5;
所以答案是5
项目>=4
[Edit2] 只是为了确保您知道二进制搜索对此意味着什么
i=0; // init value marked by `*`
j=4; // max power of 2 < m , i+j is marked by `^`
// ix: 0 1 2 3 4 5 6 7 i j i+j a[ix[i+j]]
a[ix[i]]= { 1,2,3,4,5,6,6,9 } 0 4 4 5>=4 j>>=1;
* ^
a[ix[i]]= { 1,2,3,4,5,6,6,9 } 0 2 2 3< 4 -> i+=j; j>>=1;
* ^
a[ix[i]]= { 1,2,3,4,5,6,6,9 } 2 1 3 4>=4 j>>=1;
* ^
a[ix[i]]= { 1,2,3,4,5,6,6,9 } 2 0 -> stop
*
a[ix[i]] < x -> a[ix[i+1]] >= x -> i = 2+1 = 3 in O(log(m))
所以你需要索引i
和二进制位掩码j
(2 的幂)。 首先设置i
为零, j
的最大幂为 2 仍然小于n
(或在这种情况下m
)。 例如这样的事情:
i=0; for (j=1;j<=m;j<<=1;); j>>=1;
现在在每次迭代中测试a[ix[i+j]]
满足搜索条件。 如果是,则更新i+=j
否则保持原样。 之后转到下一位,因此j>>=1
并且如果j==0
停止否则再次进行迭代。 最后你发现值是a[ix[i]]
并且索引是i
在log2(m)
迭代中,这也是表示m-1
所需的位数。
在上面的示例中,我使用条件a[ix[i]]<4
因此找到的值是数组中仍然<4
最大数字。 因为我们还需要包含4
所以我只在最后增加一次索引(我可以使用<=4
代替,但懒得重新重写整个事情)。
这些项目的数量就是数组(或间隔)中的元素数减去i
。
先前的答案描述了使用 Fenwick 树的离线解决方案,但可以在线解决此问题(甚至在对阵列进行更新时),但复杂性稍差。 我将使用段树和 AVL 树来描述这样的解决方案(任何自平衡 BST 都可以解决问题)。
首先让我们看看如何使用段树解决这个问题。 我们将通过按它覆盖的范围将数组的实际元素保留在每个节点中来做到这一点。 所以对于数组A = [9, 4, 5, 6, 1, 3, 2, 8]
我们将有:
[9 4 5 6 1 3 2 8] Node 1
[9 4 5 6] [1 3 2 8] Node 2-3
[9 4] [5 6] [1 3] [2 8] Node 4-7
[9] [4] [5] [6] [1] [3] [2] [8] Node 8-15
由于我们的段树的高度是log(n)
并且在每个级别我们保留 n 个元素,因此使用的内存总量是n log(n)
。
下一步是对这些数组进行排序,如下所示:
[1 2 3 4 5 6 8 9] Node 1
[4 5 6 9] [1 2 3 8] Node 2-3
[4 9] [5 6] [1 3] [2 8] Node 4-7
[9] [4] [5] [6] [1] [3] [2] [8] Node 8-15
注意:您首先需要构建树,然后对其进行排序以保持原始数组中元素的顺序。
现在我们可以开始我们的范围查询,它的工作方式与常规线段树基本相同,除了当我们发现一个完全重叠的区间时,我们会额外检查大于 X 的元素数量。 这可以通过日志中的二分搜索来完成(n) 通过找到大于 X 的第一个元素的索引并从该间隔中的元素数中减去它来计算时间。
假设我们的查询是(0, 5, 4)
,所以我们在区间[0, 5]
上进行段搜索并最终得到数组: [4, 5, 6, 9], [1, 3]
。 然后我们对这些数组进行二分搜索以查看大于 4 的元素数量,并得到 3(来自第一个数组)和 0(来自第二个数组),总共 3 -我们的查询答案。
段树中的间隔搜索最多可以有log(n)
路径,这意味着log(n)
数组,并且由于我们对每个数组进行二分搜索,因此每个查询都会增加log^2(n)
复杂性。
现在如果我们想更新数组,因为我们使用的是段树,所以不可能有效地添加/删除元素,但我们可以替换它们。 使用 AVL 树(或其他允许在 log(n) 时间内替换和查找的二叉树)作为节点并存储数组,我们可以在相同的时间复杂度(用log(n)
时间替换log(n)
管理此操作。
这是 2D 中正交范围计数查询的特殊变体。 每个元素el[i]
被转换为平面上的点(i, el[i])
并且查询(i,j,x)
可以转换为计算矩形[i,j] x [x, +infty]
。
您可以将 2D 范围树(例如: http : //www.cs.uu.nl/docs/vakken/ga/slides5b.pdf )用于此类查询。
简单的想法是有一个树来存储按 X 轴排序的叶子中的点(每个叶子包含一个点)。 树的每个内部节点都包含额外的树,用于存储子树中的所有点(按 Y 轴排序)。 已用空间为O(n logn)
简单版本可以在O(log^2 n)
时间内进行计数,但使用分数级联可以将其减少到O(log n)
。
Chazelle 在 1988 年 ( https://www.cs.princeton.edu/~chazelle/pubs/FunctionalDataStructures.pdf ) 有更好的解决方案,以O(n)
预处理和O(log n)
查询时间。
您可以找到一些查询时间更短的解决方案,但它们要复杂得多。
我会尝试给你一个简单的方法。
你一定学过归并排序。 在合并排序中,我们继续将数组划分为子数组,然后重新构建它,但是在这种方法中我们不存储排序的子数组,而是将它们存储为二叉树的节点。
这会占用 nlogn 空间和 nlogn 时间来构建; 现在对于每个查询,您只需要找到子数组,这将平均在 logn 中完成,在最坏的情况下以 logn^2 完成。
这些树也被称为芬威克树。 如果你想要一个简单的代码,我可以为你提供。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.