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


9

У мене є несортований масив . У мене є запити, в яких я даю діапазон, а потім максимальне значення з цього діапазону має повернутися. Наприклад:

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

Який алгоритм чи структуру даних я будую для швидкого отримання максимального значення з будь-якого діапазону. (Є багато запитів)

EDIT: Це дійсно проста версія фактичної проблеми. Я можу мати розмір масиву як 100000, а кількість запитів до 100000. Тому я, безумовно, потребую певної попередньої обробки, що полегшить швидку відповідь на запит.


5
Чому це несортовано? Проблема є тривіальною, якщо її сортують, тому очевидним є підхід до її сортування.

1
@delnan Без додаткового механізму ви втрачаєте відслідковувати, які значення спочатку були в діапазоні, на який потрібно запитувати ...
Thijs van Dien

Вкажіть всю вашу проблему. Якщо ці знання (або будь-яка інша інформація) мають значення, потрібно знати, що слід враховувати це рішення.

1
Чи щось мені не вистачає, чи це лише питання відвідування предметів від 2 до 6 та пошуку максимального значення цих елементів?
Blrfl

@Blrfl: Я не думаю, що ви нічого не пропускаєте, крім, можливо, частини про багато запитів. Не зовсім зрозуміло, чи є сенс будувати структуру, яка робить запити істотно дешевшими, ніж послідовний пошук. (Хоча не було б багато сенсу задавати питання тут, якби це не ідея.)
Майк Шеррілл 'Cat 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, як я вважаю це досить винахідливим
Ізката

+1: Це потужна методика відповіді на запити про підгрупи списку в журналі (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.
}

Якщо вам потрібно додаткове пояснення, просто дайте мені знати.

BTW, Сегментне дерево також підтримує оновлення одного елемента або діапазону елементів у O (log n)


яка складність заповнення дерева?
Пітер Б

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

1
+1 для публікації рішення вказаної проблеми. Це дійсно єдиний спосіб зробити це, якщо у вас є масив і не знаєте, які межі будуть апріорі . (Хоча я б ініціювати , maxщоб a[i]і почати forцикл в i+1.)
Blrfl

@kevincline Це не просто перезавантаження - це ще й відповідь "Так, у вас вже є найкращий алгоритм для виконання цього завдання", з незначним вдосконаленням (стрибок до start, зупинка на end). І я згоден, це є найкращим для одноразового пошуку в. @ Відповідь ThijsvanDien краща лише в тому випадку, якщо пошук буде відбуватися багаторазово, оскільки спочатку потрібно більше часу.
Ізката

Зрозуміло, що під час публікації цієї відповіді питання не включало редагування, що підтверджувало, що він буде робити багато запитів над одними і тими ж даними.
Ізката

1

Рішення на основі двійкового дерева / сегмента дерева дійсно вказують у правильному напрямку. Можна заперечити, що їм потрібно багато додаткової пам'яті. Є два варіанти вирішення цих проблем:

  1. Використовуйте неявну структуру даних замість бінарного дерева
  2. Використовуйте дерево M-ary замість двійкового дерева

Перший момент полягає в тому, що оскільки дерево є високоструктурованим, ви можете використовувати структуру, що нагадує купу, щоб неявно визначити дерево, а не представляти дерево з вузлами, лівими та правими вказівниками, інтервалами тощо. Це значно економить пам'ять жоден показник результативності - вам потрібно виконати трохи більше арифметики вказівника.

Другий момент полягає в тому, що, витративши трохи більше роботи під час оцінювання, ви можете використовувати M-ary дерево, а не двійкове дерево. Наприклад, якщо ви використовуєте 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

vs.

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
Я не думаю, що це актуально. Інтервальне дерево містить інтервали, а не цілі числа, і операції, які вони дозволяють, виглядають не так, як вимагає ОП. Можна, звичайно, генерувати всі можливі інтервали та зберігати їх у дереві інтервалів, але (1) їх експоненціально багато, тому це не масштабується, і (2) операції все ще не схожі на те, що OP просить.

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

Цікаво, я думаю, я ніколи не натрапляв на це дерево! Однак, це вимагає зберігання всіх можливих інтервалів. Я думаю, що є O (n ^ 2) таких, що досить дорого. (Також не слід запитувати O (log n + k) для результатів k?

так, недійсний 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.