Abrufen des Maximalwerts aus einem Bereich in einem unsortierten Array

9

Ich habe ein unsortiertes Array . Ich habe Abfragen, in denen ich einen Bereich gebe und dann der Maximalwert aus diesem Bereich zurückgegeben werden muss. Beispielsweise:

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

Welchen Algorithmus oder welche Datenstruktur konstruiere ich, um schnell den Maximalwert aus einem beliebigen Bereich abzurufen? (Es gibt viele Fragen)

EDIT: Dies ist in der Tat eine einfache Version des eigentlichen Problems. Ich kann eine Arraygröße von bis zu 100000 und eine Anzahl von Abfragen von bis zu 100000 haben. Daher benötige ich definitiv eine Vorverarbeitung, die eine schnelle Antwort auf Abfragen ermöglicht.

sudeepdino008
quelle
5
Warum ist es unsortiert? Das Problem ist trivial, wenn es sortiert ist, daher besteht der offensichtliche Ansatz darin, es zu sortieren.
1
@delnan Ohne einen zusätzlichen Mechanismus verlieren Sie den Überblick darüber, welche Werte ursprünglich in dem abzufragenden Bereich lagen ...
Thijs van Dien
Geben Sie Ihr gesamtes Problem an. Wenn dieses Wissen (oder andere Informationen) wichtig ist, muss man wissen, dass dies in die Lösung einfließt.
1
Vermisse ich etwas oder geht es nur darum, die Punkte 2 bis 6 zu besuchen und den Maximalwert dieser Elemente zu finden?
Blrfl
@Blrfl: Ich glaube nicht, dass dir etwas fehlt, außer vielleicht dem Teil über viele Fragen. Es ist nicht wirklich klar, ob es Sinn macht, eine Struktur zu erstellen, die Abfragen wesentlich billiger macht als eine sequentielle Suche. (Obwohl es nicht sinnvoll wäre, die Frage hier zu stellen, wenn das nicht die Idee wäre.)
Mike Sherrill 'Cat Recall'

Antworten:

14

Ich denke, Sie könnten eine Art Binärbaum erstellen, in dem jeder Knoten den Maximalwert seiner untergeordneten Knoten darstellt:

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

Dann müssen Sie nur noch einen Weg finden, um zu bestimmen, welche Knoten Sie minimal überprüfen müssen, um den Maximalwert in dem abgefragten Bereich zu finden. In diesem Beispiel [2, 6]hätten Sie max(45, 78, 4)stattdessen den Maximalwert im Indexbereich (einschließlich) max(9, 45, 78, 2, 4). Wenn der Baum wächst, ist der Gewinn größer.

Thijs van Dien
quelle
1
Damit dies funktioniert, fehlen in Ihrem Beispielbaum Informationen: Jeder interne Knoten muss sowohl das Maximum als auch die Gesamtzahl der untergeordneten Knoten haben. Andernfalls kann die Suche nicht erkennen, dass (zum Beispiel) nicht alle untergeordneten Elemente von 78(und das überspringen 2) angezeigt werden müssen , da sich der Index nach allem, was sie weiß, 6in diesem Teilbaum befindet.
Izkata
Ansonsten +1, da ich das ziemlich erfinderisch
finde
+1: Dies ist eine leistungsstarke Technik zur Beantwortung von Fragen zu Unterbereichen einer Liste in Protokollzeit (N), die verwendet werden kann, wenn die Daten am Wurzelknoten in konstanter Zeit aus den Daten der untergeordneten Knoten berechnet werden können.
Kevin Cline
Diese Idee ist großartig. Es gibt O (logn) Abfragezeit. Ich denke, @Izkata hat auch einen guten Punkt gemacht. Wir können den Baumknoten mit Informationen über den linken und rechten Bereich erweitern, den er abdeckt. Bei gegebener Reichweite weiß es also, wie man das Problem in zwei Teile aufteilt. In Bezug auf den Speicherplatz werden alle Daten auf Blattebene gespeichert. Es werden also 2 * N Speicherplatz benötigt, was O (N) zum Speichern ist. Ich weiß nicht, was ein Segmentbaum ist, aber ist dies die Idee hinter dem Segmentbaum?
Kay
In Bezug auf die Vorverarbeitung wird O (n) benötigt, um den Baum zu konstruieren.
Kay
2

Zur Ergänzung der Antwort von ngoaho91.

Der beste Weg, um dieses Problem zu lösen, ist die Verwendung der Segmentbaum-Datenstruktur. Auf diese Weise können Sie solche Abfragen in O (log (n)) beantworten. Dies bedeutet, dass die Gesamtkomplexität Ihres Algorithmus O (Q logn) ist, wobei Q die Anzahl der Abfragen ist. Wenn Sie den naiven Algorithmus verwenden würden, wäre die Gesamtkomplexität O (Q n), was offensichtlich langsamer ist.

Es gibt jedoch einen Nachteil bei der Verwendung von Segmentbäumen. Es nimmt viel Speicher in Anspruch, aber oft interessiert Sie weniger das Gedächtnis als die Geschwindigkeit.

Ich werde kurz die von diesem DS verwendeten Algorithmen beschreiben:

Der Segmentbaum ist nur ein Sonderfall eines binären Suchbaums, bei dem jeder Knoten den Wert des Bereichs enthält, dem er zugewiesen ist. Dem Wurzelknoten wird der Bereich [0, n] zugewiesen. Dem linken Kind wird der Bereich [0, (0 + n) / 2] und dem rechten Kind [(0 + n) / 2 + 1, n] zugewiesen. Auf diese Weise wird der Baum gebaut.

Baum erstellen :

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

Abfragebaum

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

Wenn Sie weitere Erklärungen benötigen, lassen Sie es mich einfach wissen.

Übrigens unterstützt der Segmentbaum auch die Aktualisierung eines einzelnen Elements oder einer Reihe von Elementen in O (log n).

Andrés
quelle
Wie komplex ist es, den Baum zu füllen?
Pieter B
Sie müssen alle Elemente durchgehen, und es dauert, O(log(n))bis jedes Element zum Baum hinzugefügt wird. Daher ist die GesamtkomplexitätO(nlog(n))
Andrés
1

Der beste Algorithmus wäre in O (n) -Zeit wie unten. Lassen Sie Start, Ende der Index der Bereichsgrenzen sein

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
quelle
4
-1 für die bloße Wiederholung des Algorithmus, den das OP verbessern wollte.
Kevin Cline
1
+1 für die Veröffentlichung einer Lösung für das angegebene Problem. Dies ist wirklich der einzige Weg, dies zu tun, wenn Sie ein Array haben und nicht wissen, welche Grenzen a priori gelten werden . (Obwohl ich würde initialisieren maxzu a[i]und starten Sie die forSchleife an i+1.)
Blrfl
@kevincline Es wird nicht nur neu formuliert, sondern auch "Ja, Sie haben bereits den besten Algorithmus für diese Aufgabe" mit einer geringfügigen Verbesserung (springen zu start, anhalten bei end). Und ich stimme zu, dies ist das Beste für eine einmalige Suche. Die Antwort von @ ThijsvanDien ist nur dann besser, wenn die Suche mehrmals durchgeführt wird, da die anfängliche Einrichtung länger dauert.
Izkata
Zugegeben, zum Zeitpunkt der Veröffentlichung dieser Antwort enthielt die Frage keine Bearbeitung, die bestätigte, dass er viele Abfragen über dieselben Daten durchführen wird.
Izkata
1

Die auf binären Bäumen / Segmentbäumen basierenden Lösungen zeigen tatsächlich in die richtige Richtung. Man könnte jedoch einwenden, dass sie viel zusätzlichen Speicher benötigen. Für diese Probleme gibt es zwei Lösungen:

  1. Verwenden Sie eine implizite Datenstruktur anstelle eines Binärbaums
  2. Verwenden Sie einen M-Baum anstelle eines Binärbaums

Der erste Punkt ist, dass Sie, da der Baum stark strukturiert ist, eine Heap-ähnliche Struktur verwenden können, um den Baum implizit zu definieren, anstatt den Baum mit Knoten, linken und rechten Zeigern, Intervallen usw. darzustellen. Dies spart im Wesentlichen viel Speicher Kein Leistungstreffer - Sie müssen etwas mehr Zeigerarithmetik ausführen.

Der zweite Punkt ist, dass Sie auf Kosten von etwas mehr Arbeit während der Auswertung einen M-ary-Baum anstelle eines binären Baums verwenden können. Wenn Sie beispielsweise einen 3-Ary-Baum verwenden, berechnen Sie maximal 3 Elemente gleichzeitig, dann 9 Elemente gleichzeitig, dann 27 usw. Der zusätzliche Speicherbedarf beträgt dann N / (M-1) - Sie können beweisen mit der geometrischen Reihenformel. Wenn Sie beispielsweise M = 11 wählen, benötigen Sie 1/10 der Speicherung der Binärbaummethode.

Sie können überprüfen, ob diese naiven und optimierten Implementierungen in Python dieselben Ergebnisse liefern:

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
quelle
0

Versuchen Sie "Segmentbaum" Datenstruktur
gibt es 2 Schritte
build_tree () O (n)
Abfrage (int min, int max) O (nlogn)

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

bearbeiten:

Ihr lest einfach nicht das Wiki, das ich gesendet habe!

Dieser Algorithmus lautet:
- Sie durchlaufen das Array 1 Mal, um einen Baum zu erstellen. O (n)
- Wenn Sie das nächste Mal 100000000+ Mal wissen möchten, wie viele Teile eines Arrays maximal sind, rufen Sie einfach die Abfragefunktion auf. O (logn) für jede Abfrage
- c ++ hier implementieren geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
alter Algorithmus ist:
jede Abfrage, einfach den ausgewählten Bereich durchlaufen und suchen.

Wenn Sie diesen Algorithmus also verwenden, um ihn einmal zu verarbeiten, ist er langsamer als bisher. Wenn Sie jedoch eine große Anzahl von Abfragen (Milliarden) verarbeiten

möchten, ist es sehr effizient, eine Textdatei wie diese für die Testzeile 1: 50000 Zufallszahl von 0-1000000 zu generieren, geteilt durch die
Zeile '(Leerzeichen)' (es ist das Array) 2: 2 Zufallszahl von 1 bis 50000, geteilt durch '(Leerzeichen)' (es ist die Abfrage)
...
Zeile 200000: mag Zeile 2, es ist auch eine zufällige Abfrage

Dies ist das Beispielproblem, sorry, aber dies ist auf vietnamesisch
http://vn.spoj.com/problems/NKLINEUP/.
Wenn Sie es auf alte Weise lösen, bestehen Sie nie.

ngoaho91
quelle
3
Ich denke nicht, dass das relevant ist. Ein Intervallbaum enthält Intervalle, keine ganzen Zahlen, und die Operationen, die sie zulassen, sehen nicht so aus, wie OP es verlangt. Sie könnten natürlich alle möglichen Intervalle generieren und in einem Intervallbaum speichern, aber (1) es gibt exponentiell viele davon, so dass dies nicht skaliert und (2) die Operationen immer noch nicht so aussehen wie OP fragt nach.
Mein Fehler, ich meine Segmentbaum, nicht Intervallbaum.
Ngoaho91
Interessant, ich denke, ich bin noch nie auf diesen Baum gestoßen! IIUC erfordert jedoch immer noch das Speichern aller möglichen Intervalle. Ich denke, es gibt O (n ^ 2) von denen, was ziemlich teuer ist. (
Ja, void build_tree () muss das Array durchqueren. und speichern Sie den maximalen (oder minimalen) Wert für jeden Knoten. In vielen Fällen sind die Speicherkosten jedoch nicht wichtiger als die Geschwindigkeit.
Ngoaho91
2
Ich kann mir nicht vorstellen, dass dies schneller ist als eine einfache O(n)Suche im Array, wie in der Antwort von tarun_telang beschrieben. Der erste Instinkt ist, dass dies O(log n + k)schneller ist als O(n), aber O(log n + k)nur das Sub-Array abgerufen wird - entspricht dem O(1)Array-Zugriff angesichts der Start- und Endpunkte. Sie müssten es immer noch durchlaufen, um das Maximum zu finden.
Izkata
0

Sie können O (1) pro Abfrage (mit O (n log n) -Konstruktion) mithilfe der Datenstruktur erreichen, die als Sparse-Tabelle bezeichnet wird. Für jede Potenz von 2 sparen wir maximal für jedes Segment dieser Länge. Wenn Sie nun das Segment [l, r) angeben, erhalten Sie maximal [l + 2 ^ k) und [r-2 ^ k, r) für das entsprechende k. Sie überlappen sich, aber es ist in Ordnung

RiaD
quelle