De ce contează?
Ai de adus curent în toate satele unei comune. Poți întinde cablu doar între sate vecine, iar fiecare tronson are un cost (lungime, teren, distanță). Vrei ca toate satele să fie conectate la rețea — direct sau prin alte sate — cheltuind cât mai puțin cablu. Nu contează cum arată rețeaua, ci doar ca totul să fie legat și factura să fie minimă. Exact asta calculează un arbore parțial de cost minim.
Intuiția
Pornești de la un graf ponderat conex: nodurile sunt satele, muchiile sunt tronsoanele posibile, iar costul fiecărei muchii e prețul cablului. Cauți o submulțime de muchii care leagă toate nodurile și are suma costurilor cea mai mică.
Dacă păstrezi o muchie în plus față de strictul necesar, ai creat un ciclu — adică un tronson care leagă două sate deja conectate pe altă cale. Acel tronson e bani aruncați: îl poți tăia fără să deconectezi pe nimeni. O rețea conexă fără niciun ciclu se numește arbore, iar cea mai ieftină dintre toate se numește arbore parțial de cost minim (APM).
Orice soluție optimă e un arbore: conexă (leagă tot) și fără cicluri (nimic
redundant), deci are exact n-1 muchii. Dar câți arbori parțiali există? Într-un
graf complet cu doar 10 noduri sunt 100.000.000 (formula lui Cayley spune că
numărul lor e n la puterea n-2). Nu-i poți încerca pe toți — ai nevoie de o
regulă locală, aplicată muchie cu muchie, care să nimerească optimul global.
Vezi cum funcționează
Urmărește cum se construiește rețeaua: muchiile sunt luate în considerare în ordinea crescătoare a costului, iar o muchie e adăugată doar dacă nu închide un ciclu. La final rămâne un arbore care atinge toate nodurile cu cost total minim.
Folosește ← și → ca să pășești muchie cu muchie, sau Redă pentru animație. La fiecare pas observă două lucruri: care e cea mai ieftină muchie încă neîncercată și dacă adăugarea ei ar uni două sate deja conectate (ciclu) sau nu.
Să încerci toate submulțimile de muchii e exclus — ai văzut că numărul de arbori
explodează. Dar știi două lucruri: vrei suma minimă și îți trebuie exact n-1
muchii. Atunci ideea simplă e: iau la grămadă cele mai ieftine n-1 muchii din
graf. Pe graful nostru cu 4 noduri și muchiile (1,2)=1, (2,3)=2, (1,3)=3,
(3,4)=4, (2,4)=5, aleg costurile 1, 2 și 3 — sumă 6, mai mică nici nu se poate.
Uită-te la ce ai cumpărat: muchiile (1,2), (2,3), (1,3) formează un
triunghi între nodurile 1, 2 și 3 — un ciclu — iar nodul 4 a rămas complet
izolat, fără curent. Ai plătit 6 și nu ai un arbore parțial deloc. Vinovatul e
muchia (1,3): 1 și 3 erau deja legate prin nodul 2, deci al treilea leu s-a dus
pe o legătură redundantă în loc să conecteze pe cineva nou.
Nu contează doar cât costă o muchie, ci și dacă leagă ceva nou. Declicul: ia muchiile tot în ordinea crescătoare a costului, dar sari peste orice muchie care ar închide un ciclu cu cele deja alese. Sună riscant să alegi lacom, dar e demonstrabil corect prin proprietatea tăieturii (cut property): oricum ai împărți nodurile în două grupuri, cea mai ieftină muchie care le leagă apare sigur într-un APM — deci alegând-o nu pierzi niciodată optimul.
Sortezi muchiile crescător după cost și le parcurgi în ordine. Adaugi o muchie
doar dacă cele două capete nu sunt deja conectate (altfel ar face ciclu). Te
oprești când ai n-1 muchii — atunci ai exact un arbore. Pe exemplu: accepți 1
și 2, respingi 3 (ciclul de mai sus), accepți 4 — cost total 7, toate nodurile
legate. Asta e ideea algoritmului lui Kruskal.
Algoritmul pas cu pas
Fie un graf cu 4 noduri și 5 muchii cu costurile:
(1,2)=1, (2,3)=2, (1,3)=3, (3,4)=4, (2,4)=5.
Întâi sortăm muchiile crescător după cost și le parcurgem în ordine. Pentru fiecare ne întrebăm doar atât: capetele sunt deja conectate prin muchiile alese? Dacă nu, o acceptăm; dacă da, am face ciclu, deci o respingem.
| cost | 1 | 2 | 3 | 4 | 5 |
| muchia | 1-2 | 2-3 | 1-3 | 3-4 | 2-4 |
accept | accept | ciclu | accept | stop |
Pas cu pas pe lista sortată:
- Costul 1, muchia (1,2): nodurile 1 și 2 nu erau conectate. Acceptăm. Avem 1 muchie.
- Costul 2, muchia (2,3): 3 nu era încă în rețea. Acceptăm. Avem 2 muchii; acum 1, 2, 3 sunt toate conectate.
- Costul 3, muchia (1,3): 1 și 3 sunt deja conectate (prin 2). Ar închide un ciclu. Respingem.
- Costul 4, muchia (3,4): 4 era izolat. Acceptăm. Avem 3 muchii
= n-1, deci ne oprim. - Costul 5, muchia (2,4): nici n-o mai privim — arborele e complet.
Muchiile alese sunt (1,2), (2,3), (3,4), cu cost total 1 + 2 + 4 = 7.
Cele 4 noduri au fost legate cu exact n-1 = 3 muchii și cost total 7.
Muchia (1,3) de cost 3 a fost respinsă nu fiindcă era scumpă, ci fiindcă ar fi
făcut un ciclu — un arbore cu n noduri are mereu fix n-1 muchii, nici una în plus.
Implementare C++
Sortăm muchiile crescător și adăugăm fiecare muchie dacă nu unește două noduri deja conectate. „Sunt conectate?" îl aflăm cu un union-find rudimentar: fiecare nod are un reprezentant; două noduri sunt în aceeași componentă dacă au același reprezentant.
#include <iostream>
#include <algorithm>
using namespace std;
struct Muchie {
int x, y, cost;
};
int rep[105]; // reprezentantul componentei fiecarui nod
// gaseste reprezentantul componentei nodului u
int gaseste(int u) {
while (rep[u] != u)
u = rep[u];
return u;
}
int main() {
int n = 4;
Muchie m[] = {{1, 2, 1}, {2, 3, 2}, {1, 3, 3}, {3, 4, 4}, {2, 4, 5}};
int nrMuchii = 5;
// sortare crescatoare dupa cost
sort(m, m + nrMuchii, [](Muchie a, Muchie b) {
return a.cost < b.cost;
});
for (int i = 1; i <= n; i++)
rep[i] = i; // initial fiecare nod e singur in componenta sa
long long costTotal = 0; // suma poate depasi int la grafuri mari
int alese = 0;
for (int i = 0; i < nrMuchii && alese < n - 1; i++) {
int rx = gaseste(m[i].x);
int ry = gaseste(m[i].y);
if (rx != ry) { // capete in componente diferite => nu face ciclu
rep[rx] = ry; // unim cele doua componente
costTotal += m[i].cost;
alese++;
}
}
cout << costTotal; // 7
return 0;
}Versiunea de mai sus e cea mai simplă posibilă pentru a vedea mecanismul. La
clasa despre Kruskal vei accelera gaseste cu compresia drumului, dar ideea
rămâne identică: sortezi, apoi accepți muchia doar dacă unește componente diferite.
Complexitate
Pentru un graf cu n noduri și m muchii, costul e dominat de sortarea muchiilor.
| Caz | Timp | Spațiu |
|---|---|---|
| Orice | O(m log m) | O(n + m) |
Sortarea costă O(m log m); parcurgerea muchiilor mai adaugă O(m) verificări de
componentă, deci termenul de sortare domină.
Prima capcană: uiți sortarea. Parcurgi muchiile în ordinea din fișier și
accepți tot ce nu face ciclu — obții un arbore parțial, dar nu pe cel minim.
Pe exemplul nostru, citite în ordinea (2,4)=5, (3,4)=4, (1,3)=3 ai accepta
toate trei (niciuna nu închide ciclu în acel moment) și ai plăti 12 în loc de 7.
Fără sortare, „lacom" nu mai înseamnă nimic.
A doua capcană: detectezi ciclul greșit. Nu e suficient să verifici dacă
între capete există deja o muchie directă — în exemplu, (1,3) închide ciclu
deși 1 și 3 nu erau legate direct, ci printr-un drum prin nodul 2. Întrebarea
corectă e „sunt capetele în aceeași componentă?", adică union-find (sau o
parcurgere), nu o căutare în lista de muchii.
A treia capcană, de concurs: overflow. Cu muchii de cost până la un miliard,
suma celor n-1 muchii alese depășește ușor limita lui int. Ține costul total
pe long long — e genul de bug care pică toate testele mari și niciun exemplu.