Cómo mantener una matriz, admitir inserción y asignación aleatorias, y consultar el kth elemento más grande en un intervalo dado

Editar: esta respuesta se basa en la discusión en los comentarios; Mi respuesta original aparece a continuación.

Aquí hay una forma en que creo que puedes hacerlo. Es una publicación larga y estoy bastante seguro de que el método se puede refinar, pero al menos es algo que funciona y tiene un rendimiento razonable.

No estoy muy seguro si solo desea poder insertar nuevos valores al final de la matriz, o también en el medio, pero para mantener las cosas simples, describiré mi solución para las inserciones solo al final. Creo que la estructura también se puede adaptar para admitir inserciones en otras posiciones, si es necesario.

La idea es la siguiente: dividimos la matriz en segmentos [matemáticos] O (\ sqrt n) [/ matemáticos]. La forma en que lo hacemos es la siguiente: supongamos que hemos hecho [math] m [/ math] segmentos hasta ahora, y queremos insertar un nuevo elemento en la matriz de longitud [math] n [/ math]. Si [math] \ sqrt {n} <m [/ math] luego [math] n [/ math], entonces agregamos el nuevo elemento al último segmento. De lo contrario, comenzamos un nuevo segmento y agregamos el elemento a ese nuevo segmento.

Esto tendrá el efecto de que los segmentos cercanos al inicio de la matriz serán más pequeños que los segmentos cercanos al final de la matriz, pero eso no importa: lo que importa es que al final, tendremos [matemáticas] O ( \ sqrt n) [/ math] segmentos, y cada segmento tendrá elementos [math] O (\ sqrt n) [/ math].

El siguiente paso es mantener un índice para todos estos segmentos. Estoy pensando en mantener montones binarios para todos los segmentos. Para cada elemento de la matriz original, mantenemos un puntero a su posición en el montón máximo de su segmento. Ahora, todos estos montones deben mantenerse actualizados bajo asignación, pero eso se puede hacer: si a un elemento de un segmento se le asigna un nuevo valor, tenemos un enlace a su posición anterior en el montón, para que podamos corregir su posición. en [matemáticas] O (\ log n) [/ matemáticas] tiempo. Y de manera similar, podemos mantener estos montones bajo inserción: cada vez que se inicia un nuevo segmento, inicializamos un nuevo montón vacío y le agregamos elementos a medida que se insertan. Por lo tanto, la inserción y la asignación son operaciones [matemáticas] O (\ log n) [/ matemáticas].

Ahora la operación difícil es encontrar el elemento más grande [math] k [/ math] en un intervalo [math] [a, b] [/ math]. Eso se puede hacer de la siguiente manera. Primero, sea sa el índice del primer segmento dividido después de a, y sea sb el índice del último segmento dividido antes de b. Reúna todos los valores de borde en los índices [a, sa) y (sb, b] en una nueva matriz, y construya un montón binario Hborder a partir de él. Esto toma tiempo [matemático] O (\ sqrt n) [/ matemático]. los montones binarios que tenemos para todos los segmentos entre sa y sb, junto con el nuevo montón que contiene los valores de borde. Deseamos encontrar el elemento más grande [math] k [/ math] de todos ellos. Podemos hacerlo mediante poner todos estos montones en otro montón binario H. Este nuevo montón binario “meta” contiene a lo sumo [math] O (\ sqrt n) [/ math] otros montones, o menos, dependiendo de cuántos segmentos se encuentren en el rango [a , b], así que también se puede construir en esa cantidad de tiempo. Ahora repita k veces: elimine el montón de segmento más grande h de H. Luego elimine el elemento más grande de h. Después de [math] k [/ math] pasos , tiene el elemento [math] k [/ math] th más grande que estaba buscando. Esto lleva tiempo [math] O (k \ log n) [/ math]. Después de la consulta, debe volver a insertar todos los valores extraídos en sus montones, lo que lleva la misma cantidad de tiempo que solía extraerlos. (Este último bit en realidad se siente como una pérdida de tiempo, y estoy seguro de que el algoritmo podría refinarse para que este trabajo no sea necesario).

Entonces, el tiempo de ejecución total depende del tiempo requerido para construir Hborder y H, que es como máximo [math] O (\ sqrt {n}) [/ math] para ambos, y el tiempo para extraer [math] k [/ math] el valor más grande, que es [math] O (k \ log n) [/ math]. Entonces, podría argumentar que la estructura de datos en realidad admite dos operaciones: setInterval (a, b), que requiere tiempo de ejecución [math] O (\ sqrt {n}) [/ math], y kthLargest (), que requiere tiempo de ejecución [math] O (k \ log n) [/ math]. Si no tiene que cambiar el intervalo de cada consulta, esto ayudará a reducir aún más el tiempo de ejecución.

Editar: mi respuesta original a continuación se realizó bajo la idea errónea de que el intervalo era un intervalo de valores , en lugar de un intervalo de índices de entrada.

Creo que la forma más fácil es usar un árbol de búsqueda binario, donde en cada nodo interno del árbol [math] n [/ math] se realiza un seguimiento del tamaño del subárbol enraizado en [math] n [/ math].

Si desea encontrar el elemento más grande [math] k [/ math] th en el intervalo [a, b], primero (1) cuente el número [math] s [/ math] de elementos en el árbol que son más pequeños que o igual a [matemáticas] b [/ matemáticas]. Entonces, (2) el elemento que busca es el elemento [math] s-k + 1 [/ math] st en el árbol.

Ambos (1) y (2) se pueden implementar en tiempo [matemático] O (\ log n) [/ matemático] si almacena tamaños de subárbol en todos los nodos internos. Como de costumbre, es posible que desee utilizar un BST equilibrado para garantizar un buen rendimiento.

En resumen, construya una lista vinculada con índices.

Al admitir la inserción y asignación al azar, la matriz debe almacenarse en la memoria como una lista con punteros. Para agilizar la inserción y la asignación, cree un índice sobre la lista en función de cómo se especifiquen los lugares de inserción y asignación.

Consultar los elementos más grandes de kth significa que quizás tenga que usar algunos punteros adicionales para mantener una lista ordenada del primer elemento más grande de k en ese intervalo. Tras la inserción, asignación o eliminación, los punteros también deben cambiar para mantener actualizado el pedido.

Si hay más de un intervalo para el kth elemento más grande, o si desea un intervalo aleatorio, una variación del árbol B + puede ser lo que está buscando. Observar cómo se organizan y administran los datos en el disco utilizando segmentaciones también puede ayudarlo a diseñar la estructura de datos.

Esta es una generalización de la consulta mínima de Rango: Wikipedia y algunos de los algoritmos descritos allí se generalizan, aunque no está claro que sean óptimos. En particular, el enfoque que mantiene los mínimos en todos los intervalos de potencia de dos y luego se minimiza durante esos intervalos puede generalizarse para darle un tiempo de ejecución O (k log n) para el problema que ha planteado.