De ce contează?
Pleci de acasă la o plimbare prin cartier. O iei pe o stradă, dai colțul, mai treci câteva intersecții și, fără să te întorci pe niciun drum pe care ai mai fost, ajungi din nou în fața casei tale. Ai descris un cerc prin oraș: ai plecat dintr-un loc și te-ai întors în același loc, fără să repeți drumul. Exact asta este un ciclu într-un graf.
Intuiția
Un lanț e un traseu care leagă două noduri mergând din vecin în vecin. Dacă traseul se închide — pleci dintr-un nod și ajungi înapoi exact în el — ai un ciclu. Imaginea mentală e un inel: un șir de noduri prinse în cerc, unde ultimul se reîntoarce la primul. Important: nu te plimbi la întâmplare înainte și înapoi pe aceeași muchie, ci faci o buclă reală.
Vezi cum funcționează
Pornește animația. Vei urmări cum se construiește ciclul 1 — 2 — 3 — 4 — 1:
plecăm din nodul 1, trecem prin nodurile noi 2, 3 și 4, iar la final
muchia 4—1 ne aduce înapoi exact în nodul de plecare. Până la acea ultimă
muchie aveam doar un lanț; ea este cea care închide traseul și îl transformă
în ciclu.
Folosește ← și → ca să pășești muchie cu muchie, sau Redă pentru
animație. Fii atent la ultimul pas, când traseul revine în nodul 1 — toate
nodurile intermediare prin care ai trecut sunt distincte, doar capetele coincid.
Ce este
Lucrăm pe un graf neorientat (muchiile nu au sens, le poți parcurge în ambele direcții).
Un ciclu este un lanț în care primul nod coincide cu ultimul nod. Adică
un traseu de forma v0 - v1 - v2 - ... - vk - v0, unde fiecare pereche vecină
e legată printr-o muchie, iar capătul se întoarce la început.
Un ciclu este elementar dacă, în afară de capetele care coincid, toate celelalte noduri sunt distincte (nu treci de două ori prin același nod intermediar). Cu alte cuvinte, e o buclă „curată”, fără bucle mai mici prinse înăuntru.
Două precizări care apar mereu la concurs:
- Lungimea unui ciclu = numărul de muchii parcurse. Ciclul
1 - 2 - 3 - 4 - 1are 4 noduri distincte și lungimea4— la un ciclu, numărul de muchii coincide cu numărul de noduri distincte, pentru că bucla se închide. - Termenul „ciclu” se folosește pe grafuri neorientate. Pe un graf orientat, unde muchiile au sens, noțiunea corespunzătoare se numește circuit — o vei întâlni într-o lecție separată.
Într-un graf simplu (fără muchii multiple și fără bucle de la un nod la el
însuși), un ciclu elementar are lungime minimum 3: ai nevoie de cel puțin 3
noduri distincte pentru a te întoarce de unde ai plecat. Cu 2 noduri ai folosi
de două ori aceeași muchie — 1 - 2 - 1 nu este ciclu, ci doar mers înainte și
înapoi pe o singură muchie.
Un graf care nu conține niciun ciclu se numește graf aciclic. Grafurile
aciclice sunt „zgârcite” la muchii: un graf neorientat aciclic cu n noduri are
cel mult n - 1 muchii — orice muchie în plus ar închide obligatoriu o buclă.
Dacă graful aciclic este și conex, obții un arbore, cea mai importantă
structură fără cicluri, studiată într-un capitol dedicat.
Cum arată
Pe graful din vizualizare există muchiile 1-2, 2-3, 3-4 și 1-4.
Parcurse în ordine, formează ciclul 1 - 2 - 3 - 4 - 1:
1 | 2 | 3 | 4 | 1 |
0 | 1 | 2 | 3 | 4 |
start | revenire |
Verifică fiecare pas: muchia 1-2 există, 2-3 există, 3-4 există, iar
4-1 închide bucla. Niciun nod intermediar nu se repetă, doar nodul de
plecare 1 apare și la final — semnătura unui ciclu elementar.
În schimb, succesiunea 1 - 2 - 5 - 1 NU este ciclu pe acest graf: muchiile
1-2 și 2-5 există, dar între 5 și 1 nu există muchie, deci traseul nu
se poate închide. Un ciclu are nevoie ca fiecare pas, inclusiv cel de
întoarcere, să calce pe o muchie reală.
Implementare C++
Cea mai utilă întrebare practică este: există vreun ciclu în graf? Pentru un graf neorientat, răspunsul vine dintr-un DFS care reține din ce nod ai venit (nodul-părinte).
Ideea: pleci în DFS și marchezi nodurile vizitate. Când, dintr-un nod curent, dai de un vecin deja vizitat care NU este părintele de unde tocmai ai venit, înseamnă că ai găsit o a doua cale către acel nod — adică o buclă. Asta este un ciclu.
#include <iostream>
#include <vector>
using namespace std;
const int NMAX = 100005;
vector<int> vecini[NMAX]; // liste de adiacenta
bool vizitat[NMAX];
// intoarce true daca gaseste un ciclu pornind din nodul curent
bool areCiclu(int nod, int parinte) {
vizitat[nod] = true;
for (int vecin : vecini[nod]) {
if (!vizitat[vecin]) {
// nod nevizitat: continuam DFS-ul mai departe
if (areCiclu(vecin, nod)) return true;
} else if (vecin != parinte) {
// vecin vizitat care NU e parintele -> muchie inapoi -> ciclu
return true;
}
}
return false;
}
int main() {
int n, m;
cin >> n >> m;
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b;
vecini[a].push_back(b); // graf neorientat: muchie in ambele sensuri
vecini[b].push_back(a);
}
bool gasit = false;
for (int i = 1; i <= n; i++) {
// -1 = nodul de start nu are parinte
if (!vizitat[i] && areCiclu(i, -1)) {
gasit = true;
break;
}
}
cout << (gasit ? "DA" : "NU") << "\n"; // DA daca graful are cel putin un ciclu
return 0;
}Bucla din main reia DFS-ul din fiecare nod nevizitat, ca să acoperi și
grafurile neconexe (mai multe componente separate). Complexitatea e
O(n + m): vizitezi fiecare nod și fiecare muchie o singură dată.
Două capcane clasice la acest subiect:
- Uiți de părinte în DFS. Când ești în nodul
2, venit din1, vecinul1este deja vizitat — dar el e chiar părintele tău, muchia pe care tocmai ai venit, nu un ciclu nou. Dacă uiți condițiavecin != parinte, orice muchie simplă1-2ar fi semnalată fals drept ciclu. Verifică mereu că nodul vizitat de care dai nu este părintele înainte de a striga „ciclu”. - Dus-întors nu e ciclu. Când ți se cere să enumeri cicluri,
1 - 2 - 1pare tentant — dar folosește aceeași muchie de două ori, deci nu e ciclu. Într-un graf simplu neorientat, cel mai scurt ciclu are lungimea3.