Pobieranie maksymalnej wartości z zakresu w nieposortowanej tablicy

9

Mam nieposortowaną tablicę . Mam zapytania, w których podaję zakres, a następnie zwracana jest maksymalna wartość z tego zakresu. Na przykład:

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

Jaki algorytm lub strukturę danych tworzę, aby szybko uzyskać maksymalną wartość z dowolnego zakresu. (Istnieje wiele zapytań)

EDYCJA: To jest rzeczywiście prosta wersja rzeczywistego problemu. Mogę mieć rozmiar tablicy nawet 100000, a liczbę zapytań do 100000. Zdecydowanie wymagam wcześniejszego przetworzenia, które ułatwi szybką odpowiedź na zapytanie.

sudeepdino008
źródło
5
Dlaczego jest nieposortowane? Problem jest trywialny, jeśli jest posortowany, więc oczywistym podejściem jest posortowanie go.
1
@delnan Bez jakiegoś dodatkowego mechanizmu tracisz orientację, które wartości były pierwotnie w zakresie, o który pytano ...
Thijs van Dien
Podaj cały swój problem. Jeśli ta wiedza (lub jakakolwiek inna informacja) ma znaczenie, należy wiedzieć, aby uwzględnić ją w rozwiązaniu.
1
Czy coś pomijam, czy to tylko kwestia odwiedzenia punktów od 2 do 6 i znalezienia maksymalnej wartości tych elementów?
Blrfl
@Blrfl: Nie wydaje mi się, żebyś niczego nie przegapił, może poza częścią wielu zapytań. Nie jest do końca jasne, czy ma sens budowanie struktury, która sprawia, że ​​zapytania są znacznie tańsze niż wyszukiwanie sekwencyjne. (Chociaż nie byłoby sensu zadawać pytania tutaj, gdyby to nie był pomysł.)
Mike Sherrill „Cat Recall”

Odpowiedzi:

14

Myślę, że możesz zbudować jakieś drzewo binarne, w którym każdy węzeł reprezentuje maksymalną wartość jego dzieci:

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

Następnie musisz tylko znaleźć sposób, aby określić, które węzły minimalnie musisz sprawdzić, aby znaleźć maksymalną wartość w pytanym zakresie. W tym przykładzie, aby uzyskać maksymalną wartość z zakresu indeksu [2, 6](włącznie), max(45, 78, 4)zamiast tego max(9, 45, 78, 2, 4). W miarę wzrostu drzewa zysk będzie większy.

Thijs van Dien
źródło
1
Aby to zadziałało, w przykładowym drzewie brakuje informacji: Każdy węzeł wewnętrzny musi mieć zarówno maksimum, jak i całkowitą liczbę węzłów podrzędnych, które ma. W przeciwnym razie wyszukiwanie nie będzie w stanie wiedzieć, że (na przykład) nie musi patrzeć na wszystkie dzieci 78(i pomijać 2), ponieważ dla wszystkich wie, że indeks 6znajduje się w tym poddrzewie.
Izkata
W przeciwnym razie +1, jak uważam, to raczej pomysłowe
Izkata
+1: Jest to potężna technika odpowiadania na zapytania dotyczące podzakresów listy w czasie log (N), użyteczna wszędzie tam, gdzie dane w węźle głównym mogą być obliczane w sposób ciągły na podstawie danych u dzieci.
kevin cline
Ten pomysł jest niesamowity. Daje czas O (logowania) zapytania. Myślę, że @Izkata też miał rację. Możemy rozszerzyć węzeł drzewa o informacje dotyczące lewego i prawego zakresu, który obejmuje. Biorąc pod uwagę zakres, wie, jak podzielić problem na dwa. Jeśli chodzi o przestrzeń, wszystkie dane są przechowywane na poziomie liścia. Wymaga więc 2 * N miejsca, czyli O (N) do przechowywania. Nie wiem, co to jest drzewo segmentów, ale czy to jest idea stojąca za drzewem segmentów?
Kay
A jeśli chodzi o przetwarzanie wstępne, zbudowanie drzewa wymaga O (n).
Kay
2

Uzupełnienie odpowiedzi ngoaho91.

Najlepszym sposobem rozwiązania tego problemu jest użycie struktury danych drzewa segmentów. To pozwala ci odpowiadać na takie zapytania w O (log (n)), co oznaczałoby, że całkowita złożoność twojego algorytmu wynosiłaby O (Q logn), gdzie Q jest liczbą zapytań. Jeśli użyjesz naiwnego algorytmu, całkowita złożoność wyniesie O (Q n), co jest oczywiście wolniejsze.

Wadą drzew segmentowych jest jednak wada. Zajmuje dużo pamięci, ale często mniej zależy ci na pamięci niż na szybkości.

Pokrótce opiszę algorytmy używane przez ten DS:

Drzewo segmentów jest tylko specjalnym przypadkiem drzewa wyszukiwania binarnego, w którym każdy węzeł przechowuje wartość zakresu, do którego jest przypisany. Węzłu root przypisano zakres [0, n]. Lewemu dziecku przypisano zakres [0, (0 + n) / 2], a prawemu dziecku [(0 + n) / 2 + 1, n]. W ten sposób drzewo zostanie zbudowane.

Utwórz drzewo :

/*
    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.
    }
}

Drzewo zapytań

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.
}

Jeśli potrzebujesz dodatkowych wyjaśnień, daj mi znać.

BTW, Drzewo segmentów obsługuje także aktualizację pojedynczego elementu lub zakresu elementów w O (log n)

Andrés
źródło
jaka jest złożoność wypełnienia drzewa?
Pieter B
Musisz przejść przez wszystkie elementy, a O(log(n))każdy element musi zostać dodany do drzewa. Dlatego całkowita złożoność toO(nlog(n))
Andrés
1

Najlepszy algorytm byłby w czasie O (n), ponieważ poniżej zacznijmy od końca, indeks granic zakresu

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; 
}
Tarun
źródło
4
-1 za zwykłe powtórzenie algorytmu, który OP próbował ulepszyć.
kevin cline
1
+1 za opublikowanie rozwiązania opisanego problemu. To naprawdę jedyny sposób, aby to zrobić, jeśli masz tablicę i nie wiesz, jakie są granice a priori . (Chociaż by zainicjować maxsię a[i]i rozpocząć forw pętli i+1).
Blrfl
@kevincline To nie tylko przekształcenie - mówi także „Tak, masz już najlepszy algorytm do tego zadania”, z niewielką poprawą (przeskocz do start, zatrzymaj się na end). I zgadzam się, że jest to najlepszy sposób na jednorazowe wyszukiwanie. @ Odpowiedź ThijsvanDien jest lepsza tylko wtedy, gdy wyszukiwanie ma się odbyć wiele razy, ponieważ początkowo trwa to dłużej.
Izkata
To prawda, że ​​w momencie publikowania tej odpowiedzi pytanie nie zawierało edycji potwierdzającej, że będzie on przeprowadzał wiele zapytań dotyczących tych samych danych.
Izkata
1

Rozwiązania oparte na drzewku binarnym / drzewie segmentu rzeczywiście wskazują właściwy kierunek. Można jednak sprzeciwić się, że wymagają one dużo dodatkowej pamięci. Istnieją dwa rozwiązania tych problemów:

  1. Użyj niejawnej struktury danych zamiast drzewa binarnego
  2. Użyj drzewa M-ary zamiast drzewa binarnego

Pierwszą kwestią jest to, że ponieważ drzewo ma wysoką strukturę, możesz użyć struktury podobnej do sterty, aby domyślnie zdefiniować drzewo, zamiast reprezentować drzewo za pomocą węzłów, lewego i prawego wskaźnika, interwału itp. To oszczędza dużo pamięci, zasadniczo brak wydajności - musisz wykonać trochę więcej arytmetyki wskaźnika.

Druga kwestia polega na tym, że kosztem nieco więcej pracy podczas oceny można użyć drzewa M-ary zamiast drzewa binarnego. Na przykład, jeśli używasz drzewa 3-arylowego, obliczysz maksymalnie 3 elementy jednocześnie, następnie 9 elementów jednocześnie, a następnie 27 itd. Wymagane dodatkowe miejsce to N / (M-1) - możesz udowodnij, używając formuły serii geometrycznej. Na przykład, jeśli wybierzesz M = 11, będziesz potrzebować 1/10 miejsca na metodę drzewa binarnego.

Możesz sprawdzić, czy te naiwne i zoptymalizowane implementacje w Pythonie dają te same wyniki:

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)
Patrick Mineault
źródło
0

wypróbuj strukturę danych „segment tree”
istnieją 2 kroki
build_tree () O (n)
zapytanie (int min, int max) O (nlogn)

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

edytować:

po prostu nie czytacie wiki, którą wysłałem!

algorytm ten:
- przemierzasz tablicę 1 raz, aby zbudować drzewo. O (n)
- kolejne 100000000+ razy, kiedy chcesz poznać maksimum dowolnej części tablicy, po prostu wywołaj funkcję zapytania. O (logowanie) dla każdego zapytania
- c ++ zaimplementuj tutaj geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
stary algorytm to:
każde zapytanie, wystarczy przejść przez zaznaczony obszar i znaleźć.

więc jeśli użyjesz tego algorytmu do przetworzenia raz, OK, będzie wolniejszy niż stary sposób. ale jeśli będziemy przetwarzać ogromną liczbę zapytań (w mld), to bardzo wydajny można wygenerować plik tekstowy takiego, dla testowanej

linii 1: 50000 liczba losowa z 0-1000000, podzielone przez „(spacja)” (jest to tablica)
linia 2: 2 losowa liczba od 1 do 50000, podzielona przez „(spację)” (to zapytanie)
...
linia 200000: lubi linię 2, to również losowe zapytanie

jest to przykładowy problem, przepraszam, ale jest to w języku wietnamskim
http://vn.spoj.com/problems/NKLINEUP/,
jeśli rozwiążesz go starym sposobem, nigdy nie przejdziesz.

ngoaho91
źródło
3
Nie sądzę, żeby to miało znaczenie. Drzewo interwałów zawiera interwały, a nie liczby całkowite, a operacje, na które pozwalają, nie przypominają tego, o co prosi OP. Można oczywiście wygenerować wszystkie możliwe interwały i zapisać je w drzewie interwałów, ale (1) jest ich wykładniczo wiele, więc to się nie skaluje, i (2) operacje nadal nie wyglądają jak OP pytać o.
mój błąd, mam na myśli drzewo segmentu, a nie drzewo interwałów.
ngoaho91
Ciekawe, myślę, że nigdy nie spotkałem tego drzewa! IIUC nadal wymaga przechowywania wszystkich możliwych interwałów. Myślę , że jest O (n ^ 2) tych, co jest dość drogie. (Ponadto, czy zapytanie nie powinno być O (log n + k) dla k wyników?
tak, void build_tree () musi przejść przez tablicę. i przechowuj wartość maksymalną (lub minimalną) dla każdego węzła. ale w wielu przypadkach koszt pamięci nie jest ważny niż szybkość.
ngoaho91
2
Nie mogę sobie wyobrazić, aby było to szybsze niż zwykłe O(n)przeszukiwanie tablicy, jak opisano w odpowiedzi tarun_telang. Pierwszym instynktem jest to, że O(log n + k)jest szybsze niż O(n), ale O(log n + k)jest to po prostu pobieranie pod-macierzy - równoważne O(1)dostępowi do tablicy, biorąc pod uwagę punkty początkowe i końcowe. Będziesz musiał przejść przez to, aby znaleźć maksimum.
Izkata
0

Możesz osiągnąć O (1) na zapytanie (z konstrukcją O (n log n)) za pomocą struktury danych zwanej tabelą rzadką. Dla każdej potęgi 2 zachowajmy maksimum dla każdego odcinka tej długości. Teraz dany segment [l, r) daje maksimum maksimum dla [l + 2 ^ k) i [r-2 ^ k, r) dla odpowiedniego k. Nakładają się na siebie, ale jest OK

RiaD
źródło