Recupero del valore massimo da un intervallo nell'array non ordinato


9

Ho un array non ordinato . Ho delle domande in cui do un intervallo e quindi deve restituire il valore massimo di quell'intervallo. Per esempio:

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

Quale algoritmo o struttura di dati devo costruire per recuperare rapidamente il massimo valore da qualsiasi intervallo. (Ci sono molte domande)

EDIT: Questa è davvero una versione semplice del problema reale. Posso avere dimensioni dell'array fino a 100000 e il numero di query fino a 100000. Quindi ho sicuramente bisogno di un po 'di preelaborazione che faciliterà una risposta rapida alle query.


5
Perché non è ordinato? Il problema è banale se viene risolto, quindi l'approccio ovvio è quello di ordinarlo.

1
@delnan Senza qualche meccanismo in più, perdi traccia di quali valori erano originariamente nell'intervallo da interrogare ...
Thijs van Dien

Specifica l'intero problema. Se questa conoscenza (o qualsiasi altra informazione) è importante, si deve sapere per includerla nella soluzione.

1
Mi sto perdendo qualcosa o è solo questione di visitare gli articoli da 2 a 6 e trovare il valore massimo di quegli elementi?
Blrfl,

@Blrfl: Non credo che ti manchi nulla, tranne forse la parte su molte domande. Non è davvero chiaro se sia utile costruire una struttura che renda le query sostanzialmente più economiche di una ricerca sequenziale. (Anche se non avrebbe molto senso fare la domanda qui se non fosse questa l'idea.)
Mike Sherrill 'Cat Recall'

Risposte:


14

Penso che potresti costruire una sorta di albero binario in cui ogni nodo rappresenta il valore massimo dei suoi figli:

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

Quindi devi solo trovare un modo per determinare quali nodi devi minimamente controllare per trovare il valore massimo nell'intervallo richiesto. In questo esempio, per ottenere il valore massimo nell'intervallo dell'indice [2, 6](incluso), max(45, 78, 4)invece di max(9, 45, 78, 2, 4). Man mano che l'albero cresce, il guadagno sarà maggiore.


1
Perché ciò funzioni, mancano informazioni nel tuo albero di esempio: ogni nodo interno deve avere sia il numero massimo che il numero totale di nodi figlio che ha. Altrimenti la ricerca non ha modo di sapere che (per esempio) non deve guardare tutti i figli di 78(e saltare il 2), perché per quanto ne sa l'indice 6è in quella sottostruttura.
Izkata,

Altrimenti, +1 lo trovo piuttosto inventivo
Izkata

+1: questa è una potente tecnica per rispondere alle domande sui sottointervalli di un elenco nel tempo di log (N), utilizzabili anche se i dati nel nodo radice possono essere calcolati in tempo costante dai dati dei bambini.
Kevin Cline,

Questa idea è fantastica. Dà il tempo di query O (logn). Penso che anche @Izkata abbia fatto un buon punto. Possiamo aumentare il nodo dell'albero con informazioni sugli intervalli sinistro e destro che copre. Quindi, dato un intervallo, sa come dividere il problema in due. Per quanto riguarda lo spazio, tutti i dati sono archiviati a livello foglia. Quindi richiede 2 * N spazio, che è O (N) da memorizzare. Non so cosa sia un albero a segmenti, ma è questa l'idea alla base dell'albero a segmenti?
Kay,

E in termini di preelaborazione, ci vuole O (n) per costruire l'albero.
Kay,

2

Per completare la risposta di ngoaho91.

Il modo migliore per risolvere questo problema è utilizzare la struttura dei dati dell'albero dei segmenti. Ciò consente di rispondere a tali query in O (log (n)), il che significherebbe che la complessità totale dell'algoritmo sarebbe O (Q logn) dove Q è il numero di query. Se si usasse l'algoritmo ingenuo, la complessità totale sarebbe O (Q n) che è evidentemente più lenta.

Vi è, tuttavia, uno svantaggio dell'utilizzo degli alberi a segmenti. Occupa molta memoria, ma molte volte ti preoccupi meno della memoria che della velocità.

Descriverò brevemente gli algoritmi utilizzati da questo DS:

L'albero dei segmenti è solo un caso speciale di un albero di ricerca binario, in cui ogni nodo contiene il valore dell'intervallo a cui è assegnato. Al nodo radice viene assegnato l'intervallo [0, n]. Al bambino sinistro viene assegnato l'intervallo [0, (0 + n) / 2] e il bambino destro [(0 + n) / 2 + 1, n]. In questo modo l'albero sarà costruito.

Crea albero :

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

Query Tree

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

Se hai bisogno di ulteriori spiegazioni, fammelo sapere.

A proposito, Segment Tree supporta anche l'aggiornamento di un singolo elemento o un intervallo di elementi in O (log n)


qual è la complessità del riempimento dell'albero?
Pieter B,

Devi passare attraverso tutti gli elementi e ci vuole O(log(n))per ogni elemento da aggiungere all'albero. Pertanto, la complessità totale èO(nlog(n))
Andrés,

1

L'algoritmo migliore sarebbe in O (n) tempo come sotto lascia iniziare, fine è l'indice dei limiti dell'intervallo

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 per la semplice ripetizione dell'algoritmo su cui l'OP stava cercando di migliorare.
Kevin Cline,

1
+1 per l'invio di una soluzione al problema indicato. Questo è davvero l'unico modo per farlo se hai un array e non sai quali saranno i limiti a priori . (Anche se avrei inizializzare maxper a[i]e avviare il forciclo a i+1.)
Blrfl

@kevincline Non si limita semplicemente a ripetere: dice anche "Sì, hai già il miglior algoritmo per questo compito", con un piccolo miglioramento (vai a start, fermati a end). E sono d'accordo, questo è il migliore per una ricerca di una volta. La risposta di @ ThijsvanDien è migliore solo se la ricerca verrà eseguita più volte, poiché inizialmente è necessario più tempo per la configurazione.
Izkata,

Concesso, al momento della pubblicazione di questa risposta, la domanda non includeva la modifica che conferma che farà molte domande sugli stessi dati.
Izkata,

1

Le soluzioni basate su albero binario / segmento puntano davvero nella giusta direzione. Si potrebbe obiettare che richiedono molta memoria aggiuntiva, tuttavia. Esistono due soluzioni a questi problemi:

  1. Utilizzare una struttura di dati implicita anziché un albero binario
  2. Usa un albero M-ary invece di un albero binario

Il primo punto è che, poiché l'albero è altamente strutturato, è possibile utilizzare una struttura simile a un heap per definire implicitamente l'albero anziché rappresentare l'albero con nodi, puntatori sinistro e destro, intervalli ecc. Ciò consente di risparmiare molta memoria essenzialmente nessun impatto sulle prestazioni: è necessario eseguire un po 'più di aritmetica del puntatore.

Il secondo punto è che, a costo di un po 'più di lavoro durante la valutazione, è possibile utilizzare un albero M-ary anziché un albero binario. Ad esempio, se usi un albero 3-ary calcolerai il massimo di 3 elementi alla volta, quindi 9 elementi alla volta, quindi 27, ecc. La memoria aggiuntiva richiesta è quindi N / (M-1) - puoi prova usando la formula della serie geometrica. Se si sceglie M = 11, ad esempio, sarà necessario 1/10 della memoria del metodo dell'albero binario.

Puoi verificare che queste implementazioni ingenue e ottimizzate in Python diano gli stessi risultati:

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

prova la struttura dei dati "albero dei segmenti"
ci sono 2 step
build_tree () O (n)
query (int min, int max) O (nlogn)

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

modificare:

voi ragazzi non leggete il wiki che ho inviato!

questo algoritmo è:
- attraversi l'array 1 volta per costruire l'albero. O (n)
- 100000000+ volte successive che si desidera conoscere al massimo qualsiasi parte dell'array, è sufficiente chiamare la funzione query. O (logn) per ogni query
- c ++ implementa qui geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
vecchio algoritmo è:
ogni query, basta attraversare l'area selezionata e trovare.

quindi, se userai questo algoritmo per elaborare una volta, OK, è più lento del vecchio modo. ma se hai intenzione di elaborare numero enorme di query (miliardi di euro), è molto efficiente è possibile generare file di testo come questo, per la prova di

linea 1: 50000 di numeri casuali 0-1.000.000, diviso da '(spazio)' (è l'array)
linea di 2: 2 numero casuale da 1 a 50000, diviso per '(spazio)' (è la query)
...
linea 200000: piace la linea 2, è anche una query casuale

questo è il problema di esempio, scusa ma questo è in vietnamita
http://vn.spoj.com/problems/NKLINEUP/
se lo risolvi alla vecchia maniera, non passi mai.


3
Non penso sia rilevante. Una struttura ad intervalli contiene intervalli, non numeri interi, e le operazioni che consentono non assomigliano affatto a ciò che OP richiede. Naturalmente, potresti generare tutti gli intervalli possibili e memorizzarli in un albero degli intervalli, ma (1) ce ne sono molti in modo esponenziale, quindi questo non si ridimensiona e (2) le operazioni non sembrano ancora ciò che OP chiede.

errore mio, intendo l'albero dei segmenti, non l'albero degli intervalli.
ngoaho91,

Interessante, penso di non aver mai incontrato questo albero! Tuttavia, ciò richiede comunque la memorizzazione di tutti gli intervalli possibili. Penso che ci sia O (n ^ 2) di quelli, che è piuttosto costoso. (Inoltre, la query non dovrebbe essere O (log n + k) per k risultati?

sì, void build_tree () deve viaggiare attraverso l'array. e memorizzare il valore massimo (o minimo) per ogni nodo. ma in molti casi, il costo della memoria non è importante della velocità.
ngoaho91,

2
Non riesco a immaginare che questo sia più veloce di una semplice O(n)ricerca dell'array, come descritto nella risposta di tarun_telang. Il primo istinto è che O(log n + k)è più veloce di O(n), ma O(log n + k)è solo il recupero dell'array secondario - equivalente O(1)all'accesso dell'array dato i punti iniziale e finale. Dovresti comunque attraversarlo per trovare il massimo.
Izkata,

0

È possibile ottenere O (1) per query (con costruzione O (n log n)) utilizzando la struttura di dati chiamata tabella sparsa. Per ogni potenza di 2 risparmiamo il massimo per ogni segmento di questa lunghezza. Ora dato il segmento [l, r) ottieni il massimo dei massimi su [l + 2 ^ k) e [r-2 ^ k, r) per k appropriato. Si sovrappongono ma va bene

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.