Otkrivanje ciklusa u grafu

Lekcija 8 od 17 · Grafovi i stabla

Da li se iz nekog čvora može krenuti i vratiti u njega? U usmerenom grafu ciklus znači da zadaci zavise jedan od drugog u krug i da nema ispravnog redosleda izvršavanja. Graf bez ciklusa je DAG (usmereni aciklični graf). Ovde su tri načina provere: DFS sa bojama, Kahnov algoritam i provera za neusmeren graf.

Pre nego što počneš: potrebno predznanje – BFS i DFS, topološko sortiranje.
Kako da učiš: svaki primer prekucaj i pokreni, a zadatke na kraju prvo reši sam/a; uputstvo i rešenje otvori tek posle toga. Svi programi su u C++-u.

Usmeren graf: DFS sa tri boje

Svaki čvor je u jednom od tri stanja:

  • 0 – bela: još nije posećen;
  • 1 – siva: DFS je ušao u čvor i još nije završio sa njim (čvor je na trenutnom putu);
  • 2 – crna: čvor i sve što je dostižno iz njega je obrađeno.

Ako iz sivog čvora naiđemo na sivi čvor, vratili smo se na trenutni put – to je ciklus. Grana ka crnom čvoru nije ciklus (tamo smo već bili drugim putem).

#include <iostream>
#include <vector>
using namespace std;

vector<vector<int>> graf;
vector<int> boja;              // 0 bela, 1 siva, 2 crna

bool imaCiklus(int v) {
    boja[v] = 1;               // v je na trenutnom putu
    for (int u : graf[v]) {
        if (boja[u] == 1) return true;              // vratili smo se na put: ciklus
        if (boja[u] == 0 && imaCiklus(u)) return true;
    }
    boja[v] = 2;               // završili smo sa v
    return false;
}

int main() {
    int n, m;
    cin >> n >> m;
    graf.resize(n + 1);
    boja.assign(n + 1, 0);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);  // usmerena grana u -> v
    }
    bool ciklus = false;
    for (int v = 1; v <= n && !ciklus; v++)
        if (boja[v] == 0 && imaCiklus(v)) ciklus = true;
    cout << (ciklus ? "IMA CIKLUS" : "NEMA CIKLUSA") << endl;
    return 0;
}

Drugi primer pokazuje zašto je potrebna treća boja: do čvora 4 se dolazi dva puta (preko 2 i preko 3), ali to nije ciklus – kada stignemo drugi put, čvor 4 je već crn.

Usmeren graf: Kahnov algoritam

Čvor koji nema ulaznih grana (ulazni stepen 0) može prvi da se „izvrši“. Uklanjamo takve čvorove jedan po jedan zajedno sa njihovim granama, kao kod topološkog sortiranja. Ako na kraju ostanu neuklonjeni čvorovi, oni su na ciklusu (nijedan od njih nikad ne dobije ulazni stepen 0).

#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int main() {
    int n, m;
    cin >> n >> m;
    vector<vector<int>> graf(n + 1);
    vector<int> ulazni(n + 1, 0);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);
        ulazni[v]++;
    }
    queue<int> q;
    for (int v = 1; v <= n; v++)
        if (ulazni[v] == 0) q.push(v);
    int uklonjeno = 0;
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        uklonjeno++;
        for (int u : graf[v])
            if (--ulazni[u] == 0) q.push(u);   // u više nema ulaznih grana
    }
    cout << (uklonjeno < n ? "IMA CIKLUS" : "NEMA CIKLUSA") << endl;
    return 0;
}

Kahnov algoritam nema rekurziju i usput daje topološki redosled. DFS sa bojama lakše vraća i sam ciklus (vidi zadatak 1).

Neusmeren graf

U neusmerenom grafu svaka grana se vidi u oba smera, pa „vraćanje“ u čvor iz koga smo upravo došli nije ciklus. Zato DFS-u prosleđujemo roditelja: ciklus postoji ako naiđemo na već posećen čvor koji nije roditelj. Druga mogućnost: neusmeren povezan graf ima ciklus tačno kada ima bar n grana; u opštem slučaju koristi se DSU – ciklus nastaje kada grana spaja dva čvora koji su već u istoj grupi.

#include <iostream>
#include <vector>
using namespace std;

vector<vector<int>> graf;
vector<bool> posecen;

bool dfs(int v, int roditelj) {
    posecen[v] = true;
    for (int u : graf[v]) {
        if (u == roditelj) continue;          // grana kojom smo došli
        if (posecen[u]) return true;          // posećen, a nije roditelj: ciklus
        if (dfs(u, v)) return true;
    }
    return false;
}

int main() {
    int n, m;
    cin >> n >> m;
    graf.resize(n + 1);
    posecen.assign(n + 1, false);
    for (int i = 0; i < m; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);
        graf[v].push_back(u);
    }
    bool ciklus = false;
    for (int v = 1; v <= n && !ciklus; v++)
        if (!posecen[v] && dfs(v, 0)) ciklus = true;
    cout << (ciklus ? "IMA CIKLUS" : "NEMA CIKLUSA") << endl;
    return 0;
}

Koju metodu izabrati?

SituacijaMetoda
usmeren graf, treba samo DA/NEKahn ili DFS sa bojama
usmeren graf, treba i redosled (raspored zadataka)Kahn – daje topološki redosled
usmeren graf, treba ispisati ciklusDFS sa bojama + niz roditelja
neusmeren grafDFS sa roditeljem ili DSU
grane stižu jedna po jedna, pita se kada nastaje prvi ciklusDSU

Zadaci za vežbu

Za svaki zadatak postoji primer, uputstvo i rešenje. Rešenja su provereno tačna na primerima iz teksta.

ZADATAK 01Težina: Teže

Ispiši ciklus

Dat je usmeren graf. Ako ima ciklus, ispiši čvorove jednog ciklusa redom, tako da se prvi čvor ponovi na kraju (na primer 2 3 4 2). Inače ispiši NEMA.

Primer 1

Ulaz

4 4
1 2
2 3
3 4
4 2

Izlaz

2 3 4 2

Primer 2

Ulaz

3 2
1 2
2 3

Izlaz

NEMA
ZADATAK 02Težina: Srednje

Raspored predmeta

Ima n predmeta i m uslova oblika a b: predmet a mora da se položi pre predmeta b. Ispiši jedan mogući redosled polaganja (kada ima više mogućnosti, uvek uzmi predmet sa najmanjim rednim brojem), ili NEMOGUCE.

Primer 1

Ulaz

4 3
1 2
3 2
2 4

Izlaz

1 3 2 4

Primer 2

Ulaz

2 2
1 2
2 1

Izlaz

NEMOGUCE
Česte greške i saveti
  • U usmerenom grafu nije dovoljan običan niz posecen – potrebne su tri boje.
  • U neusmerenom grafu ne prijavljuj ciklus za granu kojom si došao (roditelj).
  • Graf može biti nepovezan: pokreni proveru iz svakog neposećenog čvora.

Proveri znanje

Otvori kviz

Otkrivanje ciklusa

1. Šta znači da DFS iz sivog čvora naiđe na sivi čvor?




2. Kahnov algoritam je uklonio 7 od 9 čvorova. Šta to znači?




3. Šta je DAG?