Kruskal (árbol de expansión mínima)
Conecta todos los nodos de un grafo con el menor coste total: ordena las aristas por peso y coge cada una si no cierra un ciclo, algo que se comprueba con conjuntos disjuntos (union-find).
nivel avanzadoTambién: Kruskal, árbol de expansión mínima, minimum spanning tree, MST, union-find, conjuntos disjuntos, Prim
Visualízalo paso a paso
Cambia los datos, dale a reproducir y sigue cada paso en el dibujo, en la línea de Java que se ejecuta y en sus variables.
Kruskal
Escribe las aristas con su peso: Kruskal construye el árbol de expansión mínima cogiendo siempre la arista más barata que no cierre un ciclo.
- arista pendiente
Paso 1
Se ordenan las 11 aristas de menor a mayor peso. Cada nodo empieza en su propio conjunto: un bosque de 7 árboles de un solo nodo.
1record Arista(String de, String a, int peso) {}
2
3static List<Arista> kruskal(List<String> nodos, List<Arista> aristas) {
4 aristas.sort(Comparator.comparingInt(Arista::peso)); // en el árbol = 0 de 6, peso total = 0
5 Map<String, String> padre = new HashMap<>();
6 for (String n : nodos) padre.put(n, n);
7 List<Arista> arbol = new ArrayList<>();
8 for (Arista a : aristas) {
9 String ra = raiz(padre, a.de()), rb = raiz(padre, a.a());
10 if (ra.equals(rb)) continue;
11 padre.put(ra, rb);
12 arbol.add(a);
13 }
14 return arbol;
15}
16
17static String raiz(Map<String, String> padre, String x) {
18 while (!padre.get(x).equals(x)) x = padre.get(x);
19 return x;
20}Variables
- en el árbol
- 0 de 6
- peso total
- 0
Atajos con el foco dentro del visualizador: ← → paso a paso, Espacio reproducir o pausar, Inicio/Fin ir al principio o al final.
La idea
Un árbol de expansión de un grafo conexo es un conjunto de aristas que conecta todos los nodos sin formar ningún ciclo; con n nodos tiene siempre n − 1 aristas. El de expansión mínima (MST, *minimum spanning tree*) es el de menor peso total: la forma más barata de cablear unas oficinas, de unir pueblos con carreteras o de conectar los puntos de una red.
Kruskal es un algoritmo voraz que, en este problema, siempre acierta: ordena las aristas de menor a mayor peso y las recorre. Cada una entra en el árbol si une dos trozos distintos y se descarta si sus dos extremos ya estaban conectados, porque cerraría un ciclo. Al principio cada nodo es un trozo; al final queda uno solo.
La pregunta «¿ya están conectados?» se responde con conjuntos disjuntos (*union-find*): cada nodo apunta a un padre y el que se apunta a sí mismo es la raíz, el representante de su grupo. Dos nodos están conectados si tienen la misma raíz, y unir dos grupos es hacer que una raíz apunte a la otra. Con dos mejoras (comprimir los caminos al buscar la raíz y colgar el grupo pequeño del grande) cada operación es prácticamente O(1).
La alternativa clásica es Prim: en vez de juntar trozos sueltos, hace crecer un único árbol desde un nodo, añadiendo cada vez la arista más barata que sale de él (con una cola de prioridad, como Dijkstra). Los dos dan un árbol con el mismo peso mínimo; Kruskal suele ir mejor con grafos dispersos y Prim con densos.
Cuándo usarlo
- Diseñar redes con el mínimo de cable, tubería o carretera: telecomunicaciones, electricidad, agua.
- Agrupar datos (*clustering*): se calcula el árbol mínimo y se quitan sus aristas más largas; los trozos que quedan son los grupos.
- Aproximar problemas difíciles como el del viajante: el árbol mínimo da una cota y una ruta de partida.
- Union-find por sí solo: saber si dos elementos están en el mismo grupo mientras se van uniendo (redes, píxeles, cuentas duplicadas).
Cuándo no
- Para el camino más corto entre dos nodos: el árbol mínimo no lo da (eso es Dijkstra).
- En grafos dirigidos: el equivalente (arborescencia mínima) necesita otro algoritmo, el de Chu-Liu/Edmonds.
- Si el grafo no es conexo no hay árbol, sino un bosque de expansión mínima: Kruskal lo calcula igual, pero hay que tenerlo en cuenta.
Paso a paso
- Ordenar. Ordena las aristas de menor a mayor peso.
- Cada nodo, un grupo. Inicializa los conjuntos disjuntos: cada nodo es la raíz de su propio grupo.
- Probar cada arista. Si las raíces de sus extremos son distintas, la arista entra en el árbol y se unen los dos grupos; si son la misma, se descarta porque cerraría un ciclo.
- Parar. Cuando el árbol tiene n − 1 aristas ya está completo: el resto solo puede formar ciclos.
El código
Kruskal con union-find completo
Seis sedes y once posibles cables con su coste. Los conjuntos disjuntos usan compresión de caminos y unión por tamaño; en cuanto hay n − 1 cables, se para.
1import java.util.*;
2
3public class Main {
4 record Cable(String a, String b, int coste) { }
5
6 /** Conjuntos disjuntos con las dos mejoras: compresión de caminos y unión por tamaño. */
7 static class Conjuntos {
8 private final Map<String, String> padre = new HashMap<>();
9 private final Map<String, Integer> tam = new HashMap<>();
10
11 void crear(String x) {
12 padre.put(x, x);
13 tam.put(x, 1);
14 }
15
16 String raiz(String x) {
17 String r = x;
18 while (!padre.get(r).equals(r)) r = padre.get(r);
19 while (!padre.get(x).equals(r)) { // compresión: el camino apunta ya a la raíz
20 String sig = padre.get(x);
21 padre.put(x, r);
22 x = sig;
23 }
24 return r;
25 }
26
27 boolean unir(String x, String y) {
28 String rx = raiz(x), ry = raiz(y);
29 if (rx.equals(ry)) return false; // ya estaban juntos: cerraría un ciclo
30 if (tam.get(rx) < tam.get(ry)) {
31 String t = rx;
32 rx = ry;
33 ry = t;
34 }
35 padre.put(ry, rx); // el grupo pequeño cuelga del grande
36 tam.put(rx, tam.get(rx) + tam.get(ry));
37 return true;
38 }
39 }
40
41 public static void main(String[] args) {
42 List<String> sedes = List.of("Central", "Norte", "Sur", "Este", "Oeste", "Puerto");
43 List<Cable> cables = new ArrayList<>(List.of(
44 new Cable("Central", "Norte", 12), new Cable("Central", "Sur", 9), new Cable("Central", "Este", 15),
45 new Cable("Central", "Oeste", 10), new Cable("Norte", "Este", 7), new Cable("Norte", "Oeste", 18),
46 new Cable("Sur", "Oeste", 11), new Cable("Sur", "Puerto", 6), new Cable("Este", "Puerto", 20),
47 new Cable("Oeste", "Puerto", 13), new Cable("Norte", "Sur", 16)));
48 cables.sort(Comparator.comparingInt(Cable::coste));
49
50 Conjuntos grupos = new Conjuntos();
51 for (String s : sedes) grupos.crear(s);
52 System.out.println("Cables de más barato a más caro:");
53 int total = 0, puestos = 0;
54 for (Cable c : cables) {
55 if (grupos.unir(c.a(), c.b())) {
56 total += c.coste();
57 puestos++;
58 System.out.println(" sí " + c.a() + " - " + c.b() + " (" + c.coste() + ")");
59 if (puestos == sedes.size() - 1) break; // n − 1 cables: ya está todo conectado
60 } else {
61 System.out.println(" no " + c.a() + " - " + c.b() + " (" + c.coste() + "): cerraría un ciclo");
62 }
63 }
64 System.out.println("Coste total: " + total + " con " + puestos + " cables para " + sedes.size() + " sedes");
65 }
66}class Conjuntos:
"""Conjuntos disjuntos con las dos mejoras: compresión de caminos y unión por tamaño."""
def __init__(self, elementos):
self.padre = {x: x for x in elementos}
self.tam = {x: 1 for x in elementos}
def raiz(self, x):
r = x
while self.padre[r] != r:
r = self.padre[r]
while self.padre[x] != r: # compresión: el camino apunta ya a la raíz
sig = self.padre[x]
self.padre[x] = r
x = sig
return r
def unir(self, x, y):
rx, ry = self.raiz(x), self.raiz(y)
if rx == ry:
return False # ya estaban juntos: cerraría un ciclo
if self.tam[rx] < self.tam[ry]:
rx, ry = ry, rx
self.padre[ry] = rx # el grupo pequeño cuelga del grande
self.tam[rx] += self.tam[ry]
return True
sedes = ["Central", "Norte", "Sur", "Este", "Oeste", "Puerto"]
cables = [
("Central", "Norte", 12), ("Central", "Sur", 9), ("Central", "Este", 15),
("Central", "Oeste", 10), ("Norte", "Este", 7), ("Norte", "Oeste", 18),
("Sur", "Oeste", 11), ("Sur", "Puerto", 6), ("Este", "Puerto", 20),
("Oeste", "Puerto", 13), ("Norte", "Sur", 16),
]
cables.sort(key=lambda c: c[2])
grupos = Conjuntos(sedes)
print("Cables de más barato a más caro:")
total = puestos = 0
for a, b, coste in cables:
if grupos.unir(a, b):
total += coste
puestos += 1
print(f" sí {a} - {b} ({coste})")
if puestos == len(sedes) - 1:
break # n − 1 cables: ya está todo conectado
else:
print(f" no {a} - {b} ({coste}): cerraría un ciclo")
print(f"Coste total: {total} con {puestos} cables para {len(sedes)} sedes")/** Conjuntos disjuntos con las dos mejoras: compresión de caminos y unión por tamaño. */
class Conjuntos {
constructor(elementos) {
this.padre = new Map(elementos.map((x) => [x, x]));
this.tam = new Map(elementos.map((x) => [x, 1]));
}
raiz(x) {
let r = x;
while (this.padre.get(r) !== r) r = this.padre.get(r);
while (this.padre.get(x) !== r) { // compresión: el camino apunta ya a la raíz
const sig = this.padre.get(x);
this.padre.set(x, r);
x = sig;
}
return r;
}
unir(x, y) {
let rx = this.raiz(x), ry = this.raiz(y);
if (rx === ry) return false; // ya estaban juntos: cerraría un ciclo
if (this.tam.get(rx) < this.tam.get(ry)) [rx, ry] = [ry, rx];
this.padre.set(ry, rx); // el grupo pequeño cuelga del grande
this.tam.set(rx, this.tam.get(rx) + this.tam.get(ry));
return true;
}
}
const sedes = ["Central", "Norte", "Sur", "Este", "Oeste", "Puerto"];
const cables = [
["Central", "Norte", 12], ["Central", "Sur", 9], ["Central", "Este", 15],
["Central", "Oeste", 10], ["Norte", "Este", 7], ["Norte", "Oeste", 18],
["Sur", "Oeste", 11], ["Sur", "Puerto", 6], ["Este", "Puerto", 20],
["Oeste", "Puerto", 13], ["Norte", "Sur", 16],
];
cables.sort((x, y) => x[2] - y[2]);
const grupos = new Conjuntos(sedes);
console.log("Cables de más barato a más caro:");
let total = 0, puestos = 0;
for (const [a, b, coste] of cables) {
if (grupos.unir(a, b)) {
total += coste;
puestos++;
console.log(` sí ${a} - ${b} (${coste})`);
if (puestos === sedes.length - 1) break; // n − 1 cables: ya está todo conectado
} else {
console.log(` no ${a} - ${b} (${coste}): cerraría un ciclo`);
}
}
console.log(`Coste total: ${total} con ${puestos} cables para ${sedes.length} sedes`);using System;
using System.Collections.Generic;
using System.Linq;
record Cable(string A, string B, int Coste);
/// <summary>Conjuntos disjuntos con las dos mejoras: compresión de caminos y unión por tamaño.</summary>
class Conjuntos {
private readonly Dictionary<string, string> padre = new();
private readonly Dictionary<string, int> tam = new();
public Conjuntos(IEnumerable<string> elementos) {
foreach (string x in elementos) {
padre[x] = x;
tam[x] = 1;
}
}
public string Raiz(string x) {
string r = x;
while (padre[r] != r) r = padre[r];
while (padre[x] != r) { // compresión: el camino apunta ya a la raíz
string sig = padre[x];
padre[x] = r;
x = sig;
}
return r;
}
public bool Unir(string x, string y) {
string rx = Raiz(x), ry = Raiz(y);
if (rx == ry) return false; // ya estaban juntos: cerraría un ciclo
if (tam[rx] < tam[ry]) (rx, ry) = (ry, rx);
padre[ry] = rx; // el grupo pequeño cuelga del grande
tam[rx] += tam[ry];
return true;
}
}
class Program {
static void Main() {
string[] sedes = { "Central", "Norte", "Sur", "Este", "Oeste", "Puerto" };
var cables = new List<Cable> {
new("Central", "Norte", 12), new("Central", "Sur", 9), new("Central", "Este", 15),
new("Central", "Oeste", 10), new("Norte", "Este", 7), new("Norte", "Oeste", 18),
new("Sur", "Oeste", 11), new("Sur", "Puerto", 6), new("Este", "Puerto", 20),
new("Oeste", "Puerto", 13), new("Norte", "Sur", 16),
}.OrderBy(c => c.Coste).ToList();
var grupos = new Conjuntos(sedes);
Console.WriteLine("Cables de más barato a más caro:");
int total = 0, puestos = 0;
foreach (var c in cables) {
if (grupos.Unir(c.A, c.B)) {
total += c.Coste;
puestos++;
Console.WriteLine(quot; sí {c.A} - {c.B} ({c.Coste})");
if (puestos == sedes.Length - 1) break; // n − 1 cables: ya está todo conectado
} else {
Console.WriteLine(quot; no {c.A} - {c.B} ({c.Coste}): cerraría un ciclo");
}
}
Console.WriteLine(quot;Coste total: {total} con {puestos} cables para {sedes.Length} sedes");
}
}<?php
/** Conjuntos disjuntos con las dos mejoras: compresión de caminos y unión por tamaño. */
class Conjuntos {
private array $padre = [];
private array $tam = [];
public function __construct(array $elementos) {
foreach ($elementos as $x) {
$this->padre[$x] = $x;
$this->tam[$x] = 1;
}
}
public function raiz(string $x): string {
$r = $x;
while ($this->padre[$r] !== $r) $r = $this->padre[$r];
while ($this->padre[$x] !== $r) { // compresión: el camino apunta ya a la raíz
$sig = $this->padre[$x];
$this->padre[$x] = $r;
$x = $sig;
}
return $r;
}
public function unir(string $x, string $y): bool {
$rx = $this->raiz($x);
$ry = $this->raiz($y);
if ($rx === $ry) return false; // ya estaban juntos: cerraría un ciclo
if ($this->tam[$rx] < $this->tam[$ry]) [$rx, $ry] = [$ry, $rx];
$this->padre[$ry] = $rx; // el grupo pequeño cuelga del grande
$this->tam[$rx] += $this->tam[$ry];
return true;
}
}
$sedes = ["Central", "Norte", "Sur", "Este", "Oeste", "Puerto"];
$cables = [
["Central", "Norte", 12], ["Central", "Sur", 9], ["Central", "Este", 15],
["Central", "Oeste", 10], ["Norte", "Este", 7], ["Norte", "Oeste", 18],
["Sur", "Oeste", 11], ["Sur", "Puerto", 6], ["Este", "Puerto", 20],
["Oeste", "Puerto", 13], ["Norte", "Sur", 16],
];
usort($cables, fn($x, $y) => $x[2] <=> $y[2]);
$grupos = new Conjuntos($sedes);
echo "Cables de más barato a más caro:\n";
$total = 0;
$puestos = 0;
foreach ($cables as [$a, $b, $coste]) {
if ($grupos->unir($a, $b)) {
$total += $coste;
$puestos++;
echo " sí $a - $b ($coste)\n";
if ($puestos === count($sedes) - 1) break; // n − 1 cables: ya está todo conectado
} else {
echo " no $a - $b ($coste): cerraría un ciclo\n";
}
}
echo "Coste total: $total con $puestos cables para " . count($sedes) . " sedes\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Cables de más barato a más caro: sí Sur - Puerto (6) sí Norte - Este (7) sí Central - Sur (9) sí Central - Oeste (10) no Sur - Oeste (11): cerraría un ciclo sí Central - Norte (12) Coste total: 44 con 5 cables para 6 sedes
Prim: el árbol crece desde un nodo
Las mismas sedes: Prim parte de Central y añade siempre el cable más barato que sale del árbol. Elige los mismos cables, en otro orden, con el mismo total.
1import java.util.*;
2
3public class Main {
4 record Cable(String a, String b, int coste) { }
5
6 public static void main(String[] args) {
7 List<Cable> cables = List.of(
8 new Cable("Central", "Norte", 12), new Cable("Central", "Sur", 9), new Cable("Central", "Este", 15),
9 new Cable("Central", "Oeste", 10), new Cable("Norte", "Este", 7), new Cable("Norte", "Oeste", 18),
10 new Cable("Sur", "Oeste", 11), new Cable("Sur", "Puerto", 6), new Cable("Este", "Puerto", 20),
11 new Cable("Oeste", "Puerto", 13), new Cable("Norte", "Sur", 16));
12 Map<String, List<Cable>> red = new LinkedHashMap<>();
13 for (Cable c : cables) { // cada cable, en los dos sentidos
14 red.computeIfAbsent(c.a(), k -> new ArrayList<>()).add(c);
15 red.computeIfAbsent(c.b(), k -> new ArrayList<>()).add(new Cable(c.b(), c.a(), c.coste()));
16 }
17 Set<String> dentro = new HashSet<>(List.of("Central"));
18 PriorityQueue<Cable> borde = new PriorityQueue<>(Comparator.comparingInt(Cable::coste));
19 borde.addAll(red.get("Central"));
20 int total = 0;
21 System.out.println("Prim desde Central:");
22 while (!borde.isEmpty() && dentro.size() < red.size()) {
23 Cable c = borde.poll(); // el cable más barato que sale del árbol
24 if (dentro.contains(c.b())) continue; // los dos extremos ya dentro: ciclo
25 dentro.add(c.b());
26 total += c.coste();
27 System.out.println(" + " + c.a() + " - " + c.b() + " (" + c.coste() + ")");
28 for (Cable s : red.get(c.b()))
29 if (!dentro.contains(s.b())) borde.add(s);
30 }
31 System.out.println("Coste total: " + total + ", el mismo que con Kruskal");
32 }
33}import heapq
cables = [
("Central", "Norte", 12), ("Central", "Sur", 9), ("Central", "Este", 15),
("Central", "Oeste", 10), ("Norte", "Este", 7), ("Norte", "Oeste", 18),
("Sur", "Oeste", 11), ("Sur", "Puerto", 6), ("Este", "Puerto", 20),
("Oeste", "Puerto", 13), ("Norte", "Sur", 16),
]
red = {}
for a, b, coste in cables: # cada cable, en los dos sentidos
red.setdefault(a, []).append((coste, a, b))
red.setdefault(b, []).append((coste, b, a))
dentro = {"Central"}
borde = list(red["Central"])
heapq.heapify(borde)
total = 0
print("Prim desde Central:")
while borde and len(dentro) < len(red):
coste, a, b = heapq.heappop(borde) # el cable más barato que sale del árbol
if b in dentro:
continue # los dos extremos ya dentro: ciclo
dentro.add(b)
total += coste
print(f" + {a} - {b} ({coste})")
for s in red[b]:
if s[2] not in dentro:
heapq.heappush(borde, s)
print(f"Coste total: {total}, el mismo que con Kruskal")const cables = [
["Central", "Norte", 12], ["Central", "Sur", 9], ["Central", "Este", 15],
["Central", "Oeste", 10], ["Norte", "Este", 7], ["Norte", "Oeste", 18],
["Sur", "Oeste", 11], ["Sur", "Puerto", 6], ["Este", "Puerto", 20],
["Oeste", "Puerto", 13], ["Norte", "Sur", 16],
];
const red = new Map();
for (const [a, b, coste] of cables) { // cada cable, en los dos sentidos
if (!red.has(a)) red.set(a, []);
if (!red.has(b)) red.set(b, []);
red.get(a).push([a, b, coste]);
red.get(b).push([b, a, coste]);
}
const dentro = new Set(["Central"]);
const borde = [...red.get("Central")]; // un array que se ordena hace de cola de prioridad
let total = 0;
console.log("Prim desde Central:");
while (borde.length > 0 && dentro.size < red.size) {
borde.sort((x, y) => x[2] - y[2]);
const [a, b, coste] = borde.shift(); // el cable más barato que sale del árbol
if (dentro.has(b)) continue; // los dos extremos ya dentro: ciclo
dentro.add(b);
total += coste;
console.log(` + ${a} - ${b} (${coste})`);
for (const s of red.get(b)) if (!dentro.has(s[1])) borde.push(s);
}
console.log(`Coste total: ${total}, el mismo que con Kruskal`);using System;
using System.Collections.Generic;
record Cable(string A, string B, int Coste);
class Program {
static void Main() {
var cables = new List<Cable> {
new("Central", "Norte", 12), new("Central", "Sur", 9), new("Central", "Este", 15),
new("Central", "Oeste", 10), new("Norte", "Este", 7), new("Norte", "Oeste", 18),
new("Sur", "Oeste", 11), new("Sur", "Puerto", 6), new("Este", "Puerto", 20),
new("Oeste", "Puerto", 13), new("Norte", "Sur", 16),
};
var red = new Dictionary<string, List<Cable>>();
foreach (var c in cables) { // cada cable, en los dos sentidos
if (!red.ContainsKey(c.A)) red[c.A] = new List<Cable>();
if (!red.ContainsKey(c.B)) red[c.B] = new List<Cable>();
red[c.A].Add(c);
red[c.B].Add(new Cable(c.B, c.A, c.Coste));
}
var dentro = new HashSet<string> { "Central" };
var borde = new PriorityQueue<Cable, int>();
foreach (var c in red["Central"]) borde.Enqueue(c, c.Coste);
int total = 0;
Console.WriteLine("Prim desde Central:");
while (borde.Count > 0 && dentro.Count < red.Count) {
var c = borde.Dequeue(); // el cable más barato que sale del árbol
if (dentro.Contains(c.B)) continue; // los dos extremos ya dentro: ciclo
dentro.Add(c.B);
total += c.Coste;
Console.WriteLine(quot; + {c.A} - {c.B} ({c.Coste})");
foreach (var s in red[c.B])
if (!dentro.Contains(s.B)) borde.Enqueue(s, s.Coste);
}
Console.WriteLine(quot;Coste total: {total}, el mismo que con Kruskal");
}
}<?php
$cables = [
["Central", "Norte", 12], ["Central", "Sur", 9], ["Central", "Este", 15],
["Central", "Oeste", 10], ["Norte", "Este", 7], ["Norte", "Oeste", 18],
["Sur", "Oeste", 11], ["Sur", "Puerto", 6], ["Este", "Puerto", 20],
["Oeste", "Puerto", 13], ["Norte", "Sur", 16],
];
$red = [];
foreach ($cables as [$a, $b, $coste]) { // cada cable, en los dos sentidos
$red[$a][] = [$a, $b, $coste];
$red[$b][] = [$b, $a, $coste];
}
$dentro = ["Central" => true];
$borde = new SplPriorityQueue(); // saca la MAYOR prioridad: se usa -coste
foreach ($red["Central"] as $c) $borde->insert($c, -$c[2]);
$total = 0;
echo "Prim desde Central:\n";
while (!$borde->isEmpty() && count($dentro) < count($red)) {
[$a, $b, $coste] = $borde->extract(); // el cable más barato que sale del árbol
if (isset($dentro[$b])) continue; // los dos extremos ya dentro: ciclo
$dentro[$b] = true;
$total += $coste;
echo " + $a - $b ($coste)\n";
foreach ($red[$b] as $s)
if (!isset($dentro[$s[1]])) $borde->insert($s, -$s[2]);
}
echo "Coste total: $total, el mismo que con Kruskal\n";Salida al ejecutarlo (la misma en los 5 lenguajes)
Prim desde Central: + Central - Sur (9) + Sur - Puerto (6) + Central - Oeste (10) + Central - Norte (12) + Norte - Este (7) Coste total: 44, el mismo que con Kruskal
Traza: Kruskal con el grafo del visualizador
| Arista | Peso | Raíces de los extremos | Decisión | Peso del árbol |
|---|---|---|---|---|
| A-D | 5 | A y D | entra | 5 |
| C-E | 5 | C y E | entra | 10 |
| D-F | 6 | D y F | entra | 16 |
| A-B | 7 | F y B | entra | 23 |
| B-E | 7 | B y E | entra | 30 |
| B-C | 8 | E y E | se descarta: ciclo | 30 |
| E-F | 8 | E y E | se descarta: ciclo | 30 |
| B-D | 9 | E y E | se descarta: ciclo | 30 |
| E-G | 9 | E y G | entra | 39 |
| F-G | 11 | G y G | se descarta: ciclo | 39 |
| D-E | 15 | G y G | se descarta: ciclo | 39 |
Con E-G ya hay 6 aristas para 7 nodos: el árbol está completo y todas las que quedan cierran ciclos. Peso mínimo: 39.
Complejidad
| Algoritmo u operación | Coste | Nota |
|---|---|---|
| Kruskal | O(E log E) | Lo que cuesta es ordenar las aristas |
| Prim con montículo | O(E log V) | Como Dijkstra |
| Prim con matriz de adyacencia | O(V²) | Mejor en grafos muy densos |
| union-find con las dos mejoras | O(α(n)) amortizado | α crece tan despacio que no pasa de 4 para ningún tamaño real |
| union-find sin mejoras | O(n) en el peor caso | Los árboles pueden degenerar en listas |
α es la inversa de la función de Ackermann: en la práctica, una constante.
- Mejor caso: O(n log n)
- Caso medio: O(n log n)
- Peor caso: O(n log n)
O(E log E): manda ordenar las aristas. La unión-búsqueda con compresión de caminos es casi O(1) por operación; la versión corta de aquí, sin ella, puede llegar a O(V) por consulta. Las curvas grises son las demás clases, para comparar.
En la práctica
- Diseño de redes físicas: fibra entre edificios, tendido eléctrico, tuberías.
- El protocolo STP de los switches construye un árbol de expansión de la red para evitar bucles (no usa Kruskal, pero es la misma idea).
- La segmentación de imágenes y el *clustering* jerárquico de enlace simple se basan en el árbol mínimo.
- Union-find aparece en compiladores (inferencia de tipos), en simulaciones de percolación y en la detección de cuentas duplicadas.
- Kruskal con pesos aleatorios genera laberintos perfectos: un único camino entre cada par de casillas.
Errores típicos
- Comprobar si hay ciclo con un recorrido completo (DFS) por cada arista: funciona, pero es O(E · V); para eso está union-find.
- Comparar
padre[a] == padre[b]en vez de las raíces: dos nodos del mismo grupo pueden tener padres distintos. - Unir los nodos (
padre[a] = b) en vez de sus raíces: se rompen los grupos. - No comprobar al final si hay n − 1 aristas: con un grafo no conexo el resultado es un bosque, no un árbol.
- Confundir el árbol de expansión mínima con los caminos más cortos: el camino entre dos nodos dentro del árbol no tiene por qué ser el más corto.
Ejercicios
Cada ejercicio se corrige solo con sus pruebas (algunas ocultas). Escribe tu solución en el editor y pulsa Ejecutar o Comprobar; la solución explicada está debajo, por si te atascas.
1. Conjuntos disjuntos
Hay n elementos numerados de 0 a n − 1, cada uno en su propio grupo. Las órdenes unen grupos o preguntan si dos elementos están en el mismo. El main ya lee las órdenes y lleva la cuenta de los grupos: completa raiz (con compresión de caminos) y unir (el grupo pequeño cuelga del grande).
- Primera línea: n (si no es un número de 1 en adelante,
Número de elementos no válido: «…»). - Órdenes:
une 3 5→3 y 5 unidos (quedan 4 grupos)o3 y 5 ya estaban en el mismo grupo;? 3 5→3 y 5: mismo grupoo3 y 5: grupos distintos;grupos→Hay 4 grupos. - Otra cosa (o un elemento fuera de rango):
Orden no válida: «…».
Ejemplo
6 une 0 1 une 2 3 ? 1 0 ? 1 2 une 1 3 ? 0 2 une 0 2 grupos
0 y 1 unidos (quedan 5 grupos) 2 y 3 unidos (quedan 4 grupos) 1 y 0: mismo grupo 1 y 2: grupos distintos 1 y 3 unidos (quedan 3 grupos) 0 y 2: mismo grupo 0 y 2 ya estaban en el mismo grupo Hay 3 grupos
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 static int[] padre, tam;
5
6 /** El representante del grupo de x, comprimiendo el camino: todo lo recorrido apunta ya a la raíz. */
7 static int raiz(int x) {
8 if (padre[x] != x) padre[x] = raiz(padre[x]);
9 return padre[x];
10 }
11
12 /** Junta los grupos de a y b (el pequeño cuelga del grande). Devuelve false si ya estaban juntos. */
13 static boolean unir(int a, int b) {
14 int ra = raiz(a), rb = raiz(b);
15 if (ra == rb) return false;
16 if (tam[ra] < tam[rb]) {
17 int t = ra;
18 ra = rb;
19 rb = t;
20 }
21 padre[rb] = ra;
22 tam[ra] += tam[rb];
23 return true;
24 }
25
26 public static void main(String[] args) {
27 Scanner sc = new Scanner(System.in);
28 String primera = sc.hasNextLine() ? sc.nextLine().trim() : "";
29 if (!primera.matches("\\d{1,6}") || Integer.parseInt(primera) < 1) {
30 System.out.println("Número de elementos no válido: «" + primera + "»");
31 return;
32 }
33 int n = Integer.parseInt(primera);
34 padre = new int[n];
35 tam = new int[n];
36 for (int i = 0; i < n; i++) {
37 padre[i] = i;
38 tam[i] = 1;
39 }
40 int grupos = n;
41 while (sc.hasNextLine()) {
42 String linea = sc.nextLine().trim();
43 if (linea.isEmpty()) continue;
44 String[] p = linea.split("\\s+");
45 if (p.length == 1 && p[0].equals("grupos")) {
46 System.out.println("Hay " + grupos + (grupos == 1 ? " grupo" : " grupos"));
47 continue;
48 }
49 if (p.length != 3 || !(p[0].equals("une") || p[0].equals("?")) || !p[1].matches("\\d{1,6}") || !p[2].matches("\\d{1,6}")
50 || Integer.parseInt(p[1]) >= n || Integer.parseInt(p[2]) >= n) {
51 System.out.println("Orden no válida: «" + linea + "»");
52 continue;
53 }
54 int a = Integer.parseInt(p[1]), b = Integer.parseInt(p[2]);
55 if (p[0].equals("?")) System.out.println(a + " y " + b + (raiz(a) == raiz(b) ? ": mismo grupo" : ": grupos distintos"));
56 else if (unir(a, b)) {
57 grupos--;
58 System.out.println(a + " y " + b + " unidos (quedan " + grupos + (grupos == 1 ? " grupo)" : " grupos)"));
59 } else System.out.println(a + " y " + b + " ya estaban en el mismo grupo");
60 }
61 }
62}Con las dos mejoras, la altura de los árboles se mantiene diminuta y cada operación es casi O(1): se pueden procesar millones de uniones.
Sin unión por tamaño, unir siempre en el mismo sentido puede crear una cadena de n elementos, y cada raiz sin compresión la recorrería entera.
2. La red de fibra más barata
Cada línea es un posible cable entre dos sedes con su coste. Elige los cables para conectar todas las sedes con el menor coste total (Kruskal) y escríbelos en el orden en que se eligen. El main lee los cables y escribe el resultado: completa elegir, con tu propio union-find.
- Entrada:
Madrid Toledo 70(sede, sede, coste). Línea mal escrita o que une una sede consigo misma:Línea no válida: «…». - Salida: un cable elegido por línea (
Madrid - Toledo: 70) yCoste total: 245 (4 cables para 5 sedes). - Si no se puede conectar todo: los cables elegidos y
Imposible conectarlo todo: quedan 2 grupos sin unir. - Si dos cables cuestan lo mismo, se prueba antes el que aparece antes en la entrada (una ordenación estable lo respeta).
Ejemplo
Central Norte 12 Central Sur 9 Central Este 15 Central Oeste 10 Norte Este 7 Norte Oeste 18 Sur Oeste 11 Sur Puerto 6 Este Puerto 20 Oeste Puerto 13 Norte Sur 16
Sur - Puerto: 6 Norte - Este: 7 Central - Sur: 9 Central - Oeste: 10 Central - Norte: 12 Coste total: 44 (5 cables para 6 sedes)
Ver la solución explicada
1import java.util.*;
2
3public class Main {
4 record Cable(String a, String b, int coste) { }
5
6 static final List<Cable> cables = new ArrayList<>(); // en el orden de la entrada
7 static final Set<String> sedes = new TreeSet<>();
8
9 static final Map<String, String> padre = new HashMap<>();
10
11 static String raiz(String x) {
12 while (!padre.get(x).equals(x)) {
13 padre.put(x, padre.get(padre.get(x))); // compresión a medias: salta de dos en dos
14 x = padre.get(x);
15 }
16 return x;
17 }
18
19 /** Kruskal: los cables del árbol de expansión mínima, en el orden en que se eligen. Si dos cuestan
20 lo mismo, se prueba antes el que aparece antes en la entrada. */
21 static List<Cable> elegir() {
22 for (String s : sedes) padre.put(s, s);
23 List<Cable> orden = new ArrayList<>(cables);
24 orden.sort(Comparator.comparingInt(Cable::coste)); // estable: los empates mantienen su orden
25 List<Cable> elegidos = new ArrayList<>();
26 for (Cable c : orden) {
27 String ra = raiz(c.a()), rb = raiz(c.b());
28 if (ra.equals(rb)) continue; // cerraría un ciclo
29 padre.put(ra, rb);
30 elegidos.add(c);
31 }
32 return elegidos;
33 }
34
35 public static void main(String[] args) {
36 Scanner sc = new Scanner(System.in);
37 while (sc.hasNextLine()) {
38 String linea = sc.nextLine().trim();
39 if (linea.isEmpty()) continue;
40 String[] p = linea.split("\\s+");
41 if (p.length != 3 || !p[2].matches("\\d{1,6}") || p[0].equals(p[1])) {
42 System.out.println("Línea no válida: «" + linea + "»");
43 continue;
44 }
45 cables.add(new Cable(p[0], p[1], Integer.parseInt(p[2])));
46 sedes.add(p[0]);
47 sedes.add(p[1]);
48 }
49 List<Cable> elegidos = elegir();
50 int total = 0;
51 for (Cable c : elegidos) {
52 System.out.println(c.a() + " - " + c.b() + ": " + c.coste());
53 total += c.coste();
54 }
55 int grupos = sedes.size() - elegidos.size();
56 if (grupos > 1) System.out.println("Imposible conectarlo todo: quedan " + grupos + " grupos sin unir");
57 else System.out.println("Coste total: " + total + " (" + elegidos.size() + " cables para " + sedes.size() + " sedes)");
58 }
59}La regla de los empates no cambia el coste total (todos los árboles mínimos pesan lo mismo), pero sí qué cables se eligen; por eso hay que fijarla para que la salida sea única.
El número de grupos que quedan es el número de sedes menos el de cables elegidos: cada cable une dos grupos en uno.
Test
Test: Kruskal (árbol de expansión mínima)
0/5 respondidas · 0 aciertosElige una respuesta en cada pregunta: verás al momento si es correcta y por qué. Con un 80 % de aciertos se da por superada.
1.¿Cuántas aristas tiene un árbol de expansión de un grafo conexo con n nodos?
2.¿Cuándo descarta Kruskal una arista?
3.En union-find, ¿cuándo están a y b en el mismo grupo?
4.¿Qué domina el coste de Kruskal?
5.¿En qué se diferencia Prim de Kruskal?