Fundamentos

Prim vs Kruskal vs Borůvka

Prim, Kruskal y Borůvka encuentran todos un árbol de expansión mínima, pero lo construyen en órdenes muy distintos. Esta guía ejecuta los tres sobre un mismo grafo de ejemplo, muestra por qué cada uno es correcto, compara su velocidad con recuentos de pasos medidos en grafos dispersos y densos, y trata los empates, los grafos no conexos, el agrupamiento y la diferencia con el algoritmo de Dijkstra, con código Python probado en todo momento.

21 Min de lectura Actualizado: Noviembre 2026 Nivel Principiante
Mohammed Islam Hadjoudj
Mohammed Islam Hadjoudj
Expert Operations Research Engineer

1. La respuesta corta

El algoritmo de Prim, el algoritmo de Kruskal y el algoritmo de Borůvka encuentran todos un árbol de expansión mínima: el conjunto de aristas más barato que conecta todos los vértices de un grafo ponderado. Lo construyen en órdenes distintos, y eso decide cuál conviene para cada grafo. La guía sobre árboles de expansión mínima presenta el problema en sí.

PrimKruskalBorůvka
EstrategiaHacer crecer un solo árbol desde un vértice inicialTomar las aristas de la más barata hacia arriba, saltándose las que cierran un cicloCada grupo elige su arista saliente más barata, todos a la vez
Estructura de datos principalCola de prioridad o un arreglo simpleLista de aristas ordenada y union-findUnion-find o reetiquetado
TiempoO(E log V) con un montículo binario, O(V2) con un arregloO(E log E), casi lineal si las aristas ya están ordenadasO(E log V)
Entrada naturalLista de adyacencia o matrizLista de aristasLista de aristas
Grafo no conexoCubre solo la componente del vértice inicialDa un bosque de expansiónDa un bosque de expansión
Ideal paraGrafos densos, con la versión de arregloGrafos dispersos, aristas preordenadas, agrupamientoComputación paralela y distribuida

Aquí V es el número de vértices y E el número de aristas. En una frase: usa Kruskal cuando el grafo es disperso o viene dado como lista de aristas, usa Prim cuando es denso o viene dado como matriz, y considera Borůvka cuando el trabajo deba repartirse entre muchos procesadores. En un grafo disperso, Kruskal y Prim con montículo cuestan más o menos lo mismo, así que lo correcto es elegir el código más sencillo para tus datos.

Un diagrama de decisión. Primera pregunta: ¿el grafo es conexo? Si no lo es, usa Kruskal o Borůvka, que producen un bosque de expansión. Si lo es: ¿las aristas ya están ordenadas o vienen dadas como lista? Si es así, Kruskal. Si no: ¿el grafo es denso? Si lo es, Prim con un arreglo; si no, Prim con un montículo o Kruskal. Una nota al margen indica que se use Borůvka cuando el trabajo deba ejecutarse en paralelo, y que los tres dan el mismo peso total.
La forma del grafo y el formato de la entrada eligen el algoritmo. Los tres devuelven un árbol del mismo peso total.

2. Un grafo, tres ejecuciones

Ocho pueblos, de A a H, quieren unirse mediante tuberías de agua. Hay 14 rutas posibles, cada una con un coste en kilómetros de zanja, y cada pueblo debe acabar conectado con todos los demás. Las 14 rutas juntas costarían 117.

A-B 3    A-C 6    B-C 4    B-D 13   C-D 7    C-E 12   D-E 2
D-F 11   E-F 8    E-G 14   E-H 5    F-G 1    F-H 15   G-H 16

8 pueblos, 14 tuberías posibles

Una conexión de coste mínimo usa exactamente 7 tuberías, una menos que el número de pueblos, porque cualquier tubería adicional cerraría un ciclo que se podría eliminar para ahorrar dinero. Este grafo tiene 712 árboles de expansión, y exactamente uno de ellos cuesta el mínimo de 30. El programa siguiente implementa los tres algoritmos y los ejecuta sobre él.

import heapq

PIPES = [('A', 'B', 3), ('A', 'C', 6), ('B', 'C', 4), ('B', 'D', 13), ('C', 'D', 7),
         ('C', 'E', 12), ('D', 'E', 2), ('D', 'F', 11), ('E', 'F', 8), ('E', 'G', 14),
         ('E', 'H', 5), ('F', 'G', 1), ('F', 'H', 15), ('G', 'H', 16)]

class Groups:
    """Union-find: ¿en qué grupo está cada vértice?"""
    def __init__(self, items):
        self.parent = {x: x for x in items}
    def find(self, x):
        while self.parent[x] != x:
            self.parent[x] = self.parent[self.parent[x]]   # reducción del camino a la mitad
            x = self.parent[x]
        return x
    def union(self, a, b):
        ra, rb = self.find(a), self.find(b)
        if ra == rb:
            return False
        self.parent[ra] = rb
        return True

def vertices(edges):
    return sorted({x for u, v, _ in edges for x in (u, v)})

def prim(edges, start):
    """Hace crecer un árbol: añade siempre la arista más barata que sale de él."""
    adj = {v: [] for v in vertices(edges)}
    for u, v, w in edges:
        adj[u].append((w, u, v))
        adj[v].append((w, v, u))
    seen, heap, tree = {start}, list(adj[start]), []
    heapq.heapify(heap)
    while heap:
        w, u, v = heapq.heappop(heap)
        if v in seen:
            continue                                  # los dos extremos ya están en el árbol
        seen.add(v)
        tree.append((u, v, w))
        for e in adj[v]:
            if e[2] not in seen:
                heapq.heappush(heap, e)
    return tree

def kruskal(edges):
    """Toma las aristas de la más barata hacia arriba, saltándose las que cerrarían un ciclo."""
    groups, tree = Groups(vertices(edges)), []
    for u, v, w in sorted(edges, key=lambda e: e[2]):
        if groups.union(u, v):
            tree.append((u, v, w))
    return tree

def boruvka(edges):
    """En cada ronda, cada grupo elige a la vez su arista saliente más barata."""
    groups, tree, rounds = Groups(vertices(edges)), [], []
    while True:
        cheapest = {}
        for u, v, w in edges:
            ru, rv = groups.find(u), groups.find(v)
            if ru != rv:
                for r in (ru, rv):                    # los empates se resuelven igual en todas partes
                    if r not in cheapest or (w, u, v) < cheapest[r]:
                        cheapest[r] = (w, u, v)
        if not cheapest:
            return tree, rounds
        added = []
        for w, u, v in sorted(set(cheapest.values())):
            if groups.union(u, v):
                added.append((u, v, w))
        tree += added
        rounds.append(added)

def show(tree):
    return ' '.join(f'{u}{v}:{w}' for u, v, w in tree) + f'   total {sum(w for *_, w in tree)}'

print('Prim    ', show(prim(PIPES, 'A')))
# Prim     AB:3 BC:4 CD:7 DE:2 EH:5 EF:8 FG:1   total 30
print('Kruskal ', show(kruskal(PIPES)))
# Kruskal  FG:1 DE:2 AB:3 BC:4 EH:5 CD:7 EF:8   total 30
tree, rounds = boruvka(PIPES)
print('Boruvka ', show(tree))
# Boruvka  FG:1 DE:2 AB:3 BC:4 EH:5 CD:7 EF:8   total 30
for i, added in enumerate(rounds, 1):
    print('round', i, show(added))
# round 1 FG:1 DE:2 AB:3 BC:4 EH:5   total 15
# round 2 CD:7 EF:8   total 15
Tres copias del grafo de los pueblos. Prim desde A añade sus aristas en el orden A-B, B-C, C-D, D-E, E-H, E-F, F-G, haciendo crecer un único árbol conexo. Kruskal añade F-G, D-E, A-B, B-C, E-H, rechaza A-C porque cerraría un ciclo, y luego añade C-D y E-F. Borůvka añade F-G, D-E, A-B, B-C y E-H a la vez en la ronda 1, formando tres grupos, y C-D y E-F en la ronda 2. Los tres terminan con el mismo árbol de coste 30.
Tres órdenes, un solo árbol. Las aristas de Prim forman siempre una única pieza conexa; las de Kruskal y Borůvka empiezan como fragmentos dispersos que se van uniendo.

Los tres órdenes cuentan la historia de cada método:

3. Por qué los tres son correctos

Por distintos que parezcan, los tres se apoyan en el mismo hecho sobre los cortes. Un corte divide los vértices en dos grupos, como la guía del corte mínimo explica. La propiedad del corte dice: para cualquier corte, la arista más barata que lo cruza pertenece a un árbol de expansión mínima, y cuando todos los pesos son distintos pertenece a todos ellos. La demostración es por intercambio. Toma un árbol de expansión que no use esa arista y añádela: esto cierra un ciclo, el ciclo debe cruzar el corte una segunda vez por alguna arista más cara, y al quitar esa arista más cara se obtiene un árbol más barato.

A la izquierda, la propiedad del corte: los pueblos A, B y C están sombreados y las aristas que salen de ellos son B-D con 13, C-D con 7 y C-E con 12. La más barata, C-D con 7, debe estar en el árbol. A la derecha, la propiedad del ciclo: el triángulo A, B, C tiene aristas de coste 3, 4 y 6. La más cara, A-C con 6, se puede dejar fuera del árbol.
La arista más barata que cruza un corte se puede tomar con seguridad. La arista más cara de un ciclo se puede descartar con seguridad. Todo algoritmo de árbol de expansión mínima aplica una de estas reglas o ambas.

Cada algoritmo aplica esta regla a un corte distinto:

La imagen especular es la propiedad del ciclo: la arista más cara de cualquier ciclo se puede dejar fuera de un árbol de expansión mínima. Ese es el paso de rechazo de Kruskal. Cuando A-C por 6 cerraría el ciclo A, B, C, es la arista más cara de ese ciclo, porque todas las demás se tomaron antes. Un árbol de expansión mínima nunca necesita esa arista.

4. Cuándo difieren los árboles

Si todos los pesos de las aristas son distintos, el árbol de expansión mínima es único, así que los tres algoritmos deben devolver exactamente las mismas aristas, como ocurre en el ejemplo. Con pesos iguales puede haber varios árboles de expansión mínima, todos con el mismo total, y cuál obtienes depende del algoritmo e incluso del orden de la entrada. Cambia A-C para que cueste 4, igual que B-C, y hay exactamente dos árboles de expansión mínima:

PIPES = [('A', 'B', 3), ('A', 'C', 6), ('B', 'C', 4), ('B', 'D', 13), ('C', 'D', 7),
         ('C', 'E', 12), ('D', 'E', 2), ('D', 'F', 11), ('E', 'F', 8), ('E', 'G', 14),
         ('E', 'H', 5), ('F', 'G', 1), ('F', 'H', 15), ('G', 'H', 16)]

def kruskal(edges):
    parent = {x: x for u, v, _ in edges for x in (u, v)}
    def find(x):
        while parent[x] != x:
            x = parent[x]
        return x
    tree = []
    for u, v, w in sorted(edges, key=lambda e: e[2]):  # estable: los empates conservan su orden de entrada
        if find(u) != find(v):
            parent[find(u)] = find(v)
            tree.append(u + v)
    return sorted(tree), sum(w for u, v, w in edges if u + v in tree)

tied = [(u, v, 4 if (u, v) == ('A', 'C') else w) for u, v, w in PIPES]   # A-C ahora cuesta 4, igual que B-C
print(kruskal(tied))
# (['AB', 'AC', 'CD', 'DE', 'EF', 'EH', 'FG'], 30)
print(kruskal(tied[::-1]))
# (['AB', 'BC', 'CD', 'DE', 'EF', 'EH', 'FG'], 30)

Ambos árboles cuestan 30 y difieren en una tubería. La ordenación de Python es estable, así que las aristas de igual peso conservan su orden de entrada, y al invertir la lista cambia cuál de las dos se toma primero. Si tus pruebas comparan las aristas exactas de un árbol, pueden fallar con un programa correcto; compara el peso total.

Los empates son también el error clásico del algoritmo de Borůvka. Cuando dos grupos eligen cada uno entre aristas de igual peso, unas elecciones incoherentes pueden crear un ciclo. El programa anterior lo evita comparando (w, u, v) en todas partes, de modo que todos los grupos desempatan de la misma forma.

5. Velocidad: cuál es más rápido y cuándo

VersiónTiempoEn qué se va el tiempo
Prim, montículo binarioO(E log V)Hasta una operación de montículo por arista
Prim, arreglo simpleO(V2)Recorrer todos los vértices restantes, una vez por cada vértice añadido
Prim, montículo de FibonacciO(E + V log V)Rápido en teoría, constantes altas en la práctica
KruskalO(E log E)La ordenación; el trabajo de union-find posterior es casi lineal
BorůvkaO(E log V)Como máximo log2 V rondas, y cada una recorre todas las aristas

Dado que E es a lo sumo V2, log E es a lo sumo 2 log V, así que O(E log E) y O(E log V) son la misma clase. Las fórmulas por sí solas no separan los métodos, así que el programa siguiente cuenta pasos básicos, es decir, comparaciones, inspecciones de aristas y saltos de union-find, en dos grafos aleatorios con 1000 vértices. Uno es disperso, con 3000 aristas, y otro denso, con 249.750 aristas, la mitad de todos los pares posibles.

import heapq, random
from functools import cmp_to_key

def random_graph(n, m, seed):
    """Un grafo conexo: un árbol de expansión aleatorio más aristas extra aleatorias, con todos los pesos distintos."""
    rnd = random.Random(seed)
    order = list(range(n))
    rnd.shuffle(order)
    pairs = {tuple(sorted((order[i], order[rnd.randrange(i)]))) for i in range(1, n)}
    while len(pairs) < m:
        u, v = rnd.sample(range(n), 2)
        pairs.add((min(u, v), max(u, v)))
    weights = rnd.sample(range(1, 10 * m), m)
    return [(u, v, w) for (u, v), w in zip(sorted(pairs), weights)]

class Item:
    """Una entrada del montículo que cuenta cuántas veces la compara el montículo."""
    compared = 0
    def __init__(self, w, v):
        self.w, self.v = w, v
    def __lt__(self, other):
        Item.compared += 1
        return self.w < other.w

def prim_heap(n, edges):
    adj = [[] for _ in range(n)]
    for u, v, w in edges:
        adj[u].append((w, v))
        adj[v].append((w, u))
    Item.compared = 0
    seen, steps = {0}, len(adj[0])
    heap = [Item(w, v) for w, v in adj[0]]
    heapq.heapify(heap)
    while heap:
        item = heapq.heappop(heap)
        if item.v in seen:
            continue
        seen.add(item.v)
        for w, v in adj[item.v]:
            steps += 1                                # una arista inspeccionada
            if v not in seen:
                heapq.heappush(heap, Item(w, v))
    return steps + Item.compared

def prim_array(n, edges):
    """Prim sin montículo: recorre el mejor coste conocido de cada vértice restante."""
    adj = [[] for _ in range(n)]
    for u, v, w in edges:
        adj[u].append((w, v))
        adj[v].append((w, u))
    best, done, steps = [float('inf')] * n, [False] * n, 0
    best[0] = 0
    for _ in range(n):
        left = [v for v in range(n) if not done[v]]
        u = min(left, key=best.__getitem__)
        steps += len(left)
        done[u] = True
        for w, v in adj[u]:
            steps += 1
            if not done[v] and w < best[v]:
                best[v] = w
    return steps

def kruskal(n, edges):
    compared = [0]
    def cmp(a, b):
        compared[0] += 1
        return a[2] - b[2]
    parent, steps = list(range(n)), 0
    def find(x):
        nonlocal steps
        while parent[x] != x:
            steps += 1
            parent[x] = parent[parent[x]]
            x = parent[x]
        steps += 1
        return x
    for u, v, w in sorted(edges, key=cmp_to_key(cmp)):
        ru, rv = find(u), find(v)
        if ru != rv:
            parent[ru] = rv
    return compared[0] + steps

def boruvka(n, edges):
    parent, steps = list(range(n)), 0
    def find(x):
        nonlocal steps
        while parent[x] != x:
            steps += 1
            parent[x] = parent[parent[x]]
            x = parent[x]
        steps += 1
        return x
    while True:
        cheapest = {}
        for u, v, w in edges:
            ru, rv = find(u), find(v)
            if ru != rv:
                for r in (ru, rv):
                    steps += 1
                    if r not in cheapest or w < cheapest[r][2]:
                        cheapest[r] = (u, v, w)
        if not cheapest:
            return steps
        for u, v, w in set(cheapest.values()):
            ru, rv = find(u), find(v)
            if ru != rv:
                parent[ru] = rv

n = 1000
for name, m in [('sparse', 3 * n), ('dense', n * (n - 1) // 4)]:
    edges = random_graph(n, m, seed=1)
    print(f'{name:6} E={m:6,}  Prim heap {prim_heap(n, edges):9,}  Prim array {prim_array(n, edges):9,}  '
          f'Kruskal {kruskal(n, edges):9,}  Boruvka {boruvka(n, edges):9,}')
# sparse E= 3,000  Prim heap    46,360  Prim array   506,500  Kruskal    44,414  Boruvka    73,486
# dense  E=249,750  Prim heap 5,231,906  Prim array 1,000,000  Kruskal 5,151,232  Boruvka 7,417,850
Un diagrama de barras en escala logarítmica de los pasos dados en grafos aleatorios con 1000 vértices. En el grafo disperso con 3000 aristas: Prim con montículo 46.360, Prim con arreglo 506.500, Kruskal 44.414, Borůvka 73.486. En el grafo denso con 249.750 aristas: Prim con montículo 5.231.906, Prim con arreglo 1.000.000, Kruskal 5.151.232, Borůvka 7.417.850.
En el grafo disperso la versión de arreglo de Prim es, con mucho, la más lenta. En el grafo denso es la más rápida, unas cinco veces por delante de las demás.

Los resultados se invierten con la densidad. En el grafo disperso, Kruskal y Prim con montículo están a menos de un 5 % uno de otro, Borůvka necesita alrededor de un 60 % más, y Prim con arreglo es unas 11 veces más lento, porque recorre todos los vértices restantes aunque cada uno tenga pocas aristas. En el grafo denso la versión de arreglo da exactamente 1.000.000 de pasos y gana por un factor de alrededor de cinco: nunca tiene más de V candidatos que examinar, mientras que el montículo y la ordenación pagan un coste logarítmico sobre casi 250.000 aristas.

Estas cifras cuentan pasos, no segundos. Ordenar un arreglo de números es muy rápido en hardware real, lo que suele favorecer a Kruskal, y la versión con montículo de aquí es la sencilla, que puede guardar varias entradas por vértice. Una versión que actualice las entradas en su sitio mantiene el montículo más pequeño a costa de más código. Si el tiempo importa, mide con tus propios datos.

6. La forma de tu entrada

A menudo la entrada decide antes que cualquier análisis de complejidad. La guía sobre la representación de grafos compara los formatos.

Merece la pena entender por sí misma la estructura union-find de Kruskal, y la guía sobre union-find la trata a fondo.

7. Grafos no conexos y parada anticipada

Los tres métodos también difieren en lo que hacen cuando el grafo no es de una sola pieza, y en lo que pueden decirte si paras antes de tiempo.

Grafos no conexos. Prim solo alcanza la componente conexa de su vértice inicial; para cubrir todo el grafo hay que reiniciarlo en cada componente. Kruskal y Borůvka no necesitan ningún cambio: simplemente se quedan sin aristas y devuelven un bosque de expansión mínima, un árbol por componente.

Agrupamiento. Los estados intermedios de Kruskal son útiles por sí mismos. Detente cuando queden k grupos, y esos son los grupos de enlace simple de los datos: la división en k grupos en la que el par de puntos más cercano de grupos distintos está lo más alejado posible.

PIPES = [('A', 'B', 3), ('A', 'C', 6), ('B', 'C', 4), ('B', 'D', 13), ('C', 'D', 7),
         ('C', 'E', 12), ('D', 'E', 2), ('D', 'F', 11), ('E', 'F', 8), ('E', 'G', 14),
         ('E', 'H', 5), ('F', 'G', 1), ('F', 'H', 15), ('G', 'H', 16)]

def kruskal_until(edges, groups_wanted):
    """Kruskal, detenido en cuanto solo quedan groups_wanted grupos."""
    parent = {x: x for u, v, _ in edges for x in (u, v)}
    def find(x):
        while parent[x] != x:
            parent[x] = parent[parent[x]]
            x = parent[x]
        return x
    count, tree = len(parent), []
    for u, v, w in sorted(edges, key=lambda e: e[2]):
        if count == groups_wanted:
            break
        if find(u) != find(v):
            parent[find(u)] = find(v)
            tree.append((u, v, w))
            count -= 1
    members = {}
    for x in sorted(parent):
        members.setdefault(find(x), []).append(x)
    return tree, sorted(''.join(m) for m in members.values())

print(kruskal_until(PIPES, 3)[1])              # tres grupos de pueblos
# ['ABC', 'DEH', 'FG']
print(kruskal_until(PIPES, 2)[1])
# ['ABCDEH', 'FG']

# Un grafo en dos piezas: elimina todas las tuberías entre los pueblos de A a E y F, G, H.
split = [e for e in PIPES if not (set(e[:2]) & set('FGH')) or set(e[:2]) <= set('FGH')]
tree, parts = kruskal_until(split, 1)          # pide 1 grupo, pero solo pueden existir 2
print(parts, sum(w for *_, w in tree))
# ['ABCDE', 'FGH'] 32

Detenido en tres grupos, el programa devuelve {A, B, C}, {D, E, H} y {F, G}, los mismos tres que produjo la primera ronda de Borůvka en este grafo, aunque esa coincidencia es casual en el ejemplo. En la última ejecución, sin las tuberías entre las dos mitades, Kruskal devuelve un bosque de dos árboles que cuesta 32. Prim desde A se detendría tras cinco pueblos y nunca vería F, G o H.

8. Pesos negativos, árboles máximos y grafos dirigidos

9. Prim no es Dijkstra

El algoritmo de Prim y el de Dijkstra son casi idénticos en código, y la diferencia es una sola línea: Prim ordena la cola por el peso de la única arista que llega a un vértice, mientras que Dijkstra la ordena por la distancia total desde el origen. Optimizan cosas distintas y construyen árboles distintos. La guía del algoritmo de Prim los compara línea por línea.

En el ejemplo, el árbol de caminos más cortos de Dijkstra desde A usa A-C por 6 en lugar de B-C por 4. Cuesta 32 en total, frente a los 30 del árbol de expansión mínima. A cambio, cada pueblo queda al menos igual de cerca de A a través de él: C está a 6 en lugar de 7, H a 20 en lugar de 21 y G a 24 en lugar de 25. Un árbol de expansión mínima minimiza el coste de construir la red y no dice nada sobre la longitud de ninguna ruta concreta.

10. Una historia enredada

Los tres nombres inducen a error sobre quién llegó primero. El algoritmo más antiguo es el de Borůvka. En 1926 el matemático checo Otakar Borůvka lo publicó después de que Jindřich Saxel, empleado de la compañía eléctrica de Moravia Occidental, le preguntara en el invierno de 1925 cómo electrificar el suroeste de Moravia de la forma más barata posible. Vojtěch Jarník describió después, en 1930, lo que hoy se llama algoritmo de Prim, en un artículo escrito como carta a Borůvka. Joseph Kruskal publicó su método en 1956, y Robert Prim redescubrió el algoritmo de Jarník en 1957 en los Laboratorios Bell, igual que Edsger Dijkstra en 1959. Por eso el algoritmo de Prim se llama a veces algoritmo de Jarník o algoritmo DJP, aunque "algoritmo de Prim" sigue siendo, con mucho, el nombre más común. El método de Borůvka también se redescubrió varias veces, y en algunos libros lleva el nombre de Sollin, que lo presentó en un seminario en París en 1961 y nunca lo publicó.

Los métodos más rápidos que se conocen se han alejado mucho de estos clásicos. David Karger, Philip Klein y Robert Tarjan dieron en 1995 un algoritmo aleatorizado con tiempo lineal esperado. Bernard Chazelle dio en 2000 un algoritmo determinista que se ejecuta en O(E α(E, V)), donde α es la función inversa de Ackermann, que vale como máximo 4 para cualquier grafo que se pudiera llegar a almacenar. En 2002 Seth Pettie y Vijaya Ramachandran encontraron un algoritmo demostrablemente óptimo, aunque nadie sabe cuál es realmente su tiempo de ejecución: está en algún punto entre lo lineal y la cota de Chazelle. El algoritmo de Karger, Klein y Tarjan usa las rondas de Borůvka como componente básico.

11. Dónde se usa cada uno

12. Errores comunes

ErrorPor qué está malSolución
Usar Prim en un grafo no conexo y fiarse del resultadoSolo abarca la componente del vértice inicialReinícialo en cada componente, o usa Kruskal
Suponer que Prim con montículo siempre es lo mejorEn el grafo denso la versión de arreglo necesitó cinco veces menos pasosUsa la versión de arreglo cuando E esté cerca de V2
Tratar un árbol de expansión mínima como un conjunto de caminos más cortosEn el ejemplo llega a C con un coste de 7, pero la ruta más corta cuesta 6Usa Dijkstra para rutas y un árbol de expansión para el coste de construcción
Comprobar un único árbol exacto cuando los pesos pueden empatarPuede haber varios árboles mínimos, y el orden de entrada elige unoCompara el peso total
Desempatar de forma incoherente en BorůvkaLos grupos pueden elegir aristas que formen un cicloCompara por peso y luego por un orden fijo de aristas
Evitar los algoritmos por los pesos negativosLos pesos negativos no afectan a ninguno de los tresEjecútalos sin cambios
Ejecutarlos en un grafo dirigidoLa propiedad del corte se refiere a aristas no dirigidasUsa el algoritmo de Chu-Liu/Edmonds
Dejar que Kruskal recorra todas las aristasEl árbol está completo tras V - 1 aristas aceptadasDetente en cuanto las tengas

13. Glosario

TérminoSignificado
Árbol de expansiónUn árbol que contiene todos los vértices del grafo
Árbol de expansión mínimaUn árbol de expansión de peso total mínimo
Bosque de expansiónUn árbol de expansión por cada componente conexa
Propiedad del corteLa arista más barata que cruza cualquier corte puede estar en un árbol de expansión mínima
Propiedad del cicloLa arista más cara de cualquier ciclo se puede dejar fuera
Union-findUna estructura que registra grupos y los fusiona rápidamente
Cola de prioridadUna estructura que siempre devuelve la clave más pequeña; normalmente un montículo binario
Grafo densoUn grafo en el que una gran parte de todos los pares de vértices están unidos por aristas
Grafo dispersoUn grafo con pocas aristas en comparación con V2, normalmente un pequeño múltiplo de V
Agrupamiento de enlace simpleAgrupar uniendo repetidamente los dos grupos más cercanos
Arista cuello de botellaLa arista más pesada de un árbol
ArborescenciaUn árbol dirigido en el que todas las aristas se alejan de una raíz

14. Preguntas frecuentes

¿Cuál es la diferencia entre el algoritmo de Prim y el de Kruskal?

El algoritmo de Prim hace crecer un único árbol conexo desde un vértice inicial, añadiendo siempre la arista más barata que sale de él. El algoritmo de Kruskal ordena todas las aristas y las toma de la más barata hacia arriba, saltándose las que cerrarían un ciclo, así que su árbol empieza como fragmentos dispersos que se van uniendo. Ambos encuentran un árbol de expansión mínima del mismo peso total.

¿Qué es mejor, Prim o Kruskal?

Depende del grafo. Kruskal es la elección natural para grafos dispersos y para aristas dadas como lista o ya ordenadas. Prim con un arreglo simple es la elección natural para grafos densos, porque se ejecuta en O(V al cuadrado) y no necesita ordenar las aristas. En grafos dispersos, Prim con un montículo binario y Kruskal cuestan más o menos lo mismo.

¿Prim y Kruskal dan el mismo árbol de expansión mínima?

Si todos los pesos de las aristas son distintos, el árbol de expansión mínima es único y ambos devuelven exactamente las mismas aristas. Si algunos pesos son iguales, puede haber varios árboles de expansión mínima y los dos algoritmos pueden devolver árboles distintos, pero siempre tienen el mismo peso total.

¿Qué es el algoritmo de Borůvka?

El algoritmo de Borůvka trabaja por rondas. En cada ronda, cada grupo de vértices conectados elige su arista más barata hacia otro grupo, y todas esas aristas se añaden a la vez. El número de grupos se reduce al menos a la mitad en cada ronda, así que hay como máximo log2 V rondas y el tiempo de ejecución es O(E log V). Como los grupos trabajan de forma independiente, se adapta bien a la computación paralela.

¿Pueden Prim y Kruskal manejar pesos negativos en las aristas?

Sí. A diferencia del algoritmo de Dijkstra, Prim, Kruskal y Borůvka funcionan correctamente con pesos negativos. Todo árbol de expansión tiene exactamente V - 1 aristas, así que sumar la misma constante a todos los pesos cambia por igual el coste de todos los árboles y no cambia cuál es el más barato.

¿Cuál es la diferencia entre el algoritmo de Prim y el algoritmo de Dijkstra?

Su código es casi idéntico, pero Prim ordena los vértices por el peso de la única arista que llega a ellos, mientras que Dijkstra los ordena por la distancia total desde el origen. Prim construye el árbol de menor peso total. Dijkstra construye un árbol de caminos más cortos desde un origen, que puede costar más en total pero da rutas más cortas desde ese origen.

15. Referencias

Los algoritmos, las cotas y las atribuciones anteriores proceden de estas fuentes, listadas en orden cronológico.

  1. Borůvka, O. (1926). "O jistém problému minimálním." Práce Moravské přírodovědecké společnosti 3, 37 a 58.
  2. Jarník, V. (1930). "O jistém problému minimálním (Z dopisu panu O. Borůvkovi)." Práce Moravské přírodovědecké společnosti 6, 57 a 63.
  3. Kruskal, J. B. (1956). "On the Shortest Spanning Subtree of a Graph and the Traveling Salesman Problem." Proceedings of the American Mathematical Society 7(1), 48 a 50.
  4. Prim, R. C. (1957). "Shortest Connection Networks and Some Generalizations." Bell System Technical Journal 36(6), 1389 a 1401.
  5. Dijkstra, E. W. (1959). "A Note on Two Problems in Connexion with Graphs." Numerische Mathematik 1, 269 a 271.
  6. Chu, Y.-J. y Liu, T.-H. (1965). "On the Shortest Arborescence of a Directed Graph." Scientia Sinica 14, 1396 a 1400.
  7. Edmonds, J. (1967). "Optimum Branchings." Journal of Research of the National Bureau of Standards 71B, 233 a 240.
  8. Gower, J. C. y Ross, G. J. S. (1969). "Minimum Spanning Trees and Single Linkage Cluster Analysis." Journal of the Royal Statistical Society, Series C 18(1), 54 a 64.
  9. Tarjan, R. E. (1975). "Efficiency of a Good But Not Linear Set Union Algorithm." Journal of the ACM 22(2), 215 a 225.
  10. Christofides, N. (1976). Worst-Case Analysis of a New Heuristic for the Travelling Salesman Problem. Informe 388. Pittsburgh: Carnegie Mellon University.
  11. Graham, R. L. y Hell, P. (1985). "On the History of the Minimum Spanning Tree Problem." Annals of the History of Computing 7(1), 43 a 57.
  12. Fredman, M. L. y Tarjan, R. E. (1987). "Fibonacci Heaps and Their Uses in Improved Network Optimization Algorithms." Journal of the ACM 34(3), 596 a 615.
  13. Karger, D. R., Klein, P. N. y Tarjan, R. E. (1995). "A Randomized Linear-Time Algorithm to Find Minimum Spanning Trees." Journal of the ACM 42(2), 321 a 328.
  14. Chazelle, B. (2000). "A Minimum Spanning Tree Algorithm with Inverse-Ackermann Type Complexity." Journal of the ACM 47(6), 1028 a 1047.
  15. Nešetřil, J., Milková, E. y Nešetřilová, H. (2001). "Otakar Borůvka on Minimum Spanning Tree Problem." Discrete Mathematics 233(1 a 3), 3 a 36.
  16. Pettie, S. y Ramachandran, V. (2002). "An Optimal Minimum Spanning Tree Algorithm." Journal of the ACM 49(1), 16 a 34.
  17. Felzenszwalb, P. F. y Huttenlocher, D. P. (2004). "Efficient Graph-Based Image Segmentation." International Journal of Computer Vision 59(2), 167 a 181.
  18. Kleinberg, J. y Tardos, É. (2005). Algorithm Design. Boston: Addison-Wesley.
  19. Bader, D. A. y Cong, G. (2006). "Fast Shared-Memory Algorithms for Computing the Minimum Spanning Forest of Sparse Graphs." Journal of Parallel and Distributed Computing 66(11), 1366 a 1378.
  20. Cormen, T. H., Leiserson, C. E., Rivest, R. L. y Stein, C. (2009). Introduction to Algorithms, 3.ª edición. Cambridge, MA: MIT Press.
  21. Osipov, V., Sanders, P. y Singler, J. (2009). "The Filter-Kruskal Minimum Spanning Tree Algorithm." En Proceedings of the 11th Workshop on Algorithm Engineering and Experiments, 52 a 61.

Observa cómo crece el árbol

Construye los ocho pueblos en el visualizador y ejecuta Prim desde A. Luego ejecuta Kruskal en el mismo grafo y compara el orden en que aparecen las aristas.

Abrir el visualizador

Ordena tú mismo las aristas

Ejecuta Kruskal paso a paso en tu propio grafo y observa cómo se acepta o se rechaza cada arista. Luego ejecuta Prim en el mismo grafo y ve aparecer el mismo total en un orden distinto.

Abrir el visualizador de Kruskal