Estructura de datos para el mapa en intervalos


11

Sea n un número entero y Z denote el conjunto de todos los enteros. Supongamos que [a,b] denota el intervalo de enteros {a,a+1,a+2,,b} .

Busco a una estructura de datos para representar un mapa f:[1,n]Z . Quiero que la estructura de datos admita las siguientes operaciones:

  • get(i) debería devolverf(i) .

  • set([a,b],y) debe actualizarf modo quef(a)=f(a+1)==f(b)=y , es decir, actualicef a un nuevo mapaf tal quef(i)=y parai[a,b] yf(i)=f(i) parai[a,b] .

  • debería devolver el intervalo más grande [ un , b ] tal que i [ a , b ] y f es constante en [ a , b ] (es decir, f ( a ) = f ( un + 1 ) = = f ( b ) ).stab(i)[a,b]i[a,b]f[a,b]f(a)=f(a+1)==f(b)

  • add([a,b],δ) debería actualizarf a un nuevo mapaf modo quef(i)=f(i)+δ parai[a,b] yf(i)=f(i) parai[a,b] .

Quiero que cada una de estas operaciones sea eficiente. Contaría el tiempo O(1) u O(lgn) como eficiente, pero el tiempo O(n) es demasiado lento. Está bien si los tiempos de ejecución son tiempos de ejecución amortizados. ¿Existe una estructura de datos que simultáneamente hace que todas estas operaciones sean eficientes?

(He notado que surge un patrón similar en varios desafíos de programación. Esta es una generalización que sería suficiente para todos esos problemas de desafío).


Supongo que los árboles separados son el punto de partida. addSin embargo, sería lineal en el número de subintervalos de ; ¿Has pensado en un árbol de despliegue con nodos adicionales " + δ " unitarios, compactados perezosamente? [a,b]+δ
Gilles 'SO- deja de ser malvado'

Considere tal que f ( i ) f ( j ) para todo i , j . Entonces tienes que tener n valores almacenados en alguna parte. Realizar el conjunto ( [ a , b ] , y ) tiene que deshacerse de esos valores de alguna manera (reescribiéndolos o tirándolos a la basura; puede posponerlos con GC, pero tendrá que hacer operaciones O ( n ) en algún momento) . Como tal, la operación será O ( n ) . ff(i)f(j)ijnset([a,b],y)O(n)O(n)
avakar

@avakar, estaría contento con una solución que trata a GC como efectivamente "gratis". En términos más generales, estaría contento con una solución en la que los tiempos de ejecución se amortizan (por lo tanto, el costo de GC se puede amortizar en el costo de crear el valor en primer lugar).
DW

Usted notó que el tiempo constante y logarítmico es eficiente, y el tiempo lineal es lento. ¿ tiempo es demasiado lento para sus necesidades? O(nlgn)
jbapple

@jbapple, oye, ¡es un comienzo! Creo que vale la pena documentarlo como respuesta.
DW

Respuestas:


4

Creo que el tiempo logarítmico para todas las consultas es alcanzable. La idea principal es usar un árbol de intervalos, donde cada nodo en el árbol corresponde a un intervalo de índices. Desarrollaré las ideas clave comenzando con una versión más simple de la estructura de datos (que puede admitir get y set pero no las otras operaciones), luego agregaré características para admitir también las otras características.

Un esquema simple (admite get y set, pero no agrega ni apuñala)

Digamos que un intervalo es plano si la función f es constante en [ a , b ] , es decir, si f ( a ) = f ( a + 1 ) = = f ( b ) .[a,b]f[a,b]f(a)=f(a+1)==f(b)

Nuestra estructura de datos simple será un árbol de intervalos. En otras palabras, tenemos un árbol binario, donde cada nodo corresponde a un intervalo (de índices). Almacenaremos el intervalo correspondiente en cada nodo v del árbol. Cada hoja corresponderá a un intervalo plano, y se organizarán de modo que leer las hojas de izquierda a derecha nos dé una secuencia de intervalos planos consecutivos que son disjuntos y cuya unión es toda [ 1 , n ] . El intervalo para un nodo interno será la unión de los intervalos de sus dos hijos. Además, en cada nodo hoja almacenaremos el valor V ( )I(v)v[1,n]V()de la función en el intervalo I ( ) correspondiente a este nodo (tenga en cuenta que este intervalo es plano, por lo que f es constante en el intervalo, por lo que solo almacenamos un único valor de f en cada nodo hoja).fI()ff

De manera equivalente, puede imaginar que dividimos en intervalos planos, y luego la estructura de datos es un árbol de búsqueda binario donde las claves son los puntos finales izquierdos de esos intervalos. Las hojas contienen el valor de f en algún rango de índices donde f es constante.[1,n]ff

Utilice métodos estándar para garantizar que el árbol binario permanezca equilibrado, es decir, su profundidad sea (donde m cuenta el número actual de hojas en el árbol). Por supuesto, m n , por lo que la profundidad es siempre como máximo O ( lg n ) . Esto será útil a continuación.O(lgm)mmnO(lgn)

Ahora podemos soportar las operaciones get y set de la siguiente manera:

  • get(i)iO(lgn)O(lgn)

  • set([a,b],y)

    1. [a0,b0]aa0<a[a0,a1][a,b0]

    2. [a1,b1]bb<b1[a1,b][b+1,b1]

    3. [a,b]O(lgn)O(lgn)y

    4. Finalmente, dado que modificamos la forma del árbol, realizaremos las rotaciones necesarias para reequilibrar el árbol (utilizando cualquier técnica estándar para mantener un árbol equilibrado).

    O(lgn)O(lgn)O(lgn)

O(lgn)O(lgmin(n,s))s

Agregar soporte para agregar

Podemos modificar la estructura de datos anterior para que también pueda admitir la operación de agregar. En particular, en lugar de almacenar el valor de la función en las hojas, se representará como la suma de los números almacenados en un conjunto de nodos.

Más precisamente, el valor de la función en la entrada será recuperable como la suma de los valores almacenados en los nodos en la ruta desde la raíz del árbol hasta la hoja cuyo intervalo contiene . En cada nodo almacenaremos un valor ; si representan los antepasados ​​de una hoja (incluida la hoja misma), entonces el valor de la función en será .f(i)iivV(v)v0,v1,,vkvkI(vk)V(v0)++V(vk)

Es fácil admitir las operaciones get y set utilizando una variante de las técnicas descritas anteriormente. Básicamente, a medida que atravesamos el árbol hacia abajo, llevamos un registro de la suma de valores en ejecución, de modo que para cada nodo que visite el recorrido, sepamos la suma de valores de los nodos en el camino desde la raíz hasta . Una vez que hagamos eso, bastarán los ajustes simples a la implementación de get y set descritos anteriormente.xx

Y ahora podemos admitir eficiente. Primero, expresamos el intervalo como la unión de los intervalos correspondientes a algún conjunto de nodos en el árbol (dividiendo un nodo en el punto final izquierdo y el punto final derecho si es necesario) ), exactamente como se hizo en los pasos 1-3 de la operación de configuración. Ahora, simplemente agregamos al valor almacenado en cada uno de esos nodos . (No eliminamos a sus descendientes).add([a,b],δ)[a,b]O(lgn)O(lgn)δO(lgn)

Esto proporciona una manera de admitir obtener, establecer y agregar, en tiempo por operación. De hecho, el tiempo de ejecución por operación es donde cuenta el número de operaciones establecidas más el número de operaciones de suma.O(lgn)O(lgmin(n,s))s

Apoyando la operación de puñalada

La consulta de apuñalamiento es la más difícil de soportar. La idea básica será modificar la estructura de datos anterior para preservar la siguiente invariante adicional:

(*) El intervalo correspondiente a cada hoja es un intervalo plano máximo.I()

Aquí digo que un intervalo es el intervalo plano máximo si (i) es plano, y (ii) ningún intervalo que contenga es plano (en otras palabras, para todo satisface , ya sea o no es plano).[a,b][a,b][a,b]a,b1aabbn[a,b]=[a,b][a,b]

Esto hace que la operación de apuñalamiento sea fácil de implementar:

  • stab(i) encuentra la hoja cuyo intervalo contiene , y luego devuelve ese intervalo.i

Sin embargo, ahora necesitamos modificar el conjunto y agregar operaciones para mantener el invariante (*). Cada vez que dividimos una hoja en dos, podríamos violar la invariante si algún par adyacente de intervalos de hoja tiene el mismo valor de la función . Afortunadamente, cada operación de establecer / agregar agrega como máximo 4 nuevos intervalos de hoja. Además, para cada nuevo intervalo, es fácil encontrar el intervalo de hoja inmediatamente a la izquierda y derecha del mismo. Por lo tanto, podemos decir si el invariante fue violado; si lo fuera, entonces fusionamos intervalos adyacentes donde tiene el mismo valor. Afortunadamente, la fusión de dos intervalos adyacentes no desencadena cambios en cascada (por lo que no es necesario verificar si la fusión podría haber introducido violaciones adicionales de la invariante). En total, esto implica examinarf 12 = O ( 1 ) O ( lg n )ff12=O(1)pares de intervalos y posiblemente fusionándolos. Finalmente, dado que una fusión cambia la forma del árbol, si esto viola los invariantes del equilibrio, realice las rotaciones necesarias para mantener el árbol equilibrado (siguiendo las técnicas estándar para mantener equilibrados los árboles binarios). En total, esto agrega como máximo trabajo adicional a las operaciones set / add.O(lgn)

Por lo tanto, esta estructura de datos final admite las cuatro operaciones, y el tiempo de ejecución para cada operación es . Una estimación más precisa es el tiempo por operación, donde cuenta el número de operaciones de establecimiento y suma.O ( lg min ( n , s ) ) sO(lgn)O(lgmin(n,s))s

Pensamientos de despedida

Uf, este era un esquema bastante complejo. Espero no haber cometido ningún error. Por favor revise mi trabajo cuidadosamente antes de confiar en esta solución.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.