Stabla: pojmovi, obilazak, podstabla i prečnik

Lekcija 5 od 17 · Grafovi i stabla

Stablo je povezan graf bez ciklusa. Porodično stablo, struktura foldera na računaru i organizacija škole su stabla. Zbog posebnog oblika na stablima se mnogi zadaci rešavaju jednim obilaskom: dubine, veličine podstabala, prečnik.

Pre nego što počneš: potrebno predznanje – BFS i DFS, rekurzija.
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.

Šta je stablo?

Graf sa n čvorova je stablo ako važi bilo koje od ovih (sva su jednaka): povezan je i nema ciklus; povezan je i ima tačno n − 1 granu; između svaka dva čvora postoji tačno jedan put.

          1            dubina 0
        / | \
       2  3  4         dubina 1
      / \     \
     5   6     7       dubina 2
                \
                 8     dubina 3
PojamZnačenjeU primeru
korenizabrani početni čvor1
roditelj, detesused bliže korenu / dalje od korenaroditelj čvora 6 je 2; deca čvora 1 su 2, 3, 4
listčvor bez dece3, 5, 6, 8
dubinabroj grana od korena do čvoradubina čvora 8 je 3
visina stablanajveća dubina3
podstablo čvora vv i svi čvorovi ispod njegapodstablo čvora 2: {2, 5, 6}

Obilazak stabla: roditelji i dubine

Stablo se zadaje kao n − 1 grana. Obilazak u dubinu (DFS) iz korena određuje roditelja i dubinu svakog čvora. Umesto niza posecen dovoljno je ne vraćati se u roditelja – u stablu nema drugih ciklusa.

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

vector<vector<int>> graf;
vector<int> roditelj, dubina;

void dfs(int v, int r) {              // r = roditelj čvora v
    roditelj[v] = r;
    for (int u : graf[v]) {
        if (u == r) continue;         // ne vraćamo se nazad
        dubina[u] = dubina[v] + 1;
        dfs(u, v);
    }
}

int main() {
    int n;
    cin >> n;
    graf.resize(n + 1);
    roditelj.resize(n + 1);
    dubina.assign(n + 1, 0);
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);
        graf[v].push_back(u);
    }
    dfs(1, 0);                        // koren je 1, njegov "roditelj" je 0
    for (int v = 1; v <= n; v++)
        cout << v << ": roditelj " << roditelj[v] << ", dubina " << dubina[v] << endl;
    return 0;
}
Duboka stabla: ako je stablo „lanac“ od 105 i više čvorova, rekurzija je duboka isto toliko. Na takmičenjima je stek obično dovoljno velik, ali ako program pukne, DFS treba napisati sa sopstvenim stekom ili koristiti BFS.

Veličina podstabla

Veličina podstabla čvora v je 1 (sam v) plus zbir veličina podstabala njegove dece. Deca se obrađuju pre roditelja, pa veličinu računamo posle rekurzivnih poziva (obilazak „posle“, postorder).

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

vector<vector<int>> graf;
vector<int> velicina;

void dfs(int v, int r) {
    velicina[v] = 1;                  // sam čvor v
    for (int u : graf[v]) {
        if (u == r) continue;
        dfs(u, v);
        velicina[v] += velicina[u];   // dodaj podstablo deteta
    }
}

int main() {
    int n;
    cin >> n;
    graf.resize(n + 1);
    velicina.assign(n + 1, 0);
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);
        graf[v].push_back(u);
    }
    dfs(1, 0);
    for (int v = 1; v <= n; v++) cout << velicina[v] << " ";
    cout << endl;
    return 0;
}

Prečnik stabla

Prečnik je najveće rastojanje (broj grana) između dva čvora stabla. Postoji lep trik sa dva obilaska:

  1. iz bilo kog čvora nađi najudaljeniji čvor A;
  2. iz A nađi najudaljeniji čvor B;
  3. rastojanje od A do B je prečnik.
#include <iostream>
#include <vector>
#include <queue>
using namespace std;

int n;
vector<vector<int>> graf;

// BFS iz čvora s; vraća udaljenosti svih čvorova od s
vector<int> bfs(int s) {
    vector<int> d(n + 1, -1);
    queue<int> q;
    d[s] = 0;
    q.push(s);
    while (!q.empty()) {
        int v = q.front();
        q.pop();
        for (int u : graf[v])
            if (d[u] == -1) {
                d[u] = d[v] + 1;
                q.push(u);
            }
    }
    return d;
}

int main() {
    cin >> n;
    graf.resize(n + 1);
    for (int i = 0; i < n - 1; i++) {
        int u, v;
        cin >> u >> v;
        graf[u].push_back(v);
        graf[v].push_back(u);
    }
    vector<int> d1 = bfs(1);
    int A = 1;
    for (int v = 1; v <= n; v++) if (d1[v] > d1[A]) A = v;   // najdalji od 1
    vector<int> d2 = bfs(A);
    int B = A;
    for (int v = 1; v <= n; v++) if (d2[v] > d2[B]) B = v;   // najdalji od A
    cout << "precnik = " << d2[B] << endl;
    return 0;
}

U primeru je prečnik put 5 – 2 – 1 – 4 – 7 – 8 dužine 5. BFS je ovde korišćen umesto DFS-a da ne bi bilo duboke rekurzije.

Stablo zadato nizom roditelja

Često se stablo zadaje drugačije: za svaki čvor osim korena dat je njegov roditelj (p[2], p[3], …, p[n]). Tada graf pravimo tako što svakom roditelju dodamo dete: deca[p[v]].push_back(v). Ako je uvek p[v] < v, veličine podstabala mogu se izračunati i bez rekurzije – prolaskom od n unazad do 2: vel[p[v]] += vel[v].

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: Lako

Broj listova i visina

Dato je stablo sa n čvorova (koren je 1) kao n − 1 grana. Ispiši broj listova i visinu stabla. (n ≥ 2)

Primer

Ulaz

8
1 2
1 3
1 4
2 5
2 6
4 7
7 8

Izlaz

4 3
ZADATAK 02Težina: Srednje

Najveći zbir podstabla

Svaki čvor stabla ima vrednost (može biti i negativna). Koren je 1. Ispiši najveći zbir vrednosti nekog podstabla.

Ulaz: n, zatim n vrednosti, pa n − 1 grana.

Primer

Ulaz

5
3 -10 4 2 2
1 2
1 3
2 4
2 5

Izlaz

4
ZADATAK 03Težina: Srednje

Rastojanje između dva čvora

Dato je stablo i dva čvora a i b. Ispiši broj grana na putu između njih i sam put.

Ulaz: n, n − 1 grana, zatim a b.

Primer

Ulaz

8
1 2
1 3
1 4
2 5
2 6
4 7
7 8
5 8

Izlaz

5
5 2 1 4 7 8
Česte greške i saveti
  • Stablo sa n čvorova ima tačno n − 1 granu – petlja za učitavanje ide do n − 1.
  • U DFS-u na stablu prosleđuj roditelja da se ne bi vraćao nazad.
  • Zbirovi u podstablima mogu biti veliki – koristi long long.

Proveri znanje

Otvori kviz

Stabla

1. Koliko grana ima stablo sa 50 čvorova?




2. Koliko puteva postoji između dva čvora stabla?




3. Kada se računa veličina podstabla čvora v?




4. Kako se nalazi prečnik stabla?