给定一个包含n个元素的数组,如何在给定范围索引i中找到大于或等于给定值(x)的元素个数到O(log n)复杂度中的索引j?
查询的形式为(i,j,x),这意味着从数组中的第i个到第j个元素中查找大于x的元素数
数组未排序。我,j& x对于不同的查询是不同的。数组的元素是静态的。 编辑:i,j,x对于不同的查询都可以是不同的!
答案 0 :(得分:11)
如果我们事先了解所有问题,我们可以使用Fenwick tree解决此问题。
首先,我们需要根据数值对数组和查询中的所有元素进行排序。
因此,假设我们有数组[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分别是来自输入数组的查询数和元素数。
答案 1 :(得分:1)
只有排序了数组才有可能。在这种情况下,二元搜索通过条件的最小值并仅通过将索引范围除以其找到的位置到两个间隔来计算计数。然后计算通过条件的间隔的长度。
如果数组未排序且您需要保留其顺序,则可以使用索引排序。放在一起时:
<强>定义强>
让<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 = m-j;
正如您所看到的,如果对数组进行了排序,则会导致O(log(m))
复杂度,但如果不是,则需要对O(m.log(m))
进行排序,这比仅应使用的天真方法O(m)
更糟糕如果数组经常更改并且无法直接排序。
[编辑1]我的意思是索引排序
按索引排序我的意思是:让数组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搜索)哪个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
。
答案 2 :(得分:0)
这是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)
查询时间。
你可以找到一些具有更好查询时间的解决方案,但它们更复杂。
答案 3 :(得分:0)
我会尝试给你一个简单的方法。
你必须研究合并排序。 在合并排序中,我们继续将数组划分为子数组,然后将其构建回来,但我们不将这些排序的子数组存储在这种方法中,我们将它们存储为二叉树的节点。
这会占用nlogn空间和nlogn时间来构建; 现在,对于每个查询,您只需要找到这个子数组,这将在logn中平均完成,并在最坏的情况下记录^ 2。
这些树也被称为芬威克树。 如果你想要一个简单的代码,我可以为你提供。
答案 4 :(得分:0)
先前的答案描述了使用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的元素数。这可以用二进制完成通过找到第一个元素刨丝器的索引然后找到X并从该间隔中的元素数中减去它来搜索log(n)时间。
可以说我们的查询是(0, 5, 4)
,因此我们在间隔[0, 5]
上进行了段搜索,最后得到了数组:[4, 5, 6, 9], [1, 3]
。然后,我们在这些数组上进行二进制搜索,以查看元素数,然后是4,然后得到3(从第一个数组开始)和0(从第二个数组开始),总共得到3个-我们的查询答案。< / p>
段树中的间隔搜索最多可以有log(n)
条路径,这意味着log(n)
个数组,并且由于我们正在对它们中的每个进行二进制搜索,因此每个查询的log^2(n)
带来了复杂性
现在,如果我们要更新数组,因为我们正在使用段树,则不可能有效地添加/删除元素,但是我们可以替换它们。使用AVL树(或其他允许在log(n)时间中进行替换和查找的二叉树)作为节点并存储数组,我们可以以相同的时间复杂度(用log(n)
时间替换)来管理此操作。