¿Qué es una explicación intuitiva de union-find?

Si está buscando una analogía de la vida real, le diría que imagine gotas de agua en una ventana cuando llueve. (Estoy seguro de que lo has visto antes). Cada vez que dos gotas se acercan, colapsan en una sola gota de agua. Esa es la unión de union-find.

De todos modos, esto es lo que realmente es union-find. Tienes un montón de nodos y todos están agrupados, así:


Ahora, puede preguntar si dos nodos están en el mismo grupo, por lo que podría preguntar, “¿están los nodos amarillo y negro en el mismo grupo?” y vuelve “¡apuesto!”

Ahora, también se nos permite realizar una fusión o unión en dos componentes, como este:


Tenga en cuenta que la operación sindical es irreversible; la estructura de datos union-find no permite dividir un componente nuevamente.

Entonces, ahora que he descrito lo que hace, ¿cómo implementamos realmente el algoritmo de búsqueda de unión? Básicamente, en cada componente tenemos un árbol como este:

Cada nodo almacena un puntero a otro nodo. Sin embargo, cada componente tiene exactamente un nodo sin puntero: esta es la raíz de ese componente. Aquí, las raíces son los nodos rojo, azul y rosa.

Para hacer una fusión, solo agregamos una flecha más, como esta:

Ahora el nodo azul es la raíz del componente más grande; El nodo rojo ya no es una raíz.

¡Bueno! Ahora, como dijimos anteriormente, el objetivo de esta estructura de datos es saber si dos nodos están en el mismo componente. Para hacer esto, presentamos la operación de búsqueda , que toma un nodo y solo sigue las flechas y devuelve la raíz de su componente. Entonces, si ejecutamos find (amarillo), trazará el camino amarillo -> rojo -> azul y regresará azul. Si ejecutamos find (verde), solo trazará verde -> azul y devolverá azul. ¡Entonces sabemos que el amarillo y el verde están en el mismo componente!

Sin embargo, para muchos nodos, esto es ineficiente. ¡Los caminos pueden alargarse mucho! Entonces, ¿cómo soluciona union-find esto? Hace dos optimizaciones:

  • Compresión de ruta: cuando ejecutamos find en un nodo, rastreamos una ruta de nodos que sabemos que están en este componente. También podríamos mover todos sus punteros para apuntar al nodo raíz mientras estamos en él; luego ejecutar find en esos nodos en el futuro será súper rápido. (Hasta que ocurran más fusiones, de todos modos).
  • Fusionar por rango : cuando fusionamos dos árboles, es mejor hacer que el árbol más corto sea hijo del árbol más grande. Para esto, tenemos que hacer un seguimiento de las profundidades de los árboles, pero eso no es gran cosa.

Implemente estos dos, y union-find se ejecutará en [math] O (\ alpha (n)) [/ math] amortizado por operación. Aquí, [math] \ alpha [/ math] es el inverso de la función de Ackermann, que es de crecimiento extremadamente lento.

Terminaré con una aplicación, ya que de lo contrario podría pensar que el único punto de unión es cuando necesita modelar puntos bonitos en burbujas como en las imágenes de arriba.

El algoritmo de Kruskal es un algoritmo para encontrar el árbol de mínima expansión de un gráfico. Empiezas separando todos los vértices. Luego, recorre los bordes de menor a mayor peso. Usted elige el borde para agregar a su árbol de expansión a menos que esos vértices estén conectados por una ruta de bordes que ya ha elegido.

Para implementar esto, necesita una forma de determinar si dos nodos están en el mismo componente conectado; ¡esto es exactamente lo que hace la estructura de datos union-find! Cada vez que elige una nueva ventaja para su árbol de expansión, une los dos componentes que conecta. (Tenga en cuenta, sin embargo, que el árbol de expansión que construye y los árboles que usa en la estructura de datos de búsqueda de unión no tendrán conexión, aparte de dividir los vértices en componentes conectados de la misma manera).

Un algoritmo de búsqueda de unión realiza dos operaciones en una estructura de datos de conjunto disjunto: unión y búsqueda (sorpresa sorpresa). Una estructura de datos de conjunto disjunto se usa cuando tiene un conjunto de elementos que se pueden dividir en subconjuntos disjuntos, que comúnmente se representa como un bosque donde un árbol corresponde a un subconjunto dado.

La operación de unión combina dos subconjuntos de elementos en uno, y la operación de búsqueda determina qué subconjunto contiene el elemento dado y devuelve el elemento canónico de ese conjunto.

Intuitivamente, vemos que para la operación de unión es más eficiente fusionar un árbol más pequeño en uno más grande que al revés, y para la operación de búsqueda, podemos usar una técnica conocida como compresión de ruta que actualiza el puntero principal directamente al raíz al rastrear desde un nodo hasta su raíz: esto hace que el árbol sea más plano en lugar de más profundo.

En ambas operaciones, la intuición principal para mejorar la eficiencia del tiempo es terminar con un árbol que es más “espeso” que “puntiagudo”.

Escribí una publicación de blog explicando cómo funciona union-find. La publicación también contiene una implementación C / C ++ simple y eficiente de ambas operaciones. Mantener conjuntos disjuntos bajo unión