Получение максимального значения из диапазона в несортированном массиве


9

У меня есть несортированный массив . У меня есть запросы, в которых я даю диапазон, а затем должно быть возвращено максимальное значение из этого диапазона. Например:

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

Какой алгоритм или структуру данных я создаю, чтобы быстро извлечь максимальное значение из любого диапазона. (Запросов много)

РЕДАКТИРОВАТЬ: Это действительно простая версия актуальной проблемы. Я могу иметь размер массива до 100000 и количество запросов до 100000. Поэтому мне определенно требуется некоторая предварительная обработка, которая облегчит быстрый ответ на запрос.


5
Почему это не отсортировано? Проблема тривиальна, если она отсортирована, поэтому очевидный подход заключается в ее сортировке.

1
@delnan Без какого-либо дополнительного механизма вы теряете, какие значения изначально находились в диапазоне, который нужно запрашивать ...
Тийс ван Дин

Укажите всю свою проблему. Если это знание (или любая другая информация) имеет значение, нужно знать, чтобы включить это в решение.

1
Я что-то упустил или это просто вопрос посещения пунктов 2-6 и определения максимальной ценности этих элементов?
Blrfl

@Blrfl: Я не думаю, что вы что-то упускаете, кроме, может быть, части о многих запросах. Не совсем понятно, есть ли смысл строить структуру, которая делает запросы существенно дешевле, чем последовательный поиск. (Хотя не было бы особого смысла задавать вопрос здесь, если бы это была не идея.)
Майк Шеррилл 'Cat Recall'

Ответы:


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находится в этом поддереве.
Изката

В противном случае +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)


в чем сложность заполнения дерева?
Питер Б

Вы должны пройти через все элементы, и требуется, O(log(n))чтобы каждый элемент был добавлен в дерево. Таким образом, общая сложностьO(nlog(n))
Андрес

1

Лучший алгоритм будет за O (n) время, как показано ниже, пусть start, end будет индексом границ диапазона

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 для простого повторения алгоритма, который пытался улучшить ОП.
Кевин Клайн

1
+1 за размещение решения указанной проблемы. Это действительно единственный способ сделать это, если у вас есть массив и вы не знаете, какие границы будут априори . (Хотя я бы инициализировать , maxчтобы a[i]и начать forцикл в i+1.)
Blrfl

@kevincline Это не просто перезапуск, а также выражение «Да, у вас уже есть лучший алгоритм для этой задачи», с небольшим улучшением (перейти к start, остановиться на end). И я согласен, это является лучшим для единовременного поиска в. Ответ @ ThijsvanDien будет лучше только в том случае, если поиск будет выполняться несколько раз, поскольку для первоначальной настройки требуется больше времени.
Изката

Конечно, во время публикации этого ответа вопрос не включал редактирование, подтверждающее, что он будет выполнять много запросов к одним и тем же данным.
Изката

1

Решения, основанные на бинарном дереве / сегментном дереве, действительно указывают в правильном направлении. Однако можно возразить, что им требуется много дополнительной памяти. Есть два решения этих проблем:

  1. Используйте неявную структуру данных вместо двоичного дерева
  2. Используйте M-арное дерево вместо бинарного

Во-первых, поскольку дерево очень структурировано, вы можете использовать структуру, подобную куче, для неявного определения дерева, вместо того, чтобы представлять дерево с помощью узлов, левых и правых указателей, интервалов и т. Д. Это экономит много памяти по существу нет снижения производительности - вам нужно выполнить немного больше арифметики указателя.

Второй момент заключается в том, что за счет немного больше работы во время оценки вы можете использовать M-арное дерево, а не двоичное дерево. Например, если вы используете 3-арное дерево, вы будете вычислять максимум 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

попробуйте структуру данных "дерево сегментов"
есть два шага
build_tree () O (n)
запрос (int min, int max) O (nlogn)

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

редактировать:

ребята, вы просто не читаете вики, которую я отправил!

Этот алгоритм:
- Вы проходите массив 1 раз, чтобы построить дерево. O (n)
- следующие 100000000+ раз, когда вы хотите узнать максимум какой-либо части массива, просто вызовите функцию запроса. O (logn) для каждого запроса
- c ++ реализует здесь geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
старый алгоритм:
каждый запрос, просто пройти по выбранной области и найти.

так что, если вы собираетесь использовать этот алгоритм для обработки один раз, хорошо, он медленнее, чем старый. но если вы собираетесь обрабатывать огромное количество запросов (млрд), это очень эффективно вы можете создать текстовый файл , как это, для тестовой

линии 1: 50000 случайное число из 0-1000000, расщепленный на «(пробел)» (это массив)
линия 2: 2 случайное число от 1 до 50000, разделенное на '(пробел)' (это запрос)
...
строка 200000: нравится строка 2, это тоже случайный запрос

это примерная проблема, извините, но это на вьетнамском языке
http://vn.spoj.com/problems/NKLINEUP/,
если вы решите ее по-старому, вы никогда не пропустите.


3
Я не думаю, что это актуально. Дерево интервалов содержит интервалы, а не целые числа, и операции, которые они разрешают, не похожи на то, что запрашивает OP. Конечно, вы можете сгенерировать все возможные интервалы и сохранить их в дереве интервалов, но (1) их экспоненциально много, так что это не масштабируется, и (2) операции по-прежнему не похожи на то, что OP просит.

моя ошибка, я имею в виду дерево сегментов, а не интервальное дерево.
ngoaho91

Интересно, я думаю, что никогда не сталкивался с этим деревом! IIUC это все еще требует хранения всех возможных интервалов, хотя. Я думаю, что есть O (n ^ 2) из ​​тех, что довольно дорого. (Кроме того, не должен ли запрос быть O (log n + k) для k результатов?

да, void build_tree () должен перемещаться по массиву. и сохранить максимальное (или минимальное) значение для каждого узла. но во многих случаях стоимость памяти не важна, чем скорость.
ngoaho91

2
Я не могу представить, что это происходит быстрее, чем простой O(n)поиск в массиве, как описано в ответе tarun_telang. Первый инстинкт заключается в том, что O(log n + k)это быстрее, чем O(n), но O(log n + k)это просто извлечение подмассива - эквивалентно O(1)доступу к массиву с учетом начальной и конечной точек. Вам все равно придется пройти через него, чтобы найти максимум.
Изката

0

Вы можете получить O (1) на запрос (с конструкцией O (n log n)), используя структуру данных, называемую разреженной таблицей. Для каждой степени 2 давайте сохраним максимум для каждого сегмента этой длины. Теперь для данного сегмента [l, r) вы получите максимум максимумов на [l + 2 ^ k) и [r-2 ^ k, r) для соответствующего k. Они перекрываются но все нормально

Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.