Najduži put u DAG-u (DP + topološko sortiranje)
U usmerenim acikličnim grafovima (DAG) možemo efikasno pronaći najduži put (maksimalan broj grana ili maksimalnu ukupnu težinu) koristeći kombinaciju topološkog sortiranja i dinamičkog programiranja.
Za razliku od opštih usmerenih grafova, gde je problem najdužeg prostog puta težak i ne može se rešiti ovim jednostavnim pristupom, u DAG-u ovaj problem rešavamo u linearnom vremenu.
To je jedna od najlepših primena topološkog reda: čvorove obrađujemo tek kada su svi njihovi prethodnici već spremni.
Šta zapravo tražimo?
Kod problema najdužeg puta želimo da pronađemo putanju koja ima najveću ukupnu dužinu.
U zavisnosti od zadatka, možemo tražiti najduži put iz unapred zadatog početnog čvora ili najduži put u celom DAG grafu.
Dužina može da znači:
- broj grana na putu (neuteženi graf)
- zbir težina grana (težinski graf)
Na primer, u sledećem DAG grafu:
0 → 1 → 3
\ ↘
↘ 2 → 4 → 5
Postoji više mogućih puteva od čvora 0:
0 → 1 → 3
0 → 1 → 2 → 4
0 → 1 → 2 → 4 → 5
Od svih njih zanima nas onaj koji je najduži.
Kod DAG grafova možemo efikasno pronaći najduži put jer ne postoje ciklusi koji bi omogućili beskonačno produžavanje putanje.
U praksi se ovaj problem pojavljuje kada želimo da odredimo:
- najduži lanac zavisnosti između zadataka
- minimalno vreme završetka projekta
- najduži sled kurseva sa preduslovima
- najduži niz operacija u procesu proizvodnje
Osnovna ideja algoritma
Kod DAG-a ne moramo da se vraćamo unazad niti da pokušavamo sve puteve. Dovoljno je da čvorove uredimo tako da svaki čvor dolazi pre svojih naslednika. Taj redosled dobijamo topološkim sortiranjem.
Zatim definišemo DP niz:
dp[v]
gde dp[v] predstavlja dužinu najdužeg puta koji se završava u čvoru
v, posmatrano u topološkom redosledu.
Ako postoji grana u -> v, onda možemo da pređemo iz u u v
i dobijemo:
dp[v] = max(dp[v], dp[u] + w(u, v))
gde je w(u, v) težina grane.
Ako je graf neutežen, onda je w(u, v) = 1.
Topološki red garantuje da kada obrađujemo čvor
u, svi putevi koji ulaze u
u već su bili obrađeni. Zato je jedna prolazna DP obrada dovoljna.
Dve varijante problema
Važno je razlikovati dve česte verzije zadatka.
U prvoj varijanti obično računamo najduži put koji se završava u svakom čvoru i na kraju uzimamo maksimum. U drugoj varijanti startni čvor inicijalizujemo sa nulom, a ostale čvorove sa veoma malom vrednošću.
Kod neuteženog DAG-a to znači:
dp[start] = 0
dok su svi ostali čvorovi inicijalno nedostižni.
Primer neuteženog DAG-a
0 → 1 → 3
\ ↘ ↘
↘ 2 → 4 → 5
Jedan najduži put je:
Dužina puta je 4, jer sadrži četiri grane.
Ovde nije bitno samo pronaći bilo koji put, već onaj sa najvećim brojem grana.
Implementacija: neuteženi DAG
#include <bits/stdc++.h>
using namespace std;
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m;
cin >> n >> m;
// --------------------------------------------------------
// Graf i ulazni stepeni čvorova
// --------------------------------------------------------
//
// g[u] = lista svih čvorova do kojih postoji grana u -> v
// indeg[v] = broj ulaznih grana u v
//
// Ovaj niz nam treba za Kahn-ov algoritam.
// --------------------------------------------------------
vector<vector<int>> g(n);
vector<int> indeg(n, 0);
for (int i = 0; i < m; i++) {
int a, b;
cin >> a >> b; // grana a -> b
g[a].push_back(b);
indeg[b]++;
}
// --------------------------------------------------------
// Kahn-ov algoritam za topološko sortiranje
// --------------------------------------------------------
//
// U red stavljamo sve čvorove sa indeg = 0.
// To su čvorovi koji nemaju prethodnike.
// --------------------------------------------------------
queue<int> q;
for (int i = 0; i < n; i++) {
if (indeg[i] == 0)
q.push(i);
}
vector<int> topo;
while (!q.empty()) {
int u = q.front();
q.pop();
topo.push_back(u);
// Uklanjamo sve grane iz u
for (int v : g[u]) {
indeg[v]--;
if (indeg[v] == 0)
q.push(v);
}
}
// --------------------------------------------------------
// Ako nismo obradili sve čvorove,
// graf nije DAG.
// Ovaj algoritam je namenjen samo DAG-u.
// --------------------------------------------------------
if ((int)topo.size() != n) {
cout << "Graf nije DAG, pa ovaj algoritam nije primenljiv.\n";
return 0;
}
// --------------------------------------------------------
// DP:
// dp[v] = dužina najdužeg puta koji se završava u v
// --------------------------------------------------------
//
// Pošto je graf neutežen, svaki prelaz dodaje 1.
// Inicijalno svi čvorovi mogu da budu početak puta,
// pa krećemo od 0.
// --------------------------------------------------------
vector<int> dp(n, 0);
vector<int> parent(n, -1);
for (int u : topo) {
for (int v : g[u]) {
if (dp[v] < dp[u] + 1) {
dp[v] = dp[u] + 1;
parent[v] = u;
}
}
}
// --------------------------------------------------------
// Tražimo najveću vrednost dp[n]
// --------------------------------------------------------
int end = 0;
for (int i = 1; i < n; i++) {
if (dp[i] > dp[end])
end = i;
}
cout << "Najduzi put u DAG-u je: " << dp[end] << "\n";
// --------------------------------------------------------
// Rekonstrukcija puta
// --------------------------------------------------------
vector<int> path;
for (int x = end; x != -1; x = parent[x]) {
path.push_back(x);
}
reverse(path.begin(), path.end());
cout << "Putanja: ";
for (int x : path) cout << x << " ";
cout << "\n";
return 0;
}
Ova varijanta računa najduži put u celom DAG-u. Ako želiš najduži put iz jednog konkretnog startnog čvora, inicijalizacija je malo drugačija.
Primer sa težinama
0 -5→ 1 -2→ 3
\ ↘
3 7
↘ ↘
2 -1→ 4 -4→ 5
U težinskoj varijanti cilj je pronaći put sa najvećom ukupnom sumom težina.
Negativne težine nisu problem za ovaj algoritam, jer graf nema cikluse. To je potpuno drugačije od najkraćeg puta, gde negativne težine mogu biti problem.
Ako postoji startni čvor, onda koristimo:
dp[start] = 0
a sve ostale čvorove inicijalizujemo kao nedostižne.
Implementacija: najduži put iz zadatog startnog čvora
#include <bits/stdc++.h>
using namespace std;
using ll = long long;
const ll NEG_INF = -(1LL << 60);
int main() {
ios::sync_with_stdio(false);
cin.tie(nullptr);
int n, m, start;
cin >> n >> m >> start;
// --------------------------------------------------------
// Težinski graf: g[u] sadrži parove (v, w)
// gde je w težina grane u -> v
// --------------------------------------------------------
vector<vector<pair<int,int>>> g(n);
vector<int> indeg(n, 0);
for (int i = 0; i < m; i++) {
int a, b, w;
cin >> a >> b >> w;
g[a].push_back({b, w});
indeg[b]++;
}
// --------------------------------------------------------
// Kahn-ov algoritam
// --------------------------------------------------------
queue<int> q;
for (int i = 0; i < n; i++) {
if (indeg[i] == 0)
q.push(i);
}
vector<int> topo;
while (!q.empty()) {
int u = q.front();
q.pop();
topo.push_back(u);
for (auto [v, w] : g[u]) {
indeg[v]--;
if (indeg[v] == 0)
q.push(v);
}
}
if ((int)topo.size() != n) {
cout << "Graf nije DAG.\n";
return 0;
}
// --------------------------------------------------------
// DP za startni čvor
// --------------------------------------------------------
//
// start je jedini čvor sa vrednošću 0.
// Ostali su nedostižni dok ih ne poboljšamo.
// --------------------------------------------------------
vector<ll> dp(n, NEG_INF);
vector<int> parent(n, -1);
dp[start] = 0;
for (int u : topo) {
if (dp[u] == NEG_INF) continue;
for (auto [v, w] : g[u]) {
if (dp[v] < dp[u] + w) {
dp[v] = dp[u] + w;
parent[v] = u;
}
}
}
// --------------------------------------------------------
// Najveći rezultat od svih dostižnih čvorova
// --------------------------------------------------------
int end = start;
for (int i = 0; i < n; i++) {
if (dp[i] > dp[end])
end = i;
}
cout << "Maksimalna suma: " << dp[end] << "\n";
// --------------------------------------------------------
// Rekonstrukcija puta
// --------------------------------------------------------
vector<int> path;
for (int x = end; x != -1; x = parent[x]) {
path.push_back(x);
}
reverse(path.begin(), path.end());
cout << "Putanja: ";
for (int x : path) cout << x << " ";
cout << "\n";
return 0;
}
Kako rekonstruišemo najduži put?
Do sada smo naučili kako da izračunamo dužinu najdužeg puta u DAG grafu. Međutim, u praksi često želimo i da saznamo koji čvorovi čine taj put.
Zbog toga pored niza dp uvodimo i dodatni niz:
parent[v]
Niz parent pamti odakle smo došli do čvora v
kada smo pronašli trenutno najbolji put.
Drugim rečima:
parent[v] = prethodni čvor na najboljem putu do v
Kada tokom relaksacije pronađemo bolji put do nekog čvora, ažuriramo i
dp i parent.
if (dp[v] + 1 > dp[to]) {
dp[to] = dp[v] + 1;
parent[to] = v;
}
dp čuva dužinu najboljeg puta,
a parent čuva informaciju kako da taj put vratimo nazad.
Na primer, ako nakon završetka algoritma dobijemo:
parent[1] = 0
parent[2] = 1
parent[3] = 1
parent[4] = 2
parent[5] = 4
i ako je čvor 5 kraj najdužeg puta,
onda rekonstrukciju radimo tako što krećemo od njega i pratimo
parent pokazivače unazad:
5 → 4 → 2 → 1 → 0
Pošto je put dobijen unazad, na kraju samo obrnemo redosled:
0 → 1 → 2 → 4 → 5
Tako dobijamo konkretan najduži put, a ne samo njegovu dužinu.
Ako zadatak traži samo dužinu puta, dovoljan je
dp niz.
Ako zadatak traži i ispis samog puta, potreban je i parent niz.
Zbog toga se u grafovskim DP zadacima često koristi obrazac:
dp[] → najbolji rezultat
parent[] → rekonstrukcija rešenja
Ovaj isti princip se često koristi i u drugim algoritmima, na primer kada želimo da rekonstruišemo put nakon Dijkstrinog algoritma.
Objašnjenje varijante sa težinama(weithed)
Ovde je ideja ista, samo svaka grana ima svoju težinu. Pošto graf nema cikluse, negativne težine ne predstavljaju problem.
Kada obradimo čvor u topološkom redosledu, svi njegovi prethodnici su već obrađeni, pa svaki pokušaj relaksacije dolazi u pravom trenutku.
Za najduži put u DAG-u ne koristimo Dijkstru. Dijkstra je algoritam za najkraći put, i to pod posebnim uslovima. Ovde nam je potreban DAG + topološki red + DP.
Kako vratiti konkretan put?
Ako želimo samo dužinu najdužeg puta, dovoljno je da pratimo DP vrednosti. Ali ako želimo i samu putanju, čuvamo roditelja čvora iz kog smo poslednji put poboljšali vrednost.
Kad god
dp[v] postane veći, pamtimo parent[v] = u.
Na kraju krećemo od krajnjeg čvora unazad i dobijamo ceo put.
To je standardna tehnika rekonstrukcije u DP problemima.
Česte greške i zamke
Jedna od najčešćih grešaka je pokušaj da se najduži put traži i u grafu koji ima ciklus. Tada ovaj jednostavan algoritam više nije dovoljan.
Druga česta greška je pokretanje DP-a bez topološkog poretka. Tada možemo obraditi čvor pre nego što su svi njegovi prethodnici spremni, pa rezultat postaje netačan.
Ako graf nije DAG, prvo moraš da rešiš problem ciklusa ili da koristiš potpuno drugačiji pristup.
Zadaci za vežbu
Dobro je da prvo probaš najduži put iz zadatog startnog čvora, a zatim varijantu u kojoj se traži globalni maksimum u celom DAG-u. To su dve najčešće forme ovog zadatka.
Posle toga, zanimljivo je rešavati i probleme koji modeluju:
Zaključak
Najduži put u DAG-u je veoma lep primer kako se topološko sortiranje i dinamičko programiranje savršeno dopunjuju.
Topološki red daje pravilan poredak obrade, a DP čuva najbolji rezultat za svaki čvor. Zbog toga ceo problem možemo rešiti linearno, bez proveravanja svih mogućih puteva.
U DAG-u se najduži put ne traži silom kroz sve puteve, već pametnim redosledom obrade.
Zadaci za samostalno rešavanje
Zadatak 1: Najduži put iz startnog čvora
Dat je usmereni aciklični graf (DAG) sa N čvorova i M grana. Svaka grana ima pozitivnu težinu.
Za dati početni čvor S potrebno je odrediti maksimalnu moguću sumu težina
puta koji polazi iz tog čvora.
Takođe, treba ispisati i jednu konkretnu najdužu putanju.
Ako postoji više najdužih putanja, dovoljno je ispisati bilo koju od njih.
Graf je DAG, što znači da nema cikluse. Zato možemo koristiti topološko sortiranje i dinamičko programiranje.
Zadatak 2: Najduži put u celom DAG-u
Dat je usmereni aciklični graf (DAG) sa N čvorova i M grana. Svaka grana ima težinu.
Potrebno je pronaći dužinu najdužeg puta u celom grafu, odnosno puta koji može da počne i završi u bilo kojim čvorovima.
Dovoljno je ispisati samo dužinu najdužeg puta, bez same putanje.
Ovde startni čvor nije unapred zadat. Zato svaki čvor može biti potencijalni početak puta.
Zadatak 3: Kritični put u projektu
Veliki projekat sastoji se od N aktivnosti. Svaka aktivnost ima određeno trajanje i može da počne tek kada se završe sve aktivnosti od kojih zavisi.
Zavisnosti između aktivnosti formiraju usmeren aciklični graf (DAG), jer nije moguće da aktivnost indirektno zavisi sama od sebe.
Potrebno je odrediti:
- minimalno vreme završetka celog projekta
- jedan kritični put aktivnosti
Zadatak 4: Maksimalna zarada kroz mrežu gradova
Dato je N gradova povezanih jednosmernim putevima. Svaki grad ima određenu zaradu koju možemo ostvariti ako ga posetimo.
Dozvoljeno je kretanje samo u smeru puteva, a graf je garantovano DAG.
Potrebno je odrediti:
- maksimalnu moguću zaradu
- jedan optimalan put
Povezane teme i preporučene lekcije
Ako želiš potpuno da savladaš rad sa DAG grafovima, topološkim sortiranjem i dinamičkim programiranjem nad grafovima, preporučuje se da prođeš i sledeće lekcije.
1. Topološko sortiranje
2. Detekcija ciklusa u DAG-u
3. SCC komponente (Kosaraju / Tarjan)
4. Najduži put u DAG-u
5. Najkraći putevi (Dijkstra)
Topološko sortiranje
Osnovna tehnika za obradu DAG grafova. Bez topološkog poretka nije moguće pravilno raditi DP nad DAG-om.
Kahn algoritam, DFS topološko sortiranje, detekciju ciklusa i pravilno procesiranje zavisnosti.
DAG i detekcija ciklusa
Pre nego što pokreneš DP nad grafom, moraš biti siguran da graf nema ciklus.
U ovoj lekciji obrađuje se:
- šta je DAG
- zašto ciklusi prave problem
- detekcija ciklusa pomoću DFS-a
- detekcija ciklusa pomoću Kahn algoritma
SCC komponente — Kosaraju i Tarjan
Ako graf sadrži cikluse, često ih prvo sabijamo u SCC komponente kako bismo dobili DAG nad komponentama.
Mnogi teški problemi nad opštim grafovima postaju mnogo jednostavniji kada se ciklusi sabiju u SCC komponente.
SCC komponente — Kosaraju i Tarjan algoritam
Dijkstra — najkraći put
Najduži put u DAG-u i Dijkstra algoritam često se porede, jer oba koriste relaksaciju grana i DP ideju, ali rešavaju različite probleme.
Kod Dijkstre tražimo:
minimum
dok kod najdužeg puta u DAG-u tražimo:
maximum
Dinamičko programiranje nad grafovima
DAG DP je jedan od najvažnijih primera dinamičkog programiranja nad grafovima.
Kasnije se iste ideje pojavljuju u:
- kritičnom putu u projektima
- raspoređivanju zadataka
- maksimalnoj zaradi
- brojanju puteva
- problemima zavisnosti
Topološki red rešava problem redosleda, a DP čuva najbolja rešenja. Zajedno daju veoma moćnu tehniku.