Conjuntos como ABB

Conjuntos como ABB

de Rafael Agustin Castelli Ottati -
Número de respuestas: 4

Buenas, intenté resolver el ejercicio que proponían en las diapositivas sobre los órdenes de intersección/diferencia/unión para conjuntos representados por ABB y quisiera saber si lo que pensé está bien.

Creo que cualquiera de las operaciones se pueden hacer en O(n + m) donde n y m son la cardinalidad de ambos conjuntos, con el siguiente procedimiento:

Primero linealizo los arboles a una lista ordenada donde las inserciones al principio y al final sean de O(1). Linealizar ambos arboles es O(max{n, m}). Luego de eso, traduzco la lista a un árbol degenerado lo que es O(n), y ese es mi resultado.

La otra alternativa, que creo que es mejor porque no me devuelve un árbol degenerado en general sería:

Calculo las cardinalidades de ambos conjuntos, esto es O(max{n, m}).

Inserto los elementos de un árbol en un arreglo de tamaño n, y los del otro en uno de tamanio m.  Esto con una recorrida inOrder es O(max{n, m}). Luego, paso el resultado de la operación a un arreglo de tamaño n + m, con tope (de la misma forma que hacía con las listas ordenadas). Esto me queda O(n + m).

Ahora traduzco el arreglo resultado a un ABB, creando un árbol vacío e insertando nodos. La inserción la hago insertando los puntos medios. O sea, si tengo un arreglo de tamaño n lo cubro primero insertando el elemento en n/2. Luego los elementos en n/4 y 3n/4 y así sucesivamente. Suponiendo que las claves de los elementos se distribuyen uniformemente, en promedio el árbol debería quedar “bastante balanceado”.

La otra solución que se me ocurrió es simplemente recorrer todos los nodos de un árbol, y preguntar si están en el otro árbol o no, y según eso insertar en un árbol nuevo. El problema es que creo que esto quedaría de O(nlog(m)) en promedio y O(n*m) en el peor caso.

Respecto a todo esto tengo las siguientes preguntas:

1)      ¿Están bien ambas resoluciones al problema? ¿Hay alguna forma mejor de hacerlo?

2)      Cuando pido/borro un arreglo el orden de ejecución es O(n) u O(1)?

3)      Asumiendo que el segundo procedimiento está bien (el de los arreglos), realmente es más rápido este algoritmo que el que es O(n*log(m)) en promedio? La pregunta viene por lo siguiente. Si bien tengo algo de O(n + m), las constantes que quedan en la expresión deberían ser “bastante más grandes” que las constantes en el caso nlog(m), entonces  tengo una cota k(n+m). Para nlog(m) tengo algo del estilo h nlog(m) donde k es “bastante más grande que h”. Entonces eligiendo m como el más grande de los números, puedo llegar a tener que incluso para m muy grandes k(n+m) > h*nlog(m). Entonces cual algoritmo sería “más rápido” en este caso, o preferible en tiempo de ejecución.



En respuesta a Rafael Agustin Castelli Ottati

Re: Conjuntos como ABB

de Fernando Fernandez -

La primera versión está bien y está bien el análisis. El inconveniente es que los próximos accesos al árbol van a ser O(n) en vez de O(log n).

La tercera versión también está bien y también el análisis.

La duda está en la segunda versión. Se construye el árbol de manera correcta y tan balanceado como es posible, aún en el peor caso y sin importar si los valores de los elementos están bien distribuidos. La duda es que creo que asumís que esa construcción es O(n). Pero tal como la describís no lo sería, o depende de a que le estás llamando insertar. Si es la inserción habitual hay que recorrer el camino que va desde la raíz hasta la hoja en donde va a quedar el nuevo elemento, con un costo O(log n). Por lo tanto el tiempo es O(n log n) en todos los casos. Pero la construcción se puede hacer en tiempo O(n). De hecho, hay una operación de la tarea 4 que pide exactamente eso.

Con respecto a 2) es difícil decirlo porque depende del manejador de memoria, pero en principio debemos suponer que si hay disponible un bloque de memoria de tamaño n el tiempo de encontrarlo y asignarlo va a ser O(1). Pero si hay que incicializarlo sería O(n).

Con respecto a 3) el problema es que no sé exactamente cuál es tu versión 2. Si es la que antes dije que es O(n log n) o sí realmente es O(n). Pero aún si es la O(n log n) ¿por qué te parece que la constante k sería más grande que en la tercera versión?

   

En respuesta a Fernando Fernandez

Re: Conjuntos como ABB

de Rafael Agustin Castelli Ottati -

Gracias por las respuestas, el mecanismo de inserción que proponía (me olvide de escribirlo) era O(n) (creo). El arreglo lo iba a insertar de manera recursiva, pero como la inserción la voy haciendo desde el medio, estoy construyendo el árbol de arriba a abajo, entonces en cada llamada recursiva (que se encargaría de insertar una de las mitades del arreglo), pasaría el "padre" así no tengo que buscar en cada inserción donde insertarlo. Puede ser que con este algoritmo (o sea de ir insertando de a mitades) se construya un ABB "completo" (que solo le falten nodos en el ultimo nivel) ?

Lo de las constantes venia porque para hacer esta versión, hago un montón de acciones O(n) mas que en el caso que busco en un árbol e inserto en el otro. En una hago 2 acciones O(log)  n veces, entonces cuando calcule mas detalladamente el tiempo de ejecución me quedaría algo como k1log + k2log. Sin embargo para la otra solución (con los arreglos auxiliares), tengo que hacer dos recorridas completas en los arboles , que seria acciones de tiempo c1 y c2. Pero como el código de recorrer un camino o el árbol entero se hacen cosas muy similares, la diferencia de los tiempos esta dada por la cantidad de veces que se repiten las acciones y no por la acciones en si,por lo que asumo c1 , c2 son aproximadamente iguales a k1, k2 (hago cosas parecidas). Sin embargo mas adelante en esta solución tengo que hacer mas cosas O(n), como cargar los datos a un arreglo (c3*n), encontrar la intersección de los dos arreglos (por ejemplo) (c4*n), y pasar el arreglo a un árbol (c5*n) , entonces tendría en el resultado algo como :

(c1 +c2 +c3 +c4 c+5)(n) contra algo (k1 + k2)n log(n). Como c1 y c2 estoy asumiendo que soy muy parecidas a k1 y k2, tengo que las constante del procedimiento O(n) son mas grande que las de O(nlog). Donde ese "son mas grandes" sea algo del orden de ser la suma 20 veces mas grande, le llevaría datos de alrededor n = 1.000.000 para que el logaritmo supere a las constantes. 

No se si el razonamiento esta bien, la idea era como comparar lo que pasaría si leo datos de la entrada y quiero imprimirlos en el orden que los leí. Si por algún motivo los guardo en una pila, para imprimirlos en el orden correcto voy a demorar el doble de tiempo que si los leyera en una cola, porque tengo que invertir la pila antes de recorrerla, y esto para aunque las soluciones sean ambas O(n). Tiene sentido razonar así al analizar algoritmos?

En respuesta a Rafael Agustin Castelli Ottati

Re: Conjuntos como ABB

de Fernando Fernandez -

Bien.

Primero, antes te dije que el análisis de la tercera versión está bien. Pero hay que tener cuidado como se hace la recorrida del árbol. Si es en preorden está bien, pero si enorden entonces el árbol resultado va a ser lineal y cada inserción sería O(n) (y \Omega(n)).

Supongamos que la recorrida es en preorden por lo que el algoritmo es O(n log n). Es cierto que entre O(n) y O(n log n) las diferencias no son grandes y la constantes son muy importantes. Sin embargo, no es una ley, pero suele ocurrir lo opuesto, que cuando hay un log n las constantes son más grandes. Por ejemplo insertionSort, que  es O(n^2) se prefiere para n relativamente chicos ante mergeSort o quicksort que son O(n log n). 

En una ejecución real intervienen y son decisivos otros temas como localidad de referencia o análisis predictivo. Aclaro que nada de esto es relevante para este curso.

En lo que planteás creo que las constantes c3, c4 y c5 serían muy chicas porque involucran arreglos. Incluso creo que c1 y c2 son menores que k1 y k2 porque estas tienen que ver con recorrer caminos en un árbol y suelen ser mayores que insertar en una lista o avanzar en un arreglo.

De última, lo que puede determinar una decisión son los experimentos. Pero hay que tener en cuenta que en distintos ambientes (por ejemplo dependiendo del tamaño de la memoria caché) se pueden obtener resultados distintos.

Repito lo que dije antes: estos análisis no son tema de este curso. Acá vamos a asumir que un algoritmo O(n) es mejor que uno \Omega(n \log n).

.