Estructuras de Datos. Estructuras de Datos para Conjuntos

Anuncio
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
Descargar