Recuperando el valor máximo de un rango en una matriz sin clasificar

9

Tengo una matriz sin clasificar . Tengo consultas en las que doy un rango y luego se debe devolver el valor máximo de ese rango. Por ejemplo:

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

¿Qué algoritmo o estructura de datos construyo para recuperar rápidamente el valor máximo de cualquier rango? (Hay muchas consultas)

EDITAR: Esta es de hecho una versión simple del problema real. Puedo tener un tamaño de matriz tan grande como 100000 y un número de consultas de hasta 100000. Por lo tanto, definitivamente necesito un cierto preprocesamiento que facilitará una respuesta de consulta rápida.

sudeepdino008
fuente
55
¿Por qué está sin clasificar? El problema es trivial si está ordenado, por lo que el enfoque obvio es ordenarlo.
1
@delnan Sin algún mecanismo adicional, pierdes la noción de qué valores estaban originalmente en el rango a consultar ...
Thijs van Dien
Especifica todo tu problema. Si este conocimiento (o cualquier otra información) es importante, hay que saber tenerlo en cuenta en la solución.
1
¿Me estoy perdiendo algo, o es solo una cuestión de visitar los elementos 2 a 6 y encontrar el valor máximo de esos elementos?
Blrfl
@Blrfl: No creo que te estés perdiendo nada, excepto tal vez la parte de muchas consultas. No está realmente claro si hay algún punto en la construcción de una estructura que haga las consultas sustancialmente más baratas que una búsqueda secuencial. (Aunque no tendría mucho sentido hacer la pregunta aquí si esa no fuera la idea.)
Mike Sherrill 'Cat Recall'

Respuestas:

14

Creo que podría construir algún tipo de árbol binario donde cada nodo representa el valor máximo de sus hijos:

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

Entonces solo necesita encontrar una manera de determinar qué nodos debe verificar mínimamente para encontrar el valor máximo en el rango consultado. En este ejemplo, para obtener el valor máximo en el rango de índice [2, 6](inclusive) que tendría en max(45, 78, 4)lugar de max(9, 45, 78, 2, 4). A medida que el árbol crece, la ganancia será mayor.

Thijs van Dien
fuente
1
Para que esto funcione, falta información en su árbol de ejemplo: cada nodo interno debe tener el máximo y el número total de nodos secundarios que tiene. De lo contrario, la búsqueda no tiene forma de saber que (por ejemplo) no tiene que mirar a todos los hijos de 78(y omitir 2), porque por lo que sabe, el índice 6está en ese subárbol.
Izkata
De lo contrario, +1 como encuentro esto bastante inventivo
Izkata
+1: Esta es una técnica poderosa para responder consultas sobre subrangos de una lista en tiempo de registro (N), utilizable siempre que los datos en el nodo raíz se puedan calcular en tiempo constante a partir de los datos en los elementos secundarios.
Kevin Cline
Esta idea es asombrosa. Le da tiempo de consulta O (logn). Creo que @Izkata también hizo un buen punto. Podemos aumentar el nodo del árbol con información sobre los rangos izquierdo y derecho que cubre. Entonces, dado un rango, sabe cómo dividir el problema en dos. En cuanto al espacio, todos los datos se almacenan a nivel de hoja. Por lo tanto, requiere 2 * N de espacio, que es O (N) para almacenar. No sé qué es un árbol de segmentos, pero ¿es esta la idea detrás del árbol de segmentos?
Kay
Y en términos de preprocesamiento, se necesita O (n) para construir el árbol.
Kay
2

Para complementar la respuesta de ngoaho91.

La mejor manera de resolver este problema es usar la estructura de datos del Árbol de segmentos. Esto le permite responder a tales consultas en O (log (n)), eso significaría que la complejidad total de su algoritmo sería O (Q logn) donde Q es el número de consultas. Si utilizó el algoritmo ingenuo, la complejidad total sería O (Q n), que obviamente es más lenta.

Sin embargo, existe un inconveniente en el uso de los árboles de segmentos. Ocupa mucha memoria, pero muchas veces te importa menos la memoria que la velocidad.

Describiré brevemente los algoritmos utilizados por este DS:

El árbol de segmentos es solo un caso especial de un árbol de búsqueda binaria, donde cada nodo contiene el valor del rango al que está asignado. Al nodo raíz, se le asigna el rango [0, n]. Al hijo izquierdo se le asigna el rango [0, (0 + n) / 2] y al hijo derecho [(0 + n) / 2 + 1, n]. De esta manera se construirá el árbol.

Crear árbol :

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

Árbol de consulta

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

Si necesita más explicaciones, hágamelo saber.

Por cierto, el árbol de segmentos también admite la actualización de un solo elemento o un rango de elementos en O (log n)

Andrés
fuente
¿Cuál es la complejidad de llenar el árbol?
Pieter B
Tienes que pasar por todos los elementos, y se necesita O(log(n))para que cada elemento se agregue al árbol. Por lo tanto, la complejidad total esO(nlog(n))
Andrés
1

El mejor algoritmo estaría en el tiempo O (n) como se muestra a continuación, inicio, final será el índice de los límites del rango

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
fuente
44
-1 por simplemente repetir el algoritmo que el OP estaba tratando de mejorar.
Kevin Cline
1
+1 para publicar una solución al problema tal como se indicó. Esta es realmente la única forma de hacerlo si tiene una matriz y no sabe cuáles serán los límites a priori . (Aunque me gustaría inicializar maxa a[i]e iniciar el forbucle en i+1.)
Blrfl
@kevincline No es solo una actualización, sino que también dice "Sí, ya tiene el mejor algoritmo para esta tarea", con una mejora menor (saltar start, detenerse en end). Y estoy de acuerdo, esto es lo mejor para una búsqueda única. La respuesta de @ ThijsvanDien solo es mejor si la búsqueda va a suceder varias veces, ya que lleva más tiempo configurarla inicialmente.
Izkata
Por supuesto, en el momento de publicar esta respuesta, la pregunta no incluía la edición que confirma que hará muchas consultas sobre los mismos datos.
Izkata
1

Las soluciones basadas en árbol binario / árbol de segmentos apuntan en la dirección correcta. Sin embargo, uno podría objetar que requieren mucha memoria adicional. Hay dos soluciones a estos problemas:

  1. Utilice una estructura de datos implícita en lugar de un árbol binario.
  2. Use un árbol M-ary en lugar de un árbol binario

El primer punto es que debido a que el árbol está altamente estructurado, puede usar una estructura similar a un montón para definir implícitamente el árbol en lugar de representarlo con nodos, punteros izquierdo y derecho, intervalos, etc. Eso ahorra mucha memoria esencialmente sin impacto en el rendimiento: debe realizar un poco más de aritmética de puntero.

El segundo punto es que, a costa de un poco más de trabajo durante la evaluación, puede usar un árbol M-ary en lugar de un árbol binario. Por ejemplo, si usa un árbol de 3 arios, calculará el máximo de 3 elementos a la vez, luego 9 elementos a la vez, luego 27, etc. El almacenamiento adicional requerido es N / (M-1): puede probar usando la fórmula de la serie geométrica. Si elige M = 11, por ejemplo, necesitará 1/10 del almacenamiento del método del árbol binario.

Puede verificar que estas implementaciones ingenuas y optimizadas en Python den los mismos resultados:

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

intente la estructura de datos del "árbol de segmentos"
hay 2 pasos
build_tree () O (n)
consulta (int min, int max) O (nlogn)

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

editar:

¡Ustedes simplemente no leen el wiki que envié!

este algoritmo es:
- atraviesas la matriz 1 vez para construir el árbol. O (n)
: las siguientes 100000000+ veces que desee saber el máximo de cualquier parte de la matriz, simplemente llame a la función de consulta. O (logn) para cada consulta
- c ++ implementa aquí geeksforgeeks.org/segment-tree-set-1-range-minimum-query/
antiguo algoritmo es:
cada consulta, solo atraviesa el área seleccionada y encuentra.

entonces, si vas a usar este algoritmo para procesar una vez, OK, es más lento que antes. pero si se va a procesar gran número de consultas (mil millones), que es muy eficiente puede generar archivo de texto como este, para la prueba de

la línea 1: 50.000 número aleatorio a partir 0-1000000, dividida por '(espacio)' (que es la matriz)
line Número aleatorio 2: 2 de 1 a 50000, dividido por '(espacio)' (es la consulta)
...
línea 200000: le gusta la línea 2, también es consulta aleatoria

Este es el problema de ejemplo, lo siento, pero está en vietnamita
http://vn.spoj.com/problems/NKLINEUP/
si lo resuelve de la manera anterior, nunca pasa.

ngoaho91
fuente
3
No creo que sea relevante. Un árbol de intervalos contiene intervalos, no enteros, y las operaciones que permiten no se parecen en nada a lo que pide OP. Podría, por supuesto, generar todos los intervalos posibles y almacenarlos en un árbol de intervalos, pero (1) hay exponencialmente muchos de ellos, por lo que esto no escala, y (2) las operaciones aún no se parecen a lo que OP pregunta por.
mi error, quiero decir árbol de segmentos, no árbol de intervalos.
ngoaho91
¡Interesante, creo que nunca me he encontrado con este árbol! IIUC esto todavía requiere almacenar todos los intervalos posibles, sin embargo. Creo que hay O (n ^ 2) de esos, lo cual es bastante caro. (Además, ¿no debería consultarse O (log n + k) para obtener k resultados?
Sí, void build_tree () debe viajar a través de la matriz. y almacenar el valor máximo (o mínimo) para cada nodo. pero en muchos casos, el costo de la memoria no es importante que la velocidad.
ngoaho91
2
No puedo imaginar que esto sea más rápido que una simple O(n)búsqueda de la matriz, como se describe en la respuesta de tarun_telang. El primer instinto es que O(log n + k)es más rápido que O(n), pero O(log n + k)es solo la recuperación de la submatriz, equivalente al O(1)acceso a la matriz dados los puntos de inicio y fin. Aún necesitarías atravesarlo para encontrar el máximo.
Izkata
0

Puede lograr O (1) por consulta (con construcción O (n log n)) utilizando una estructura de datos llamada tabla dispersa. Por cada potencia de 2, guardemos el máximo para cada segmento de esta longitud. Ahora dado el segmento [l, r) obtienes el máximo de máximos en [l + 2 ^ k) y [r-2 ^ k, r) para k apropiado. Se superponen pero está bien

RiaD
fuente