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.TT′TT′TT′
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 .dd+1O(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.nO(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.dd
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)nO(nlogn)