从未排序数组的范围中检索最大值


9

我有一个未排序的数组。我在查询中给出一个范围,然后必须返回该范围的最大值。例如:

array[]={23,17,9,45,78,2,4,6,90,1};
query(both inclusive): 2 6
answer: 78

我构造了哪种算法或数据结构来快速检索任何范围内的最大值。(有很多查询)

编辑: 这确实是实际问题的简单版本。我可以将数组的大小设置为最大100000,查询数量最多为100000。因此,我绝对需要进行一些预处理,以利于快速查询响应。


5
为什么没有分类?如果对它进行排序,那么这个问题将是微不足道的,因此显而易见的方法是对其进行排序。

1
@delnan如果没有任何其他机制,您将无法跟踪最初要查询的范围内的值...
Thijs van Dien

指定您的整个问题。如果这一知识(或任何其他信息)很重要,则必须知道将其纳入解决方案中。

1
我是否缺少某些东西,还是仅仅是访问第2项到第6项并找到这些元素的最大值?
Blrfl 2013年

@Blrfl:我认为您除了缺少有关许多查询的内容外,不会丢失任何内容。目前尚不清楚建立一个结构使查询比顺序搜索便宜得多的结构是否有意义。(尽管在这里问这个主意不是
很有意义

Answers:


14

我认为您可以构建某种二叉树,其中每个节点代表其子节点的最大值:

            78           
     45            78     
  23    45     78      6  
23 17  9 45   78 2    4 6   

然后,您只需要找到一种方法即可确定您最少需要检查哪些节点才能找到所查询范围内的最大值。在此示例中,要获得索引范围[2, 6](含)内的最大值,您将拥有max(45, 78, 4)而不是max(9, 45, 78, 2, 4)。随着树的生长,增益将更大。


1
为此,示例树中缺少一些信息:每个内部节点必须同时具有最大值和子节点总数。否则,搜索将无法得知(例如)它不必查看78(跳过2)的所有6子项,因为对于所有信息,它都知道索引在该子树中。
2013年

否则,我认为这很具有创造性,因此+1
Izkata

+1:这是一项强大的技术,可以在log(N)时间内回答有关列表子范围的查询,但可以使用根节点上的数据以恒定的时间从子节点上的数据计算出来的情况下使用。
凯文·克莱恩

这个想法很棒。它提供O(logn)查询时间。我认为@Izkata也很好。我们可以使用有关其涵盖的左右范围信息来扩充树节点。因此,给定一个范围,它知道如何将问题一分为二。在空间方面,所有数据都存储在叶级。因此它需要2 * N个空间,即O(N)才能存储。我不知道什么是分段树,但这是分段树背后的想法吗?

在预处理方面,需要O(n)来构造树。

2

补充ngoaho91的答案。

解决此问题的最佳方法是使用细分树数据结构。这使您可以在O(log(n))中回答此类查询,这意味着算法的总复杂度为O(Q logn),其中Q是查询数。如果您使用朴素算法,则总复杂度将为O(Q n),这显然要慢一些。

但是,使用段树有一个缺点。它会占用大量内存,但是很多时候您对内存的关心不如对速度的关心。

我将简要描述此DS使用的算法:

段树只是二进制搜索树的一种特例,其中每个节点都保存了分配给它的范围的值。根节点的范围为[0,n]。左边的孩子被分配了范围[0,(0 + n)/ 2],右边的孩子被分配了范围[(0 + n)/ 2 + 1,n]。这样树将被建立。

创建树

/*
    A[] -> array of original values
    tree[] -> Segment Tree Data Structure.
    node -> the node we are actually in: remember left child is 2*node, right child is 2*node+1
    a, b -> The limits of the actual array. This is used because we are dealing
                with a recursive function.
*/

int tree[SIZE];

void build_tree(vector<int> A, int node, int a, int b) {
    if (a == b) { // We get to a simple element
        tree[node] = A[a]; // This node stores the only value
    }
    else {
        int leftChild, rightChild, middle;
        leftChild = 2*node;
        rightChild = 2*node+1; // Or leftChild+1
        middle = (a+b) / 2;
        build_tree(A, leftChild, a, middle); // Recursively build the tree in the left child
        build_tree(A, rightChild, middle+1, b); // Recursively build the tree in the right child

        tree[node] = max(tree[leftChild], tree[rightChild]); // The Value of the actual node, 
                                                            //is the max of both of the children.
    }
}

查询树

int query(int node, int a, int b, int p, int q) {
    if (b < p || a > q) // The actual range is outside this range
        return -INF; // Return a negative big number. Can you figure out why?
    else if (p >= a && b >= q) // Query inside the range
        return tree[node];
    int l, r, m;
    l = 2*node;
    r = l+1;
    m = (a+b) / 2;
    return max(query(l, a, m, p, q), query(r, m+1, b, p, q)); // Return the max of querying both children.
}

如果您需要进一步的解释,请告诉我。

顺便说一句,Segment Tree还支持更新O(log n)中的单个元素或一系列元素


填满树的复杂性是什么?
Pieter B

您必须遍历所有元素,并且需要O(log(n))将每个元素添加到树中。因此,总的复杂性O(nlog(n))
安德烈斯

1

最佳算法将在O(n)时间中,如下所示,开始,结束是范围边界的索引

int findMax(int[] a, start, end) {
   max = Integer.MIN; // initialize to minimum Integer

   for(int i=start; i <= end; i++) 
      if ( a[i] > max )
         max = a[i];

   return max; 
}

4
-1仅表示重复OP尝试改进的算法。
凯文·克莱恩

1
+1用于发布上述问题的解决方案。如果您有一个数组并且不知道先验条件是什么,那么这确实是唯一的方法。(尽管我会初始化maxa[i]并从for处开始循环i+1。)
Blrfl 2013年

@kevincline这不仅是在重述-还说“是的,您已经为该任务提供了最好的算法”,但略有改进(跳转到start,停在end)。我同意,这一次性查找的最佳选择。@ThijsvanDien的答案只有在查找将要发生多次的情况下才会更好,因为初始设置需要更长的时间。
2013年

当然,在发布此答案时,问题不包括确认他将对相同数据进行多次查询的编辑。
2013年

1

基于二叉树/分段树的解决方案确实指向了正确的方向。但是,可能有人反对说他们需要很多额外的内存。这些问题有两种解决方案:

  1. 使用隐式数据结构代替二进制树
  2. 使用Mary树而不是二叉树

第一点是因为树是高度结构化的,因此您可以使用类似堆的结构隐式定义树,而不用用节点,左右指针,间隔等表示树。这样就节省了很多内存不会影响性能-您确实需要执行更多的指针运算。

第二点是,您可以使用M元树而不是二叉树来进行评估,而这需要花费更多的精力进行评估。例如,如果您使用三叉树,则一次将最多计算3个元素,一次将计算9个元素,然后计算27个,依此类推。那么所需的额外存储量为N /(M-1)-您可以用几何级数公式证明。例如,如果选择M = 11,则将需要二叉树方法存储空间的1/10。

您可以验证Python中的这些天真的和优化的实现是否给出了相同的结果:

class RangeQuerier(object):
    #The naive way
    def __init__(self):
        pass

    def set_array(self,arr):
        #Set, and preprocess
        self.arr = arr

    def query(self,l,r):
        try:
            return max(self.arr[l:r])
        except ValueError:
            return None

class RangeQuerierMultiLevel(object):
    def __init__(self):
        self.arrs = []
        self.sub_factor = 3
        self.len_ = 0

    def set_array(self,arr):
        #Set, and preprocess
        tgt = arr
        self.len_ = len(tgt)
        self.arrs.append(arr)
        while len(tgt) > 1:
            tgt = self.maxify_one_array(tgt)
            self.arrs.append(tgt)

    def maxify_one_array(self,arr):
        sub_arr = []
        themax = float('-inf')
        for i,el in enumerate(arr):
            themax = max(el,themax)
            if i % self.sub_factor == self.sub_factor - 1:
                sub_arr.append(themax)
                themax = float('-inf')
        return sub_arr

    def query(self,l,r,level=None):
        if level is None:
            level = len(self.arrs)-1

        if r <= l:
            return None

        int_size = self.sub_factor ** level 

        lhs,mid,rhs = (float('-inf'),float('-inf'),float('-inf'))

        #Check if there's an imperfect match on the left hand side
        if l % int_size != 0:
            lnew = int(ceil(l/float(int_size)))*int_size
            lhs = self.query(l,min(lnew,r),level-1)
            l = lnew
        #Check if there's an imperfect match on the right hand side
        if r % int_size != 0:
            rnew = int(floor(r/float(int_size)))*int_size
            rhs = self.query(max(rnew,l),r,level-1)
            r = rnew

        if r > l:
            #Handle the middle elements
            mid = max(self.arrs[level][l/int_size:r/int_size])
        return max(max(lhs,mid),rhs)

0

尝试“段树”数据结构
有2个步骤
build_tree()O(n)
query(int min,int max)O(nlogn)

http://en.wikipedia.org/wiki/Segment_tree

编辑:

你们只是不读我发送的维基!

该算法是:
-遍历数组1次以构建树。O(n)
-接下来的100000000次您想知道数组任何部分的最大值,只需调用查询函数即可。每个查询的O(登录)
-C ++在此处实现geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
旧算法是:
每个查询都只需遍历选定区域并查找。

因此,如果您要使用此算法处理一次,那么,它比以前的方法要慢。但是,如果您要处理大量查询(十亿),则可以非常高效地生成这样的文本文件,对于测试

行1:从0-1000000中的50000个随机数,用“(space)”(它是数组)
行分割2:从1到50000的2个随机数,除以'(space)'(这是查询)
...
200000行:喜欢第2行,它也是随机查询

这是示例问题,很抱歉,但是这在越南语中
http://vn.spoj.com/problems/NKLINEUP/
如果以旧方式解决,则您永远不会通过。


3
我认为这无关紧要。间隔树保存间隔,而不是整数,并且它们允许的操作与OP所要求的完全不同。当然,您可以生成所有可能的间隔并将它们存储在间隔树中,但是(1)指数增长了很多,因此无法扩展,(2)操作看起来仍然不像OP要求。

我的错误是指段树,而不是间隔树。
ngoaho91 2013年

有趣的是,我想我从未遇到过这棵树!IIUC仍然需要存储所有可能的间隔。我认为其中有O(n ^ 2),这相当昂贵。(此外,是否应该为k个结果查询O(log n + k)?

是的,void build_tree()必须穿越数组。并存储每个节点的最大值(或最小值)。但是在许多情况下,内存成本并不重要,而速度却不重要。
ngoaho91 2013年

2
我无法想象这比普通O(n)搜索数组快得多,如tarun_telang的答案所述。首先的直觉是,O(log n + k)它比快O(n),但O(log n + k)仅是子数组的检索-等同于O(1)给定起点和终点的数组访问。您仍然需要遍历它才能找到最大值。
2013年

0

您可以使用称为稀疏表的数据结构来实现每个查询的O(1)(具有O(n log n)构造)。对于2的乘方,我们为该长度的每个段保存最大值。现在给定段[l,r),对于适当的k,您将在[l + 2 ^ k)和[r-2 ^ k,r)上获得最大值的最大值。他们重叠但没关系

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.