Estructuras de Datos. Estructuras de Datos para Conjuntos Disjuntos Santiago Zanella 2009 1 Introducción Para ciertas aplicaciones se requiere mantener n elementos distintos agrupándolos en una colección de conjuntos disjuntos, permitiendo dos operaciones: (1) la unión de dos conjuntos de la colección y (2) conocer en qué conjunto se encuentra un elemento dado. Entre estas aplicaciones se cuentan el algoritmo de Kruskal para la determinación del árbol de expansión mı́nimo de un grafo, la inferencia de tipos, la determinación de las componentes conexas de un grafo, el reconocimiento de objetos en imágenes, étc... 2 Definición de la Estructura Una estructura de datos de conjuntos disjuntos (disjoint-set data structure) mantiene una colección S = {S1 , S2 , . . . , Sk } de conjuntos disjuntos entre sı́ ∀ i, j ∈ {1, . . . , k} | i 6= j · Si ∩ Sj = ∅ con elementos dentro de un cierto universo U = {x1 , . . . , xn } Cada conjunto de la colección se identifica por un representante que puede ser un miembro cualquiera del conjunto. Solo se requiere que al consultar por el representante de un conjunto dos veces consecutivas sin modificar el conjunto el resultado sea el mismo. Sin perder generalidad se puede asumir que U = {1, . . . , n}. Si no fuera ası́ se puede encontrar una función biyectiva que realice la traducción entre U y el conjunto {1, . . . , n}. Sean x, y ∈ U se desea implementar las siguientes operaciones: 1 MakeSet(x): Crea un nuevo conjunto cuyo único miembro (y representante) es x. Se requiere que x no esté en ningún conjunto de la estructura. S pre: x ∈ U − ki=1 Si pos: S ′ = S ∪ {{x}} Union(x, y): Une los conjuntos que contienen a x y a y, digamos Sx y Sy . El representante del conjunto resultante puede ser cualquier miembro de Sx ∪ Sy . Como se requiere que la colección sea disjunta, se eliminan los conjuntos Sx y Sy de la colección S. pre: x ∈ Sx ∧ y ∈ Sy ∧ Sx 6= Sy pos: S ′ = S ∪ (Sx ∪ Sy ) − {Sx , Sy } Find(x): Encuentra el representante del único conjunto al que pertenece x o 0 si x no está en ningún conjunto de la colección. pre: x ∈ U S pos: (x ∈ Si i = 1, . . . , k ∧ F ind(x) ∈ Si ) ∨ (x ∈ / ki=1 Si ∧ F ind(x) = 0) En lo que sigue se intentará analizar el costo de diferentes implementaciones de la estructura al ejecutar una secuencia σ de m de estas operaciones, de las cuales n son operaciones MakeSet, q son operaciones Union y f son operaciones Find (Ver Tabla 1). Como los conjuntos son disjuntos, cada operación Union reduce el número de conjuntos en 1. Luego de n − 1 operaciones Union solamente queda un conjunto, por lo tanto el número de operaciones Union es a lo sumo n − 1. Número de Operaciones σ m MakeSet Union Find n q ≤ n − 1 f = m − (n + q) Tabla 1: Número de operaciones de cada tipo en la secuencia σ. Se considerará además que en cada una de las operaciones Union(x, y), x e y son los representantes de los conjuntos a los que pertencen. 3 Aplicación Una aplicación simple de la estructura de conjuntos disjuntos es determinar si dos vértices de un grafo no-dirigido pertenecen o no a la misma componente conexa. 1 procedure co nnect ed co mpo nent s (G) 2 foreach v ∈ V(G) 2 3 MakeSet ( v ) ; 4 end 5 foreach ( u , v ) ∈ E(G) 6 i f Find ( u ) 6= Find ( v ) then 7 Union ( u , v ) ; 8 end 9 end 10 end 1 procedure same 2 i f Find ( u ) 3 return 4 else 5 return 6 end 7 end component ( u , v ) = Find ( v ) then True ; False ; Ejemplo a b c d e g h i Figura 1: Un grafo no-dirigido con 8 vértices y 3 componentes conexas. Paso 0 1 2 3 4 5 Arista Procesada inicio (a,b) (a,e) (a,g) (c,d) (c,i) {a} {b} {a, b} {a, b, e} {a, b, e, g} {a, b, e, g} {a, b, e, g} {c} {c} {c} {c} {c, d} {c, d, i} S {d} {e} {g} {h} {i} {d} {e} {g} {h} {i} {d} {g} {h} {i} {d} {h} {i} {h} {i} {h} Tabla 2: Colección de conjuntos disjuntos luego de proecesar cada arista. La Tabla 2 muestra la colección S que mantiene la estructura en cada paso del procedimiento. Al finalizar, dos vértices se encuentran en el mismo conjunto de la colección si y solo sı́ se encuentran en la misma componente conexa. Luego, Find(u) = Find(v) si y solo si u y v están en la misma componente conexa. 3 La relación R donde u R v si y solo sı́ u y v están en la misma componente conexa es una relación de equivalencia. En general, siempre que se divide un grupo de objetos en conjuntos disjuntos, la relación R donde x R y si y solo sı́ x e y están en el mismo conjunto es una relación de equivalencia. 4 Representación por Arreglos Una manera simple de implementar la estructura es utilizando un arreglo rep de tamaño n de tal forma que rep[x] indique el representante del conjunto al que pertence x. • Se incializan todas las posiciones del arreglo rep en 0. • La operación Makeset(x) asigna rep[x] := x. O(1). • La operación Find(x) simplemente devuelve el valor de rep[x]. O(1). • Para la operación Union(x, y) se recorre el arreglo rep y se cambian, por ejemplo, todas las posiciones iguales a y por x. O(n). 1 procedure MakeSet ( x ) 2 r ep [ x ] := x ; 3 end 1 function Find ( x ) 2 return r ep [ x ] ; 3 end 1 procedure Union ( x , y ) 2 for i :=1 to n do 3 i f r ep [ i ] = y then 4 r ep [ i ] := x ; 5 end 6 end 7 end El costo de m operaciones está dominado por la cantidad de operaciones Union, cada una de las cuales tiene costo O(n). El orden de una secuencia σ de m operaciones resulta O(n + qn + f ). En el peor caso, q = n − 1 = Θ(m) y la secuencia σ completa tiene un orden de tiempo de O(m2 ). 4 (a) (b) rep b c c c d f e c f f rep b c c c d c e c f c g f g c h c h c Figura 2: (a) Representación por arreglos de los conjuntos {c, h, e, b} con c como representante y {f, g, d} con f como representante. (b) Resultado luego de ejecutar Union(c, f ). 5 Representación por Listas Se puede mejorar el costo de la operación Union si en lugar de recorrer todas las entradas del arreglo rep se recorren solamente aquellas que corresponden a uno de los dos conjuntos. Esto se puede lograr representando los conjuntos mediante listas usando arreglos. Se representa la estructura usando un arreglo rep como en el caso anterior, un arreglo next que indica el sucesor de un elemento en la lista a la que pertenece y un arreglo last que almacena el último elemento de cada lista. El primer elemento de cada lista sirve como representante del conjunto. • Se incializan todas las posiciones del arreglo rep en 0. • La operación Makeset(x) crea una nueva lista que contiene solo al elemento x en O(1). • La operación Find(x) simplemente devuelve el valor de rep[x]. O(1). • Para la operación Union(x, y) se recorre, por ejemplo, la lista que comienza en x cambiando las entradas correspondientes en el arreglo rep por y y luego se concatena al final de la lista que comienza en y. (Fig. 3) c b h b e b f b g b d b b b f b g (a) b d b c b h b e b b b (b) Figura 3: (a) Representación en forma de lista de los conjuntos {c, h, e, b} y {f, g, d}. (b) Resultado luego de ejecutar Union(c, f ). 5 1 procedure MakeSet ( x ) 2 r ep [ x ] := x ; 3 next [ x ] := 0 ; 4 l a s t [ x ] := x ; 5 end 1 function Find ( x ) 2 return r ep [ x ] ; 3 end 1 procedure Union ( x , y ) 2 p := x ; 3 do 4 r ep [ p ] := y ; 5 p := next [ p ] ; 6 while p 6= 0 7 next [ l a s t [ y ] ] := x ; 8 l a s t [ y ] := l a s t [ x ] ; 9 end Para realizar una operación Union se recorre toda la lista x, lo que lleva un tiempo proporcional a la longitud de la lista. Se puede construir una secuencia de m operaciones que lleve tiempo Θ(m2 ). Considere la secuencia de m operaciones con n = ⌈m/2⌉ + 1 y q = m − n = ⌊m/2⌋ − 1 que se muestra en la Tabla 3. Operación MakeSet(x1 ) .. . Número de Actualizaciones a rep 1 .. . MakeSet(xn ) Union(x1 , x2 ) Union(x2 , x3 ) Union(x3 , x4 ) .. . 1 1 2 3 .. . Union(xq−1 , xq ) q−1 Tabla 3: Una secuencia de m operaciones que toma tiempo Θ(m2 ). Cada operación MakeSet realiza una actualización a rep. La i-ésima operación Union realiza i actualizaciones. El número de actualizaciones en las q operaciones Union es q−1 X i = Θ(q 2 ) i=1 6 El número total de actualizaciones es de orden Θ(n + q 2 ) = Θ(m2 ) porque n = Θ(m) y q = Θ(m). En promedio cada una de las m operaciones tiene orden Θ(m). 5.1 Unión por Tamaño En la implementación anterior no se consideraba la longitud de cada lista al realizar una operación Union. ¿Qué sucede si se considera y se actualizan siempre los representantes de la lista de menor tamaño? Claramente, la secuencia anterior de operaciones se ejecutarı́a en orden Θ(m). Aunque el anterior es un caso particular, se observa una mejora en general. 1 procedure Union ( x , y ) 2 i f s i z e [ x ] > s i z e [ y ] then 3 swap ( x , y ) ; 4 end 5 6 p := x ; 7 8 do 9 r ep [ p ] := y ; 10 p := next [ p ] ; 11 while p 6= 0 12 next [ l a s t [ y ] ] := x ; 13 s i z e [ y ] := s i z e [ x ] + s i z e [ y ] ; 14 l a s t [ y ] := l a s t [ x ] ; 15 end Teorema 5.1 Usando la representación por listas y la técnica de unión por tamaño una secuencia de m operaciones MakeSet, Union y Find, n de las cuales son operaciones MakeSet, toma tiempo O(m + n log n). Demostración Se trata de encontrar para cada elemento x una cota superior del número de veces que se actualiza rep[x]. Cada vez que se actualiza rep[x] al realizar una operación Union(Sx , Sy ) el elemento x se debe encontrar en la lista de menor tamaño: |Sx | ≤ |Sy | y la lista resultante tiene tamaño |Sx | + |Sy | ≥ 2|Sx | Es decir, cada vez que el conjunto al que pertence un elemento x participa en una unión, y se modifica rep[x], el elemento termina dentro de un conjunto que es al menos dos veces mayor. 7 Luego de actualizar ⌈log k⌉ veces el representante de un elemento, el conjunto en el que está debe tener al menos k miembros. Como un conjunto no puede tener más de n elementos, a lo sumo se actualiza el representante de un elemento ⌈log n⌉ veces. El número de actualizaciones en todas las operaciones Union es por lo tanto de orden O(q log n). Cada operación MakeSet y cada operación Find toma tiempo O(1), por lo tanto la secuencia completa toma tiempo O(n + q log n + f ). En el peor caso, cuando n = Θ(m), q = Θ(m) y f = Θ(m), el orden es O(m log m). 6 Representación por Árboles Se puede representar la colección S como una colección de árboles donde cada árbol representa un conjunto y cada nodo es un miembro del conjunto. El representante de un conjunto es el nodo raı́z en el árbol y para cada nodo solo se mantiene un puntero a su nodo padre en un arreglo llamado f ather (Fig. 4). c f e h f c d g b h d e g b (a) (b) Figura 4: Un bosque de conjuntos disjuntos. (a) Dos árboles representando los conjuntos de la Fig. 3. El árbol de la izquierda representa el conjunto {b, c, e, h}, con c como el representante, y el árbol de la derecha representa el conjunto {d, f, g} con f como representante. (b) El resultado luego de Union(c, f ). • Se incializan todas las posiciones del arreglo f ather en -1. • La operación Makeset(x) crea un nuevo árbol que solo contiene al elemento x en O(1). • La operación Find(x) recorre el árbol donde está x hasta la raı́z para buscar el representante del conjunto. El orden temporal está dado por la altura del árbol. • La operación Union(x, y) asigna, por ejemplo, f ather[x] := y en O(1). (Fig. 3) 1 procedure MakeSet ( x ) 2 f a t h e r [ x ] := 0 ; 3 end 8 1 function Find ( x ) 2 i f f a t h e r [ x ] = −1 then 3 return 0 ; 4 end ; 5 while f a t h e r [ x ] 6= 0 do 6 x := f a t h e r [ x ] ; 7 end 8 return x ; 9 end 1 procedure Union ( x , y ) 2 f a t h e r [ x ] := y ; 3 end Hasta el momento, no se obtuvo ninguna mejora sobre la representación por listas en el orden de tiempo que lleva ejecutar una secuencia de m operaciones. Es posible crear una secuencia de aproximadamente m/3 operaciones MakeSet seguidas de aproximadamente m/3 operaciones Union de tal forma que se obtenga un único conjunto representado como un árbol lineal –una cadena de nodos–. Se puede encontrar luego una secuencia de aproximadamente m/3 operaciones Find cada una de las cuales lleve tiempo Θ(m/3). La secuencia completa se ejecutará en tiempo Θ(m2 ). 6.1 Heurı́sticas Utilizando dos simples heurı́sticas en las operaciones Union y Find se puede mejorar el orden de tiempo para ejecutar una secuencia σ de m operaciones. 6.1.1 Unión por Tamaño La heurı́stica de unión por tamaño no es más que una extensión de la heurı́stica de unión por tamaño para la representación por listas. La idea consiste en modificar la operación Union para que agregue el árbol con menos nodos como subárbol del árbol con más nodos. 6.1.2 Compresión de Caminos Cada vez que se ejecuta una instrucción Find(x) se recorre el camino desde x hasta la raı́z del árbol que lo contiene. Se puede mejorar el tiempo que llevarán futuras operaciones si durante la operación se convierte cada nodo en el camino de búsqueda en un hijo de la raı́z. (Fig. 5) 9 (a) (b) f e f e d c b a d c b a Figura 5: Efecto de la compresión de caminos sobre un árbol lineal. (a) Árbol original. (b) Árbol luego de ejecutar Find(a) utilizando compresión de caminos. 6.2 Implementación La cantidad de nodos en el subárbol de un nodo x se conserva en size[x]. Cuando se aplica Union sobre dos árboles, el árbol de menor tamaño se convierte en un subárbol de la raı́z del árbol con mayor tamaño. En caso de empate se realiza una elección arbitraria. 1 procedure MakeSet ( x ) 2 s i z e [ x ] := 0 ; 3 end 1 function Find ( x ) 2 r := x ; 3 while f a t h e r [ r ] 6= 0 do 4 r := f a t h e r [ r ] ; 5 end 6 /∗ r e s ahora l a r aı́ z d e l á r b o l ∗/ 7 8 p := x ; 9 while p 6= r do 10 t := f a t h e r [ p ] ; 11 f a t h e r [ p ] := r ; 12 p := t ; 13 end 14 return r ; 15 end 10 1 procedure Union ( x , y ) 2 i f s i z e [ x ] > s i z e [ y ] then 3 f a t h e r [ y ] := x ; 4 s i z e [ x ] := s i z e [ x ] + s i z e [ y ] ; 5 else 6 f a t h e r [ x ] := y ; 7 s i z e [ y ] := s i z e [ y ] + s i z e [ x ] ; 8 end 9 end 6.3 Impacto de las Heurı́sticas Utilizando solamente la heurı́stica de unión por tamaño, cada vez que un nodo se mueve a un nuevo árbol suceden dos cosas: • La distancia del nodo a la raı́z del árbol donde se encuentra se incrementa en uno. • El nuevo árbol tiene al menos dos veces más nodos que el anterior. Si en total existen n elementos en el universo, ningún nodo puede moverse más de log n veces y por lo tanto la distancia de cualquier nodo a la raı́z de su árbol no puede exceder log n y cada operación Find requiere tiempo O(log n). El impacto de la compresión de caminos es más difı́cil de analizar. Cuando se utilizan ambas heurı́sticas, el orden de complejidad temporal del peor caso es O(m α(m, n)), donde α(m, n) es una función que se define como una especie de inversa de la función de Ackermann y crece en forma extremadamente lenta. Para cualquier aplicación práctica concevible, resulta α(m, n) ≤ 4 y por lo tanto el orden de complejidad de la ejecución de una secuencia de m operaciones es prácticamente lineal. En lugar de probar que el orden de complejidad temporal del peor caso al ejecutar una secuencia σ de m operaciones es O(m α(m, n)) se puede probar más fácilmente una cota ligeramente menos ajustada, como se muestra a continuación. 6.4 Análisis de Complejidad Definición F y G Definimos las funciones F y G como sigue: 1 k=0 F (k) = 2F (k−1) k ≥ 1 11 (1) G(n) = min{k ∈ N | F (k) ≥ n} (2) La función F crece muy rápidamente y la función G muy lentamente: n ≤ 265536 =⇒ G(n) ≤ 5 n 0 1 2 F (n) 20 = 1 0 22 = 2 20 22 = 4 0 22 22 3 4 5 20 22 22 2 = 16 = 65536 20 22 22 = 265536 Tabla 4: Primeros valores de F . n 0 ≤ n 1 < n 2 < n 4 < n 16 < n 65536 < n ≤ ≤ ≤ ≤ ≤ ≤ 1 2 4 16 65536 265536 G(n) 0 1 2 3 4 5 Tabla 5: Primeros 265536 + 1 valores de G(n). Definición σ ′ σ ′ es la secuencia de operaciones obtenida al eliminar de σ todas las operaciones Find. Definición F F es el bosque resultante luego de ejecutar σ ′ . Definición Ti Ti es el subárbol con raı́z en el nodo i dentro de F . Definición Rango El rango de un vértice i en F , r(i), es la altura de Ti . Lema 6.1 Todo nodo i tiene al menos 2r(i) descendientes, o equivalentemente |Ti | ≥ 2r(i) . 12 Demostración Por inducción sobre r(i) • Caso Base: r(i) = 0 |Ti | tiene al menos un vértice, i. Por lo tanto |Ti | ≥ 1 = 20 = 2r(i) . • Caso Inductivo: (HI) ∀ i ∈ F | r(i) ≤ k · |Ti | ≥ 2r(i) Sea i un nodo con rango k. En algún momento tuvo rango menor que k y fue unido con otro árbol con raı́z en un vértice j de rango k − 1 y menos nodos: |Ti | = |Ti′ | + |Tj′| ≥ 2|Tj′| ≥ 2 2k−1 = 2k Corolario 6.2 Ningún vértice en F tiene rango mayor que ⌊log n⌋ . Lema 6.3 Existen a lo sumo n/2r vértices de rango r en F Demostración Cada vértice de rango r tiene al menos 2r descendientes. Los nodos de los subárboles de dos nodos en el bosque con el mismo rango son disjuntos. Hay n nodos en total, puede a lo sumo haber n/2r nodos de rango r. Lema 6.4 Si en algún momento durante la ejecución de σ ′ , w es un descendiente propio de v, entonces el rango de w es estrictamente menor que el rango de v. Demostración Si en algún momento un vértice w es descendiente propio de otro vértice v, lo será hasta que se termine de ejecutar la secuencia (no hay operaciones Find en σ ′ ) y en el bosque resultante F su altura será estrictamente menor. Teorema 6.5 Una secuencia σ de m operaciones de las cuales n ≥ 2 son operaciones MakeSet se ejecuta en tiempo O(m G(n)). Demostración Como antes, con q indicaremos el número de operaciones Union y con f el número de operaciones Find. Como cada operación Union y cada operación MakeSet se realiza en tiempo de orden constante y q < n, todas las operaciones MakeSet y Union se realizan en orden O(n). Para acotar el costo de todas las operaciones Find se utiliza una técnica de contabilidad. El costo de ejecutar una sola operación Find se reparte entre la operación misma y algunos vértices en el camino de búsqueda. El costo total se calcula sumando los costos sobre todas las operaciones Find y sobre todos los vértices. 13 Se dividen los rangos de los vértices en F en grupos. Los vértices de rango r se colocan en el grupo G(r). Los vértices con el mayor rango posible, ⌊log n⌋ . se colocan en el grupo G(⌊log n⌋) ≤ G(n). G(0) = {0, 1} G(1) = {2} G(2) = {3, 4} G(3) = {5, 6, . . . , 16} . . . Para los vértices v en el camino de búsqueda de una operación Find(x) (sin contar el propio x), la polı́tica para repartir el costo es la siguiente: 1. Si v es una raı́z, o f ather[v] está en un grupo diferente de v, se le carga una unidad de costo a la operación. 2. Si v y su padre están en el mismo grupo, se le carga una unidad a v Por el Lema 6.4 los vértices en un camino de búsqueda desde un nodo hasta la raı́z del árbol crecen estrictamente en rango. Cómo a lo sumo hay G(n) grupos diferentes, ninguna operación Find puede tener costo mayor que G(n). Cada vez que se aplica la segunda regla en un vértice v, se le asigna un nuevo padre que debe tener un rango mayor por el Lema 6.4. Si v está en el grupo g > 0, solamente puede incrementarse su costo F (g) − F (g − 1) veces por la segunda regla antes de que adquiera un padre en un grupo de rango mayor a su propio rango (el costo de un vértice en el grupo 0 solo puede incrementarse una vez). Cuando pasa a tener un padre en un grupo de rango mayor, nunca más se incrementa su costo porque la segunda regla ya no es aplicable. Para obtener una cota superior de los costos de los vértices, se multiplica el máximo posible costo de un vértice en un grupo por el número de vértices en el grupo. Sea N(g) el número de vértices en el grupo g, por el Lema 6.3: F (g) N(g) ≤ X n/2r (3) r=F (g−1)+1 ≤ ≤ = = (n/2F (g−1)+1 ) (1 + 1/2 + 1/4 + · · ·) (n/2F (g−1)+1 ) 2 n/2F (g−1) n/F (g) (4) (5) (6) (7) El máximo costo para un vértice en el grupo g es menor o igual que F (g) − F (g − 1), luego el costo máximo de todos los vértices en el grupo g, C(g) resulta C(g) = (F (g) − F (g − 1)) N(g) 14 (8) ≤ (F (g) − F (g − 1)) = n− F (g − 1) F (g) ≤ n n F (g) (9) (10) (11) Por lo tanto, como no puede haber más de G(n) grupos, el máximo costo asignado a todos los vértices es n G(n). Como el máximo costo asignado a una operación Find es G(n), el máximo costo asignado a todas las operaciones Find es f G(n). Por último, todas las operaciones Find se ejecutan en tiempo O((n + f ) G(n)). Por todo lo visto, la secuencia σ se ejecuta en tiempo O((n + f ) G(n)) y como n + f ≤ m en tiempo O(m G(n)). Referencias [AHU74] Alfred V. Aho, J. E. Hopcroft, and Jeffrey D. Ullman. The Design and Analysis of Computer Algorithms. Addison-Wesley, 1974. [CSRL01] Thomas H. Cormen, Clifford Stein, Ronald L. Rivest, and Charles E. Leiserson. Introduction to Algorithms. McGraw-Hill Higher Education, 2001. [Sed92] Robert Sedgewick. Algorithms in C++. Addison-Wesley, 1992. ISBN: 0-20151059-6. 15