我们有一个序列a 1 ,一个 2 ,...,一个 n ,其中一个 i 是整数,| a i | &lt; = 10 6 且n <= 10 6 。我们有以下形式的查询:&#34; i j&#34;这意味着&#34; i ,a i + 1 ,..., j 的顺序中位数是什么?&# 34;
你知道怎么做吗?我知道有一种算法可以在线性时间内找到序列中位数(https://en.wikipedia.org/wiki/Median_of_medians),但在每个查询中应用它都太慢了。
答案 0 :(得分:4)
问题称为范围中位数查询。有些算法具有不同的复杂性和属性,请参阅this和this作为起点。
Mark Gordon对Quora的回答:
创建从表格的N个点(A [i],i)创建的二维正交范围树。构造这个树可以在O(N log ^ 2 N)时间内轻松完成(尽管O(N log N)是可能的)。
现在查询第k个元素,我们遍历树的第一个维度。如果左子树中查询索引范围内的点数小于k,则遵循左子树。这只是对左子树的第二维树的查询。如果第k个元素不在左子树中,我们适当地调整k并在右子树中搜索。整个搜索需要O(N log ^ 2 N)时间。基本上,我们通过将二进制搜索包装到树的遍历中,从Johnny的解决方案中删除了一个log N因子。
实际上,每次查询可以将其降低到O(N log N)预处理和O(log N)。跳至6.851: Advanced Data Structures (Spring'12)中的大约17:00,看Erik Demaine解释正交范围树以及如何实现更快的预处理和查询时间,分别采用温和的聪明和分数级联。
如果您要搜索问题名称,还有一些专门针对这些主题的研究论文。这不是一个简单的问题,你可能需要做一些文档来掌握解决方案。我首先观看我引用的Quora答案中链接的视频。
不幸的是,我不能很好地理解这个主题,不能用这种格式自己解释。如果有人这样做,请随时编辑或发布您自己的答案,我将删除我的。
答案 1 :(得分:0)
您可以更快地找到中位数,但您必须确保它值得。
首先,请注意你需要找到所有j&gt; i的a_i .... a_j的O(n ^ 2)个中位数。您每次计算中位数的方法的时间复杂度为O(N ^ 3)。
如果你已经知道 a_i的中位数,那么一旦你发现你不需要O(N)时间来找到a_i .... a_j 的中位数,你就可以改善它.... a_j-1。
事实证明,通过使用Orderd Statistics Trees,您可以在O(log N)中找到中位数。因此,要找到所有1的a_1 ... a_j *的中位数
用a_1开始构建树。找到中位数。添加a_2。找到中位数。添加a_3。找到中位数,依此类推,直到添加a_n为止。并找到中位数。
此处的每个操作都需要O(logN)并且您有2 * N个操作,因此您可以在O(NlogN)中找到a_1 ... a_j的所有中位数,与原始中的O(N ^ 2)相比较建议。
为所有起点(从a_2开始,然后是a_3等等),你可以在O(N ^ 2 * log N)中找到所有O(N ^ 2)个中位数
现在,O(N ^ 2 * log N)加上O(N ^ 2)的记忆(记住所有中位数)比你现有的更好吗?这取决于m。如果m>&gt; N,则可能值得。
答案 2 :(得分:0)
这样做的实用快捷方式如下:
int median(const std::vector<int>& a, int i, int j) {
std::vector<int> subsequence(a.begin() + i, a.begin() + j);
std::sort(subsequence.begin(), subsequence.end());
return subsequence[subsequence.size() / 2];
}
即。复制子序列,对其进行排序,然后返回中心元素。
答案 3 :(得分:0)
执行mergesort并创建一个二叉树,其中每个层都是mergesort的一个阶段,作为已排序数组的集合。
例如,假设我们想要查询的范围是: [3,2,5,5,9,6,10,1]
然后我们的树看起来像:
[1, 2, 3, 5, 5, 6, 9, 10]
/ \
[2, 3, 5, 5] [1, 6, 9, 10]
/ \ / \
[2, 3] [5, 5] [6, 9] [1, 10]
/ \ / \ / \ / \
[3] [2] [5] [5] [6] [9][10] [1]
这需要存储O(NlogN)空间并生成O(NlogN)时间(只需合并元素并存储排序的每个阶段的结果)。 另外,我们想要存储每个内部节点所代表的段的端点(这也可以通过查看每个数组的长度来动态计算,但是如果我们只是想象我们将这些值存储在树与数组一起)。
(1,8)
/ \
(1,4) (5,8)
/ \ / \
(1,2) (3,4) (5,6) (7,8)
/ \ / \ / \ / \
(1,1) (2,2) (3,3) (4,4) (5,5) (6,6) (7,7) (8,8)
现在我们可以要求任何两个范围:
现在,假设我们给出了边界b =(3,7),我们想找到一个覆盖这个范围的最小节点集。
换句话说,我们想获得:(3,4)(5,6)(7,7)
请注意,此边界列表将始终具有O(logN)长度,因为对于任何数字P,最多可以有两个长度为P且长度为2的幂。
要在O(logN)时间内找到此设置,我们从根节点开始并遍历每个节点处的树,询问:
一旦我们获得了对应于这些范围的数组(在3,7例子中,相应的数组是[5,5],[6,9],[10])我们现在可以回答以下问题: O(log ^ 2N)时间通过二进制搜索每一个(这些排序数组有O(logN),并且在最坏的情况下它们各自具有长度O(N))然后对索引求和(快速注释;这是唯一的情况我曾经见过将索引添加到不同的数组中以获得总和是有意义的:
给定一些x,所有数组中的元素数量减少了多少 比x?
我们二进制搜索每个数组的x,结果索引是小于x的元素数。
现在,为了找到某个范围的中位数(s,t),我们要问:
具有属性的最小元素x之前的元素是什么 索引s和t之间的(t-s + 1)/ 2个元素小于x?
要回答这个问题,我们可以二元搜索原始排序列表。由于我们的二进制搜索中的每次查找都需要O(log ^ 2N)时间,因此回答查询的总时间为O(log ^ 3N)
这是伪代码:
fn create_tree(elems, lb, ub):
if len(elems) == 1:
return leaf((lb, ub), elems)
mid = midpt(lb, ub)
(left, right) = splitInHalf(elems)
tleft = create_tree(left, lb, midpt-1)
tright = create_tree(right, midpt, ub)
return internal(merge(tleft.elems, tright.elems), lb, ub, tleft, tright)
fn get_cover_arrays(tree,bnds):
if covers(bnds,tree.bnds):
return [tree.elems]
else if intersects(bnds,tree.bnds):
return get_cover_arrays(tree.left,bnds) `concat` get_cover_arrays(tree.right,bnds)
else:
return []
fn numElemsLT(cover_arrays, x):
return sum(idx=binary_search(arr,x) for arr in cover_arrays)
fn getKthInRange(tree, bnds, k):
cover_arrays = get_cover_arrays(tree, bnds)
all_elems = tree.elems
next_idx = binary_search(lambda i: numElemsLT(cover_arrays, all_elems[i]), k)
return all_elems[next_idx - 1]
fn getMedian(tree, (lb,ub)):
return getKthInRange(tree, (lb,ub), (ub-lb+1) / 2)
答案 4 :(得分:-1)
此问题可以通过持久段树来解决。基本上,为每个段(1,r)构建一个频率数组,然后你可以使用范围和的东西在你的seg树中向左或向右移动。
看起来你需要O(N ^ 2)空间,但你可以看到(1,x)和(1,x + 1)的树只有O(lg n)个不同的节点。所以你可以构建一个持久的结构 内存:O(N lg n)时间:O(N lg N)预处理,每个查询O(lg N)