Considere árboles binarios enraizados sin etiquetar. Podemos comprimir dichos árboles: cada vez que hay punteros a subárboles y con (que interpretan como la igualdad estructural), almacenamos (sin pérdida de generalidad) y reemplazamos todos los punteros a con punteros a . Ver la respuesta de uli para un ejemplo.T ′ T = T ′ = T T ′ T
Proporcione un algoritmo que tome un árbol en el sentido anterior como entrada y calcule el número (mínimo) de nodos que quedan después de la compresión. El algoritmo debe ejecutarse en tiempo (en el modelo de costo uniforme) con el número de nodos en la entrada.
Esta ha sido una pregunta de examen y no he podido encontrar una buena solución, ni la he visto.
Respuestas:
Sí, puede realizar esta compresión en tiempo , pero no es fácil :) Primero hacemos algunas observaciones y luego presentamos el algoritmo. Asumimos que el árbol inicialmente no está comprimido; esto no es realmente necesario, pero facilita el análisis.O(nlogn)
En primer lugar, caracterizamos la "igualdad estructural" inductivamente. Deje que y sean dos (sub) árboles. Si y son ambos árboles nulos (sin vértices), son estructuralmente equivalentes. Si y son árboles nulos, entonces son estructuralmente equivalentes si sus hijos izquierdos son estructuralmente equivalentes y sus hijos derechos son estructuralmente equivalentes. La "equivalencia estructural" es el punto fijo mínimo sobre estas definiciones.T T′ T T′ T T′
Por ejemplo, cualquiera de los dos nodos de hoja son estructuralmente equivalentes, ya que ambos tienen los árboles nulos como sus dos hijos, que son estructuralmente equivalentes.
Como es bastante molesto decir "sus hijos izquierdos son estructuralmente equivalentes y también lo son sus hijos correctos", a menudo diremos "sus hijos son estructuralmente equivalentes" y pretenden lo mismo. También tenga en cuenta que a veces decimos 'este vértice' cuando queremos decir 'el subárbol enraizado en este vértice'.
La definición anterior nos da una pista inmediata de cómo realizar la compresión: si conocemos la equivalencia estructural de todos los subárboles con profundidad como máximo , entonces podemos calcular fácilmente la equivalencia estructural de los subárboles con profundidad . Tenemos que hacer este cálculo de manera inteligente para evitar un tiempo de ejecución .d d+1 O(n2)
El algoritmo asignará identificadores a cada vértice durante su ejecución. Un identificador es un número en el conjunto . Los identificadores son únicos y nunca cambian: por lo tanto, suponemos que establecemos alguna variable (global) en 1 al comienzo del algoritmo, y cada vez que asignamos un identificador a algún vértice, asignamos el valor actual de esa variable al vértice y al incremento El valor de esa variable.{1,2,3,…,n}
Primero transformamos el árbol de entrada en (como máximo ) listas que contienen vértices de igual profundidad, junto con un puntero a su padre. Esto se hace fácilmente en tiempo.n O(n)
Primero comprimimos todas las hojas (podemos encontrar estas hojas en la lista con vértices de profundidad 0) en un solo vértice. Asignamos a este vértice un identificador. La compresión de dos vértices se realiza redirigiendo el padre de cualquier vértice para que apunte al otro vértice.
Hacemos dos observaciones: en primer lugar, cualquier vértice tiene hijos de profundidad estrictamente menor, y en segundo lugar, si hemos realizado compresión en todos los vértices de profundidad más pequeños que (y les hemos dado identificadores), entonces dos vértices de profundidad son estructuralmente equivalentes y se puede comprimir si los identificadores de sus hijos coinciden. Esta última observación se desprende del siguiente argumento: dos vértices son estructuralmente equivalentes si sus hijos son estructuralmente equivalentes, y después de la compresión, esto significa que sus punteros apuntan a los mismos hijos, lo que a su vez significa que los identificadores de sus hijos son iguales.d d
Realizamos iteraciones a través de todas las listas con nodos de igual profundidad desde profundidad pequeña hasta profundidad grande. Para cada nivel creamos una lista de pares enteros, donde cada par corresponde a los identificadores de los hijos de algún vértice en ese nivel. Tenemos que dos vértices en ese nivel son estructuralmente equivalentes si sus pares enteros correspondientes son iguales. Usando el orden lexicográfico, podemos ordenarlos y obtener los conjuntos de pares enteros que son iguales. Comprimimos estos conjuntos en vértices individuales como arriba y les damos identificadores.
Las observaciones anteriores demuestran que este enfoque funciona y da como resultado el árbol comprimido. El tiempo total de ejecución es más el tiempo necesario para ordenar las listas que creamos. Como el número total de pares enteros que creamos es , esto nos da que el tiempo total de ejecución es , según sea necesario. Contar cuántos nodos nos quedan al final del procedimiento es trivial (solo mira cuántos identificadores hemos entregado).O(n) n O(nlogn)
fuente
degree
grado estrictamente más pequeño " probablemente debería serdepth
? Y a pesar de que los árboles CS crecen hacia abajo, encuentro "altura de un árbol" menos confuso que "profundidad de un árbol".La compresión de una estructura de datos no mutable para que no duplique ningún subterráneo estructuralmente igual se conoce como hash consing . Esta es una técnica importante en la gestión de la memoria en la programación funcional. Hash consing es una especie de memorización sistemática para estructuras de datos.
Vamos a hacer hash-cons del árbol y contar los nodos después de hash consing. El hash que consiste en una estructura de datos de tamaño siempre se puede hacer en operaciones ; contar el número de nodos al final es lineal en el número de nodos.n O(nlg(n))
Consideraré que los árboles tienen la siguiente estructura (escrita aquí en la sintaxis de Haskell):
Para cada constructor, necesitamos mantener una asignación de sus posibles argumentos al resultado de aplicar el constructor a estos argumentos. Las hojas son triviales. Para los nodos, mantenemos un mapa parcial finito donde es el conjunto de identificadores de árbol y es el conjunto de identificadores de nodo; donde es el único identificador de hoja. (En términos concretos, un identificador es un puntero a un bloque de memoria).nodes:T×T→N T N T=N⊎{ℓ} ℓ
Podemos utilizar una estructura de datos de tiempo logarítmico para
nodes
, como un árbol de búsqueda binario equilibrado. A continuación, llamaré alookup nodes
la operación que busca una clave en lanodes
estructura de datos, yinsert nodes
la operación que agrega un valor bajo una nueva clave y devuelve esa clave.Ahora atravesamos el árbol y agregamos los nodos a medida que avanzamos. Aunque estoy escribiendo en pseudocódigo similar a Haskell, lo trataré
nodes
como una variable mutable global; solo lo agregaremos alguna vez, pero las inserciones deben estar enhebradas por completo. Laadd
función se repite en un árbol, agregando sus subárboles alnodes
mapa y devuelve el identificador de la raíz.El número de
insert
llamadas, que también es el tamaño final de lanodes
estructura de datos, es el número de nodos después de la compresión máxima. (Agregue uno para el árbol vacío si es necesario).fuente
nodes
insert
yadd
debería hacerse explícito y, en mi opinión, debería asignarse una función que realmente resuelva el problema.nodes
una variable mutable por conveniencia, pero puedes enhebrarla por completo. No voy a dar el código completo, esto no es SO.Aquí hay otra idea que apunta a codificar (inyectivamente) la estructura de los árboles en números, en lugar de simplemente etiquetarlos arbitrariamente. Para eso, usamos que la factorización prima de cualquier número es única.
Esta última suposición es un tramo en máquinas reales; en este caso, uno preferiría usar algo similar a la función de emparejamiento de Cantor en lugar de exponenciación.
fuente
Como las imágenes no están permitidas en los comentarios:
arriba a la izquierda: un árbol de entrada
arriba a la derecha: los subárboles enraizados en los nodos 5 y 7 también son isomórficos.
inferior izquierda y derecha: los árboles comprimidos no están definidos de forma exclusiva.
Tenga en cuenta que en este caso el tamaño del árbol ha bajado detambién.7+5|T| 6+|T|
fuente
Editar: leí la pregunta ya que T y T 'eran hijos del mismo padre. Tomé la definición de compresión como recursiva también, lo que significa que podría comprimir dos subárboles previamente comprimidos. Si esa no es la pregunta real, entonces mi respuesta puede no funcionar.
Dónde
hasSameStructure()
es una función que compara dos subárboles ya comprimidos en tiempo lineal para ver si tienen exactamente la misma estructura. Escribir una función recursiva de tiempo lineal que atraviese cada una y verifique si un subárbol tiene un hijo izquierdo cada vez que el otro tiene, etc.Deje que y sean los tamaños de los subárboles izquierdo y derecho respectivamente (después de la compresión). Entonces el tiempo de ejecución es ynℓ nr
fuente