Uvod


U operacijskim istraživanjima, naš je temeljni cilj donositi bolje, kvantitativno argumentirane odluke. Linearno programiranje (LP) je i dalje naš osnovni alat za to – ono nam omogućuje optimalnu alokaciju ograničenih resursa (poput novca, vremena ili sirovina) kako bismo maksimizirali profit ili minimizirali trošak (Ahuja et al. 1993; Bertsimas & Tsitsiklis, 1997; Anderson et al., 2012).

No, u stvarnom svijetu susrećemo se s problemima koji premašuju pitanje jednostavne alokacije. Razmislite o logistici, telekomunikacijama, prometu ili čak vođenju projekata. Svi ti problemi imaju zajedničku strukturu: sastoje se od čvorova (lokacija, ljudi, zadataka) i bridova/lukova (cesta, kabela, veza) koji ih povezuju. Drugim riječima, oni su mreže.

Kako poslati robu iz tvornice u skladište? Kuda provući internetski promet da se izbjegne zagušenje? Koji je redoslijed zadataka ključan da projekt završi na vrijeme?

Ovdje na scenu stupaju mrežni modeli. Oni su elegantna ekstenzija linearnog programiranja, specijalizirana za rješavanje upravo ovakvih problema.

Svi mrežni problemi, u svojoj srži, temelje se na teoriji grafova (Van Steen, 2010; Diestel, 2017). Ne brinite, nećemo ulaziti u dokaze i leme, samo u osnovne pojmove koji nam nužni.


Graf je jednostavno matematička struktura koja modelira odnose (veze) između objekata.

  • Graf (\(G\)): Sastoji se od dva skupa: skupa čvorova (engl. vertices) i skupa bridova (engl. edges) ili lukova (engl. arcs). Pišemo \(G = (V, E)\).


  • Čvor (\(V\), Vertex/Node): Čvorovi mogu predstavljati lokacije, osobe, poduzeća i sl. U modelima u kojima čvorovi predstavljaju osobe, poduzeća ili autonomni računalni softver, čvorovi se često referiraju kao agenti ili akteri. Čvorovi mogu biti povezani bridovima ili lukovima.


  • Brid ili Luk (\(E\), Edge/Arc): Predstavlja “vezu” između dva čvora, a može se raditi o cestama, interakcijama, transakcijama i drugim oblicima povezanosti između čvorova.

    • Neusmjereni graf ima bridove: Veza je dvosmjerna (npr. prijateljstvo na Facebooku).
    • Usmjereni graf ima lukove: Veza ima smjer (npr. jednosmjerna ulica, praćenje na Twitteru). Logistički problemi su gotovo uvijek usmjereni – roba ide od tvornice do skladišta, a ne obrnuto.
    • Kad ističemo smjer, reći ćemo luk; za neusmjerene veze – brid.
    • Ako u mreži postoji \(n\) čvorova, tada mreža može imati i do \(\frac{n(n-1)}{2}\) brida ili \(n(n−1)\) lukova.
    • Ako su svi čvorovi međusobno povezani, tada je riječ o potpunom grafu.
    • Ukoliko u mreži, tj. grafu postoje nepovezane skupine čvorova, kaže se da se sastoji od komponenti. S obzirom na specifičan oblik, neki česti podgrafovi nazvani su npr. zvijezda ili klika.


  • Težinski graf (engl. weighted graph): Svakom bridu/luku dodijeljena je numerička vrijednost ili “težina”. Može se raditi o udaljenostima, trošku, broju interakcija i slično. Graf u kojem su bridovima pripisane težine zove se težinski graf.

    • U transportnom problemu, ta težina je, u pravilu, trošak (\(c_{ij}\)). U problemu najkraćeg puta, to je udaljenost ili vrijeme. U problemu maksimalnog toka, to je kapacitet.


Kada u operacijskim istraživanjima kažemo “Mreža” (engl. network), u pravilu mislimo na usmjereni, težinski graf.


Vizualni primjer mreže

Evo kako bismo vizualizirali jednostavnu mrežu koristeći DiagrammeR. Ovo je opći primjer grafa s 4 čvora (A, B, C, D) i usmjerenim, ponderiranim (težinskim) vezama.

grViz("
digraph simple_network {
  layout = dot;
  node [shape = circle, fontname = Helvetica];
  edge [fontname = Helvetica];
  
  A [label = 'Čvor A'];
  B [label = 'Čvor B'];
  C [label = 'Čvor C'];
  D [label = 'Čvor D'];
  
  A -> B [label = '5'];
  A -> C [label = '3'];
  B -> C [label = '2'];
  B -> D [label = '8'];
  C -> D [label = '4'];
}
")

Graf koji smo upravo nacrtali (simple_network) pomaže nam definirati još nekoliko ključnih pojmova.

Ovaj usmjereni, težinski graf \(G\) možemo formalno definirati kao par \(G = (V, E)\). Skup čvorova su jednostavno lokacije na našoj karti.

\[ V = \{A, B, C, D\} \]

Skup lukova su veze među tim čvorovima:

\[ E = \{(A, B), (A, C), (B, C), (B, D), (C,D)\} \]

što možemo zapisati i kao:

\[ E = \{x_{AB}, x_{AC}, x_{BC}, x_{BD}, x_{CD}\} \]

Ovdje je brid označen s \(x\), jer ćemo ga mi tako zapisivati u nastavku, iako se uobičajeno u zapisu koristi i \(e\) (pa bi tada to bilo: \(E = \{e_{AB}, e_{AC}, e_{BC}, e_{BD}, e_{CD}\}\)).

  • Matrica susjedstva je najprecizniji način da se opišu veze (bridovi, lukovi). Matrica susjedstva (engl. adjacency matrix), \(A\), opisuje topologiju (strukturu) grafa. Element \(A_{ij}\) je 1 ako postoji luk koji ide iz čvora \(i\) u čvor \(j\), a 0 ako ne postoji.

\(A_{ij} = \begin{cases} 1, & \text{ako postoji luk } (i, j) \in E \\ 0, & \text{inače} \end{cases}\)

\(A_{ij}\) A B C D
A 0 1 1 0
B 0 0 1 1
C 0 0 0 1
D 0 0 0 0

Čitamo:

  • \(A_{AC} = 1\): Postoji veza iz A u C.
  • \(A_{CA} = 0\): Ne postoji veza iz C u A.


  • Susjedi (engl. neighbors): Dva čvora su susjedi ako su izravno povezani bridom/lukom. Ovdje moramo biti precizni, jer je naš graf usmjeren:

    • Izlazni susjedi (engl. out-neighbors): Čvor \(A\) ima dva izlazna susjeda: \(B\) i \(C\). To su čvorovi do kojih \(A\) može izravno doći.
    • Ulazni susjedi (engl. in-neighbors): Čvor \(D\) ima dva ulazna susjeda: \(B\) i \(C\). To su čvorovi koji mogu izravno doći do \(D\).
    • U neusmjerenom grafu (bez strelica), susjedstvo je simetrično: ako je \(A\) susjed \(B\), onda je i \(B\) susjed \(A\).


  • Susjedstvo (engl. neighborhood): Skup svih susjeda jednog čvora.

    • Izlazno susjedstvo čvora \(A\) je skup \(\{B, C\}\).
    • Ulazno susjedstvo čvora \(D\) je skup \(\{B, C\}\).
    • Zašto je ovo važno? Mnogi mrežni algoritmi (npr. pretraživanje, traženje puta, analiza toka) u suštini se oslanjaju na sustavno pretraživanje susjedstva čvorova kako bi donijeli odluku o sljedećem koraku.


  • Matrica težina (ili troškova), \(C\), definira “cijenu” putovanja po lukovima definiranim u matrici \(A\).

Ako ne postoji luk (gdje je \(A_{ij} = 0\)), trošak putovanja tim putem smatramo beskonačnim (\(\infty\)), jer je put nemoguć.

\(C_{ij} = \begin{cases} \text{težina luka}(i, j), & \text{ako } A_{ij} = 1 \\ \infty, & \text{ako } A_{ij} = 0 \text{ i } i \neq j \\ 0, & \text{ako } i = j \end{cases}\)

\(C_{ij}\) A B C D
A 0 5 3 \(\infty\)
B \(\infty\) 0 2 8
C \(\infty\) \(\infty\) 0 4
D \(\infty\) \(\infty\) \(\infty\) 0

Napomena: U praksi solveri umjesto \(\infty\) koriste veliki broj (Big-M, npr. \(10^6\) ili veći od svake realne rute) kako bi nedopuštene veze bile “ekonomski nemoguće”, ali numerički konačne.


Iako su matrica susjedstva (\(A\)) i matrica težina (\(C\)) teorijski precizan način definiranja grafa, u računarskoj praksi one često nisu najučinkovitiji format za pohranu, posebno kod velikih mreža (Cormen et al., 2009). Alternativni i često preferirani pristup je eksplicitno navođenje samo onih lukova koji postoje putem liste lukova (engl. edge list). Lista lukova je tablična struktura, tipično s tri stupca: (izvor, odredište, težina).

Za naš primjer, lista lukova \(E\) je:

\[ E = \{ (A, B, 5), (A, C, 3), (B, C, 2), (B, D, 8), (C, D, 4) \} \]

Ili, u obliku data.frame (kako bi to R paketi očekivali):

from to weight
A B 5
A C 3
B C 2
B D 8
C D 4


  • Put (engl. path):

    • Što je to? Niz povezanih lukova koji vode od jednog čvora do drugog, poštujući smjer lukova. Put \(A \to D\) u našem gornjem primjeru može biti \(A \to C \to D\).
    • Zašto je bitno? Pojam puta je fundamentalan. Algoritmi poput Dijkstrinog ili Bellman-Fordovog traže put s najmanjom ukupnom težinom (tzv. najkraći put). Analiza dosežnosti (može li se uopće doći od A do B?) ovisi o samom postojanju puta.


  • Ciklus (engl. cycle):

    • Što je to? Put koji počinje i završava u istom čvoru. Na primjer, kad bi u našem grafu postojao luk \(D \to A\), imali bismo ciklus \(A \to C \to D \to A\).

    • Zašto je bitno? Ciklusi su izuzetno važni.

      • U problemima najkraćeg puta, ciklusi s negativnim zbrojem težina (npr. put \(B \to C \to B\) s ukupnim troškom -5) mogu stvoriti paradoks “beskonačno jeftinog” puta i algoritam se neće moći izvršiti (npr. Bellman-Ford detektira takve cikluse).
      • U projektnom planiranju (PERT/CPM), ciklus bi značio nemoguću situaciju logičke ovisnosti (npr. “Zadatak A mora biti gotov prije Zadatka B, a Zadatak B prije Zadatka A”).


  • Izvor (engl. source) i Ponor (engl. sink):

    • Što je to? Ovo su formalni nazivi za početne i završne točke u mnogim mrežnim problemima.
    • Izvor (source): Čvor koji nema ulaznih lukova (njegov “ulazni stupanj” ili in-degree je 0). Sve “teče” iz njega.
    • Ponor/ odredište (sink): Čvor koji nema izlaznih lukova (njegov “izlazni stupanj” ili out-degree je 0). Sve “teče” u njega.
    • Zašto je bitno? Ovi su pojmovi temelj za probleme toka (engl. flow problems). U problemu maksimalnog toka, cilj je poslati što više “materijala” (npr. podataka, vode, prometa) od jednog ili više izvora do jednog ili više ponora, poštujući kapacitete lukova.


  • Povezanost (engl. connectedness):

    • Što je to? Opisuje je li graf “u jednom komadu”. Usmjereni graf je jako povezan (strongly connected) ako iz svakog čvora možete doći do svakog drugog. Slabo je povezan (weakly connected) ako je povezan, ali samo ako ignorirate smjerove lukova (tretirate ga kao neusmjerenog).
    • Dosežnost kaže da je čvor \(v\) dosežan iz \(u\) ako postoji put od \(u\) do \(v\). Skup svih čvorova dosežnih iz \(u\) čini komponentu dosežnosti čvora \(u\) (u usmjerenom grafu razlikujemo “prema naprijed” i “prema natrag”).
    • Zašto je bitno? Povezanost je osnovna provjera “zdravlja” mreže. Prije pokretanja algoritma za traženje puta od A do B, moramo znati je li B uopće dosežan iz A. Analiza povezanosti može identificirati izolirane dijelove mreže (komponente) koji se moraju promatrati zasebno.

Ovi osnovni pojmovi čine “abecedu” kojom ćemo opisivati i rješavati sve složenije mrežne modele, počevši od transportnog problema. Neki od osnovnih mrežnih modela koje susrećemo u praksi su:


  • Transportni problem (engl. transportation problem):

    • Pitanje: Kako optimalno rasporediti dostupnu ponudu s \(m\) izvora da bi se zadovoljila potražnja na \(n\) odredišta, uz minimalni ukupni trošak?
    • Primjer: Distribucija robe iz tvornica u skladišta, alokacija sirovina na proizvodne linije.


  • Problem najkraćeg puta (engl. shortest path pProblem):

    • Pitanje: Koji je najbrži / najjeftiniji / najkraći put od točke A do točke B?
    • Primjer: Google Maps, usmjeravanje (rutanje) podataka na internetu.


  • Problem maksimalnog toka (engl.maximum flow problem):

    • Pitanje: Koji je maksimalni kapacitet (npr. podataka, vode, vozila) koji možemo “progurati” kroz mrežu od izvora do ponora?
    • Primjer: Dizajn telekomunikacijskih mreža, cjevovoda, planiranje evakuacije.


  • PERT/CPM (metode mrežnog planiranja):

    • Pitanje: Koji su zadaci u projektu kritični? Koliko će projekt minimalno trajati?
    • Primjer: Upravljanje projektima u građevini, softverskom inženjerstvu, organizaciji događaja.


  • Problem dodjele (engl. assignment problem):

    • Pitanje: Kako dodijeliti \(n\) agenata (npr. radnika, strojeva) na \(n\) zadataka, strogo jedan-na-jedan, tako da se minimizira ukupni trošak (ili vrijeme) ili maksimizira ukupna učinkovitost?
    • Primjer: Dodjela zadataka zaposlenicima, profesora na kolegije, taksija na vožnje. (Ovo je, pak, specijalni slučaj transportnog problema).


  • Problem trgovačkog putnika (engl. TSP - Traveling Salesperson Problem):

    • Pitanje: Koji je najkraći put koji posjećuje svaki grad točno jednom i vraća se na početak?
    • Primjer: Optimizacija ruta dostave (Glovo, pošta), planiranje bušenja rupa na tiskanim pločicama (Ovo je znatno teži, NP-težak problem, ali spada u istu domenu).


  • Minimalno razapinjuće stablo (engl. Minimum Spanning Tree - MST):

    • Pitanje: Koji je najjeftiniji način da se povežu svi čvorovi u mreži u jedinstvenu infrastrukturu, bez stvaranja krugova (ciklusa)?
    • Primjer: Postavljanje temeljne infrastrukture (struja, voda, optički kabeli) koja mora doseći svaku kuću uz minimalni utrošak materijala.


  • Problem minimalnog troška toka (engl. Minimum Cost Flow - MCF):

    • Pitanje: Kako poslati određenu količinu toka (npr. 5000 jedinica) od izvora do ponora, poštujući i kapacitete i troškove na svakoj ruti, uz minimalni ukupni trošak?
    • Primjer: Složeni logistički lanci. (Ovo objedinjava više problema; i najkraći put i transportni problem su njegovi specijalni slučajevi).



Transportni problem

Transportni problem će nam poslužiti kao ulaznica u svijet mrežnih modela, kojima pristupamo kao ekstenzijama linearnog programiranja. On izravno odgovara na temeljno pitanje alokacije: kako optimalno rasporediti dostupnu ponudu s \(m\) izvora da bi se zadovoljila potražnja na \(n\) odredišta, najčešće uz minimalni ukupni trošak?

Jednu od tipičnih varijanti u svom radu koriste Nahar et al. (2018) pri minimizaciji troška distribucije riže:


Definicija


Transportni problem (TP) je posebna klasa problema linearnog programiranja (LP) koja se bavi minimiziranjem troškova distribucije homogenog proizvoda iz određenog broja izvora (npr. tvornica) do određenog broja odredišta (npr. skladišta).

Svaki izvor ima ograničenu ponudu (kapacitet), a svako odredište ima određenu potražnju. Cilj je zadovoljiti potražnju odredišta unutar ograničenja ponude izvora, uz minimalne ukupne troškove transporta (Ahuja et al., 1993; Bazaraa et al., 2010).

Primjer vizualizacije transportnog problema:


Veza s linearnim programiranjem

TP je specijalni slučaj LP-a jer ima:

  1. Funkciju cilja: (najčešće) minimizacija ukupnih troškova transporta

  2. Ograničenja:

  • Količina poslana iz svakog izvora ne smije premašiti njegov kapacitet (ponudu)
  • Količina primljena na svakom odredištu mora zadovoljiti potraživanu količinu (potražnju)
  1. Nenegativnost: Količina poslana bilo kojom rutom ne može biti negativna.

Zbog svoje specifične strukture (svi koeficijenti u ograničenjima su ili 0 ili 1), TP se može rješavati efikasnijim algoritmima od standardnog Simplex algoritma, iako se i Simplex može koristiti.


Matematička formulacija


Pretpostavimo da imamo \(m\) izvora i \(n\) odredišta.

Parametri:

  • \(m\): Broj izvora (npr. tvornice)
  • \(n\): Broj odredišta (npr. skladišta)
  • \(s_i\): Ponuda (kapacitet) izvora \(i\) (za \(i = 1, \dots, m\))
  • \(d_j\): Potražnja odredišta \(j\) (za \(j = 1, \dots, n\))
  • \(c_{ij}\): Jedinični trošak transporta od izvora \(i\) do odredišta \(j\)

Varijable odluke:

  • \(x_{ij}\): Količina proizvoda koja se transportira od izvora \(i\) do odredišta \(j\)

Funkcija cilja (minimizacija troškova):

\[ \min Z = \sum_{i=1}^{m} \sum_{j=1}^{n} c_{ij} x_{ij} \]

Ograničenja:

  1. Ograničenja ponude (za svaki izvor \(i\)): \[ \sum_{j=1}^{n} x_{ij} \le s_i \quad \text{za} \quad i = 1, \dots, m \] (Količina poslana iz izvora \(i\) ne može premašiti njegovu ponudu)

  2. Ograničenja potražnje (za svako odredište \(j\)): \[ \sum_{i=1}^{m} x_{ij} \ge d_j \quad \text{za} \quad j = 1, \dots, n \] (Količina primljena na odredištu \(j\) mora zadovoljiti njegovu potražnju)

  3. Ograničenje nenegativnosti: \[ x_{ij} \ge 0 \quad \text{za sve} \quad i, j \]


Važna napomena: balansirani vs. nebalansirani problemi

  • Problem je balansiran ako je ukupna ponuda jednaka ukupnoj potražnji: \(\sum_{i=1}^{m} s_i = \sum_{j=1}^{n} d_j\).
  • U tom slučaju, sva ograničenja ponude i potražnje postaju jednakosti (\(=\)).
  • Kod minimizacije, nebalansiran TP često se zadaje s ≤ (ponuda) i ≥ (potražnja); balansiran TP pišemo strogo kao jednakosti. U praksi se često se uvodi “fiktivni” izvor (ako potražnja > ponuda) ili “fiktivno” odredište (ako ponuda > potražnja) kako bi se problem izbalansirao, obično s troškovima transporta 0.


Primjer

Recimo da ste Vi menadžer/ica u “Agrosfera Savjetovanje - AS” i Vaš je zadatak organizirati tjednu isporuku svježih rajčica iz tri glavna proizvodna centra (koje ćemo zvati polazišta ili izvori) u četiri velika distributivna centra (odredišta).

  • Polazišta: Vaša polja i staklenici nalaze se u Vrani, okolici Osijeka i okolici Pule.
  • Odredišta: Vaši glavni klijenti (veleprodaje i trgovački lanci) nalaze se u Zagrebu, Splitu, Rijeci i Dubrovniku.

Vaš zadatak je zadovoljiti potražnju svakog odredišta bez premašivanja ponude svakog izvora, i to sve uz minimalne troškove transporta.

Logistički odjel vam je dao podatke o tjednoj ponudi (koliko tona svaka lokacija može proizvesti), tjednoj potražnji (koliko tona svaki grad traži) i troškovima prijevoza (u EUR po toni) za svaku moguću rutu. Takvi podaci mogu se sažeti u jednu, preglednu tablicu (ovo je standardni način zadavanja transportnog problema).


Polazište (Izvor) Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4) Ponuda (tone)
Vrana (P1) 8 3 7 5 100
Osijek (P2) 4 12 9 16 150
Pula (P3) 6 11 2 18 80
Potražnja (tone) 120 90 70 50 330 / 330


Iščitavanje tablice:

  • Brojevi u “ćelijama” (npr. 8, 3, 7…) predstavljaju jedinični trošak \(c_{ij}\). Na primjer, slanje jedne tone iz Vrane (P1) u Zagreb (O1) košta 8 EUR. Slanje tone iz Pule (P3) u Rijeku (O3) košta samo 2 EUR.
  • Ponuda (Supply): Vrana nudi 100 tona, Osijek 150, a Pula 80.
  • Potražnja (Demand): Zagreb traži 120 tona, Split 90, Rijeka 70, a Dubrovnik 50.


Provjera balansa:

  • Ukupna ponuda = 100 + 150 + 80 = 330 tona
  • Ukupna potražnja = 120 + 90 + 70 + 50 = 330 tona
  • Budući da je ponuda = potražnja, problem je balansiran. Ovo je idealan scenarij.


U sljedećem koraku grafički prikazujemo problem. Ovo je bipartitni graf (particije: izvori | odredišta). Bipartitni graf je graf u kojem se čvorovi mogu podijeliti u dvije neprazne grupe tako da svi lukovi idu samo između grupa (nikad unutar iste). U TP-u, grupe su izvori i odredišta. U ovom grafu, na lijevoj strani su izvori (ponuda), na desnoj odredišta (potražnja), a lukovi (veze) predstavljaju sve moguće rute, svaka sa svojim troškom.

grViz("
digraph transport_problem {
  layout = dot;
  rankdir = LR;
  splines = false;
  nodesep = 0.5;
  ranksep = 2.5;

  node [shape = record, style = filled, fontname = Helvetica, fontsize=12];
  
  # Postavljamo globalne atribute za SVE lukove
  # Manji font i 'taillabel' da se makne s centra
  edge [fontsize=10, fontcolor=\"#444444\"];

  subgraph cluster_izvori {
    label = 'Polazišta (Izvori)';
    color = 'blue';
    P1 [label = '{Vrana (P1) | Ponuda: 100 t}'];
    P2 [label = '{Osijek (P2) | Ponuda: 150 t}'];
    P3 [label = '{Pula (P3) | Ponuda: 80 t}'];
  }
  
  subgraph cluster_odredista {
    label = 'Odredišta';
    color = 'darkgreen';
    O1 [label = '{Zagreb (O1) | Potražnja: 120 t}'];
    O2 [label = '{Split (O2) | Potražnja: 90 t}'];
    O3 [label = '{Rijeka (O3) | Potražnja: 70 t}'];
    O4 [label = '{Dubrovnik (O4) | Potražnja: 50 t}'];
  }
  
  { rank = same; P1; P2; P3; }
  { rank = same; O1; O2; O3; O4; }
  

  # Iz P1 (Vrana)
  P1 -> O1 [taillabel = ' 8€'];  # Možemo ostaviti 'label' ili koristiti 'taillabel'
  P1 -> O2 [taillabel = ' 3€'];  # Graphviz će ih s 'splines=false' bolje pozicionirati
  P1 -> O3 [taillabel = ' 7€'];
  P1 -> O4 [taillabel = ' 5€'];
  
  # Iz P2 (Osijek)
  P2 -> O1 [taillabel = ' 4€'];
  P2 -> O2 [taillabel = ' 12€'];
  P2 -> O3 [taillabel = ' 9€'];
  P2 -> O4 [taillabel = ' 16€'];
  
  # Iz P3 (Pula)
  P3 -> O1 [taillabel = ' 6€'];
  P3 -> O2 [taillabel = ' 11€'];
  P3 -> O3 [taillabel = ' 2€'];
  P3 -> O4 [taillabel = ' 18€'];
}
")

Kreiranje strukture problema

Indeksi:

  • Neka je \(i\) indeks za polazišta (izvore), \(i \in \{1, 2, 3\}\), gdje:

    • \(1\) = Vrana
    • \(2\) = Osijek
    • \(3\) = Pula
  • Neka je \(j\) indeks za odredišta, \(j \in \{1, 2, 3, 4\}\), gdje:

    • \(1\) = Zagreb
    • \(2\) = Split
    • \(3\) = Rijeka
    • \(4\) = Dubrovnik

Parametri (poznate vrijednosti):

  • \(c_{ij}\): Jedinični trošak transporta od polazišta \(i\) do odredišta \(j\) (vrijednosti iz naše tablice).

  • \(s_i\): Ponuda (kapacitet) polazišta \(i\).

    • \(s_1 = 100\) (Vrana)
    • \(s_2 = 150\) (Osijek)
    • \(s_3 = 80\) (Pula)
  • \(d_j\): Potražnja odredišta \(j\).

    • \(d_1 = 120\) (Zagreb)
    • \(d_2 = 90\) (Split)
    • \(d_3 = 70\) (Rijeka)
    • \(d_4 = 50\) (Dubrovnik)

Varijable odluke (što tražimo):

  • \(x_{ij}\): Količina (u tonama) rajčica koja se transportira od polazišta \(i\) do odredišta \(j\), preciznije:

    • \(x_{11}\): Količina (tone) iz Vrane (P1) za Zagreb (O1)
    • \(x_{12}\): Količina (tone) iz Vrane (P1) za Split (O2)
    • \(x_{13}\): Količina (tone) iz Vrane (P1) za Rijeku (O3)
    • \(x_{14}\): Količina (tone) iz Vrane (P1) za Dubrovnik (O4)
    • \(x_{21}\): Količina (tone) iz Osijeka (P2) za Zagreb (O1)
    • \(x_{22}\): Količina (tone) iz Osijeka (P2) za Split (O2)
    • \(x_{23}\): Količina (tone) iz Osijeka (P2) za Rijeku (O3)
    • \(x_{24}\): Količina (tone) iz Osijeka (P2) za Dubrovnik (O4)
    • \(x_{31}\): Količina (tone) iz Pule (P3) za Zagreb (O1)
    • \(x_{32}\): Količina (tone) iz Pule (P3) za Split (O2)
    • \(x_{33}\): Količina (tone) iz Pule (P3) za Rijeku (O3)
    • \(x_{34}\): Količina (tone) iz Pule (P3) za Dubrovnik (O4)

Funkcija cilja (minimizacija troškova):

Želimo minimizirati ukupan trošak, koji je zbroj svih poslanih količina pomnoženih s njihovim jediničnim troškovima.

\[ \min Z = \sum_{i=1}^{3} \sum_{j=1}^{4} c_{ij} x_{ij} \]

Ili, raspisano s našim podacima: \[ \min ( 8x_{11} + 3x_{12} + 7x_{13} + 5x_{14} \quad \text{(Rute iz Vrane)} \\ \qquad + 4x_{21} + 12x_{22} + 9x_{23} + 16x_{24} \quad \text{(Rute iz Osijeka)} \\ \qquad + 6x_{31} + 11x_{32} + 2x_{33} + 18x_{34}) \quad \text{(Rute iz Pule)} \]

Ograničenja:

Budući da je problem balansiran (\(\sum s_i = \sum d_j = 330\)), sva ograničenja možemo pisati kao stroge jednakosti (osobito na strani potražnje).

1. Ograničenja ponude (engl. supply constraints):

Količina poslana iz svakog polazišta mora biti jednaka njegovoj ukupnoj ponudi.

  • \(x_{11} + x_{12} + x_{13} + x_{14} = 100 \quad \text{(Vrana)}\)
  • \(x_{21} + x_{22} + x_{23} + x_{24} = 150 \quad \text{(Osijek)}\)
  • \(x_{31} + x_{32} + x_{33} + x_{34} = 80 \quad \text{(Pula)}\)

2. Ograničenja potražnje (engl.demand constraints):

Količina primljena u svako odredište mora biti jednaka njegovoj potražnji.

  • \(x_{11} + x_{21} + x_{31} = 120 \quad \text{(Zagreb)}\)
  • \(x_{12} + x_{22} + x_{32} = 90 \quad \text{(Split)}\)
  • \(x_{13} + x_{23} + x_{33} = 70 \quad \text{(Rijeka)}\)
  • \(x_{14} + x_{24} + x_{34} = 50 \quad \text{(Dubrovnik)}\)

3. Ograničenje nenegativnosti:

Ne možemo slati negativne količine rajčica. \[ x_{ij} \ge 0 \quad \text{za sve} \quad i, j \]

Rješavanje koristeći lp()

library(lpSolve)

# Vektor funkcije cilja (c) - troškovi
# Redom: x11, x12, x13, x14, x21, x22, ...
f.obj <- c(8, 3, 7, 5, 4, 12, 9, 16, 6, 11, 2, 18)

# Matrica ograničenja (A) - 7 redova (ograničenja) x 12 stupaca (varijabli)
# Kreiramo ju iz jednog vektora, redak po redak
matrica_A_podaci <- c(
  # 3 reda ograničenja PONUDE (svaki redak = 1 polazište)
  1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, # Vrana (P1)
  0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, # Osijek (P2)
  0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, # Pula (P3)
  
  # 4 reda ograničenja POTRAŽNJE (svaki redak = 1 odredište)
  1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, # Zagreb (O1)
  0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, # Split (O2)
  0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1, 0, # Rijeka (O3)
  0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0, 1  # Dubrovnik (O4)
)

f.con <- matrix(matrica_A_podaci, nrow = 7, byrow = TRUE)

# Vektor smjerova ograničenja
# Problem je balansiran, stoga sva ograničenja mogu biti jednakosti "="
f.dir <- rep("=", 7)

# Vektor desne strane (b) - Vrijednosti ponude pa potražnje
f.rhs <- c(100, 150, 80,   # Ponude (P1, P2, P3)
           120, 90, 70, 50) # Potražnje (O1, O2, O3, O4)

# (Opcionalno) Dodavanje imena za lakšu provjeru
colnames(f.con) <- c("x11", "x12", "x13", "x14", "x21", "x22", "x23", "x24", "x31", "x32", "x33", "x34")
rownames(f.con) <- c("P1_Pon", "P2_Pon", "P3_Pon", "O1_Pot", "O2_Pot", "O3_Pot", "O4_Pot")

# Brza provjera
f.con
##        x11 x12 x13 x14 x21 x22 x23 x24 x31 x32 x33 x34
## P1_Pon   1   1   1   1   0   0   0   0   0   0   0   0
## P2_Pon   0   0   0   0   1   1   1   1   0   0   0   0
## P3_Pon   0   0   0   0   0   0   0   0   1   1   1   1
## O1_Pot   1   0   0   0   1   0   0   0   1   0   0   0
## O2_Pot   0   1   0   0   0   1   0   0   0   1   0   0
## O3_Pot   0   0   1   0   0   0   1   0   0   0   1   0
## O4_Pot   0   0   0   1   0   0   0   1   0   0   0   1
lp_rjesenje <- lp(direction = "min",
                  objective.in = f.obj,
                  const.mat = f.con,
                  const.dir = f.dir,
                  const.rhs = f.rhs,
                  compute.sens = 1)

cat("Optimalni ukupni trošak (Z):", lp_rjesenje$objval, "EUR\n")
## Optimalni ukupni trošak (Z): 1490 EUR
optimalni_plan <- matrix(lp_rjesenje$solution, nrow = 3, byrow = TRUE)

# Dodajemo imena za čitljivost
rownames(optimalni_plan) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(optimalni_plan) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

print(optimalni_plan)
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)            0         50           0             50
## Osijek (P2)         120         30           0              0
## Pula (P3)             0         10          70              0

Iščitavanje:

  • Vrana šalje 50t za Split i 50t za Dubrovnik.
  • Osijek šalje 120t za Zagreb i 30t za Split.
  • Pula šalje 10t za Split i 70t za Rijeku.
sjene_cijena <- lp_rjesenje$duals[1:7]

# Dajmo im imena da znamo što gledamo
names(sjene_cijena) <- rownames(f.con)

print(sjene_cijena)
## P1_Pon P2_Pon P3_Pon O1_Pot O2_Pot O3_Pot O4_Pot 
##     -6      3      2      1      9      0     11
  • \(O3\_Pot\) (Rijeka) = 0: U ovom rješenju, solver je postavio Rijeku kao referentnu točku. Sve ostale vrijednosti tumačimo u odnosu na nju. (U balansiranom problemu, jedno ograničenje je uvijek redundantno, pa mu solver dodijeli sjenu cijene 0).
  • \(O1\_Pot\) (Zagreb) = 1: Ako se potražnja u Zagrebu poveća za 1 tonu (sa 120 na 121), ukupni trošak (1490€) raste za 1€.
  • \(O2\_Pot\) (Split) = 9: Ako Split zatraži 1 tonu više, ukupni trošak raste za 9€.
  • \(O4\_Pot\) (Dubrovnik) = 11: Dubrovnik je “najskuplje” odredište za povećanje potražnje; +1 tona košta nas +11€.
  • \(P1\_Pon\) (Vrana) = -6: Ako Vrana može ponuditi 1 tonu više (sa 100 na 101), naš ukupni trošak se smanjuje za 6€. Vrana je, dakle, vrlo “vrijedan” izvor.
  • \(P2\_Pon\) (Osijek) = 3: Ako Osijek ponudi 1 tonu više, naš ukupni trošak zapravo raste za 3€. To nam govori da je Osijek, u kontekstu cijelog sustava, “skup” izvor i da bi nam bilo draže da taj višak dođe odnekud drugdje (poput Vrane).

Reducirani troškovi (dualne vrijednosti varijabli) - ovo nam govori o rutama koje ne koristimo:

# Vrijednosti od 8. do 19. u $duals (ili $duals.from)
# Formatiramo ih u matricu
reducirani_troskovi <- matrix(lp_rjesenje$duals[8:19], nrow = 3, byrow = TRUE)

# Dodajemo imena za čitljivost
rownames(reducirani_troskovi) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(reducirani_troskovi) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

print(round(reducirani_troskovi, 2))
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)           13          0          13              0
## Osijek (P2)           0          0           6              2
## Pula (P3)             3          0           0              5
  • NULE (0): Sve rute s vrijednošću reduciranih troškova 0 su ili u bazi (koriste se), ili su nebazične ali postoji alternativno optimalno rješenje u kojem se mogu koristiti. Ovdje te rute već koristimo (npr. Vrana -> Split, Osijek -> Zagreb, Pula -> Rijeka…). Logično je da je “kazna” za njihovo korištenje nula, jer su već najbolje.
  • Vrana -> Zagreb = 13: Ako bismo na silu poslali 1 tonu iz Vrane u Zagreb (što ne radimo), ukupni trošak bi porastao za 13€.
  • Vrana -> Rijeka = 13: Isto vrijedi i za ovu rutu. Dodatnih 13€ troška po toni.
  • Osijek -> Rijeka = 6: Ova ruta je 6€ skuplja po toni od optimalne alternative.
  • Pula -> Dubrovnik = 5: Ruta koju definitivno želimo izbjeći. Slanje 1 tone ovuda povećalo bi ukupni trošak za 5€.

Sljedeće odgovaramo na pitanje: “Koliko se trošak jedne rute (npr. Vrana -> Split) smije promijeniti prije nego što se naš cijeli optimalni plan dostave (ona matrica \(x_{ij}\)) mora mijenjati?”

# 1. Stvarni troškovi (Originalni f.obj) - Za usporedbu
stvarni_troskovi <- matrix(f.obj, nrow = 3, byrow = TRUE)
rownames(stvarni_troskovi) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(stvarni_troskovi) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

# 2. Donje granice raspona (Koliko trošak smije PASTI)
donje_granice_c <- matrix(lp_rjesenje$sens.coef.from, nrow = 3, byrow = TRUE)
rownames(donje_granice_c) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(donje_granice_c) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

# 3. Gornje granice raspona (Koliko trošak smije RASTI)
gornje_granice_c <- matrix(lp_rjesenje$sens.coef.to, nrow = 3, byrow = TRUE)
rownames(gornje_granice_c) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(gornje_granice_c) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

# Ispisimo sve tri za laku usporedbu
print("--- Stvarni Troškovi (EUR/toni) ---")
## [1] "--- Stvarni Troškovi (EUR/toni) ---"
print(stvarni_troskovi)
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)            8          3           7              5
## Osijek (P2)           4         12           9             16
## Pula (P3)             6         11           2             18
print("--- Donja Granica Troška (sens.coef.from) ---")
## [1] "--- Donja Granica Troška (sens.coef.from) ---"
print(round(donje_granice_c, 2))
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)       -5e+00          1      -6e+00       -1.0e+30
## Osijek (P2)      -1e+30          9       3e+00        1.4e+01
## Pula (P3)         3e+00          5      -1e+30        1.3e+01
print("--- Gornja Granica Troška (sens.coef.to) ---")
## [1] "--- Gornja Granica Troška (sens.coef.to) ---"
print(round(gornje_granice_c, 2))
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)        1e+30         16       1e+30          7e+00
## Osijek (P2)       7e+00         14       1e+30          1e+30
## Pula (P3)         1e+30         14       8e+00          1e+30

Rezultati su prikazani u tri matrice: stvarni troškovi, donje granice dopuštenog raspona i gornje granice. Tumačenje raspona koeficijenata uz varijable odluka u funkciji cilja (troškovi):

Ove raspone tumačimo odvojeno za rute koje su dio rješenja (bazične varijable) i one koje nisu (nebazične).

  1. Rute koje KORISTIMO (npr. Pula -> Rijeka, \(x_{33}\)):
  • Stvarni trošak: 2 €
  • Raspon: [ -1.0e+30 (beskonačno) , 8.00 € ]
  • Tumačenje: Trenutni optimalni plan (slanje 70t ovom rutom) vrijedi sve dok trošak prijevoza na relaciji Pula-Rijeka ostaje između -\(\infty\) i 8 €. Ako bi taj trošak porastao na, primjerice, 9 €, trenutni plan više ne bi bio optimalan. Solver bi morao pronaći novo rješenje, vjerojatno preusmjeravanjem dijela pošiljki na drugu, sada isplativiju rutu (npr. Osijek -> Rijeka).
  1. Rute koje NE KORISTIMO (npr. Vrana -> Zagreb, \(x_{11}\)):
  • Stvarni trošak: 8 €
  • Raspon: [ -5.00 € , 1.0e+30 (beskonačno) ]
  • Tumačenje: Ova ruta nije dio rješenja. Da bi postala dio optimalnog rješenja, njen trošak bi morao pasti ispod -5 € (što je StvarniTrošak - ReduciraniTrošak, odnosno 8 - 13 = -5). Budući da je stvarni trošak 8 €, a granica -5 €, ova ruta je daleko od isplativosti i promjene cijene (osim drastičnog pada) neće utjecati na naš plan.

Sljedeći dio odgovara na pitanje: “OK, izračunali smo sjene cijena (npr. +1 tona za Zagreb košta 1€). Ali do kada vrijedi ta računica?”

Sve dok se naša ponuda i potražnja kreću unutar ovih granica, naše sjene cijena su važeće.

# 1. Uzimamo postojeće vrijednosti za usporedbu
trenutni_rhs <- f.rhs
sjene_cijena <- lp_rjesenje$duals[1:7] # Već izračunato

# 2. Izvlačimo raspone SAMO za prvih 7 ograničenja
# (ostatak vektora odnosi se na granice varijabli, ne ograničenja)
donje_granice_rhs <- lp_rjesenje$duals.from[1:7]
gornje_granice_rhs <- lp_rjesenje$duals.to[1:7]

# 3. Spojimo sve u jednu preglednu tablicu
rhs_analiza <- data.frame(
  Trenutni_RHS = trenutni_rhs,
  Sjena_Cijena = sjene_cijena,
  Donja_Granica_RHS = donje_granice_rhs,
  Gornja_Granica_RHS = gornje_granice_rhs
)

# Dodajemo imena redaka koja već imamo
rownames(rhs_analiza) <- rownames(f.con)
print(round(rhs_analiza, 2))
##        Trenutni_RHS Sjena_Cijena Donja_Granica_RHS Gornja_Granica_RHS
## P1_Pon          100           -6           1.0e+02            1.0e+02
## P2_Pon          150            3           1.5e+02            1.5e+02
## P3_Pon           80            2           8.0e+01            8.0e+01
## O1_Pot          120            1           1.2e+02            1.2e+02
## O2_Pot           90            9           9.0e+01            9.0e+01
## O3_Pot           70            0          -1.0e+30            1.0e+30
## O4_Pot           50           11           5.0e+01            5.0e+01

Tumačenje raspona ograničenja (fenomen degeneracije):

Degenerirano rješenje znači da barem jedna bazična varijabla u optimumu ima vrijednost 0; posljedica su nulti ili “točkasti” RHS-rasponi i nestabilne sjene cijene — svaka mala promjena traži reoptimizaciju.

Izlazna tablica pokazuje izuzetno važan fenomen: degeneraciju.

  • Za gotovo svako ograničenje (osim O3_Pot), Donja_Granica_RHS i Gornja_Granica_RHS jednake su Trenutni_RHS (npr. za P1_Pon, raspon je [100, 100]).

  • Tumačenje: Ovo znači da je optimalno rješenje degenerirano. U praktičnom smislu, izračunate sjene cijena su iznimno osjetljive.

    • Na primjer, sjena cijena za P1_Pon (Vrana) iznosi -6 €. To znači da bi 1 dodatna tona ponude iz Vrane smanjila ukupni trošak za 6 €.
    • MEĐUTIM, zbog raspona [100, 100], ta sjena cijena vrijedi samo i isključivo za trenutnu vrijednost od 100 tona. Čim bi se ponuda promijenila na 101 tonu (ili 99 tona), ta sjena cijena od -6 € više ne bi bila valjana.
    • Isto vrijedi i za O1_Pot (Zagreb): sjena cijena od 1 € vrijedi samo za potražnju od točno 120 tona.
  • sjene cijena su točne, ali ih se ne može koristiti za predviđanje promjene ukupnog troška ako se ponuda ili potražnja promijene (tzv. “what-if” analiza). Svaka promjena u desnoj strani ograničenja (osim za O3_Pot) zahtijevala bi ponovno pokretanje optimizacije kako bi se dobile nove, valjane sjene cijena.

  • Izuzetak ($O3_Pot$ - Rijeka): sjena cijena je 0 (Rijeka je referentna točka), a raspon je beskonačan. To znači da promjene u potražnji Rijeke ne utječu na sjene cijena ostalih ograničenja.

Rješavanje koristeći lp.transport()

Uz klasični pristup i funkcijulp(), u dokumentaciji ćete vidjeti da postoji i lp.transport(). To je zapravo wrapper funkcije lp()s ciljem pojednostavljenja unosa potrebnih podataka za provedbu analize. U nastavku ćemo uočavati prednosti i nedostatke korištenja ove funkcije.

Započinjemo s pripremom podataka za upotrebu u lp.transport().

# 1. Matrica troškova (c_ij)
# 3 reda (Polazišta) x 4 stupca (Odredišta)
costs <- matrix(c(8, 3, 7, 5,    # red 1: Vrana (P1)
                  4, 12, 9, 16, # red 2: Osijek (P2)
                  6, 11, 2, 18),# red 3: Pula (P3)
                nrow = 3, byrow = TRUE)

# Postavljanje imena za lakše čitanje
rownames(costs) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")
colnames(costs) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

# Postavljanje smjera ograničenja
smjer_redak <- rep("<=", 3)
smjer_stupac <- rep("=", 4)

# 2. Vektor ponude (s_i)
# Redoslijed mora odgovarati redovima matrice!
supply <- c(100, 150, 80)
names(supply) <- c("Vrana (P1)", "Osijek (P2)", "Pula (P3)")

# 3. Vektor potražnje (d_j)
# Redoslijed mora odgovarati stupcima matrice!
demand <- c(120, 90, 70, 50)
names(demand) <- c("Zagreb (O1)", "Split (O2)", "Rijeka (O3)", "Dubrovnik (O4)")

# Ispis ulaznih podataka radi provjere
print(costs)
##             Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
## Vrana (P1)            8          3           7              5
## Osijek (P2)           4         12           9             16
## Pula (P3)             6         11           2             18
print(supply)
##  Vrana (P1) Osijek (P2)   Pula (P3) 
##         100         150          80
print(demand)
##    Zagreb (O1)     Split (O2)    Rijeka (O3) Dubrovnik (O4) 
##            120             90             70             50

U dokumentaciji provjerimo kako pravilno koristiti funkciju lp.transport iz paketa lpSolve.

transportni_rj <- lp.transport (cost.mat = costs, 
                                direction = "min", 
                                row.signs = smjer_redak, 
                                row.rhs = supply, 
                                col.signs = smjer_stupac,
                                col.rhs = demand, 
                                compute.sens=1)

transportni_rj
## Success: the objective function is 1490

Što se događa u pozadini?


  • lp.transport je samo “omot” (wrapper) oko lp() funkcije.


  • Kad uključite compute.sens=1, opća lp() funkcija izračuna SVE dualne vrijednosti. To su:

    • Dualne vrijednosti (sjene cijena) za svako ograničenje. Imamo 3 ograničenja ponude (za polazišta) i 4 ograničenja potražnje (za odredišta). To je ukupno 7 dualnih vrijednosti za ograničenja.
    • Dualne vrijednosti (reducirani troškovi) za svaku varijablu. Imamo 3×4=12 varijabli odluke.
    • Ukupno, interna lp() funkcija vraća vektor od 7+12=19 dualnih vrijednosti.


  • Zbog toga, R može javljati grešku (uz uključno compute.sens=1):

    • Kad je sve gotovo, lp.transport wrapper pokušava te rezultate (njih 19) lijepo preoblikovati natrag u matricu koja ima 3 reda (naš broj polazišta).
    • Tada se R pobuni i može javiti upozorenje, jer broj 19 nije djeljiv s 3 - nešto ne štima s dimenzijama. No to ne znači da vrijednosti nisu izračunate i dostupne, samo ih moramo izravno pozvati.


-“the objective function is 1490”: Ovo je minimalna vrijednost funkcije cilja (Z). Ovo je identično rezultatu lp() funkcije. Potvrđuje da je minimalni ukupni trošak transporta 1490 EUR. U kontekstu našeg problema, ovo znači: najmanji mogući ukupni trošak za distribuciju svih 330 tona rajčica, uz poštivanje svih ograničenja ponude i potražnje, iznosi 1490 EUR.


transportni_rj$solution
##      [,1] [,2] [,3] [,4]
## [1,]    0   50    0   50
## [2,]  120   30    0    0
## [3,]    0   10   70    0

Ovo je optimalni plan transporta (naša matrica \(x_{ij}\)). Čitamo ovako (redak po redak):

[red 1] Vrana (P1) šalje:

  • 0 tona u Zagreb (stupac 1)
  • 50 tona u Split (stupac 2)
  • 0 tona u Rijeku (stupac 3)
  • 50 tona u Dubrovnik (stupac 4)

-Provjera ponude: 50 + 50 = 100. Vrana je poslao sve što ima.

[red 2] Osijek (P2) šalje:

  • 120 tona u Zagreb (stupac 1)

  • 30 tona u Split (stupac 2)

  • 0 tona u Rijeku (stupac 3)

  • 0 tona u Dubrovnik (stupac 4)

  • Provjera ponude: 120 + 30 = 150. Osijek je poslao sve što ima.

[red 3] Pula (P3) šalje:

  • 0 tona u Zagreb (stupac 1)

  • 10 tona u Split (stupac 2)

  • 70 tona u Rijeku (stupac 3)

  • 0 tona u Dubrovnik (stupac 4)

  • Provjera ponude: 10 + 70 = 80. Pula je poslala sve što ima.

Također, ako provjerimo stupce, vidimo da je potražnja zadovoljena:

  • Zagreb (stupac 1) prima: 0 + 120 + 0 = 120 tona (traženo: 120)
  • Split (stupac 2) prima: 50 + 30 + 10 = 90 tona (traženo: 90)
  • Rijeka (stupac 3) prima: 0 + 0 + 70 = 70 tona (traženo: 70)
  • Dubrovnik (stupac 4) prima: 50 + 0 + 0 = 50 tona (traženo: 50)

Rješenje je valjano i optimalno.

transportni_rj$duals
##      [,1] [,2] [,3] [,4]
## [1,]   -9    0   -1    4
## [2,]   12    3   14   13
## [3,]    0    3    0    0

Ovdje dolazimo do problema. Iščitavanje ($duals):

Ovaj izlaz bi trebao predstavljati reducirane troškove za svaku rutu. Usporedimo li ovu matricu s matricom reduciranih troškova koju smo dobili iz lp() funkcije:

  • lp.transport()$duals[1,1] (Vrana -> Zagreb) je sada -9.

  • Ispravan lp() reducirani trošak (Vrana -> Zagreb) je 13.

  • lp.transport()$duals[2,4] (Osijek -> Dubrovnik) je 13.

  • Ispravan lp() reducirani trošak (Osijek -> Dubrovnik) je 2.


Napomena: Izlaz je neupotrebljiv


  • Ove matrice se očito ne podudaraju. Kao što je prethodno navedeno, interna lp() funkcija vraća jedan vektor od 19 elemenata (7 sjena cijena + 12 reduciranih troškova).
  • Funkcija lp.transport() pokušava taj vektor “prelomiti” u matricu 3x4. Zbog toga se vrijednosti sjena cijena prelijevaju u matricu reduciranih troškova, a sami reducirani troškovi bivaju smješteni na potpuno kriva mjesta.
  • lp.transport() ne formatira duale u lako čitljiv izlaz; za pouzdanu interpretaciju ranges/duals/reduced costs treba koristiti lp().


Temeljem ovoga možemo zaključiti da je upotreba lp.transport() primjerena samo ako nam nije potrebna i analiza osjetljivosti.


Transportni problem s posrednim čvorovima (Transshipment)

Do sada smo pretpostavljali da roba, podaci ili ljudi putuju isključivo od točke ponude (izvor) do točke potražnje (odredište). U stvarnosti, logistički lanci su složeniji.


Definicija problema

Problem s posrednim čvorovima (ili engl. transshipment problem) je ekstenzija transportnog problema gdje postoji bar jedan čvor u mreži koji može istovremeno biti i primatelj i pošiljatelj robe (Ahuja et al., 1993; Bertsimas & Tsitsiklis, 1997; Anderson et al., 2012).

Drugim riječima, čvorovi se dijele u tri kategorije:

  1. Izvori ili polazišta (Sources): Čvorovi koji samo šalju robu (neto ponuda je pozitivna).
  2. Ponori ili odredišta (Sinks): Čvorovi koji samo primaju robu (neto potražnja je pozitivna).
  3. Posredni čvorovi (Transshipment nodes): Čvorovi koji mogu i primati i slati robu (npr. centralno skladište, distributivni centar, “hub”).

Cilj ostaje isti: zadovoljiti svu potražnju uz dostupnu ponudu (uobičajeno, minimizirati troškove).

Primjer transportnog problema s posrednim čvorovima:


Strukturiranje problema

Za razliku od klasičnog transportnog problema, kod problema s posrednicima (transshipment) tretiramo čvorove prema kategoriji: polazišni, odredišni i posredni čvorovi.

Skupovi i indeksi

  • Neka je \(I\) skup svih izvorišnih čvorova (polazišta). Indeks: \(i \in I\).
  • Neka je \(J\) skup svih posrednih čvorova (skladišta). Indeks: \(j \in J\).
  • Neka je \(K\) skup svih odredišnih čvorova. Indeks: \(k \in K\).

Parametri

  • \(s_i\): Ponuda (kapacitet) izvorišnog čvora \(i\).
  • \(d_k\): Potražnja odredišnog čvora \(k\).
  • \(c_{ij}\): Jedinični trošak transporta od izvora \(i\) do posrednika \(j\).
  • \(c_{jk}\): Jedinični trošak transporta od posrednika \(j\) do odredišta \(k\).

Varijable odluke

Modeliramo tok u dva segmenta:

  • \(x_{ij}\): količina robe (tok) koja se šalje od izvora \(i\) do posrednika \(j\),
  • \(x_{jk}\): količina robe (tok) koja se šalje od posrednika \(j\) do odredišta \(k\).

Funkcija cilja (minimizacija, najčešće - troškova)

Zbrajamo troškove svih segmenata transporta:

\[ \min Z = \sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} + \sum_{j \in J} \sum_{k \in K} c_{jk} x_{jk} \]

Ograničenja

Imamo točno tri vrste ograničenja, po jednu za svaki skup čvorova:

1. Ograničenja ponude (za svaki izvor \(i \in I\)):

Ukupna količina poslana iz izvora \(i\) (prema svim posrednicima) ne smije premašiti njegovu ponudu.

\[ \sum_{j \in J} x_{ij} \le s_i \]

2. Ograničenja potražnje (za svako odredište \(k \in K\)):

Ukupna količina primljena u odredištu \(k\) (od svih posrednika) mora zadovoljiti njegovu potražnju.

\[ \sum_{j \in J} x_{jk} \ge d_k \]

3. Ograničenje očuvanja toka (za svakog posrednika \(j \in J\)):

Ovo je ključno. Količina koja “zapne” u posredniku mora biti nula. Drugim riječima, sve što uđe, mora i izaći.

(Ukupan IZLAZ iz \(j\)) - (Ukupan ULAZ u \(j\)) = 0

\[ \sum_{k \in K} x_{jk} - \sum_{i \in I} x_{ij} = 0 \]

Ili, kako se češće piše (Ulaz = Izlaz): \[ \sum_{i \in I} x_{ij} = \sum_{k \in K} x_{jk} \]

Ograničenje nenegativnosti

Količina poslana bilo kojom rutom ne može biti negativna. \[ x_{ij} \ge 0 \quad \text{za sve} \quad i \in I, j \in J \] \[ x_{jk} \ge 0 \quad \text{za sve} \quad j \in J, k \in K \]

  • Važna napomena: Ova formulacija je savršena ako roba mora ići isključivo putem Izvor \(\to\) Posrednik \(\to\) Odredište.
  • Ako bismo dozvolili i izravne rute (Izvor \(i \to\) Odredište \(k\)), model bi postao složeniji. Morali bismo dodati nove varijable \(x_{ik}\) i pripadajuće troškove \(c_{ik}\), a ograničenja ponude i potražnje morala bi uključiti i te direktne pošiljke.


Primjer


Vraćamo se našem originalnom problemu s distribucijom rajčica, ali ga činimo realističnijim. Uprava “Agrosfera Savjetovanja - AS” odlučila je uložiti u logistiku i izgradila je 2 nova sabirna centra (posredna čvora):

  • Jedan u Jastrebarskom (SC-JA)
  • Jedan u Slavonskom Brodu (SC-SB)

Sada imamo 3 vrste čvorova:

  • Izvori (\(I\)): Vrana (P1), Osijek (P2), Pula (P3)
  • Posrednici (\(J\)): SC Jastrebarsko (SC-JA), SC Sl. Brod (SC-SB)
  • Odredišta (\(K\)): Zagreb (O1), Split (O2), Rijeka (O3), Dubrovnik (O4)

Logistički menadžer sada ima tri opcije za svaku pošiljku:

  1. Ruta preko posrednog čvora (1. dio): Poslati robu iz Izvora u Posrednika (npr. Osijek \(\to\) SC-SB).

  2. Ruta preko posrednog čvora (2. dio): Poslati robu iz Posrednika u Odredište (npr. SC-SB \(\to\) Dubrovnik).

(Izravne rute - Izvor \(\to\) Odredište, nisu dozvoljene, jer npr., sva roba mora proći certifikaciju u sabirnim centrima).

Cilj je, kao i uvijek, zadovoljiti svu potražnju uz minimalne ukupne troškove transporta, koristeći optimalnu kombinaciju svih dostupnih ruta.

Ponuda i potražnja ostaju iste kao u originalnom problemu. Posrednici imaju neto ponudu/potražnju jednaku 0 (sve što u njih uđe, mora i izaći).

  • Ponuda (izvori): Vrana (100t), Osijek (150t), Pula (80t)
  • Potražnja (odredišta): Zagreb (120t), Split (90t), Rijeka (70t), Dubrovnik (50t)
  • Bilanca (posredni čvorovi): SC-JA (0t), SC-SB (0t)
  • Ukupno: 330t ponude vs 330t potražnje. Sustav je balansiran.

Sada su nam potrebne dvije tablice troškova:

Tablica 1: Troškovi \(c_{ij}\) (Izvor \(\to\) Posrednik)

(Iz \(I\)) SC-JA (J1) SC-SB (J2)
Vrana (P1) 5 8
Osijek (P2) 4 2
Pula (P3) 3 7

Tablica 2: Troškovi \(c_{jk}\) (Posrednik \(\to\) Odredište)

(Iz \(J\)) Zagreb (O1) Split (O2) Rijeka (O3) Dubrovnik (O4)
SC-JA (J1) 1 4 2 6
SC-SB (J2) 2 5 4 7

Grafički, naš problem je čista trodijelna (bipartitna u dva koraka).


grViz("
digraph transshipment_problem {
  layout = dot;
  rankdir = LR;
  node [shape = record, style = filled, fontname = Helvetica, fontsize=12];
  ranksep = 1.5;
  splines = false;
  pad = 0.5;
  
  subgraph cluster_izvori {
    label = 'Izvori (I)';
    pencolor = none;
    P1 [label = 'Vrana (P1) \n Ponuda: 100'];
    P2 [label = 'Osijek (P2) \n Ponuda: 150'];
    P3 [label = 'Pula (P3) \n Ponuda: 80'];
  }
  
  subgraph cluster_posrednici {
    label = 'Posrednici (J)';
    pencolor = none;
    J1 [label = 'SC-JA (J1) \n Bilanca: 0'; color = lightyellow];
    J2 [label = 'SC-SB (J2) \n Bilanca: 0'; color = lightyellow];
  }
  
  subgraph cluster_odredista {
    label = 'Odredišta (K)';
    pencolor = none;
    K1 [label = 'Zagreb (O1) \n Potražnja: 120'; color=lightblue];
    K2 [label = 'Split (O2) \n Potražnja: 90'; color=lightblue];
    K3 [label = 'Rijeka (O3) \n Potražnja: 70'; color=lightblue];
    K4 [label = 'Dubrovnik (O4) \n Potražnja: 50'; color=lightblue];
  }
  
  edge [color = 'black'];
  
  # Rute I -> J
  P1 -> {J1, J2};
  P2 -> {J1, J2};
  P3 -> {J1, J2};
  
  # Rute J -> K
  J1 -> {K1, K2, K3, K4};
  J2 -> {K1, K2, K3, K4};
}
")
  • Indeksi: \(i \in \{P1, P2, P3\}\), \(j \in \{J1, J2\}\), \(k \in \{K1, K2, K3, K4\}\)

  • Varijable (14):

    • \(x_{ij}\) (6 varijabli, npr. \(x_{\text{P1,J1}}\))
    • \(x_{jk}\) (8 varijabli, npr. \(x_{\text{J1,K1}}\))

Funkcija cilja:

\[ \min Z = \sum_{i \in I} \sum_{j \in J} c_{ij} x_{ij} + \sum_{j \in J} \sum_{k \in K} c_{jk} x_{jk} \] (Zbroj troškova svih 14 ruta)

Ograničenja (ukupno 3+2+4 = 9):

(Koristimo jednakosti jer je sustav balansiran)

  1. Ponuda (izvori \(i\)):
  • \(x_{\text{P1,J1}} + x_{\text{P1,J2}} = 100\) (P1)
  • \(x_{\text{P2,J1}} + x_{\text{P2,J2}} = 150\) (P2)
  • \(x_{\text{P3,J1}} + x_{\text{P3,J2}} = 80\) (P3)
  1. Očuvanje toka (posrednici \(j\)): (Ulaz = Izlaz)
  • \((x_{\text{P1,J1}} + x_{\text{P2,J1}} + x_{\text{P3,J1}}) - (x_{\text{J1,K1}} + x_{\text{J1,K2}} + x_{\text{J1,K3}} + x_{\text{J1,K4}}) = 0\) (J1)
  • \((x_{\text{P1,J2}} + x_{\text{P2,J2}} + x_{\text{P3,J2}}) - (x_{\text{J2,K1}} + x_{\text{J2,K2}} + x_{\text{J2,K3}} + x_{\text{J2,K4}}) = 0\) (J2)
  1. Potražnja (odredišta \(k\)):
  • \(x_{\text{J1,K1}} + x_{\text{J2,K1}} = 120\) (K1 - Zagreb)
  • \(x_{\text{J1,K2}} + x_{\text{J2,K2}} = 90\) (K2 - Split)
  • \(x_{\text{J1,K3}} + x_{\text{J2,K3}} = 70\) (K3 - Rijeka)
  • \(x_{\text{J1,K4}} + x_{\text{J2,K4}} = 50\) (K4 - Dubrovnik)
library(lpSolve)

# 1. cvec (Vektor troškova, 14 varijabli)
# Redoslijed: P1J1, P1J2, P2J1, P2J2, P3J1, P3J2, J1K1, J1K2, J1K3, J1K4, J2K1, J2K2, J2K3, J2K4
cvec <- c(5, 8, 4, 2, 3, 7, 1, 4, 2, 6, 2, 5, 4, 7)

# 2. Amat (Matrica ograničenja A, 9x14)
# (126 elemenata, poredanih po redcima)
Amat_data <- c(
  # 3 Ograničenja ponude (Izvori P1, P2, P3)
  1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  # red 1: P1
  0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,  # red 2: P2
  0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 0, 0, 0,  # red 3: P3
  
  # 2 Ograničenja toka (Posrednici J1, J2) -> ULAZ - IZLAZ = 0
  1, 0, 1, 0, 1, 0, -1, -1, -1, -1, 0, 0, 0, 0,  # red 4: J1
  0, 1, 0, 1, 0, 1, 0, 0, 0, 0, -1, -1, -1, -1,  # red 5: J2
  
  # 4 Ograničenja potražnje (Odredišta K1, K2, K3, K4)
  0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0,  # red 6: K1 (Zagreb)
  0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0,  # red 7: K2 (Split)
  0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1, 0,  # red 8: K3 (Rijeka)
  0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1   # red 9: K4 (Dubrovnik)
  )

Amat <- matrix(Amat_data, nrow = 9, byrow = TRUE)

# 3. bvec (Vektor desne strane b, 9 ograničenja)
bvec <- c(100, 150, 80,   # Ponude (P1, P2, P3)
          0, 0,           # Tokovi (J1, J2)
          120, 90, 70, 50 # Potražnje (K1, K2, K3, K4)
          ) 

# 4. dirvec (Smjerovi ograničenja)
dirvec <- rep("=", 9)


# --- Rješavanje problema ---

rjesenje_pos_cv <- lp(direction = "min",
                      objective.in = cvec,
                      const.mat = Amat,
                      const.dir = dirvec,
                      const.rhs = bvec,
                      compute.sens = 1)


# --- Ispis i tumačenje rezultata ---
cat(paste("Minimalni ukupni trošak: ", rjesenje_pos_cv$objval, " EUR\n\n"))
## Minimalni ukupni trošak:  2110  EUR
# Formatiranje rješenja
plan_pos_cv <- matrix(rjesenje_pos_cv$solution[1:6], nrow = 3, byrow = TRUE,
                          dimnames = list(c("Vrana (P1)", "Osijek (P2)", "Pula (P3)"), 
                                          c("SC-JA (J1)", "SC-SB (J2)")))

plan_J_K_pos_cv <- matrix(rjesenje_pos_cv$solution[7:14], nrow = 2, byrow = TRUE,
                          dimnames = list(c("SC-JA (J1)", "SC-SB (J2)"), 
                                          c("Zagreb(K1)", "Split(K2)", "Rijeka(K3)", "Dubrovnik(K4)")))

cat("--- Optimalni Plan (Izvor -> Posrednik) ---\n")
## --- Optimalni Plan (Izvor -> Posrednik) ---
print(round(plan_pos_cv))
##             SC-JA (J1) SC-SB (J2)
## Vrana (P1)         100          0
## Osijek (P2)          0        150
## Pula (P3)           80          0
cat("\n--- Optimalni Plan (Posrednik -> Odredište) ---\n")
## 
## --- Optimalni Plan (Posrednik -> Odredište) ---
print(round(plan_J_K_pos_cv))
##            Zagreb(K1) Split(K2) Rijeka(K3) Dubrovnik(K4)
## SC-JA (J1)         60         0         70            50
## SC-SB (J2)         60        90          0             0
  • Minimalni ukupni trošak: 2110 EUR.

  • Optimalni plan (izvor \(\to\) posrednik):

    • Vrana (P1) šalje svih 100 t u SC-JA (J1).
    • Osijek (P2) šalje svih 150 t u SC-SB (J2).
    • Pula (P3) šalje svih 80 t u SC-JA (J1).
  • Optimalni plan (posrednik \(\to\) odredište):

    • SC-JA (J1) (koji je primio 100+80=180 t) šalje: 60 t u Zagreb, 70 t u Rijeku i 50 t u Dubrovnik.
    • SC-SB (J2) (koji je primio 150 t) šalje: 60 t u Zagreb i 90 t u Split.
  1. Faza (izvor \(\to\) posrednik): Solver je logično poslao svu robu iz svakog izvora u njemu najbliži/najjeftiniji sabirni centar. Vrani i Puli je jeftinije Jastrebarsko (JA), a Osijeku je jeftiniji Slavonski Brod (SB).

  2. Faza (posrednik \(\to\) odredište):

  • Rijeka (K3) i Dubrovnik (K4) su namireni iz SC-JA (J1), jer je J1 jeftiniji za obje te rute (RI: 2 € < 4 €; DU: 6 € < 7 €).
  • Split (K2) je namiren iz SC-SB (J2). Iako je ruta J1 \(\to\) Split jeftinija (4 € < 5 €), J1 je već potrošio sav svoj kapacitet (180 t) na Rijeku, Dubrovnik i dio Zagreba. Stoga je Split morao biti namiren iz skupljeg sabirnog centra (J2).
  • Zagreb (K1) je “krpao” rupe. Dobio je 60 t iz jeftinijeg J1, a preostalih 60 t iz skupljeg J2.

Obavezna certifikacija i transport preko hubova koštaju nas 2110 EUR. To je znatno skuplje od modela direktne isporuke (1490 EUR). U ovom scenariju, centralizacija je, zbog zadanih troškova, financijski lošija opcija.


# Izvlačimo imena ograničenja
imena_ogranicenja <- c("Ponuda_P1", "Ponuda_P2", "Ponuda_P3", 
                       "Tok_J1", "Tok_J2", 
                       "Potraznja_K1", "Potraznja_K2", "Potraznja_K3", "Potraznja_K4")

# Izvlačimo prvih 9 dualnih vrijednosti
sjene_cijena_pos_cv <- rjesenje_pos_cv$duals[1:9]
names(sjene_cijena_pos_cv) <- imena_ogranicenja

cat("--- Sjene Cijena (Dualne Vrijednosti Ograničenja) ---\n")
## --- Sjene Cijena (Dualne Vrijednosti Ograničenja) ---
print(round(sjene_cijena_pos_cv, 2))
##    Ponuda_P1    Ponuda_P2    Ponuda_P3       Tok_J1       Tok_J2 Potraznja_K1 
##            6            4            4           -1           -2            0 
## Potraznja_K2 Potraznja_K3 Potraznja_K4 
##            3            1            5


  • Potraznja_K2 (Split) = 3: Ako se potražnja u Splitu poveća za 1 tonu (na 91t), ukupni trošak sustava (2110€) raste za 3 EUR.
  • Potraznja_K4 (Dubrovnik) = 5: Povećanje potražnje u Dubrovniku za 1 tonu košta nas 5 EUR.
  • Ponuda_P2 (Osijek) = 4: Ako Osijek može ponuditi 1 tonu više (151t), ukupni trošak sustava raste za 4 EUR. To znači da je Osijek, unatoč jeftinoj ruti do J2, u ukupnom sustavu “skup” izvor u usporedbi s drugima.
  • Tok_J1 (SC-JA) = -1: Negativna vrijednost na čvoru toka je zanimljiva. Tumači se kao: “Ako bismo ‘magično’ stvorili 1 tonu u Jastrebarskom (a da nije došla iz izvora), ukupni trošak sustava bi pao za 1 EUR.”
  • Tok_J2 (SC-SB) = -2: Slično, 1 tona “stvorena” u Slavonskom Brodu štedi 2 EUR. To nam govori da je SC-SB (J2) korisnija lokacija za distribuciju od SC-JA (J1).


# Izvlačimo zadnjih 14 dualnih vrijednosti (reducirani troškovi)
reducirani_troskovi_pos_cv <- rjesenje_pos_cv$duals[10:23] 

# Formatiramo ih u matrice radi čitljivosti
rc_plan_I_J <- matrix(reducirani_troskovi_pos_cv[1:6], nrow = 3, byrow = TRUE,
                   dimnames = list(c("Vrana (P1)", "Osijek (P2)", "Pula (P3)"), 
                                   c("SC-JA (J1)", "SC-SB (J2)")))

rc_plan_J_K <- matrix(reducirani_troskovi_pos_cv[7:14], nrow = 2, byrow = TRUE,
                   dimnames = list(c("SC-JA (J1)", "SC-SB (J2)"), 
                                   c("Zagreb(K1)", "Split(K2)", "Rijeka(K3)", "Dubrovnik(K4)")))

cat("--- Reducirani Troškovi (Rute Izvor -> Posrednik) ---\n")
## --- Reducirani Troškovi (Rute Izvor -> Posrednik) ---
print(round(rc_plan_I_J))
##             SC-JA (J1) SC-SB (J2)
## Vrana (P1)           0          4
## Osijek (P2)          1          0
## Pula (P3)            0          5
cat("\n--- Reducirani Troškovi (Rute Posrednik -> Odredište) ---\n")
## 
## --- Reducirani Troškovi (Rute Posrednik -> Odredište) ---
print(round(rc_plan_J_K))
##            Zagreb(K1) Split(K2) Rijeka(K3) Dubrovnik(K4)
## SC-JA (J1)          0         0          0             0
## SC-SB (J2)          0         0          1             0


  • Rute s NULOM (0): Sve rute s vrijednošću 0 su ili dio optimalnog plana (npr. P1 -> J1, P2 -> J2) ili predstavljaju alternativno optimalno rješenje.

  • Vrana (P1) -> SC-SB (J2) = 4: Ova ruta se ne koristi. Ako bismo na silu poslali 1 tonu iz Vrane u Sl. Brod, ukupni trošak (2110€) porastao bi za 4 EUR.

  • SC-SB (J2) -> Rijeka(K3) = 1: Slično, slanje 1 tone ovom rutom (koja se ne koristi) povećalo bi ukupni trošak za 1 EUR.

  • Tumačenje nula iz perspektive alternativnih rješenja:

    • Pogledajmo rutu SC-JA (J1) -> Split(K2). U našem planu (plan_J_K_pos_cv), vrijednost je 0 (ne koristi se).
    • Međutim, njen reducirani trošak je također 0.
    • To znači da postoji više optimalnih rješenja! Mogli bismo preusmjeriti dio tereta (npr. iz J1 u Split) bez da se ukupni trošak od 2110 EUR promijeni. Ovo menadžeru daje fleksibilnost.


# Izvlačimo raspone SAMO za prvih 9 ograničenja
donje_granice_rhs <- rjesenje_pos_cv$duals.from[1:9]
gornje_granice_rhs <- rjesenje_pos_cv$duals.to[1:9]

rhs_analiza <- data.frame(
  Trenutni_RHS = bvec,
  Sjena_Cijena = sjene_cijena_pos_cv,
  Donja_Granica_RHS = donje_granice_rhs,
  Gornja_Granica_RHS = gornje_granice_rhs
)
rownames(rhs_analiza) <- imena_ogranicenja

cat("--- Analiza Osjetljivosti Ograničenja (RHS) ---\n")
## --- Analiza Osjetljivosti Ograničenja (RHS) ---
print(round(rhs_analiza, 2))
##              Trenutni_RHS Sjena_Cijena Donja_Granica_RHS Gornja_Granica_RHS
## Ponuda_P1             100            6           1.0e+02            1.0e+02
## Ponuda_P2             150            4           1.5e+02            1.5e+02
## Ponuda_P3              80            4           8.0e+01            8.0e+01
## Tok_J1                  0           -1           0.0e+00            0.0e+00
## Tok_J2                  0           -2           0.0e+00            0.0e+00
## Potraznja_K1          120            0          -1.0e+30            1.0e+30
## Potraznja_K2           90            3           9.0e+01            9.0e+01
## Potraznja_K3           70            1           7.0e+01            7.0e+01
## Potraznja_K4           50            5           5.0e+01            5.0e+01


  • Pogledajte raspone. Za gotovo svako ograničenje (osim Potraznja_K1), donja i gornja granica su identične trenutnoj vrijednosti (npr. Ponuda_P1 ima raspon [100, 100]; Potraznja_K2 ima raspon [90, 90]).
  • Što to znači: Ovo je klasičan znak degeneriranog rješenja.
  • Praktično tumačenje: Sjene cijena koje smo izračunali (npr. +3 EUR za tonu u Splitu) su iznimno nestabilne. One vrijede samo i isključivo za ovaj točan scenarij (Split traži 90t, Vrana nudi 100t, itd.).
  • Zaključak: Ne možemo koristiti ove sjene cijena za “što-ako” analizu. Ne možemo reći: “Ako Split zatraži 10 tona više, trošak će porasti za 10 * 3 = 30 EUR”. Raspon [90, 90] nam govori da čim se potražnja Splita promijeni na 91 tonu, sjena cijene od 3 EUR više ne vrijedi i cijeli problem se mora ponovno izračunati.
  • Izuzetak: Jedino stabilno ograničenje je Potraznja_K1 (Zagreb), koje je solver koristio kao referentnu točku (sjena cijene 0 i beskonačan raspon).


Najkraći put


Problem najkraćeg puta (engl. Shortest Path Problem ili SPP) jedan je od fundamentalnih i najčešće proučavanih problema optimizacije u teoriji grafova i operacijskim istraživanjima. Dok se transportni problem bavi alokacijom ukupne ponude uz minimalni trošak (problem “protoka mase”), problem najkraćeg puta fokusira se na pronalaženje optimalne, pojedinačne rute između dvije točke u mreži.


Motivacija za njegovo proučavanje proizlazi iz izravne i široke primjenjivosti u stvarnom svijetu:

  • Navigacijski sustavi (GPS): Najočitija primjena. Kada zatražite upute od točke A do točke B, algoritam izračunava put koji minimizira ukupnu udaljenost, procijenjeno vrijeme putovanja ili čak potrošnju goriva. “Težina” svake ceste (luka) u ovom slučaju može biti njezina duljina, prosječno vrijeme prolaska ili kombinacija faktora (npr. zastoji, cestarine).

  • Računalne mreže (Internet): Kada šaljete e-poštu ili otvarate web stranicu, paketi podataka moraju putovati od vašeg računala do poslužitelja. Usmjerivači (routeri) koriste protokole (poput OSPF-a) koji se temelje na algoritmima najkraćeg puta kako bi odredili “najjeftiniju” (najbržu) rutu s najmanjom latencijom za slanje tih paketa kroz globalnu mrežu.

  • Logistika i transport: Osim općeg transportnog problema, SPP se koristi za optimizaciju pojedinačnih dostavnih ruta, planiranje putovanja servisnih tehničara ili određivanje najbržeg slijeda za preuzimanje robe iz različitih dijelova skladišta.


Neki od novijih radova koji primjenjuju ovu metodu (ili kombinaciju ove metode s drugim metodama), odnose se na upravljanje rutama hibridnih vozila (Dascioglu i Tuzkaya, 2019; Felipe et al., 2014; Montoya et al., 2017), autonomnih vozila (Aggrawal i Kumar, 2020), kao i određivanje gradskih ruta (Roopa et al, 2019) te ruta policijskih patrola (Dewinter et al., 2020).


Definicija


Problem najkraćeg puta formalno se definira na ponderiranom (težinskom) grafu, \(G = (V, E)\).

  • \(V\): Skup čvorova (vrhova), koji predstavljaju lokacije (npr. gradovi, raskrižja, serveri).
  • \(E\): Skup bridova (u neusmjerenom grafu) ili lukova (u usmjerenom grafu), koji predstavljaju veze (npr. ceste, kabeli) između čvorova.
  • \(c_{ij}\): Ponder (težina) dodijeljen luku \((i, j)\) koji spaja čvor \(i\) i čvor \(j\). Ovaj ponder predstavlja mjeru koju želimo minimizirati (npr. udaljenost, vrijeme, trošak).

Primjer grafa na kojem možemo tražiti najkraći put:

Definicija puta:

Put (\(P\)) od početnog čvora \(s\) do završnog čvora \(t\) je niz čvorova \(P = (v_0, v_1, \dots, v_k)\) gdje je \(v_0 = s\) (početak) i \(v_k = t\) (kraj), a svaki luk \((v_{i-1}, v_i)\) postoji u skupu \(E\).


Definicija cijene puta:

Cijena/ trošak, vrijeme ili duljina (ovisno o tome kako je problem postavljen) puta \(C(P)\) je suma težina svih lukova koji čine taj put:

\[ C(P) = \sum_{i=1}^{k} c_{v_{i-1}, v_i} \]


Cilj problema:

Zadani su početni (izvorišni) čvor \(s \in V\) i završni (odredišni) čvor \(t \in V\). Cilj je pronaći put \(P^*\) od \(s\) do \(t\) takav da je njegova ukupna cijena \(C(P^*)\) manja ili jednaka cijeni bilo kojeg drugog puta \(P\) od \(s\) do \(t\).

\[ C(P^*) = \min \{ C(P) \mid P \text{ je put od } s \text{ do } t \} \]


Važne napomene o varijacijama:

  • SSSP (Single-Source Shortest Path): Većina klasičnih algoritama (poput Dijkstrinog) zapravo rješava malo općenitiji problem: pronalaze najkraće puteve od početnog čvora \(s\) do svih ostalih čvorova u grafu.
  • Negativne težine: Standardni Dijkstrin algoritam funkcionira samo ako su sve težine lukova nenegativne (\(c_{ij} \ge 0\)) (Dijkstra, 1959). Ako mreža ima lukove s negativnim troškom (ali bez negativnih ciklusa), koriste se složeniji algoritmi poput Bellman-Fordovog (Bellman, 1958). Ako postoje negativni lukovi, ali nema negativnih ciklusa, LP i Bellman–Ford će riješiti problem, ali ako postoji negativni ciklus dostupan između \(s\) i \(t\), problem najkraćeg puta nije dobro definiran.
  • APSP (All-Pairs Shortest Path): Ponekad je potrebno pronaći najkraće puteve između svakog para čvorova u mreži, za što se obično koristi Floyd-Warshallov algoritam (Floyd, 1962).
  • Više na ovu temu naći ćete u poglavlju “Algoritmi na mrežama”.


Problem najkraćeg puta može se formulirati kao linearni program. Temeljna ideja je zamisliti da želimo poslati jednu jedinicu “toka” (npr. jedan paket, jedan automobil, jednu osobu) od početnog čvora \(s\) do završnog čvora \(t\), uz najniži mogući trošak.

Svaki luk na mreži ima “cijenu” \(c_{ij}\) za slanje te jedinice toka preko njega.

  • \(N\): Skup svih čvorova u mreži.
  • \(E\): Skup svih lukova (usmjerenih veza) \((i, j)\) između čvorova.
  • \(s\): Početni (izvorišni) čvor, \(s \in N\).
  • \(t\): Završni (odredišni) čvor, \(t \in N\).
  • \(c_{ij}\): Trošak (ili udaljenost, vrijeme) slanja jedne jedinice toka preko luka \((i, j)\).


Varijable odluke

  • \(x_{ij}\): predstavljaju “tok” na luku \((i, j)\).
    • Zbog svojstava problema, ova varijabla će u konačnom rješenju biti 1 ako je luk \((i, j)\) dio najkraćeg puta, a 0 ako nije.

Cilj je minimizirati ukupni trošak slanja te jedne jedinice toka od \(s\) do \(t\). Ukupni trošak je zbroj troškova svih lukova koji se koriste, pomnožen s tokom na njima.

\[ \min Z = \sum_{(i,j) \in E} c_{ij} x_{ij} \]

Sljedeći dio funkcionira identično kao kod problema s posrednim čvorovima. Moramo osigurati da naša “jedinica toka” ne nestane ili zapne u mreži i da stigne na odredište. To radimo definiranjem neto ponude/potražnje (\(b_i\)) za svaki čvor:

  • Početni čvor \(s\): Stvara 1 jedinicu toka. Njegova neto ponuda je \(b_s = 1\).
  • Završni čvor \(t\): Dobiva 1 jedinicu toka. Njegova neto potražnja je \(b_t = 1\).
  • Svi ostali čvorovi \(v\): Kroz njih tok samo prolazi. Njihova neto ponuda je \(b_v = 0\).

Sada možemo napisati jedno ograničenje očuvanja toka za svaki čvor \(i \in N\):

(Ukupan tok IZ čvora \(i\)) - (ukupan tok U čvor \(i\)) = (neto ponuda čvora \(i\))

\[ \sum_{j \mid (i,j) \in E} x_{ij} - \sum_{k \mid (k,i) \in E} x_{ki} = b_i \quad \text{za svaki } i \in N \]

Tok ne može biti negativan.

\[ x_{ij} \ge 0 \quad \text{za sve } (i,j) \in E \]


  • Možda se pitate - ako je \(x_{ij}\) samo \(\ge 0\), zašto rješenje ne može biti \(x_{AC} = 0.5\) i \(x_{AB} = 0.5\)? Zar ne trebamo binarno programiranje (\(x_{ij} \in \{0, 1\}\))?

    • Odgovor je ne. Zbog posebnog matematičkog svojstva mrežnih problema (tzv. potpuna unimodularnost matrice ograničenja \(A\)), kada su sve desne strane ograničenja (\(b_i\)) cijeli brojevi (kao što naši 1, 1 i 0 jesu), standardni Simplex algoritam za LP garantirano pronalazi optimalno rješenje gdje su i sve varijable \(x_{ij}\) također cijeli brojevi (Bertsimas & Tsitsiklis, 1997; Hoffman & Kruskal, 1956).
    • Budući da je naš tok samo 1, jedini cijeli brojevi koje \(x_{ij}\) može poprimiti su 0 ili 1. Time smo efektivno riješili problem binarnog odabira puta koristeći brži i jednostavniji LP model.


Primjer


Kao mrežni administrator, morate poslati kritični paket podataka s glavnog Servera (S) na Korisnički uređaj (K). Mreža se sastoji od četiri posredna Routera (A, B, C, D).

Svaka veza (luk) između dva uređaja (čvora) ima određenu latenciju (kašnjenje), izraženu u milisekundama (ms). Vaš je cilj pronaći put (rutu) od Servera do Korisnika koji ima minimalnu ukupnu latenciju.

Mrežu i pripadajuće latencije (troškove) možemo vizualizirati na sljedeći način:

grViz("
digraph network_latency {
  layout = dot;
  rankdir = LR;
  node [shape = circle, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 12];
  
  # Definicija čvorova
  S [label = 'Server (S)\nČvor 1'];
  A [label = 'Router A\nČvor 2'];
  B [label = 'Router B\nČvor 3'];
  C [label = 'Router C\nČvor 4'];
  D [label = 'Router D\nČvor 5'];
  K [label = 'Korisnik (K)\nČvor 6'];

  # Definicija veza (lukova) s latencijama (troškovima)
  S -> A [label = ' 5 ms'];  # x12
  S -> B [label = ' 2 ms'];  # x13
  
  A -> C [label = ' 3 ms'];  # x24
  A -> D [label = ' 6 ms'];  # x25
  
  B -> C [label = ' 7 ms'];  # x34
  B -> D [label = ' 4 ms'];  # x35
  
  C -> K [label = ' 4 ms'];  # x46
  D -> K [label = ' 8 ms'];  # x56
}
")

Naš problem je pronaći najkraći put od Čvora 1 (Server) do Čvora 6 (Korisnik).

Problem formuliramo kao slanje 1 jedinice toka (naš paket) od \(s\) do \(t\) uz minimalni trošak (latenciju).

  • Čvorovi (N): \(\{1, 2, 3, 4, 5, 6\}\)
  • Početni čvor (\(s\)): 1 (Server)
  • Završni čvor (\(t\)): 6 (Korisnik)
  • Varijable (\(x_{ij}\)): Imamo 8 lukova, dakle 8 varijabli:

\(x_{12}, x_{13}, x_{24}, x_{25}, x_{34}, x_{35}, x_{46}, x_{56}\)

(gdje \(x_{ij} = 1\) ako je luk dio puta, \(0\) inače)

  • Neto ponuda/potražnja (\(b_i\)):

    • \(b_1 = 1\) (Server stvara 1 paket - ponuda)
    • \(b_6 = 1\) (Korisnik prima 1 paket - potražnja)
    • \(b_2 = b_3 = b_4 = b_5 = 0\) (Routeri samo prosljeđuju)


Funkcija cilja (minimizacija latencije):

\[ \min Z = 5x_{12} + 2x_{13} + 3x_{24} + 6x_{25} + 7x_{34} + 4x_{35} + 4x_{46} + 8x_{56} \]

Ograničenja (očuvanje toka, po 1 za svaki posredni čvor): \[ \sum (IZLAZ) - \sum (ULAZ) = b_i \]

što je raspisano:

  1. Čvor 1 (Server): \(x_{12} + x_{13} = 1\) (ponuda)
  2. Čvor 2 (Router A): \((x_{24} + x_{25}) - x_{12} = 0\) (posredni čvor)
  3. Čvor 3 (Router B): \((x_{34} + x_{35}) - x_{13} = 0\) (posredni čvor)
  4. Čvor 4 (Router C): \(x_{46} - (x_{24} + x_{34}) = 0\) (posredni čvor)
  5. Čvor 5 (Router D): \(x_{56} - (x_{25} + x_{35}) = 0\) (posredni čvor)
  6. Čvor 6 (Korisnik): \(x_{46} + x_{56} = 1\) (potražnja)


library(lpSolve)

# Redoslijed varijabli (na ovo najviše treba paziti u kodu):
# 1: x12, 2: x13, 3: x24, 4: x25, 5: x34, 6: x35, 7: x46, 8: x56

# 1. cvec (Vektor troškova - latencija)
cvec <- c(5, 2, 3, 6, 7, 4, 4, 8)

# 2. Amat (Matrica ograničenja A, 6x8)
# (Redovi = Čvorovi 1-6; Stupci = Varijable 1-8)
Amat_data <- c(
  # (IZLAZ) - (ULAZ) = b_i
  # Čvor 1 (Server): x12 + x13 = 1
   1,  1,  0,  0,  0,  0,  0,  0,
  # Čvor 2 (Router A): (x24 + x25) - x12 = 0
  -1,  0,  1,  1,  0,  0,  0,  0,
  # Čvor 3 (Router B): (x34 + x35) - x13 = 0
   0, -1,  0,  0,  1,  1,  0,  0,
  # Čvor 4 (Router C): x46 - (x24 + x34) = 0
   0,  0, -1,  0, -1,  0,  1,  0,
  # Čvor 5 (Router D): x56 - (x25 + x35) = 0
   0,  0,  0, -1,  0, -1,  0,  1,
  # Čvor 6 (Korisnik): x46 + x56 = 1
   0,  0,  0,  0,  0,  0, 1, 1
)

Amat <- matrix(Amat_data, nrow = 6, byrow = TRUE)

# 3. bvec (Vektor desne strane b)
bvec <- c(1,  # Čvor 1 (Ponuda)
          0,  # Čvor 2
          0,  # Čvor 3
          0,  # Čvor 4
          0,  # Čvor 5
          1) # Čvor 6 (Potražnja)

# 4. dirvec (Smjerovi ograničenja)
dirvec <- rep("=", 6)

rjesenje_spp <- lp(direction = "min",
                   objective.in = cvec,
                   const.mat = Amat,
                   const.dir = dirvec,
                   const.rhs = bvec)

# priprema ispisa i ispis
cat(paste("Minimalna ukupna latencija (Z):", rjesenje_spp$objval, " ms\n\n"))
## Minimalna ukupna latencija (Z): 12  ms
# Imenujemo varijable za lakše čitanje
imena_varijabli <- c("x12 (S->A)", "x13 (S->B)", 
                     "x24 (A->C)", "x25 (A->D)", 
                     "x34 (B->C)", "x35 (B->D)", 
                     "x46 (C->K)", "x56 (D->K)")
names(rjesenje_spp$solution) <- imena_varijabli

cat("--- Optimalna Ruta (Varijable = 1) ---\n")
## --- Optimalna Ruta (Varijable = 1) ---
print(rjesenje_spp$solution[rjesenje_spp$solution > 0.5]) # Pokaži samo one koje su 1
## x12 (S->A) x24 (A->C) x46 (C->K) 
##          1          1          1


Solver je pronašao optimalnu rutu s ukupnim kašnjenjem od 12 ms.

Analizom vektora rješenja, vidimo da su sljedeće varijable (lukovi) dobile vrijednost 1:

  • x12 (\(S \to A\)) = 1 (Latencija: 5 ms)
  • x24 (\(A \to C\)) = 1 (Latencija: 3 ms)
  • x46 (\(C \to K\)) = 1 (Latencija: 4 ms)

Najkraći put nije onaj koji započinje najjeftinije (ruta S\(\to\)B za 2 ms), već onaj čiji je ukupan zbroj najmanji.

Optimalna ruta je: Server \(\to\) Router A \(\to\) Router C \(\to\) Korisnik. Ukupni trošak (latencija): 5 + 3 + 4 = 12 ms.


Poseban slučaj: Problem trgovačkog putnika (TSP)


Standardni problem najkraćeg puta traži optimalnu rutu od točke A do točke B. Problem trgovačkog putnika (engl. Traveling Salesperson Problem, TSP) traži optimalnu turu.

Definicija: Zadan je skup gradova (čvorova) i udaljenosti (cijene) između svakog para. Cilj je pronaći najkraći mogući put koji:

  1. Posjećuje svaki čvor točno jednom.
  2. Završava u početnom čvoru.

Ovo je NP-težak problem (engl. nondeterministic polynomial time), što znači da je računski izuzetno intenzivan (Garey & Johnson, 1979). Naš “jednostavan” LP model za najkraći put ovdje nije dovoljan. Ako bismo samo rekli “iz svakog grada idi u jedan i u svaki grad uđi iz jednog”, dobili bismo krive rezultate (npr. dvije odvojene petlje), što zovemo pod-ture (subtours).

Da bismo to riješili LP-om, moramo dodati Miller-Tucker-Zemlin (MTZ) ograničenja (Miller, Tucker & Zemlin, 1960).

Zamislimo sljedeću situaciju. Dron kreće iz Baze (1) i mora dostaviti pakete u Ured A (2), Ured B (3) i Server Sobu (4), te se vratiti u Bazu (1).

Matrica troškova (vrijeme u minutama):

(Napomena: Postavljamo \(c_{ii} = 999\) (veliki broj “M”) da spriječimo dron da leti sam u sebe, npr. 1 \(\to\) 1)

\(c_{ij}\) 1 (Baza) 2 (A) 3 (B) 4 (Server)
1 (Baza) 999 10 15 20
2 (A) 5 999 9 10
3 (B) 6 13 999 12
4 (Server) 8 8 9 999


Varijable odluke (dvije vrste!):

  1. \(x_{ij}\): Binarne varijable. \(x_{ij} = 1\) ako putujemo iz čvora \(i\) u čvor \(j\), inače \(0\).
  2. \(u_i\): Pomoćne varijable (kontinuirane). Služe za praćenje “redoslijeda” posjeta čvorova kako bi se spriječile pod-ture. (npr. \(u_1=1, u_2=2, u_3=3...\))

Ukupan broj čvorova, \(n = 4\).

Funkcija cilja (minimizacija troška): \[ \min Z = \sum_{i=1}^{4} \sum_{j=1}^{4} c_{ij} x_{ij} \] (min \(Z = 10x_{12} + 15x_{13} + 20x_{14} + 5x_{21} + 9x_{23} + ... + 9x_{43}\))

Ograničenja:

Grupa 1: Standardna ograničenja

(Osiguravaju da se svaki čvor posjeti točno jednom)

  • Svaki čvor se mora napustiti točno jednom: \[ \sum_{j=1}^{4} x_{ij} = 1 \quad \text{za svaki } i = 1, 2, 3, 4 \]
  • U svaki čvor se mora ući točno jednom: \[ \sum_{i=1}^{4} x_{ij} = 1 \quad \text{za svaki } j = 1, 2, 3, 4 \]

Grupa 2: MTZ Ograničenja (eliminacija pod-tura)

(Ovo je komplicirani dio. \((n-1) \times (n-2) = 3 \times 2 = 6\) ograničenja)

Za sve \(i, j \in \{2, 3, 4\}\) gdje \(i \neq j\): \[ u_i - u_j + n \cdot x_{ij} \le n-1 \]

  • \(u_2 - u_3 + 4x_{23} \le 3\)
  • \(u_2 - u_4 + 4x_{24} \le 3\)
  • \(u_3 - u_2 + 4x_{32} \le 3\)
  • \(u_3 - u_4 + 4x_{34} \le 3\)
  • \(u_4 - u_2 + 4x_{42} \le 3\)
  • \(u_4 - u_3 + 4x_{43} \le 3\)

Grupa 3: Ograničenja za \(u_i\) varijable

(Osiguravaju da \(u_i\) varijable rade ispravno)

\[ u_1 = 1 \] \[ 2 \le u_i \le n \quad \text{za } i = 2, 3, 4 \]

Kao što vidite, čak i za 4 čvora, problem je kompliciran. Imamo \(16\) binarnih varijabli (\(x_{ij}\)) i \(4\) pomoćne varijable (\(u_i\)), te \(1 + 8 + 6 + 3 = 18\) ograničenja.

Za 10 čvorova, imali bismo \(100\) \(x_{ij}\) varijabli i \(9 \times 8 = 72\) MTZ ograničenja. Ovo je razlog zašto se za TSP češće koriste metaheuristike (genetski algoritmi, simulirano kaljenje) nego egzaktni LP (osim ako nije apsolutno nužno).


library(lpSolve)
n <- 4

# 1. Vektor cilja (cvec) - 20 varijabli
# Troškovi za x_ij (prvih 16), pa 0 za u_i (zadnje 4)
cvec <- c(
  999, 10, 15, 20, # Troškovi iz 1
  5, 999,  9, 10, # Troškovi iz 2
  6,  13, 999, 12, # Troškovi iz 3
  8,   8,   9, 999, # Troškovi iz 4
  0,   0,   0,   0  # Troškovi za u1, u2, u3, u4
)

# 2. Smjerovi ograničenja (dirvec) - 21 ograničenje
dirvec <- c(
  rep("=", 4),         # Ogr. 1-4: Napusti svaki čvor
  rep("=", 4),         # Ogr. 5-8: Uđi u svaki čvor
  rep("<=", 6),        # Ogr. 9-14: MTZ (u_i - u_j + ...)
  "=",                 # Ogr. 15: u_1 = 1
  rep(">=", 3),        # Ogr. 16-18: u_i >= 2
  rep("<=", 3)         # Ogr. 19-21: u_i <= 4
)

# 3. Vektor desne strane (bvec) - 21 ograničenje
bvec <- c(
  rep(1, 4),           # Ogr. 1-4: = 1
  rep(1, 4),           # Ogr. 5-8: = 1
  rep(n - 1, 6),       # Ogr. 9-14: <= 3 (n-1)
  1,                   # Ogr. 15: = 1
  rep(2, 3),           # Ogr. 16-18: >= 2
  rep(n, 3)            # Ogr. 19-21: <= 4 (n)
)

# 4. Matrica ograničenja (Amat_data) - 21 red x 20 stupaca
# prvo ćemo kreirati vektor, a poslije ga pretvoriti u matricu 
# (nema razlike u odnosu na izravni unos, ovako je samo malo lakše poravnati stupce)
Amat_data <- c(
  # Ogr. 1-4: Napusti čvor i (SUM x_i* = 1)
  # red 1 (iz 1): x11..x14
   1, 1, 1, 1,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
  # red 2 (iz 2): x21..x24
   0, 0, 0, 0,  1, 1, 1, 1,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
  # red 3 (iz 3): x31..x34
   0, 0, 0, 0,  0, 0, 0, 0,  1, 1, 1, 1,  0, 0, 0, 0,  0, 0, 0, 0,
  # red 4 (iz 4): x41..x44
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  1, 1, 1, 1,  0, 0, 0, 0,
  
  # Ogr. 5-8: Uđi u čvor j (SUM x_*j = 1)
  # red 5 (u 1): x11, x21, x31, x41
   1, 0, 0, 0,  1, 0, 0, 0,  1, 0, 0, 0,  1, 0, 0, 0,  0, 0, 0, 0,
  # red 6 (u 2): x12, x22, x32, x42
   0, 1, 0, 0,  0, 1, 0, 0,  0, 1, 0, 0,  0, 1, 0, 0,  0, 0, 0, 0,
  # red 7 (u 3): x13, x23, x33, x43
   0, 0, 1, 0,  0, 0, 1, 0,  0, 0, 1, 0,  0, 0, 1, 0,  0, 0, 0, 0,
  # red 8 (u 4): x14, x24, x34, x44
   0, 0, 0, 1,  0, 0, 0, 1,  0, 0, 0, 1,  0, 0, 0, 1,  0, 0, 0, 0,
  
  # Ogr. 9-14: MTZ (u_i - u_j + n*x_ij <= n-1)
  # Stupci 17-20 su u1, u2, u3, u4
  # red 9: u2 - u3 + 4*x23 <= 3 (x23 = var 7)
   0, 0, 0, 0,  0, 0, 4, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 1,-1, 0,
  # red 10: u2 - u4 + 4*x24 <= 3 (x24 = var 8)
   0, 0, 0, 0,  0, 0, 0, 4,  0, 0, 0, 0,  0, 0, 0, 0,  0, 1, 0,-1,
  # red 11: u3 - u2 + 4*x32 <= 3 (x32 = var 10)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 4, 0, 0,  0, 0, 0, 0,  0,-1, 1, 0,
  # red 12: u3 - u4 + 4*x34 <= 3 (x34 = var 12)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 4,  0, 0, 0, 0,  0, 0, 1,-1,
  # red 13: u4 - u2 + 4*x42 <= 3 (x42 = var 14)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 4, 0, 0,  0,-1, 0, 1,
  # red 14: u4 - u3 + 4*x43 <= 3 (x43 = var 15)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 4, 0,  0, 0,-1, 1,
  
  # Ogr. 15: u_1 = 1 (u1 = var 17)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  1, 0, 0, 0,
  
  # Ogr. 16-18: u_i >= 2
  # red 16: u2 >= 2 (u2 = var 18)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 1, 0, 0,
  # red 17: u3 >= 2 (u3 = var 19)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 1, 0,
  # red 18: u4 >= 2 (u4 = var 20)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 1,
  
  # Ogr. 19-21: u_i <= 4
  # red 19: u2 <= 4 (u2 = var 18)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 1, 0, 0,
  # red 20: u3 <= 4 (u3 = var 19)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 1, 0,
  # red 21: u4 <= 4 (u4 = var 20)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 1
)

# Kreiramo matricu
Amat <- matrix(Amat_data, nrow = 21, ncol = n*n + n, byrow = TRUE)

# 5. Rješavanje
rjesenje_tsp <- lp(
  direction = "min",
  objective.in = cvec,
  const.mat = Amat,
  const.dir = dirvec,
  const.rhs = bvec,
  # zadajemo lpSolve-u koji su indeksi integer, a koji binarni
  int.vec = 17:20,    # Varijable u_i (17, 18, 19, 20) su integer
  binary.vec = 1:16   # Varijable x_ij (1 do 16) su binarne
  # ovu temu/ postavku detaljnije prolazimo u cjelobrojnom programiranju
)

# 6. Tumačenje (isto kao prije)
if (rjesenje_tsp$status == 0) {
  cat(paste("Optimalni trošak (vrijeme) ture:", rjesenje_tsp$objval, "minuta\n"))
  
  # Izdvajamo samo x_ij varijable (prvih 16)
  rjesenje_x <- rjesenje_tsp$solution[1:(n*n)]
  
  # Preoblikujemo ih u matricu za lakše čitanje
  rjesenje_matrica <- matrix(rjesenje_x, nrow = n, byrow = TRUE, 
                             dimnames = list(paste0("Iz ", 1:n), paste0("U ", 1:n)))
  
  cat("\nOptimalna matrica puta (x_ij = 1):\n")
  print(round(rjesenje_matrica))
  
  # Pronađimo put
  cat("\nOptimalna ruta (tura):\n")
  trenutni_cvor <- 1
  put <- c(1)
  for (i in 1:(n)) { 
    sljedeci_cvor <- which(rjesenje_matrica[trenutni_cvor, ] > 0.99)
    put <- c(put, sljedeci_cvor)
    trenutni_cvor <- sljedeci_cvor
    if (trenutni_cvor == 1) break 
  }
  
  cat(paste(put, collapse = " -> "))
  
  # Pokažimo i 'u' varijable
  cat("\n\nMTZ pomoćne varijable (u_i):\n")
  rjesenje_u <- rjesenje_tsp$solution[(n*n + 1):(n*n + n)]
  names(rjesenje_u) <- c("u1 (Baza)", "u2 (A)", "u3 (B)", "u4 (Server)")
  print(rjesenje_u)
  
} else {
  cat("Model nije pronašao optimalno rješenje. Status:", rjesenje_tsp$status)
}
## Optimalni trošak (vrijeme) ture: 35 minuta
## 
## Optimalna matrica puta (x_ij = 1):
##      U 1 U 2 U 3 U 4
## Iz 1   0   1   0   0
## Iz 2   0   0   0   1
## Iz 3   1   0   0   0
## Iz 4   0   0   1   0
## 
## Optimalna ruta (tura):
## 1 -> 2 -> 4 -> 3 -> 1
## 
## MTZ pomoćne varijable (u_i):
##   u1 (Baza)      u2 (A)      u3 (B) u4 (Server) 
##           1           2           4           3


Naravno, ovakve probleme (iako možemo), nećemo rješavati koristeći linearno programiranje. Ako Vas je tema zainteresirala, pročitajte poglavlje Algoritmi na mrežama.

Također, možda će Vam biti zanimljiv prikaz slučaja na temu određivanja pješačke rute po turističkim atrakcijama, za što je odabrano 10 turističkih atrakcija grada Pule: Aquarium, Arena, Arheološki muzej, Augustov Hram, Dvojna vrata, Herkulova vrata, Titov park, Tvrđava Kaštel, Zerostrasse i Zlatna Vrata (Slavoluk Sergijevaca), uz prikaz postupka rješavanja u MS Excelu (Kostelić et al. 2021).


Maksimalni tok


Do sada su naši mrežni modeli (transportni problem, najkraći put) bili primarno fokusirani na optimizaciju troška ili udaljenosti. Cilj je bio obaviti zadatak (distribuirati robu, pronaći rutu) što jeftinije ili brže moguće.

Problem maksimalnog toka (engl. Maximum Flow Problem) mijenja paradigmu. Ovdje nas trošak ne zanima. Zanima nas isključivo propusnost.

Pitanje više nije “koji je najjeftiniji put?”, već “koji je maksimalni kapacitet (propusnost) cijelog sustava?”

Motivacija je svuda oko nas:

  • Promet: Koliko maksimalno vozila može proći kroz gradsku mrežu ulica (lukova) od ulaza (izvora) do izlaza (ponora) u sat vremena, s obzirom na kapacitet svake ulice?
  • Logistika: Koliko maksimalno tona tereta možemo prebaciti iz centralnog skladišta (izvora) do luke (ponora) u jednom danu, koristeći mrežu cesta i željeznica, svaka sa svojim dnevnim limitom tonaže?
  • Računalne mreže: Kolika je maksimalna propusnost podataka (u Gbps) koja se može ostvariti između dva servera (izvora i ponora) u podatkovnom centru, s obzirom na to da svaki kabel i router (luk) ima svoj limit?
  • Komunalne usluge: Koliko maksimalno litara vode u sekundi može proći od crpne stanice (izvora) do naselja (ponora) kroz postojeći cjevovod?

Ovaj problem ilustrira srž istraživanja o “uskim grlima” (engl. bottlenecks). Nije važno ako imate autocestu s 10 traka ako se ona na kraju spaja na most s jednom trakom. Taj most je usko grlo koje definira maksimalni tok cijelog sustava.

Iako se problem maksimalnog toka najčešće u elementarnim primjerima na nastavi vezuje uz rješavanje problema u prometu, koristi se i pri modeliranju toka mreže električne energije (Bozorgavari et al, 2020; Patil i Kalkhambkar, 2020), toka informacija (Hu et al., 2021) i drugih sličnih situacija koje se mogu koncipirati na prikladan način.


Definicija


Problem maksimalnog toka definira se na usmjerenom, težinskom grafu \(G = (V, E)\), ali težine sada predstavljaju kapacitete luka.

Primjer grafa za kojeg možemo tražiti maksimalni tok:

Osnovni elementi:

  1. Graf \(G = (V, E)\): skup čvorova \(V\) (raskrižja, routeri) i lukova \(E\) (ceste, kabeli),
  2. Izvor (\(s\)): početni čvor iz kojeg sav tok potječe (nema ulaznih lukova),
  3. Ponor (\(t\)): završni čvor u kojem sav tok završava (nema izlaznih lukova),
  4. Kapaciteti (\(u_{ij}\)): svaki luk \((i, j) \in E\) ima dodijeljen nenegativni kapacitet \(u_{ij}\), koji predstavlja maksimalnu količinu “toka” (npr. vozila/sat, Gbps, tone/dan) koja može proći tim lukom.

Definicija toka:

Tok (ili flow) je funkcija \(f\) koja svakom luku \((i, j)\) dodjeljuje vrijednost \(f_{ij}\) (količina koja stvarno prolazi tim lukom), uz poštivanje dva stroga pravila:

  1. Ograničenje kapaciteta: Količina toka na luku ne može premašiti kapacitet tog luka. \[ 0 \le f_{ij} \le u_{ij} \quad \text{za svaki } (i,j) \in E \]
  2. Očuvanje toka: Za svaki posredni čvor (svaki čvor osim izvora \(s\) i ponora \(t\)), ukupni ulazni tok mora biti jednak ukupnom izlaznom toku. (Roba ne smije “nestati” ili se “stvoriti” unutar mreže). \[ \sum_{k \mid (k,i) \in E} f_{ki} = \sum_{j \mid (i,j) \in E} f_{ij} \quad \text{za svaki } i \in V \setminus \{s, t\} \]

Cilj problema:

Cilj je pronaći takav tok \(f\) koji maksimizira ukupnu vrijednost toka (\(Z\)) koja napušta izvor \(s\) (ili, ekvivalentno, ulazi u ponor \(t\)).

\[ \text{Vrijednost toka } (Z) = \sum_{j \mid (s,j) \in E} f_{sj} \] Cilj je, dakle, \(\max Z\).

Problem maksimalnog toka je klasičan primjer ekstenzije linearnog programiranja.

Parametri

  • \(N\): Skup svih čvorova.
  • \(E\): Skup svih lukova.
  • \(s\): Izvorišni čvor.
  • \(t\): Odredišni (ponor) čvor.
  • \(u_{ij}\): Kapacitet luka \((i, j)\).

Varijabla odluke

  • \(x_{ij}\): Količina toka (stvarna propusnost) na luku od čvora \(i\) do čvora \(j\).

Funkcija cilja

Maksimiziramo ukupan tok koji napušta izvor \(s\).

\[ \max Z = \sum_{j \mid (s,j) \in E} x_{sj} \] (Alternativno, mogli bismo maksimizirati tok koji ulazi u ponor \(t\): \(\max Z = \sum_{i \mid (i,t) \in E} x_{it}\))

Ograničenja

1. Ograničenja kapaciteta (engl. capacity constraints):

Tok na svakom luku mora biti manji ili jednak kapacitetu tog luka. \[ x_{ij} \le u_{ij} \quad \text{za svaki luk } (i,j) \in E \]

2. Ograničenja očuvanja toka (engl. flow conservation):

Za sve posredne čvorove (svi osim \(s\) i \(t\)), ulazni tok mora biti jednak izlaznom toku. \[ \sum_{k \mid (k,i) \in E} x_{ki} - \sum_{j \mid (i,j) \in E} x_{ij} = 0 \quad \text{za svaki čvor } i \in N \setminus \{s, t\} \] (Ili: \(\sum \text{ULAZ} = \sum \text{IZLAZ}\))

3. Ograničenje nenegativnosti:

Tok ne može biti negativan. \[ x_{ij} \ge 0 \quad \text{za sve } (i,j) \in E \]


Primjer


Radite kao DevOps inženjer za tvrtku koja lansira globalno iščekivanu igru “Cyber-Badger 2050”. Vaš zadatak je otkriti koliki je maksimalni tok podataka (u Terabitima po sekundi, Tbps) koji možete “progurati” od glavnog data centra u New Yorku (Izvor) do krajnjih korisnika u Europi (Ponor).

Ako vaša mreža ima “usko grlo” (bottleneck) i ne može podnijeti predviđeni promet od 15 Tbps, serveri će se srušiti, a lansiranje će biti katastrofa.

  • Izvor (\(s\)): NY Data Centar (Čvor 1)
  • Ponor (\(t\)): Europski Korisnici (Čvor 6)
  • Posredni čvorovi: Proxy serveri u Londonu (2), Parizu (3), Frankfurtu (4) te glavni Europski Router (5).

Mreža optičkih kabela i njihovi maksimalni kapaciteti (u Tbps) su sljedeći:

grViz("
digraph max_flow {
  layout = dot;
  rankdir = LR;
  node [shape = record, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 12, color=\"#444444\"];
  
  # Definicija čvorova
  N1 [label = 'NY-DC (1)\nIzvor (s)'];
  N2 [label = 'London (2)'];
  N3 [label = 'Pariz (3)'];
  N4 [label = 'Frankfurt (4)'];
  N5 [label = 'EU-Router (5)'];
  N6 [label = 'Korisnici-EU (6)\nPonor (t)'];

  # Definicija lukova i kapaciteta (u_ij)
  # Oznake sada predstavljaju KAPACITET
  N1 -> N2 [label = ' 10 Tbps']; # x12
  N1 -> N3 [label = ' 8 Tbps']; # x13
  
  N2 -> N4 [label = ' 5 Tbps']; # x24
  N2 -> N5 [label = ' 6 Tbps']; # x25
  
  N3 -> N4 [label = ' 3 Tbps']; # x34
  N3 -> N5 [label = ' 4 Tbps']; # x35
  
  N4 -> N5 [label = ' 9 Tbps']; # x45
  N5 -> N6 [label = ' 12 Tbps']; # x56
}
")
  • Varijable (\(x_{ij}\)): 8 varijabli za 8 lukova (x12, x13, x24, x25, x34, x35, x45, x56).
  • Kapaciteti (\(u_{ij}\)): 10, 8, 5, 6, 3, 4, 9, 12.

Funkcija cilja (maksimizacija ulaza u ponor):

Budući da samo luk \(x_{56}\) ulazi u ponor (čvor 6): \[ \max Z = x_{56} \]

Ograničenja (ukupno 8+4 = 12):

  • Ograničenja kapaciteta (8 ograničenja):

    • \(x_{12} \le 10\)
    • \(x_{13} \le 8\)
    • \(x_{24} \le 5\)
    • \(x_{25} \le 6\)
    • \(x_{34} \le 3\)
    • \(x_{35} \le 4\)
    • \(x_{45} \le 9\)
    • \(x_{56} \le 12\)
  • Očuvanje toka (4 ograničenja za posredne čvorove 2, 3, 4, 5):

    • Čvor 2 (London): (Ulaz) = (Izlaz) \(\to x_{12} = x_{24} + x_{25}\)
    • Čvor 3 (Pariz): (Ulaz) = (Izlaz) \(\to x_{13} = x_{34} + x_{35}\)
    • Čvor 4 (Frankfurt): (Ulaz) = (Izlaz) \(\to x_{24} + x_{34} = x_{45}\)
    • Čvor 5 (EU-Router): (Ulaz) = (Izlaz) \(\to x_{25} + x_{35} + x_{45} = x_{56}\)


library(lpSolve)

# Redoslijed varijabli (8):
# 1: x12, 2: x13, 3: x24, 4: x25, 5: x34, 6: x35, 7: x45, 8: x56

# 1. cvec (Vektor funkcije cilja)
# Maksimiziramo Z = x56
cvec <- c(0, 0, 0, 0, 0, 0, 0, 1)

# 2. Amat (Matrica ograničenja A, 12x8)
# Prvih 8 redova su Ograničenja Kapaciteta
# Zadnja 4 reda su Očuvanje Toka (Ulaz - Izlaz = 0)
Amat_data <- c(
  # Ograničenja Kapaciteta (8 redova)
  1, 0, 0, 0, 0, 0, 0, 0,  # x12 <= 10
  0, 1, 0, 0, 0, 0, 0, 0,  # x13 <= 8
  0, 0, 1, 0, 0, 0, 0, 0,  # x24 <= 5
  0, 0, 0, 1, 0, 0, 0, 0,  # x25 <= 6
  0, 0, 0, 0, 1, 0, 0, 0,  # x34 <= 3
  0, 0, 0, 0, 0, 1, 0, 0,  # x35 <= 4
  0, 0, 0, 0, 0, 0, 1, 0,  # x45 <= 9
  0, 0, 0, 0, 0, 0, 0, 1,  # x56 <= 12
  
  # Očuvanje Toka (4 reda)
  # Čvor 2: x12 - x24 - x25 = 0
  1, 0, -1, -1, 0, 0, 0, 0,
  # Čvor 3: x13 - x34 - x35 = 0
  0, 1, 0, 0, -1, -1, 0, 0,
  # Čvor 4: x24 + x34 - x45 = 0
  0, 0, 1, 0, 1, 0, -1, 0,
  # Čvor 5: x25 + x35 + x45 - x56 = 0
  0, 0, 0, 1, 0, 1, 1, -1
)

Amat <- matrix(Amat_data, nrow = 12, byrow = TRUE)

# 3. bvec (Vektor desne strane b)
bvec <- c(
  # Vrijednosti kapaciteta
  10, 8, 5, 6, 3, 4, 9, 12,
  # Vrijednosti očuvanja toka
  0, 0, 0, 0
)

# 4. dirvec (Smjerovi ograničenja)
dirvec <- c(
  # Kapaciteti
  rep("<=", 8),
  # Tokovi
  rep("=", 4)
)

# --- Rješavanje problema ---
rjesenje_max_flow <- lp(direction = "max",
                        objective.in = cvec,
                        const.mat = Amat,
                        const.dir = dirvec,
                        const.rhs = bvec,
                        compute.sens = 1)

# priprema za ispis

cat(paste("Maksimalni tok (Z):", rjesenje_max_flow$objval, " Tbps\n\n"))
## Maksimalni tok (Z): 12  Tbps
# Imenujemo varijable za lakše čitanje
imena_varijabli <- c("x12 (NY->LON)", "x13 (NY->PAR)", 
                     "x24 (LON->FRA)", "x25 (LON->EUR)", 
                     "x34 (PAR->FRA)", "x35 (PAR->EUR)", 
                     "x45 (FRA->EUR)", "x56 (EUR->KOR)")
names(rjesenje_max_flow$solution) <- imena_varijabli

print(round(rjesenje_max_flow$solution, 2))
##  x12 (NY->LON)  x13 (NY->PAR) x24 (LON->FRA) x25 (LON->EUR) x34 (PAR->FRA) 
##              8              4              2              6              0 
## x35 (PAR->EUR) x45 (FRA->EUR) x56 (EUR->KOR) 
##              4              2             12


  • Maksimalni tok: 12 Tbps.
  • Naša mreža NE MOŽE podnijeti predviđeni promet od 15 Tbps. Maksimum koji možemo progurati je 12 Tbps. Lansiranje će (bez nadogradnje) rezultirati padom sustava.

Analiza “uskog grla”:

Gledamo R-ov ispis optimalnog toka i uspoređujemo ga s kapacitetima (bvec):

Kabel (Varijabla) Kapacitet (Maks.) Stvarni Tok (Rezultat) Zagušeno?
x12 (NY->LON) 10 Tbps 8 Tbps Ne
x13 (NY->PAR) 8 Tbps 4 Tbps Ne
x24 (LON->FRA) 5 Tbps 2 Tbps Ne
x25 (LON->EUR) 6 Tbps 6 Tbps DA!
x34 (PAR->FRA) 3 Tbps 0 Tbps Ne
x35 (PAR->EUR) 4 Tbps 4 Tbps DA!
x45 (FRA->EUR) 9 Tbps 2 Tbps Ne
x56 (EUR->KOR) 12 Tbps 12 Tbps DA!

Što se dogodilo?

  1. NY-DC (Izvor) šalje ukupno 12 Tbps (8 u London, 4 u Pariz). Početni kabeli (x12, x13) su dobro dimenzionirani.
  2. London (koji prima 8 Tbps) šalje 2 Tbps u Frankfurt (x24), a maksimalnih 6 Tbps šalje direktno na EU-Router (x25).
  3. Pariz (koji prima 4 Tbps) šalje maksimalnih 4 Tbps na EU-Router (x35).
  4. Frankfurt (koji prima 2 Tbps iz Londona) prosljeđuje ta 2 Tbps na EU-Router (x45).
  5. EU-Router (Čvor 5) prima ukupno: 6 (iz LON) + 4 (iz PAR) + 2 (iz FRA) = 12 Tbps.
  6. Tih 12 Tbps udara u kabel x56 (EUR->KOR), koji ima kapacitet točno 12 Tbps, i time ga zagušuje.

Minimalni rez (Min-Cut):

\(s-t\) rez je podjela čvorova na dvije skupine, s izvorom \(s\) u prvoj i ponorom \(t\) u drugoj; kapacitet reza je zbroj kapaciteta lukova koji idu iz prve u drugu skupinu. Teorem kaže da je maksimalni tok jednak minimalnom kapacitetu takvog reza.

Tj. teorem o maksimalnom toku i minimalnom rezu kaže da je Max-Flow = Min-Cut. Naš maksimalni tok je 12 Tbps. Model nam pokazuje da je “rez” (usko grlo) koji odvaja Izvor od Ponora zapravo samo jedan kabel: x56, čiji je kapacitet točno 12 Tbps.

Ako je dual = d > 0, povećanje kapaciteta za \(+ \Delta\) povećava maksimalni tok za \(d \cdot \Delta\) (ovdje d=1).

Iako su i x25 i x35 zagušeni, oni su zagušeni zato što je krajnji kabel x56 limitirajući faktor.

Da prestanemo procjenjivati i da nam model kaže što treba nadograditi, pozivamo sjene cijena (duale).

  • Zanima nas prvih 8 ograničenja (naši kapaciteti).
  • Ako je dual > 0, taj kabel je dio “minimalnog reza” i vrijedi ga nadograditi.


# Izvlačimo duale. Prvih 8 se odnosi na ograničenja kapaciteta.
duali_kapaciteta <- rjesenje_max_flow$duals[1:8]

# Dajmo im imena
imena_varijabli <- c("x12 (NY->LON)", "x13 (NY->PAR)", 
                     "x24 (LON->FRA)", "x25 (LON->EUR)", 
                     "x34 (PAR->FRA)", "x35 (PAR->EUR)", 
                     "x45 (FRA->EUR)", "x56 (EUR->KOR)")
names(duali_kapaciteta) <- imena_varijabli

cat("--- Sjene cijena (Vrijednost nadogradnje) ---\n")
## --- Sjene cijena (Vrijednost nadogradnje) ---
print(round(duali_kapaciteta, 2))
##  x12 (NY->LON)  x13 (NY->PAR) x24 (LON->FRA) x25 (LON->EUR) x34 (PAR->FRA) 
##              0              0              0              0              0 
## x35 (PAR->EUR) x45 (FRA->EUR) x56 (EUR->KOR) 
##              0              0              1


  1. Svi kabeli = 0, OSIM…

    • Svi kabeli, uključujući i one koji su “zagušeni” (poput x25 (LON->EUR) koji radi na 6/6 Tbps), imaju sjenu cijene 0.
  • Značenje: Nadogradnja tih kabela (npr. povećanje x25 sa 6 na 7 Tbps) neće nimalo povećati ukupni tok (koji će i dalje biti 12). Oni jesu na 100% kapaciteta, ali nisu limitirajuće usko grlo.
  1. x56 (EUR->KOR) = 1 Samo jedan kabel ima sjenu cijene veću od nule. Ovo je “minimalni rez” (Min-Cut) koji tražimo.
  • Značenje: “Za svaki +1 Tbps kapaciteta koji dodate na kabel x56 (EU-Router \(\to\) Korisnici), ukupni maksimalni tok vaše mreže će porasti za +1 Tbps.”

Cijela mreža je ‘zagušena’ samo na jednom mjestu: na izlaznom kabelu x56 prema korisnicima. Trenutno je na 12/12. Ako želimo preživjeti lansiranje od 15 Tbps, moramo taj jedan kabel nadograditi s 12 na 15 Tbps. To je jedina investicija koja će riješiti problem.


Poseban slučaj: problem maksimalnog protoka


Zbog hitnih radova, obilaznica grada je potpuno zatvorena. Sav promet s autoceste koji inače zaobilazi grad sada se mora preusmjeriti kroz gradsku prometnu mrežu.

Kao gradski planer, vaš je zadatak utvrditi maksimalni protok vozila (izražen u vozilima po satu) koji grad može podnijeti od zapadnog ulaza do istočnog izlaza.

  • Izvor (\(s\)): Ulaz Zapad (čvor 1)
  • Ponor (\(t\)): Izlaz Istok (čvor 5)
  • Posredni čvorovi: Sjeverni Rotor (čvor 2), Centar (čvor 3), Južni Most (čvor 4)

Mreža gradskih prometnica i njihov procijenjeni maksimalni kapacitet (vozila/sat) su sljedeći:

grViz("
digraph city_flow {
  layout = dot;
  rankdir = LR;
  node [shape = record, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 12, color=\"#444444\"];
  
  # Definicija čvorova
  N1 [label = 'Ulaz Zapad (1)\nIzvor (s)'];
  N2 [label = 'Sjeverni Rotor (2)'];
  N3 [label = 'Centar (3)'];
  N4 [label = 'Južni Most (4)'];
  N5 [label = 'Izlaz Istok (5)\nPonor (t)'];

  # Definicija lukova i kapaciteta (u_ij)
  N1 -> N2 [label = ' 2000']; # x12
  N1 -> N3 [label = ' 1500']; # x13
  
  N2 -> N4 [label = ' 1000']; # x24
  N2 -> N5 [label = ' 500'];  # x25
  
  N3 -> N2 [label = ' 300'];  # x32
  N3 -> N4 [label = ' 1200']; # x34
  
  N4 -> N5 [label = ' 2500']; # x45
}
")

Da bismo riješili ovaj problem, uvodimo zamišljeni (virtualni) luk koji ide od Ponora (5) natrag do Izvora (1). Ovaj luk \(x_{51}\) predstavlja ukupan tok koji je uspješno prošao kroz sustav.

Cirkulacija je model toka bez eksplicitnog izvora/ponora gdje za svaki čvor vrijedi ulaz = izlaz. Dodavanjem virtualnog povratnog luka pretvaramo max-flow u max-cirkulaciju s ciljem maksimizacije tog povratnog toka.

  • Varijable: Imamo 7 stvarnih prometnica + 1 zamišljeni luk = 8 varijabli.

    • Stvarne: \(x_{12}, x_{13}, x_{24}, x_{25}, x_{32}, x_{34}, x_{45}\)
    • Pomoćna: \(x_{51}\) (ovo je naš \(Z\))
  • Kapaciteti (\(u_{ij}\)): 7 ograničenja za stvarne prometnice. Zamišljeni luk \(x_{51}\) nema gornje ograničenje kapaciteta, jer ga želimo maksimizirati.

grViz("
digraph city_flow {
  layout = dot;
  rankdir = LR;
  node [shape = record, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 12, color=\"#444444\"];
  
  # Definicija čvorova
  N1 [label = 'Ulaz Zapad (1) (s)'];
  N2 [label = 'Sjeverni Rotor (2)'];
  N3 [label = 'Centar (3)'];
  N4 [label = 'Južni Most (4)'];
  N5 [label = 'Izlaz Istok (5) (t)'];

  # Definicija lukova i kapaciteta (u_ij)
  N1 -> N2 [label = ' 2000']; # x12
  N1 -> N3 [label = ' 1500']; # x13
  
  N2 -> N4 [label = ' 1000']; # x24
  N2 -> N5 [label = ' 500'];  # x25
  
  N3 -> N2 [label = ' 300'];  # x32
  N3 -> N4 [label = ' 1200']; # x34
  
  N4 -> N5 [label = ' 2500']; # x45
  
  N5 -> N1 [label = '(x51)', style = dashed, color = red; fontcolor = red];
}
")

Funkcija cilja: \[ \max Z = x_{51} \]

Ograničenja (ukupno 7 + 5 = 12):

  • Ograničenja kapaciteta (7 ograničenja):

    • \(x_{12} \le 2000\)
    • \(x_{13} \le 1500\)
    • \(x_{24} \le 1000\)
    • \(x_{25} \le 500\)
    • \(x_{32} \le 300\)
    • \(x_{34} \le 1200\)
    • \(x_{45} \le 2500\)
  • Očuvanje toka (5 ograničenja - JEDNO ZA SVAKI ČVOR):

    Budući da je sustav zatvoren (cirkulacija), za svaki čvor vrijedi \(\sum \text{ULAZ} = \sum \text{IZLAZ}\). (Format: \(\sum \text{IZLAZ} - \sum \text{ULAZ} = 0\))

    • Čvor 1 (Ulaz-Zapad): \((x_{12} + x_{13}) - x_{51} = 0\)
    • Čvor 2 (Sj. Rotor): \((x_{24} + x_{25}) - (x_{12} + x_{32}) = 0\)
    • Čvor 3 (Centar): \((x_{32} + x_{34}) - x_{13} = 0\)
    • Čvor 4 (J. Most): \(x_{45} - (x_{24} + x_{34}) = 0\)
    • Čvor 5 (Izlaz-Istok): \(x_{51} - (x_{25} + x_{45}) = 0\)


library(lpSolve)

# Redoslijed varijabli (8):
# 1: x12, 2: x13, 3: x24, 4: x25, 5: x32, 6: x34, 7: x45
# 8: x51 (Naš Z - povratni luk)

# 1. cvec (Vektor funkcije cilja)
# Maksimiziramo Z = x51
cvec <- c(0, 0, 0, 0, 0, 0, 0, 1)

# 2. Amat (Matrica ograničenja A, 12x8)
# Prvih 7 redova su Ograničenja Kapaciteta
# Zadnjih 5 redova su Očuvanje Toka
Amat_data <- c(
  # Ograničenja Kapaciteta (7 redova)
  1, 0, 0, 0, 0, 0, 0, 0,  # x12 <= 2000
  0, 1, 0, 0, 0, 0, 0, 0,  # x13 <= 1500
  0, 0, 1, 0, 0, 0, 0, 0,  # x24 <= 1000
  0, 0, 0, 1, 0, 0, 0, 0,  # x25 <= 500
  0, 0, 0, 0, 1, 0, 0, 0,  # x32 <= 300
  0, 0, 0, 0, 0, 1, 0, 0,  # x34 <= 1200
  0, 0, 0, 0, 0, 0, 1, 0,  # x45 <= 2500
  
  # Očuvanje Toka (5 redova)
  # Čvor 1: (x12 + x13) - x51 = 0
   1,  1,  0,  0,  0,  0,  0, -1,
  # Čvor 2: (x24 + x25) - (x12 + x32) = 0
  -1,  0,  1,  1, -1,  0,  0,  0,
  # Čvor 3: (x32 + x34) - x13 = 0
   0, -1,  0,  0,  1,  1,  0,  0,
  # Čvor 4: x45 - (x24 + x34) = 0
   0,  0, -1,  0,  0, -1,  1,  0,
  # Čvor 5: x51 - (x25 + x45) = 0
   0,  0,  0, -1,  0,  0, -1,  1
)

Amat <- matrix(Amat_data, nrow = 12, byrow = TRUE)

# 3. bvec (Vektor desne strane b)
bvec <- c(
  # Vrijednosti kapaciteta
  2000, 1500, 1000, 500, 300, 1200, 2500,
  # Vrijednosti očuvanja toka (sve 0)
  0, 0, 0, 0, 0
)

# 4. dirvec (Smjerovi ograničenja)
dirvec <- c(
  # Kapaciteti
  rep("<=", 7),
  # Tokovi
  rep("=", 5)
)

# --- Rješavanje problema ---
rjesenje_max_flow_circ <- lp(direction = "max",
                             objective.in = cvec,
                             const.mat = Amat,
                             const.dir = dirvec,
                             const.rhs = bvec,
                             compute.sens = 1)

# priprema za čitkiji ispis
cat(paste("Maksimalni tok (Z):", rjesenje_max_flow_circ$objval, " vozila/sat\n\n"))
## Maksimalni tok (Z): 2700  vozila/sat
# Imenujemo varijable za lakše čitanje
imena_varijabli <- c("x12 (U-Z -> Sj.R)", "x13 (U-Z -> Cen)", 
                     "x24 (Sj.R -> J.Most)", "x25 (Sj.R -> I-I)", 
                     "x32 (Cen -> Sj.R)", "x34 (Cen -> J.Most)", 
                     "x45 (J.Most -> I-I)", "x51 (Povratni Luk)")
names(rjesenje_max_flow_circ$solution) <- imena_varijabli

cat("--- Optimalni Tok (Stvarna propusnost po prometnici) ---\n")
## --- Optimalni Tok (Stvarna propusnost po prometnici) ---
# Prikazujemo samo prvih 7 (stvarnih) prometnica
print(round(rjesenje_max_flow_circ$solution[1:7], 2))
##    x12 (U-Z -> Sj.R)     x13 (U-Z -> Cen) x24 (Sj.R -> J.Most) 
##                 1500                 1200                 1000 
##    x25 (Sj.R -> I-I)    x32 (Cen -> Sj.R)  x34 (Cen -> J.Most) 
##                  500                    0                 1200 
##  x45 (J.Most -> I-I) 
##                 2200


Sustav može podnijeti maksimalno 2700 vozila na sat. Ako je priljev s autoceste (koji ulazi u čvor 1) veći od toga, doći će do kolapsa i zastoja na samom ulazu u grad, jer je to “limit” koji gradska mreža može progutati.

Da bismo vidjeli gdje su gužve (npr. gdje poslati prometnu policiju da regulira promet), uspoređujemo stvarni tok (R ispis) s kapacitetima prometnica (iz bvec vektora):

Prometnica (Varijabla) Kapacitet (Maks.) Stvarni tok (rezultat) Zagušeno? (Tok = Kapacitet?)
x12 (Ulaz -> Sj.R) 2000 1500 Ne
x13 (Ulaz -> Cen) 1500 1200 Ne
x24 (Sj.R -> J.Most) 1000 1000 DA!
x25 (Sj.R -> Izlaz) 500 500 DA!
x32 (Cen -> Sj.R) 300 0 Ne (nitko ne vozi ovuda)
x34 (Cen -> J.Most) 1200 1200 DA!
x45 (J.Most -> Izlaz) 2500 2200 Ne

Uska grla (“minimalni rez”) nisu na ulazu ili izlazu iz grada, već u samoj gradskoj jezgri.


# Imenujemo prvih 7 ograničenja (naše prometnice)
imena_prometnica <- c("x12 (U-Z -> Sj.R)", "x13 (U-Z -> Cen)", 
                      "x24 (Sj.R -> J.Most)", "x25 (Sj.R -> I-I)", 
                      "x32 (Cen -> Sj.R)", "x34 (Cen -> J.Most)", 
                      "x45 (J.Most -> I-I)")

# Izvlačimo duale. Prvih 7 se odnosi na ograničenja kapaciteta.
# rjesenje_max_flow_circ je naš LP objekt iz prethodnog bloka
duali_kapaciteta <- rjesenje_max_flow_circ$duals[1:7]

# Dajmo im imena radi čitljivosti
names(duali_kapaciteta) <- imena_prometnica

print(round(duali_kapaciteta, 2))
##    x12 (U-Z -> Sj.R)     x13 (U-Z -> Cen) x24 (Sj.R -> J.Most) 
##                    0                    0                    1 
##    x25 (Sj.R -> I-I)    x32 (Cen -> Sj.R)  x34 (Cen -> J.Most) 
##                    1                    0                    1 
##  x45 (J.Most -> I-I) 
##                    0


Sjena cijena odgovara na pitanje: “Ako povećamo kapacitet ove prometnice za +1 vozilo/sat, za koliko će se povećati ukupni tok (Z)?”

  1. Prometnice s dualnom vrijednošću 0:
  • x12, x13, x32, x45
  • Značenje: Proširenje bilo koje od ovih prometnica je bacanje novca. Iako je x45 (J.Most -> I-I) radio na visokom kapacitetu (2200 od 2500), njegovo proširenje ne bi donijelo nikakvo poboljšanje jer on nije limitirajuće usko grlo.
  1. Prometnice s dualnom vrijednošću 1:
  • x24 (Sj.R -> J.Most)
  • x25 (Sj.R -> I-I)
  • x34 (Cen -> J.Most)
  • Značenje: Ovo su tri prometnice koje čine Minimalni rez (Min-Cut). One su pravi razlog zašto tok ne može biti veći od 2700. Ako proširite bilo koju od ove tri prometnice za +1 vozilo/sat, ukupni tok (Z) će porasti za +1 vozilo/sat.

Naša analiza pokazuje da je maksimalni protok kroz grad 2700 vozila/sat. Problem nije na ulazu u grad (x12, x13) niti na izlazu (x45). Pravi ‘čep’ čine tri prometnice: ona od Sjevernog Rotora do Južnog Mosta (x24), od Sjevernog Rotora do Izlaza Istok (x25) i od Centra do Južnog Mosta (x34). Zbroj njihovih kapaciteta je 1000 + 500 + 1200 = 2700. Da bismo povećali protok, moramo proširiti jednu od te tri ceste.


Kritični put (CPM)


Problem najkraćeg puta, kako smo ga definirali, traži optimalnu rutu za jedan entitet (paket, kamion) kroz mrežu. Međutim, većina pothvata u stvarnom svijetu—bilo da se radi o izgradnji zgrade, lansiranju softvera ili planiranju marketinške kampanje—nisu jedan zadatak, već mreža međusobno ovisnih aktivnosti.

Neke aktivnosti mogu se odvijati paralelno (npr. bojanje zidova u jednoj sobi i postavljanje pločica u drugoj), dok su druge strogo sekvencijalne (npr. ne možete graditi krov prije nego što su temelji i zidovi gotovi).

Metoda kritičnog puta (engl. Critical Path Method, CPM) je alat za upravljanje projektima koji odgovara na dva ključna pitanja:

  1. Koliko će minimalno trajati cijeli projekt?
  2. Koje su točno one aktivnosti koje su “kritične”, tj. koje ne smiju kasniti ni jedan dan, a da ne odgode cijeli projekt?

Motivacija je jasna: ako resursi (novac, ljudi) nisu neograničeni, menadžer mora znati gdje fokusirati svoju pažnju. CPM matematički identificira one zadatke koji su “usko grlo” samog vremena.

Paradoksalno, da bismo pronašli najkraće moguće vrijeme završetka projekta, moramo pronaći najduži slijed (put) ovisnih aktivnosti u mreži.


Definicija


Problem se modelira kao usmjereni aciklički graf (DAG), poznat kao projektna mreža (Kelley & Walker, 1959).

  • Aktivnosti (lukovi): Ovo su stvarni zadaci koje treba obaviti (npr. “Kopanje temelja”, “Pisanje koda modula A”). Svaka aktivnost \((i, j)\) ima definirano trajanje (\(t_{ij}\)).

  • Događaji ili prekretnice (čvorovi): Ovo su točke u vremenu koje označavaju početak ili kraj jedne ili više aktivnosti (npr. “Temelji gotovi”, “Kod modula A testiran”).

    • Početni čvor (\(s\)): Događaj koji označava početak projekta.
    • Završni čvor (\(t\)): Događaj koji označava kraj projekta.


  • Prethodnost (engl. precedence): Struktura mreže definira ovisnosti. Ako luk \((i, j)\) postoji, to znači da aktivnost \((j, k)\) ne može započeti dok se događaj \(j\) (kraj aktivnosti \((i, j)\)) nije dogodio.


Definicija kriterija:

  1. Trajanje puta: Ukupno vrijeme potrebno da se završe sve aktivnosti duž određenog puta od \(s\) do \(t\).
  2. Kritični put (Critical Path): To je onaj put od \(s\) do \(t\) koji ima najduže ukupno trajanje.
  3. Trajanje projekta: Minimalno ukupno vrijeme potrebno za dovršetak projekta jednako je trajanju kritičnog puta.
  4. Slobodni vremenski razmak (Slack/Float): Količina vremena za koju “nekritična” aktivnost može kasniti, a da ne utječe na ukupno trajanje projekta. Aktivnosti na kritičnom putu imaju nula “slacka”.


CPM možemo elegantno formulirati kao problem linearnog programiranja. Ovdje ne optimiziramo tok (\(x_{ij}\)), već vremena događaja.

  • \(N\): Skup svih čvorova (događaja) u mreži.
  • \(s \in N\): Početni čvor projekta.
  • \(t \in N\): Završni čvor projekta.
  • \(t_{ij}\): Poznato trajanje aktivnosti predstavljene lukom \((i, j)\).


Varijable odluke

  • \(v_i\): Varijabla koja predstavlja najranije moguće vrijeme kada se događaj (čvor) \(i\) može dogoditi (tj. kada sve aktivnosti koje vode do njega mogu biti završene).

Funkcija cilja

Naš cilj je završiti projekt što je brže moguće. Projekt je gotov kada se dogodi završni čvor (\(t\)). Ako početni čvor (\(s\)) započne u vremenu \(v_s\), a završni se dogodi u \(v_t\), ukupno trajanje je \((v_t - v_s)\).

Ako fiksiramo početno vrijeme na nulu (\(v_s = 0\)), cilj postaje jednostavno minimizirati vrijeme završetka.

\[ \min Z = v_t \] (Minimiziramo vrijeme događanja završnog čvora.)

Ograničenja

Ograničenja osiguravaju da se poštuje redoslijed i trajanje aktivnosti.

Za svaku aktivnost (luk) \((i, j)\) u mreži, vrijeme završetka događaja \(j\) (\(v_j\)) mora biti barem onoliko kasno koliko je vrijeme završetka događaja \(i\) (\(v_i\)) plus trajanje (\(t_{ij}\)) same aktivnosti.

1. Ograničenja prethodnosti (Precedence Constraints): \[ v_j \ge v_i + t_{ij} \quad \text{za svaki luk } (i, j) \]

Ili, češće zapisano kao:

\[ v_j - v_i \ge t_{ij} \]

2. Ograničenje početka (normalizacija):

Da bismo imali referentnu točku, fiksiramo početak projekta na vrijeme nula.

\[ v_s = 0 \]

3. Ograničenje nenegativnosti:

(Iako su prethodna ograničenja obično dovoljna, formalno možemo reći da se vremena ne mogu događati prije početka.)

\[ v_i \ge 0 \quad \text{za svaki } i \in N \]

Kada LP solver pronađe minimalnu vrijednost za \(v_t\), ta vrijednost će biti točno jednaka zbroju trajanja \(t_{ij}\) duž najdužeg puta od \(s\) do \(t\).

Za aktivnost \((i,j)\):

  • ES/EF (Earliest Start/Finish): najraniji start i završetak (iz forward passa).

  • LS/LF (Latest Start/Finish): najkasniji start i završetak bez kašnjenja projekta (iz backward passa).

  • Slack = LS − ES = LF − EF; na kritičnom putu slack = 0.


Primjer


Vi ste voditelj malog tima koji razvija novu indie igru, “Projekt Poltergeist”. Imate zadan budžet i rokove. Prije nego što obećate fanovima datum izlaska, morate znati koje je minimalno ukupno vrijeme potrebno za lansiranje igre i koje su vam kritične aktivnosti (one koje ne smiju kasniti ni tjedan dana).

Projekt ste podijelili na ključne događaje (čvorove), koji označavaju završetak jedne ili više faza.


Događaji (čvorovi):

  • 1: Početak projekta (start)
  • 2: Završen “game design doc” (GDD)
  • 3: Završen “core engine” (programiranje)
  • 4: Završena grafika (modeli likova i okoliš)
  • 5: Igra je “feature complete” (sve je ubačeno i spojeno)
  • 6: Beta testiranje završeno
  • 7: Igra lansirana (kraj)


Aktivnosti (Lukovi) i trajanje:

Aktivnosti su zadaci koji povezuju događaje. Svaka ima procijenjeno trajanje u tjednima.

Oznaka Aktivnost (Luk) Opis Trajanje (\(t_{ij}\)) Preduvjet (Čvor)
A (1) \(\to\) (2) Pisanje GDD-a 4 1 (Start)
B (2) \(\to\) (3) Programiranje Enginea 8 2 (GDD)
C (2) \(\to\) (4) Dizajn Grafike 12 2 (GDD)
D (3) \(\to\) (5) Integracija Mehanika 6 3 (Engine)
E (4) \(\to\) (5) Integracija Grafike 3 4 (Grafika)
F (5) \(\to\) (6) Beta Testiranje 5 5 (Feat. Complete)
G (6) \(\to\) (7) Popravci Bugova i Lansiranje 2 6 (Beta Test)


Ključne ovisnosti:

  • Programiranje Enginea (B) i Grafika (C) mogu se raditi paralelno, ali obje ovise o GDD-u (A).
  • Da bi igra bila “Feature Complete” (Događaj 5), obje grane (Integracija Mehanika [D] i Integracija Grafike [E]) moraju biti završene.
grViz("
digraph cpm_game_dev {
  layout = dot;
  rankdir = LR;
  node [shape = circle, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 11, color=\"#444444\"];
  
  # Definicija čvorova (Događaja)
  N1 [label = 'Start\n(1)'];
  N2 [label = 'GDD Gotov\n(2)'];
  N3 [label = 'Engine\nGotov (3)'];
  N4 [label = 'Grafika\nGotova (4)'];
  N5 [label = 'Feat. Comp.\n(5)'];
  N6 [label = 'Beta Test\nGotov (6)'];
  N7 [label = 'Lansiranje\n(7)'];

  # Definicija lukova (Aktivnosti) s trajanjem
  N1 -> N2 [label = ' A (4 tjedna)'];
  N2 -> N3 [label = ' B (8 tjedana)'];
  N2 -> N4 [label = ' C (12 tjedana)'];
  N3 -> N5 [label = ' D (6 tjedana)'];
  N4 -> N5 [label = ' E (3 tjedna)'];
  N5 -> N6 [label = ' F (5 tjedana)'];
  N6 -> N7 [label = ' G (2 tjedna)'];
}
")
  • Varijable (\(v_i\)): 7 varijabli (\(v_1\) do \(v_7\)). \(v_i\) predstavlja najranije vrijeme (u tjednima) kada događaj/ aktivnost \(i\) može započeti (tj. kad je prethodna aktivnost izvršena).

  • Funkcija Cilja: Minimiziramo vrijeme završetka, \(v_7\). \[ \min Z = v_7 \]

  • Ograničenja (8 ograničenja): Jedno za početak, i jedno za svaku od 7 aktivnosti.

    \[ v_j - v_i \ge t_{ij} \]

    1. \(v_1 = 0\) (Počinjemo u tjednu 0)
    2. \(v_2 - v_1 \ge 4\) (Aktivnost A)
    3. \(v_3 - v_2 \ge 8\) (Aktivnost B)
    4. \(v_4 - v_2 \ge 12\) (Aktivnost C)
    5. \(v_5 - v_3 \ge 6\) (Aktivnost D)
    6. \(v_5 - v_4 \ge 3\) (Aktivnost E)
    7. \(v_6 - v_5 \ge 5\) (Aktivnost F)
    8. \(v_7 - v_6 \ge 2\) (Aktivnost G)


library(lpSolve)

# Varijable (7): v1, v2, v3, v4, v5, v6, v7

# 1. cvec (Vektor funkcije cilja)
# Minimiziramo Z = 0*v1 + ... + 0*v6 + 1*v7
cvec <- c(0, 0, 0, 0, 0, 0, 1)

# 2. Amat (Matrica ograničenja A, 8x7)
# (Redovi = Ograničenja 1-8; Stupci = Varijable v1-v7)
Amat_data <- c(
  # 1. v1 = 0
   1,  0,  0,  0,  0,  0,  0,
  # 2. v2 - v1 >= 4 (A)
  -1,  1,  0,  0,  0,  0,  0,
  # 3. v3 - v2 >= 8 (B)
   0, -1,  1,  0,  0,  0,  0,
  # 4. v4 - v2 >= 12 (C)
   0, -1,  0,  1,  0,  0,  0,
  # 5. v5 - v3 >= 6 (D)
   0,  0, -1,  0,  1,  0,  0,
  # 6. v5 - v4 >= 3 (E)
   0,  0,  0, -1,  1,  0,  0,
  # 7. v6 - v5 >= 5 (F)
   0,  0,  0,  0, -1,  1,  0,
  # 8. v7 - v6 >= 2 (G)
   0,  0,  0,  0,  0, -1,  1
)

Amat <- matrix(Amat_data, nrow = 8, byrow = TRUE)

# 3. bvec (Vektor desne strane b)
bvec <- c(0,  # v1 = 0
          4,  # t_A
          8,  # t_B
          12, # t_C
          6,  # t_D
          3,  # t_E
          5,  # t_F
          2)  # t_G

# 4. dirvec (Smjerovi ograničenja)
dirvec <- c("=", 
            rep(">=", 7))

# --- Rješavanje problema ---
rjesenje_cpm <- lp(direction = "min",
                   objective.in = cvec,
                   const.mat = Amat,
                   const.dir = dirvec,
                   const.rhs = bvec,
                   compute.sens = 1)

# Priprema za ispis i ispis rezultata
cat(paste("Minimalno vrijeme za lansiranje igre (Z):", rjesenje_cpm$objval, " tjedana\n\n"))
## Minimalno vrijeme za lansiranje igre (Z): 26  tjedana
# Imenujemo varijable (Događaje)
imena_dogadaja <- c("v1 (Start)", "v2 (GDD Gotov)", 
                   "v3 (Engine Gotov)", "v4 (Grafika Gotova)", 
                   "v5 (Feature Complete)", "v6 (Beta Gotova)", 
                   "v7 (Lansiranje)")

# Vremena događaja
rjesenje_vremena <- rjesenje_cpm$solution
names(rjesenje_vremena) <- imena_dogadaja

print(round(rjesenje_vremena, 2))
##            v1 (Start)        v2 (GDD Gotov)     v3 (Engine Gotov) 
##                     0                     4                    12 
##   v4 (Grafika Gotova) v5 (Feature Complete)      v6 (Beta Gotova) 
##                    16                    19                    24 
##       v7 (Lansiranje) 
##                    26


Nakon što imamo najranija vremena događaja (rjesenje_cpm$solution), možemo izračunati “slack” (slobodni vremenski razmak) za svaku aktivnost.

Slack je količina vremena za koju aktivnost može kasniti, a da ne odgodi cijeli projekt. Formula: Slack = (v_j - v_i) - t_ij

  • Ako je Slack == 0 ili Dual \(\neq\) 0, aktivnost je KRITIČNA.
  • Ako je Slack > 0 ili Dual = 0, aktivnost NIJE KRITIČNA i ima “lufta”.

Umjesto ručnog izračuna “slacka”, koristit ćemo sjene cijena (duale) koje je LP solver već izračunao za svako ograničenje. Znamo da će sjena cijene biti različita od (veća od) 0 ako je slack 0, odnosno jednaka nuli za pozitivnu vrijednost slacka.


# 1. Definiramo imena za 7 aktivnosti
# (Ona odgovaraju ograničenjima 2 do 8 u našem modelu)
imena_aktivnosti <- c("A (Pisanje GDD-a)", 
                       "B (Program. Enginea)", 
                       "C (Dizajn Grafike)", 
                       "D (Integ. Mehanika)", 
                       "E (Integ. Grafike)", 
                       "F (Beta Testiranje)", 
                       "G (Popravci/Lans.)")

# 2. Izvlačimo dualne varijable (sjenovite cijene)
# rjesenje_cpm$duals ima 8 elemenata (za naših 8 ograničenja)
# Prvi dual (za ograničenje v1 = 0) nas ne zanima za ovu analizu.
# Trebaju nam duali [2:8], koji odgovaraju aktivnostima A-G.
duali_aktivnosti <- rjesenje_cpm$duals[2:8]

analiza_duala_df <- data.frame(
  Naziv_aktivnosti = imena_aktivnosti,
  Trajanje_t_ij = bvec[2:8],
  Sjenovita_Cijena_Dual = duali_aktivnosti,
  Kriticna_aktivnost = ifelse(duali_aktivnosti > 0.001, "DA", "Ne")
)

print(analiza_duala_df)
##       Naziv_aktivnosti Trajanje_t_ij Sjenovita_Cijena_Dual Kriticna_aktivnost
## 1    A (Pisanje GDD-a)             4                     1                 DA
## 2 B (Program. Enginea)             8                     0                 Ne
## 3   C (Dizajn Grafike)            12                     1                 DA
## 4  D (Integ. Mehanika)             6                     0                 Ne
## 5   E (Integ. Grafike)             3                     1                 DA
## 6  F (Beta Testiranje)             5                     1                 DA
## 7   G (Popravci/Lans.)             2                     1                 DA


Sjena cijene (dual) nam govori o “cijeni kašnjenja”. Ako je dual = 1, aktivnost je kritična. Ako je dual = 0, aktivnost ima “lufta” (slack).

  1. Kritične aktivnosti (sjena_cijene_dual = 1):
  • A (pisanje GDD-a)
  • C (dizajn grafike)
  • E (integ. grafike)
  • F (beta testiranje)
  • G (popravci/lans.)
  • Zaključak: ovo je naš kritični put. Ako bilo koja od ovih pet aktivnosti kasni jedan tjedan, cijeli projekt kasni jedan tjedan.


  1. Nekritične aktivnosti (sjena_cijene_dual = 0): -B (program. enginea) -D (integ. mehanika) -Zaključak: cijeli “programerski” ogranak (B i D) nije kritičan.


Ukupno trajanje projekta je 26 tjedana.

  • Put grafičara (C+E): \(12 + 3 = 15\) tjedana.
  • Put programera (B+D): \(8 + 6 = 14\) tjedana.


Programerski tim je tjedan dana brži od grafičkog. Zbog toga, cijeli njihov put (B i D) ima sjenu cijene nula (dual = 0). Oni imaju 1 tjedan “lufta” (slacka).

Zaključak analize:

Minimalno ukupno trajanje projekta, kako je utvrđeno LP modelom, iznosi 26 tjedana. Analiza sjena cijena (duala) omogućuje preciznu identifikaciju kritičnih i nekritičnih aktivnosti.

  • Kritični put (sjena cijene \(\neq\) 0) sastoji se od aktivnosti: A \(\to\) C \(\to\) E \(\to\) F \(\to\) G. Bilo kakvo kašnjenje u trajanju ijedne od ovih aktivnosti (npr. ‘C - dizajn grafike’ ili ‘E - integ. grafike’) izravno će uzrokovati kašnjenje ukupnog datuma završetka projekta.

  • Nekritični put (sjena cijene = 0) čine aktivnosti B (‘program. enginea’) i D (‘integ. mehanika’). Ovaj put ima ukupan slobodni vremenski razmak (slack) od 1 tjedna. To je izračunata razlika između trajanja dužeg, kritičnog paralelnog puta (C+E = 15 tjedana) i kraćeg, nekritičnog puta (B+D = 14 tjedana).

  • Menadžerska implikacija: Resursi i nadzor prvenstveno moraju biti usmjereni na aktivnosti na kritičnom putu kako bi se osigurao rok od 26 tjedana. Programerski ogranak (B i D) posjeduje fleksibilnost od jednog tjedna kašnjenja bez negativnog utjecaja na konačni rok projekta.


Isti problem možemo riješiti i koristeći pakete criticalpath i critpath.


Rješavanje koristeći critpath


Dok je rješavanje problema kritičnog puta pomoću linearnog programiranja (lp) fundamentalno i točno, ono je često “ručni” i spor proces koji zahtijeva pažljivo sastavljanje matrica. U praksi se koriste specijalizirani paketi koji sadrže već optimizirane algoritme. Ovaj pristup preslikava našu LP formulaciju, gdje su čvorovi “događaji” (npr. “GDD gotov”), a lukovi su “aktivnosti” (npr. “Pisanje GDD-a”).

Prije nego što uronimo u kod, važno je razumjeti kako ovaj paket (i većina modernih softvera za upravljanje projektima) pristupa problemu.

Iza kulisa, paket ne rješava linearni program. On provodi klasični Two-Pass CPM algoritam:

  1. Prolaz prema naprijed (engl. forward pass): Počevši od starta (vrijeme 0), algoritam prolazi kroz mrežu i za svaku aktivnost izračunava njeno najranije moguće vrijeme početka (Early Start - ES) i najranije moguće vrijeme završetka (Early Finish - EF).
  • EF = ES + Trajanje
  • ES iduće aktivnosti je max(EF) svih njenih preduvjeta.
  • Ukupno trajanje projekta je maksimalni EF zadnje aktivnosti.
  1. Prolaz prema natrag (engl. backward pass): Počevši od kraja (sad, kad znamo ukupno trajanje), algoritam ide unatrag i računa najkasnije dozvoljeno vrijeme početka (Late Start - LS) i najkasnije dozvoljeno vrijeme završetka (Late Finish - LF), a da se cijeli projekt ne odgodi.

  2. Izračun “Slacka”: Za svaku aktivnost izračunava “Total slack” (luft):

  • Slack = LS - ES (ili LF - EF)
  • Ako je Slack == 0, aktivnost je kritična.

Ovaj pristup je računski izuzetno brz i direktno daje sve potrebne podatke (ES, EF, LS, LF, Slack), što vidimo i u kodu.


# install.packages("critpath")
library(critpath)

# 1. Definiramo aktivnosti (lukove) i trajanja
activities_aoa <- data.frame(
  Naziv_Aktivnosti = c("A", "B", "C", "D", "E", "F", "G"),
  Trajanje = c(4, 8, 12, 6, 3, 5, 2)
)

# 2. Definiramo topologiju mreže (veze)
# Ovo su naši lukovi (i, j)
edges_aoa <- data.frame(
  Start_node = c(1, 2, 2, 3, 4, 5, 6), # Početni događaj (i) ili neposredni prethodnici
  End_node   = c(2, 3, 4, 5, 5, 6, 7)  # Završni događaj (j)
)

# 3. Spajamo podatke u format koji solve_pathAOA traži
# Moramo spojiti trajanje sa svakim lukom.
# U našem slučaju, redoslijed je već 1-na-1
data_game_dev <- cbind(
  edges_aoa, 
  Activity = activities_aoa$Naziv_Aktivnosti, 
  Duration = activities_aoa$Trajanje
)

# 4. Rješavamo problem
# Koristimo AOA (Activity on Arc) rješavač
critical_game <- solve_pathAOA(data_game_dev, deterministic = TRUE)
## Completion time:  26
# 5. Izdvajamo tablicu s rezultatima
rez_crit <- critical_game$schedule

# Filtriramo samo kritične aktivnosti
rez_crity <- subset(rez_crit, Crit == "*")
print(rez_crity)
##   Name Duration ESij LSij EFij LFij TFij Crit
## 1    A        4    0    0    4    4    0    *
## 3    C       12    4    4   16   16    0    *
## 5    E        3   16   16   19   19    0    *
## 6    F        5   19   19   24   24    0    *
## 7    G        2   24   24   26   26    0    *


Paket critpath je odradio sav posao umjesto nas i potvrdio našu ručnu analizu duala.

  • Ukupno trajanje: 26 tjedana.
  • Kritični put: Tablica rez_crity (gdje je Crit == "*") jasno ispisuje kritični put: A \(\to\) C \(\to\) E \(\to\) F \(\to\) G.
  • Analiza ‘slacka’ (TSij): Iako ih nismo ispisali, da smo pogledali cijelu tablicu rez_crit, vidjeli bismo da aktivnosti B i D (programerski tim) imaju TSij (Total Slack) jednak 1, dok sve kritične aktivnosti imaju 0.

Ovo je nešto brži i pregledniji način za dobivanje rješenja od ručnog postavljanja LP-a.


# 6. Crtanje grafa
plot_graphAOA(data_game_dev)
plot_graphAOA(data_game_dev, solved = critical_game)


Rješavanje koristeći criticalpath


Nakon što smo vidjeli kako lpSolve rješava problem “sirovom” optimizacijom i kako critpath koristi specijalizirani AOA algoritam, pogledajmo i paket criticalpath.

Paket criticalpath pristupa problemu fundamentalno drugačije od critpath (i našeg LP modela).

  • lpSolve / critpath: Koristili su AOA (Activity-on-Arc) model. Čvorovi su bili događaji (prekretnice), a lukovi su bili aktivnosti koje imaju trajanje.
  • criticalpath: Koristi AON (Activity-on-Node) model. Ovdje su aktivnosti sami čvorovi (imaju ID, ime i trajanje). Lukovi (koje definiramo kao relations) služe samo da pokažu redoslijed, odnosno preduvjete.


Ovaj AON pristup je danas češći u softverima za upravljanje projektima jer je ljudima često intuitivnije prvo popisati sve zadatke, a zatim definirati koji zadatak ovisi o kojem.

U pozadini, algoritam je i dalje isti (klasični prolaz naprijed i natrag za izračun ES, EF, LS, LF i slacka), ali je unos podataka prilagođen AON logici.

Najveća prednost criticalpath paketa nije samo izračun, već njegova izvrsna sposobnost vizualizacije. Dok nam je critpath dao mrežni dijagram (koristan za analizu ovisnosti), criticalpath nam daje Ganttov dijagram ili gantogram (sch_xy_gantt_matrix). Ovo je standardni industrijski alat koji prikazuje trajanje svake aktivnosti na vremenskoj crti i vizualno nam pokazuje “luft” (slack) za nekritične zadatke.

Prvo moramo “prevesti” naš problem u AON format: lista aktivnosti i lista veza (preduvjeta). To znači da moramo prevesti naš AOA (aktivnost na luku) model u AON (aktivnost na čvoru) model. Iako se topologija mreže time mijenja, logika ovisnosti i, što je najvažnije, trajanja samih aktivnosti, ostaju identični.

Evo kako prenosimo trajanja i kako AON algoritam koristi ta trajanja za izračun ukupnog vremena projekta.


Trajanja aktivnosti (sada na čvorovima)

Ovo su trajanja koja smo unosimo u activities_aon data frame. Ona su identična trajanju lukova u originalnom AOA problemu:

-A = 4 tjedna -B = 8 tjedana -C = 12 tjedana -D = 6 tjedana -E = 3 tjedna -F = 5 tjedana -G = 2 tjedna


# install.packages("criticalpath")
library(criticalpath)

# 1. Definiramo aktivnosti (čvorove)
# Ovo su naši zadaci, sada kao čvorovi
activities_aon <- data.frame(
  id = 1:7,
  name = c("A (Pisanje GDD-a)", 
           "B (Program. Enginea)", 
           "C (Dizajn Grafike)", 
           "D (Integ. Mehanika)", 
           "E (Integ. Grafike)", 
           "F (Beta Testiranje)", 
           "G (Popravci/Lans.)"),
  duration = c(4L, 8L, 12L, 6L, 3L, 5L, 2L)   # Paket zahtijeva integer, a ne numeric.
                                              # Dodajemo 'L' da forsiramo integer tip.
  )

# 2. Definiramo veze (relations / preduvjete)
# 'from' je ID preduvjeta, 'to' je ID aktivnosti koja slijedi
relations_aon <- data.frame(
  from = c(1, 1, 2, 3, 4, 5, 6), # ID-ovi: A, A, B, C, D, E, F
  to   = c(2, 3, 4, 5, 6, 6, 7)  # ID-ovi: B, C, D, E, F, F, G
  )
# Napomena: Moramo imati dva luka koja vode u F (ID=6), 
# jedan iz D (ID=4) i jedan iz E (ID=5). 
relations_aon <- data.frame(
  from = c(1L, # A -> B
           1L, # A -> C
           2L, # B -> D
           3L, # C -> E
           4L, # D -> F
           5L, # E -> F
           6L  # F -> G
           ), 
  # Paket zahtijeva integer, a ne numeric.
  # Dodajemo 'L' da forsiramo integer tip.
  to   = c(2L, 
           3L, 
           4L, 
           5L, 
           6L, 
           6L, 
           7L)
)


# 3. Kreiramo raspored
raspored_igre <- sch_new() %>%
  sch_add_activities(
   id = activities_aon$id,
   name = activities_aon$name,
   duration = activities_aon$duration
  ) %>%
  sch_add_relations(
   from = relations_aon$from,
   to = relations_aon$to
  ) %>%
  sch_plan() # Izvršava CPM algoritam

cat(paste("Ukupno trajanje projekta:", sch_duration(raspored_igre), "tjedana\n"))
## Ukupno trajanje projekta: 26 tjedana
# 5. Izdvajamo cijelu tablicu rasporeda
raspored_df <- sch_activities(raspored_igre)

raspored_df
## # A tibble: 7 × 14
##      id name     duration milestone critical early_start early_finish late_start
##   <int> <chr>       <int> <lgl>     <lgl>          <int>        <int>      <int>
## 1     1 A (Pisa…        4 FALSE     TRUE               0            4          0
## 2     2 B (Prog…        8 FALSE     FALSE              4           12          5
## 3     3 C (Diza…       12 FALSE     TRUE               4           16          4
## 4     4 D (Inte…        6 FALSE     FALSE             12           18         13
## 5     5 E (Inte…        3 FALSE     TRUE              16           19         16
## 6     6 F (Beta…        5 FALSE     TRUE              19           24         19
## 7     7 G (Popr…        2 FALSE     TRUE              24           26         24
## # ℹ 6 more variables: late_finish <int>, total_float <int>, free_float <int>,
## #   progr_level <int>, regr_level <int>, topo_float <int>


Najvažniji stupci u tibble ispisu su total_float (naš “slack” ili “luft”) i critical.

  • Kritični put (critical == TRUE): Aktivnosti A, C, E, F i G imaju total_float (slack) 0. One su kritične.

  • Nekritični put (critical == FALSE): Aktivnosti B i D imaju total_float (slack) 1. Ovo je najočitiji dokaz do sada: programerski tim (aktivnosti B i D) može kasniti točno 1 tjedan prije nego što njihov put postane duži od grafičarskog (A-C-E) i time ugrozi cijeli projekt.


plot(sch_xy_gantt_matrix(raspored_igre))


PERT/CPM


Metoda kritičnog puta (CPM), koju smo upravo obradili, fundamentalna je, ali ima jedno veliko ograničenje: pretpostavlja da je trajanje svake aktivnosti determinističko i točno poznato (\(t_{ij} = 5\)).

U stvarnim projektima, posebno u IT-u, inženjerstvu ili istraživanju, ovo je rijetko slučaj. Trajanje aktivnosti poput “dizajn grafike” ili “programiranje enginea” je stohastičko (vjerojatnosno). Može trajati 8 tjedana ako sve ide glatko, ali i 15 ako se pojave neočekivani bugovi.

PERT je ekstenzija CPM-a dizajnirana da upravlja tom neizvjesnošću.

PERT (kratica od engl. Program Evaluation and Review Technique) je tehnika koju je u 1950-ima razvila američka mornarica za Polaris missile project (Malcolm, Roseboom, Clark & Fazar, 1959). Primjenjuje se u planiranju projekata s neizvjesnim vremenima izvršenja aktivnosti. CPM je metoda razvijena za potrebe industrijskih projekata (DuPont i Remington Rand) (Kelley & Walker, 1959) i primjenjuje se za planiranje projekata u kojima ne postoji neizvjesnost vremena izvršenja aktivnosti, a varijabilnost ne predstavlja problem (Anderson et al., 2012; Collier et al., 2018). U novije vrijeme, softverska rješenja kombiniraju najbolje karakteristike oba pristupa, pa se često na njih referira kao PERT/CPM.

Umjesto da se menadžera pita za jedan broj (“koliko će trajati?”), PERT traži tri procjene, kako bi se modelirala nesizvjesnost. Glavni cilj PERT-a više nije samo pronaći kritični put, već i odgovoriti na pitanja poput:

  • “Kolika je vjerojatnost da ćemo završiti projekt u roku od 26 tjedana?”
  • “Koja je vjerojatnost da će samo programerski dio kasniti?”
  • “Koliki nam vremenski ‘buffer’ treba za 95%-tnu vjerojatnost da ćemo završiti u roku?”


PERT metodologija temelji se na Beta distribuciji (koja je fleksibilna i dobro modelira trajanje projekata). Da bi se definirala ta distribucija za svaku aktivnost, PERT zahtijeva tri ulazna podatka od stručnjaka (npr. voditelja tima, programera, dizajnera):

  1. Optimistično vrijeme (\(a\)):
  • Najkraće moguće vrijeme potrebno za dovršetak aktivnosti.
  • Pretpostavlja da sve ide savršeno (najbolji ljudi, bez grešaka, bez vanjskih ometanja). “Best-case” scenarij.
  1. Najvjerojatnije vrijeme (\(m\)):
  • Modalno vrijeme (mod - vrijednost varijable koja se najčešće pojavljuje).
  • Najrealnija procjena trajanja.
  • Vrijeme koje bi se dogodilo da se aktivnost ponovi više puta pod normalnim okolnostima. “Realna” procjena.
  1. Pesimistično vrijeme (\(b\)):
  • Najduže moguće vrijeme potrebno za dovršetak aktivnosti.
  • Pretpostavlja da sve što može poći po zlu, poći će po zlu (u stilu Murphyjevog zakona). “Worst-case” scenarij (unutar razumnih granica).


Iz ove tri točke, PERT izvodi dvije ključne mjere za svaku aktivnost:

Za očekivano trajanje (\(t_e\)) ne koristimo jednostavni prosjek (\(a+m+b / 3\)), jer su ekstremi (\(a\) i \(b\)) manje vjerojatni. PERT koristi ponderirani prosjek koji daje 4 puta veću težinu najvjerojatnijem vremenu (\(m\)):

\[ t_e = \frac{a + 4m + b}{6} \]

Ovo očekivano trajanje (\(t_e\)) je broj koji ćemo kasnije koristiti umjesto onog fiksnog trajanja u našoj CPM analizi (prolaz naprijed/natrag).

Varijanca (\(\sigma^2\)) i standardna devijacija (\(\sigma\)) su ključni dodatak. Varijanca mjeri nesizvjesnost ili rizik neizvršenja aktivnosti.

  • Ako su pesimistično (\(b\)) i optimistično (\(a\)) vrijeme blizu, raspon je malen i varijanca je niska (aktivnost je predvidljiva).
  • Ako je raspon (\(b - a\)) ogroman, varijanca je visoka (aktivnost je vrlo rizična).

Standardna devijacija se aproksimira kao: \[ \sigma = \frac{b - a}{6} \]

A varijanca (koja će nam trebati kasnije za zbrajanje) je jednostavno kvadrat toga: \[ \sigma^2 = \left(\frac{b - a}{6}\right)^2 \]

Sljedeći korak: Nakon što imamo \(t_e\) za svaku aktivnost, provodimo normalnu CPM analizu (prolaz naprijed/natrag) koristeći te \(t_e\) vrijednosti. To nam daje očekivano trajanje projekta i očekivani kritični put.


Primjer


Vraćamo se u “Projekt Poltergeist”. Ovaj put smo pitali voditelje timova (programere, grafičare) da nam realne (tri-točkovne) procjene. Deterministički svijet CPM-a je gotov; ulazimo u stohastiku s PERT-om.

Nakon konzultacija s timom, dobili smo sljedeće procjene trajanja (u tjednima):

Oznaka Aktivnost Optimistično (\(a\)) Najvjerojatnije (\(m\)) Pesimistično (\(b\))
A (1) \(\to\) (2) Pisanje GDD-a 3 4 5
B (2) \(\to\) (3) Program. Enginea 6 8 10
C (2) \(\to\) (4) Dizajn Grafike 8 12 22
D (3) \(\to\) (5) Integ. Mehanika 5 6 7
E (4) \(\to\) (5) Integ. Grafike 2 3 10
F (5) \(\to\) (6) Beta Testiranje 4 5 12
G (6) \(\to\) (7) Popravci/Lans. 1 2 9


Prva opažanja:

  • Aktivnosti A, B i D (pisanje i mehanika) su predvidljive. Raspon (\(b-a\)) je malen.
  • Aktivnosti C, E, F i G (grafika, integracija, testiranje, popravci) su vrlo rizične. Raspon je ogroman. Ovdje leži neizvjesnost projekta.


Ručni izračun - sada primjenjujemo PERT formule na svaku aktivnost.

1. Očekivano trajanje: \(t_e = (a + 4m + b) / 6\)

2. Varijanca: \(\sigma^2 = ((b - a) / 6)^2\)

Oznaka Aktivnost Procjene (\(a, m, b\)) Očekivano trajanje (\(t_e\)) Varijanca (\(\sigma^2\))
A Pisanje GDD-a (3, 4, 5) \((3 + 4*4 + 5) / 6 = 4.0\) \(((5-3)/6)^2 = 0.11\)
B Program. enginea (6, 8, 10) \((6 + 4*8 + 10) / 6 = 8.0\) \(((10-6)/6)^2 = 0.44\)
C Dizajn grafike (8, 12, 22) \((8 + 4*12 + 22) / 6 = 13.0\) \(((22-8)/6)^2 = 5.44\)
D Integ. mehanika (5, 6, 7) \((5 + 4*6 + 7) / 6 = 6.0\) \(((7-5)/6)^2 = 0.11\)
E Integ. grafike (2, 3, 10) \((2 + 4*3 + 10) / 6 = 4.0\) \(((10-2)/6)^2 = 1.78\)
F Beta testiranje (4, 5, 12) \((4 + 4*5 + 12) / 6 = 6.0\) \(((12-4)/6)^2 = 1.78\)
G Popravci/Lans. (1, 2, 9) \((1 + 4*2 + 9) / 6 = 3.0\) \(((9-1)/6)^2 = 1.78\)


  1. Očekivana trajanja (\(t_e\)):
  • Ovo su brojevi koje ćemo sada unijeti u našu CPM (prolaz naprijed/natrag) analizu.
  • Primijetite da “Dizajn grafike” (C) sada očekivano traje 13 tjedana (ne 12).
  • “Integracija Grafike” (E) sada traje 4 tjedna (ne 3).
  • “Beta test” (F) traje 6 tjedana (ne 5).
  • Svi najrizičniji zadaci su se, očekivano, produžili.


  1. Variance (\(\sigma^2\)):
  • Ovi brojevi su naša mjera rizika. “Dizajn grafike” (C) ima ogromnu varijancu (5.44), što znači da je procjena \(t_e=13.0\) vrlo nepouzdana. Varijance ćemo zbrajati duž kritičnog puta da dobijemo ukupnu nesigurnost projekta.

Za stohastičku analizu (PERT), paket critpath treba podatke u AOA (activity-on-arc) formatu, ali s tri procjene trajanja. Vraćamo se na prethodni primjer s dizajnom igre.


library(critpath)

# 1. definiramo podatke za PERT
# unosimo topologiju (Start_node, End_node) i tri procjene (a, m, b)
data_game_pert <- data.frame(
  Start_node = c(1, 2, 2, 3, 4, 5, 6),
  End_node   = c(2, 3, 4, 5, 5, 6, 7),
  Activity   = c("A", "B", "C", "D", "E", "F", "G"),
  Optimistic = c(3, 6, 8, 5, 2, 4, 1),    # Vrijednosti 'a'
  MostLikely = c(4, 8, 12, 6, 3, 5, 2),    # Vrijednosti 'm'
  Pessimistic = c(5, 10, 22, 7, 10, 12, 9) # Vrijednosti 'b'
)

# 2. rješavamo problem
# pozivamo solve_pathAOA, ali s deterministic=FALSE
critp_stohastic_igre <- solve_pathAOA(data_game_pert, deterministic = FALSE)
## Expected compl. time distribution: N( 30 , 3.299833 )
# 3. ispisujemo osnovne rezultate
print(critp_stohastic_igre)
## $graphAOA
## DiagrammeR Graph // 7 nodes / 7 edges
##   -- directed / connected / DAG / simple
## 
##   NODES / type: 7 vals - complete / label: 7 vals - complete & unique
##     -- 2 additional node attributes (ES, LF)
##   EDGES / rel: <unused>                                    info: `get_edge_df()`
##     -- 4 additional edge attributes (label, time, timevar + 1 more)
##   SELECTION / <none>
##   CACHE / <none>
##   GLOBAL ATTRS / 17 are set                 info: `get_global_graph_attr_info()`
##   GRAPH ACTIONS / <none>
##   GRAPH LOG / <57 actions> -> () -> () -> set_edge_attrs()
## 
## $schedule
##   Name Duration    Var ESij LSij EFij LFij TFij Crit
## 1    A        4 0.1111    0    0    4    4    0    *
## 2    B        8 0.4444    4    7   12   15    3     
## 3    C       13 5.4444    4    4   17   17    0    *
## 4    D        6 0.1111   12   15   18   21    3     
## 5    E        4 1.7778   17   17   21   21    0    *
## 6    F        6 1.7778   21   21   27   27    0    *
## 7    G        3 1.7778   27   27   30   30    0    *
## 
## $ComplTi
## [1] 30
## 
## $SDevTi
## [1] 3.299833
## 
## $CritAct
## [1] "A" "C" "E" "F" "G"
## 
## $AddSlacks
##   Name FST CST
## 1    A   0   0
## 2    B   0   3
## 3    C   0   0
## 4    D   3   0
## 5    E   0   0
## 6    F   0   0
## 7    G   0   0
# 4. crtamo graf
# crvenom bojom su označene aktivnosti s najvećom vjerojatnošću da budu kritične
plot_graphAOA(data_game_pert, solved = critp_stohastic_igre)


Kao što smo i očekivali, naš CPM (deterministički) rezultat od 26 tjedana bio je previše optimističan. Stvarno očekivano trajanje projekta, kada se uzme u obzir rizik (posebno kod grafike i testiranja), je 30 tjedana.

U ovom kontekstu, standardna devijacija od 3.3 tjedna je naša mjera ukupne nesizvjesnosti projekta.

Prema centralnom graničnom teoremu (ako ste zaboravili, možete se prisjetiti ovdje), zbroj trajanja aktivnosti duž kritičnog puta (čak i ako su beta-distribuirane) teži normalnoj distribuciji. Sada možemo analizirati naš projekt koristeći ta dva broja.


# Postavljamo ključne parametre iz 'critpath' ispisa
ocekivano_trajanje_igre <- 30.0
standardna_devijacija_igre <- 3.3

# Crtamo normalnu distribuciju za naš projekt
curve(dnorm(x, ocekivano_trajanje_igre, sd = standardna_devijacija_igre), 
     from = ocekivano_trajanje_igre - 4*standardna_devijacija_igre, 
     to = ocekivano_trajanje_igre + 4*standardna_devijacija_igre, 
     xlab = "Ukupno trajanje projekta (u tjednima)", 
     ylab = "Gustoća vjerojatnosti",
     main = paste("Normalna distribucija trajanja projekta N(", ocekivano_trajanje_igre, ",", round(standardna_devijacija_igre, 2), ")"))

grid()


Ovdje možemo iskoristiti i Empirijsko pravilo za dodatne uvide. Sjetite se, Empirijsko pravilo vrijedi za normalnu distribuciju i tumači se pomoću površina pod krivuljom vezanih uz intervale odstupanja 1, 2 i 3 standardne devijacije od prosjeka. Pa se tako unutar intervala od jedne standardne devijacije od prosjeka (prosjek +/- 1 sd) nalazi približno 68.28% podataka. Unutar intervala od dvije standardne devijacije od prosjeka (prosjek +/- 2 sd) nalazi 95.44% podataka. Unutar intervala od tri standardne devijacije od prosjeka nalazi se približno 99.7% podataka.


# Grafički prikaz osnovne distribucije
curve(dnorm(x, mean=ocekivano_trajanje_igre, sd=standardna_devijacija_igre), 
     from = ocekivano_trajanje_igre - 4*standardna_devijacija_igre, 
     to = ocekivano_trajanje_igre + 4*standardna_devijacija_igre, 
     xlab = "Ukupno trajanje projekta (u tjednima)", 
     ylab = "Gustoća vjerojatnosti",
     main = "Empirijsko pravilo (68-95-99.7%) za projekt igre")

# Dodajemo površine
for (i in 1:3) {
  x_vals <- seq(ocekivano_trajanje_igre - i*standardna_devijacija_igre, ocekivano_trajanje_igre + i*standardna_devijacija_igre, length.out = 100)
  y_vals <- dnorm(x_vals, mean=ocekivano_trajanje_igre, sd=standardna_devijacija_igre)
  polygon(c(ocekivano_trajanje_igre - i*standardna_devijacija_igre, x_vals, ocekivano_trajanje_igre + i*standardna_devijacija_igre),
        c(0, y_vals, 0),
        col = rgb(0.8, 0.8, 0.8, 0.2 / i), border = NA)
}
# Ponovno crtamo osnovnu krivulju da bude iznad površina
curve(dnorm(x, mean=ocekivano_trajanje_igre, sd=standardna_devijacija_igre), 
     from = ocekivano_trajanje_igre - 4*standardna_devijacija_igre, 
     to = ocekivano_trajanje_igre + 4*standardna_devijacija_igre, 
     add = TRUE)

grid()


Najtamnija nijansa sive boje na grafu odnosi se na interval odstupanja jedne standardne devijacije od prosjeka.

  • ~68.3% (1 std. dev.): Postoji vjerojatnost od približno 68.3% da će razvoj igre trajati između 26.7 i 33.3 tjedana.
  • ~95.4% (2 std. dev.): Postoji vjerojatnost od približno 95.4% da će razvoj igre trajati između 23.4 i 36.6 tjedana.
  • ~99.7% (3 std. dev.): Gotovo je sigurno (99.7%) da će projekt biti proveden u intervalu od 20.1 i 39.9 tjedana.

Digresija. Jeste li već čuli za Six Sigma (\(6\sigma\))? Možete li prepoznati s čim je povezano? Ako niste ranije čuli, metodologija Six Sigma je skup alata osmišljenih za poboljšanje cjelokupnog učinka organizacije kroz sustavno unapređenje poslovnih procesa. Tvrtke obično primjenjuju metodu Six Sigma kako bi dramatično smanjile nedostatke, održale kvalitetu proizvoda i povećale učinkovitost. Naziv “Six Sigma” dolazi od statistike, budući da grčko slovo sigma (σ) predstavlja standardnu devijaciju, odnosno mjeru varijabilnosti u procesu. Six Sigma nastoji osigurati da gotovo svi rezultati budu unutar tog željenog raspona (a standardna devijacija što manja), minimizirajući nedostatke, kvarove i pogreške na praktički beznačajne razine.


Sada dolazimo do ključnog pitanja za menadžment. Naš optimistični CPM plan bio je 26 tjedana. Pitajmo kolika je vjerojatnost da to ostvarimo. Također, provjerimo vjerojatnost za realističniji rok, npr. 35 tjedana.

Za to koristimo funkciju pnorm() (kumulativna funkcija distribucije).


# 1. Pitanje: Kolika je vjerojatnost da završimo u roku od 26 tjedana (naš CPM plan)?
vjerojatnost_26_tjedana <- pnorm(26, mean=ocekivano_trajanje_igre, sd=standardna_devijacija_igre)

# 2. Pitanje: Kolika je vjerojatnost da završimo u roku od 35 tjedana (naš "buffer" rok)?
vjerojatnost_35_tjedana <- pnorm(35, mean=ocekivano_trajanje_igre, sd=standardna_devijacija_igre)

cat("Vjerojatnost da će igra biti gotova u 26 tjedana (stari CPM rok) je", round(vjerojatnost_26_tjedana*100, 2), "%.")
## Vjerojatnost da će igra biti gotova u 26 tjedana (stari CPM rok) je 11.27 %.
cat("\n")
cat("Vjerojatnost da će igra biti gotova u 35 tjedana (novi rok) je", round(vjerojatnost_35_tjedana*100, 2), "%.")
## Vjerojatnost da će igra biti gotova u 35 tjedana (novi rok) je 93.51 %.


Kao što vidimo, postoji samo 11.27% šanse da u roku dostignemo naš originalni, optimistični CPM rok od 26 tjedana. Ovo je ključna informacija koju PERT pruža.

Međutim, vjerojatnost da ćemo projekt završiti u roku, ako procijenimo trajanje na 35 tjedana je 93.51%.




Fokusirajući se na modele u kojima se u obzir uzimaju višestruke vještine potrebne za izvršenje aktivnosti, Afshar-Nadjafi (2021) pruža pregled raznovrsnih metoda koje se koriste pri utvrđivanju rasporeda i planiranju u proteklih nekoliko godina, s naglaskom na primjeni heurističkih algoritama. Pri utvrđivanju rasporeda projekata, u većini slučajeva radi se o jednom postavljenom cilju, a mjerne jedinice najčešće su vezane uz vrijeme izvršenja (manje učestalo pojavljuje se trošak, dok su ostali oblici rjeđe zastupljeni). Iako bi se moglo činiti da su noviji algoritmi u potpunosti zamijenili PERT/CPM metodu, primjena ove metode i dalje je prisutna u znanstvenim istraživanjima i praksi. Njezina upotreba u praksi opravdana je jednostavnošću metode. Na primjer, Zareei (2018) primjenjuje ovu metodu pri utvrđivanju rasporeda kompleksnog projekta izgradnje postrojenja za ekstrakciju prirodnog plina. Collier et al. (2018) kombiniraju ovu metodu sa scenario tehnikom pri analizi prekida projekta i utjecaja tih prekida na troškove i promjene prioriteta.


Problem pridruživanja


Problem pridruživanja postavlja drugačije pitanje: Imamo dvije jednako velike grupe (npr. \(n\) radnika i \(n\) zadataka) i matricu troškova (\(c_{ij}\)) koja nam govori koliko košta (ili koliko je efikasno) da radnik \(i\) obavi zadatak \(j\).

Cilj je napraviti savršeno uparivanje 1-na-1 tako da je ukupan trošak minimiziran.

  • Svaki radnik mora dobiti točno jedan zadatak.
  • Svaki zadatak mora biti dodijeljen točno jednom radniku.

Primjer grafa:


Ovo je temelj za raspoređivanje, dodjelu resursa, matchmaking i (što je najvažnije) Mađarsku metodu. Mađarska metoda rješava problem dodjele (min-trošak 1–1) tako da prvo oduzme najmanje vrijednosti po redcima i stupcima, stvarajući matricu reducirnih troškova sa što više nula. Zatim minimalnim brojem linija “pokrije” sve nule; ako broj linija \(< n\), najmanji nepokriveni broj se oduzima od nepokrivenih elemenata i dodaje dvostruko pokrivenima, čime nastaju nove nule. Kada je broj linija \(= n\), neovisnim nulama konstruira se savršeno sparivanje (optimalna dodjela) (Kuhn, 1955). Metoda je ekvivalentna nalaženju potencijala \(u,v\) s nultim reduciranim troškovima na odabranim parovima i radi u \(O(n^3)\).


Problem pridruživanja oslanja se na mrežne modele uz rješavanje koristeći cjelobrojno programiranje. Pritom se može raditi, općenito, o situacijama u kojima postoje određene potrebe kojima treba pridružiti optimalno rješenje. Na primjer, Hashemi-Petroodi et al. (2020) bavili su se problemom ljudsko-robotske suradnje u različitim okruženjima. Miri i Razavi (2018) bave se problemom optimalnog pridruživanja dijelova postrojenja prema diskretnom rasporedu objekta. U medicini, ova se metoda može primijeniti pri pridruživanju liječnika pacijentima (uzimajući u obzir simptome pacijenta i specijalizaciju liječnika), kao i pridruživanje liječnika odgovarajućoj sali za operacije (Gür i Eren, 2018). Ova se metoda može koristiti i kao komplementarna metoda uz metodu određivanja Raspored projekata: PERT/CPM metoda, na način da se nakon određivanja rasporeda aktivnosti na projektu, svakoj aktivnosti pridruži djelatnik koji će tu aktivnost izvršiti.


Definicija


Problem pridruživanja je zapravo poseban, vrlo jednostavan slučaj problema transportnog problema (Kuhn, 1955; Koopmans & Beckmann, 1957) u kombinaciji s cjelobrojnim programiranjem (više o tome u lekciji cjelobrojno programiranje).

Parametri

  • \(n\): Broj agenata (npr. radnika) i broj zadataka.
  • \(c_{ij}\): Trošak pridruživanja agenta \(i\) zadatku \(j\).

Varijable odluke

  • \(x_{ij}\): Binarna varijabla (0 ili 1).
    • \(x_{ij} = 1\) ako je agent \(i\) pridružen zadatku \(j\).
    • \(x_{ij} = 0\) ako nije.

Funkcija cilja

Minimiziramo ukupan trošak svih odabranih parova.

\[ \min Z = \sum_{i=1}^{n} \sum_{j=1}^{n} c_{ij} x_{ij} \]

Ograničenja

1. Ograničenja agenata/ zaposlenika (Redovi):

Svaki agent \(i\) mora biti pridružen točno jednom zadatku. \[ \sum_{j=1}^{n} x_{ij} = 1 \quad \text{za svaki } i = 1, \dots, n \] (Zbroj u svakom redu mora biti 1)

2. Ograničenja zadataka (Stupci):

Svaki zadatak \(j\) mora biti pridružen točno jednom agentu. \[ \sum_{i=1}^{n} x_{ij} = 1 \quad \text{za svaki } j = 1, \dots, n \] (Zbroj u svakom stupcu mora biti 1)

Važna napomena (unimodularnost):

Iako smo definirali \(x_{ij}\) kao binarne, ovaj problem ima posebno svojstvo (totalna unimodularnost matrice ograničenja). To znači da ako ga riješimo kao običan LP (s \(0 \le x_{ij} \le 1\)), rješenje će automatski biti cjelobrojno (sve 0 ili 1), bez dodavanja posebnih ograničenja za to. Zato je ovaj problem tako “lijep” i brz za rješavanje.


Primjeri


Četiri studenta (Ana, Branko, Cvita, Darko) moraju biti dodijeljeni na četiri tima (Alfa, Beta, Gama, Delta) za seminarski rad. Svaki student je dao svoje preferencije (ocjenom od 1 do 20) koliko ne želi raditi u određenom timu. Viša ocjena znači veći “trošak” (manja želja).

Cilj je minimizirati ukupno “nezadovoljstvo” tima.

Matrica troškova (\(c_{ij}\)):

Student Tim Alfa (j=1) Tim Beta (j=2) Tim Gama (j=3) Tim Delta (j=4)
Ana (i=1) 5 10 15 3
Branko (i=2) 2 18 5 9
Cvita (i=3) 8 7 2 10
Darko (i=4) 4 12 10 6


Imamo \(4 \times 4 = 16\) binarnih varijabli (\(x_{ij}\)). Da bismo ih kasnije stavili u vektor, moramo ih “poravnati”. Koristit ćemo redoslijed po redovima:

  • Var 1-4: \(x_{11}, x_{12}, x_{13}, x_{14}\) (Anine opcije)
  • Var 5-8: \(x_{21}, x_{22}, x_{23}, x_{24}\) (Brankove opcije)
  • Var 9-12: \(x_{31}, x_{32}, x_{33}, x_{34}\) (Cvitine opcije)
  • Var 13-16: \(x_{41}, x_{42}, x_{43}, x_{44}\) (Darkove opcije)

Funkcija cilja (Minimizacija):

Želimo minimizirati ukupni trošak (nezadovoljstvo).

\[ \min Z = \sum_{i=1}^{4} \sum_{j=1}^{4} c_{ij} x_{ij} \]

Raspisano:

\(\min Z = 5x_{11} + 10x_{12} + 15x_{13} + 3x_{14} + 2x_{21} + ... + 6x_{44}\)

Ograničenja:

  1. Ograničenja studenata (svaki student radi 1 zadatak):

Zbroj u svakom redu mora biti 1.

  • \(x_{11} + x_{12} + x_{13} + x_{14} = 1\) (Ana)
  • \(x_{21} + x_{22} + x_{23} + x_{24} = 1\) (Branko)
  • \(x_{31} + x_{32} + x_{33} + x_{34} = 1\) (Cvita)
  • \(x_{41} + x_{42} + x_{43} + x_{44} = 1\) (Darko)
  1. Ograničenja timova (svaki tim dobiva 1 studenta):

Zbroj u svakom stupcu mora biti 1.

  • \(x_{11} + x_{21} + x_{31} + x_{41} = 1\) (Tim Alfa)
  • \(x_{12} + x_{22} + x_{32} + x_{42} = 1\) (Tim Beta)
  • \(x_{13} + x_{23} + x_{33} + x_{43} = 1\) (Tim Gama)
  • \(x_{14} + x_{24} + x_{34} + x_{44} = 1\) (Tim Delta)
  1. Ograničenje tipa varijable:
  • \(x_{ij} \in \{0, 1\}\) (Sve varijable su binarne)


cvec <- c(5, 10, 15, 3,  # Anini troškovi (Var 1-4)
          2, 18,  5, 9,  # Brankovi troškovi (Var 5-8)
          8,  7,  2, 10, # Cvitini troškovi (Var 9-12)
          4, 12, 10, 6)  # Darkovi troškovi (Var 13-16)
Amat_data <- c(
  # --- Ograničenja studenata (redovi 1-4) ---
  # red 1: Ana (x11+x12+x13+x14 = 1)
   1, 1, 1, 1,  0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,
  # red 2: Branko (x21+x22+x23+x24 = 1)
   0, 0, 0, 0,  1, 1, 1, 1,  0, 0, 0, 0,  0, 0, 0, 0,
  # red 3: Cvita (x31+x32+x33+x34 = 1)
   0, 0, 0, 0,  0, 0, 0, 0,  1, 1, 1, 1,  0, 0, 0, 0,
  # red 4: Darko (x41+x42+x43+x44 = 1)
   0, 0, 0, 0,  0, 0, 0, 0,  0, 0, 0, 0,  1, 1, 1, 1,

  # --- Ograničenja timova (redovi 5-8) ---
  # red 5: Tim Alfa (x11+x21+x31+x41 = 1)
   1, 0, 0, 0,  1, 0, 0, 0,  1, 0, 0, 0,  1, 0, 0, 0,
  # red 6: Tim Beta (x12+x22+x32+x42 = 1)
   0, 1, 0, 0,  0, 1, 0, 0,  0, 1, 0, 0,  0, 1, 0, 0,
  # red 7: Tim Gama (x13+x23+x33+x43 = 1)
   0, 0, 1, 0,  0, 0, 1, 0,  0, 0, 1, 0,  0, 0, 1, 0,
  # red 8: Tim Delta (x14+x24+x34+x44 = 1)
   0, 0, 0, 1,  0, 0, 0, 1,  0, 0, 0, 1,  0, 0, 0, 1
  )

Amat <- matrix(Amat_data, nrow = 8, byrow = TRUE)

dirvec <- rep("=", 8)
bvec <- rep(1, 8)

rjesenje_assign_lp <- lp(direction = "min",
                         objective.in = cvec,
                         const.mat = Amat,
                         const.dir = dirvec,
                         const.rhs = bvec)
rjesenje_assign_lp$objval
## [1] 19
rjesenje_assign_lp$solution
##  [1] 0 0 0 1 0 0 1 0 0 1 0 0 1 0 0 0
# prilagodba ispisa za iščitavanje
rjesenje_matrica_lp <- matrix(rjesenje_assign_lp$solution, 
                              nrow = 4, 
                              byrow = TRUE)
    
dimnames(rjesenje_matrica_lp) <- list(Student = c("Ana", "Branko", "Cvita", "Darko"),
                                      Tim = c("Alfa", "Beta", "Gama", "Delta"))
rjesenje_matrica_lp
##         Tim
## Student  Alfa Beta Gama Delta
##   Ana       0    0    0     1
##   Branko    0    0    1     0
##   Cvita     0    1    0     0
##   Darko     1    0    0     0


Kreatori lpSolve-a znaju koliko je ovaj problem čest, pa su kreirali specijaliziranu, super-brzu funkciju za njega: lp.assign().


library(lpSolve)

# 1. Definiramo matricu troškova (c_ij)
troskovi_c <- matrix(c(
  5, 10, 15, 3,  # red 1 (Ana)
  2, 18,  5, 9,  # red 2 (Branko)
  8,  7,  2, 10, # red 3 (Cvita)
  4, 12, 10, 6   # red 4 (Darko)
), nrow = 4, byrow = TRUE)

# 2. Rješavamo problem
# Dajemo matricu troškova i zadajemo "minimiziraj"
rjesenje_assign <- lp.assign(troskovi_c, direction = "min")

# Ispis optimalnog ukupnog troška (nezadovoljstva)
cat(paste("Minimalni ukupni 'trošak' (nezadovoljstvo):", rjesenje_assign$objval, "\n"))
## Minimalni ukupni 'trošak' (nezadovoljstvo): 19
# Prikaz rješenja
imena_studenata <- c("Ana", "Branko", "Cvita", "Darko")
imena_timova <- c("Alfa", "Beta", "Gama", "Delta")

# 'rjesenje_assign$solution' je matrica 0/1 koja pokazuje tko ide kamo
# Dodajemo imena radi čitljivosti
rjesenje_matrica <- rjesenje_assign$solution
dimnames(rjesenje_matrica) <- list(Student = imena_studenata, Tim = imena_timova)

cat("\n--- Matrica pridruživanja (1 = odabran) ---\n")
## 
## --- Matrica pridruživanja (1 = odabran) ---
print(rjesenje_matrica)
##         Tim
## Student  Alfa Beta Gama Delta
##   Ana       0    0    0     1
##   Branko    0    0    1     0
##   Cvita     0    1    0     0
##   Darko     1    0    0     0


Ne postoji nijedna druga kombinacija studenata i timova koja bi dala manji ukupan zbroj “nezadovoljstva” od 19. To je apsolutni minimum.

  • Ana (red 1): [0, 0, 0, 1] → Pridružena je timu Delta.
  • Branko (red 2): [0, 0, 1, 0] → Pridružen je timu Gama.
  • Cvita (red 3): [0, 1, 0, 0] → Pridružena je timu Beta.
  • Darko (red 4): [1, 0, 0, 0] → Pridružen je timu Alfa.


Provjera (zašto je 19?): Ako pogledamo originalnu matricu troškova:

  • Ana → Delta: Trošak 3
  • Branko → Gama: Trošak 5
  • Cvita → Beta: Trošak 7
  • Darko → Alfa: Trošak 4
  • Ukupno: 3+5+7+4=19.


Svaki student ima tim, svaki tim ima studenta, a ukupno nezadovoljstvo je na najnižoj mogućoj razini.

Iako lpSolve paket nudi oba alata, lp() i lp.assign(), oni funkcioniraju na fundamentalno različitim razinama apstrakcije i važno je razumjeti njihove specifičnosti.

Funkcija lp() je općeniti rješavač (solver) za probleme linearnog programiranja. Ona “ne zna” da rješava problem pridruživanja; ona rješava bilo koji problem koji joj se dostavi u ispravnom matričnom obliku. Ovaj pristup zahtijeva od korisnika da eksplicitno konstruira sve elemente LP modela. Prednost ovog pristupa je potpuna kontrola. Problem maksimizacije (npr. maksimiziranje efikasnosti) rješava se trivijalnom promjenom argumenta direction = "max". Bilo kakve dodatne specifičnosti (npr. “Radnik A ne može raditi na zadatku 3”) rješavaju se jednostavnim dodavanjem novog retka u Amat.

Funkcija lp.assign() je namjenski alat (wrapper) dizajniran isključivo za rješavanje klasičnog problema pridruživanja. Funkcija implicitno razumije strukturu problema. Ona pretpostavlja da će matrica ograničenja (Amat) uvijek slijediti strogu formu “zbroj svakog retka = 1” i “zbroj svakog stupca = 1”. Korisnik ne definira Amat ili bvec. Potrebno je proslijediti samo matricu troškova (argument cost.mat). Funkcija sama interno generira i rješava problem, što drastično smanjuje kompleksnost koda za korisnika. Dakle, funkcija lp.assign() je optimizirana za jedan zadatak. Iako i ona posjeduje argument direction koji se može postaviti na "max", važno je razumjeti općenitiji koncept. U teoriji optimizacije, standardna je tehnika konvertirati problem maksimizacije u problem minimizacije (i obrnuto). Da je lp.assign bila striktno minimizacijska (bez direction argumenta), problem maksimizacije efikasnosti (\(E\)) morao bi se riješiti transformacijom \(\max(E) \equiv \min(-E)\). U tom slučaju, korisnik bi morao funkciji lp.assign proslijediti matricu efikasnosti pomnoženu s -1. Optimalan raspored (\(x_{ij}\)) bio bi identičan, a rezultirajuća vrijednost funkcije cilja (objval) bila bi negativna te bi ju trebalo pomnožiti s -1 kako bi se dobila stvarna maksimalna efikasnost.

lp() zahtijeva više “ručne” pripreme i poznavanje LP strukture, ali pruža potpunu kontrolu nad modelom, uključujući maksimizaciju i dodavanje nestandardnih ograničenja. lp.assign() je ekstremno učinkovita i brza prečica, ali je ograničena isključivo na standardni problem pridruživanja \(n \times n\).




Razmotrimo sljedeći slučaj.

IT tvrtka pokreće 5 novih softverskih projekata. Na raspolaganju imaju 5 glavnih tehnoloških “stackova” (razvojnih okvira). Tim arhitekata procijenio je “trošak” (u satima prilagodbe, obuke i rizika) za korištenje svakog okvira na svakom projektu.

Cilj je, kao i prije, pridružiti jedan-na-jedan (1-na-1) okvir svakom projektu uz minimalan ukupan trošak.

Projekti (\(i\)):

  • 1: Brza marketinška stranica (P1)
  • 2: Interni alat za HR (P2)
  • 3: Veliki e-commerce sustav (P3)
  • 4: Mobilni API (P4)
  • 5: Analitička nadzorna ploča (P5)


Okviri (\(j\)):

  • 1: React (F1)
  • 2: Vue.js (F2)
  • 3: Angular (F3)
  • 4: Svelte (F4)
  • 5: .NET Core (F5)


Matrica troškova (\(c_{ij}\)):

\(c_{ij}\) F1 (React) F2 (Vue) F3 (Angular) F4 (Svelte) F5 (.NET)
P1 (Stranica) 5 3 15 2 20
P2 (HR Alat) 10 8 9 12 14
P3 (E-comm) 11 12 10 18 4
P4 (API) 20 22 18 25 3
P5 (Ploča) 4 6 10 9 16


Dodana su dva stroga pravila koja mijenjaju sve:

  1. Mora se koristiti: Projekt 3 (E-commerce) mora koristiti .NET Core (Okvir 5), zbog kompatibilnosti s postojećim sustavom naplate.
  2. Ne smije se koristiti: Projekt 1 (Stranica) ne smije koristiti Angular (Okvir 3), jer je tech lead procijenio da je pretežak (overkill) za taj zadatak.


Ovo više nije čisti problem pridruživanja. To je problem pridruživanja s dodatnim ograničenjima. Zbog toga ne možemo koristiti lp.assign(). Moramo koristiti lp().


Varijable:

Imamo \(5 \times 5 = 25\) binarnih varijabli (\(x_{ij}\)). Slijede redoslijed po redovima:

  • Var 1-5: \(x_{11}, x_{12}, x_{13}, x_{14}, x_{15}\) (P1 opcije)
  • Var 6-10: \(x_{21}, \dots, x_{25}\) (P2 opcije)
  • Var 21-25: \(x_{51}, \dots, x_{55}\) (P5 opcije)

Funkcija cilja (cvec):

Minimiziramo trošak. cvec je poravnata matrica troškova (25 elemenata).


cvec <- c(5, 3, 15, 2, 20,
          10, 8, 9, 12, 14,
          11, 12, 10, 18, 4,
          20, 22, 18, 25, 3,
          4, 6, 10, 9, 16)


Ograničenja (Amat, bvec, dirvec):

  1. Standardna ograničenja projekata (redovi 1-5): Svaki projekt dobiva 1 okvir. (5 ograničenja)
  • \(\sum_{j=1}^{5} x_{1j} = 1\) (Projekt 1)
  • \(\sum_{j=1}^{5} x_{5j} = 1\) (Projekt 5)
  1. Standardna ograničenja okvira (redovi 6-10): Svaki okvir se koristi 1 put. (5 ograničenja)
  • \(\sum_{i=1}^{5} x_{i1} = 1\) (Okvir 1)
  • \(\sum_{i=1}^{5} x_{i5} = 1\) (Okvir 5)
  1. Naša nova poslovna ograničenja (redovi 11-12): (2 ograničenja)
  • Pravilo 1 (Mora se): \(x_{3,5} = 1\) (Projekt 3 mora dobiti Okvir 5)
  • Pravilo 2 (Ne smije se): \(x_{1,3} = 0\) (Projekt 1 ne smije dobiti Okvir 3)

Ukupno imamo 12 ograničenja i 25 varijabli. Amat će biti 12x25.


library(lpSolve)
n <- 5

# 1. cvec (već definiran gore)
cvec <- c(5, 3, 15, 2, 20, 10, 8, 9, 12, 14, 11, 12, 10, 18, 4,
          20, 22, 18, 25, 3, 4, 6, 10, 9, 16)

# 2. dirvec i bvec (12 ograničenja)
dirvec <- c(
  rep("=", n),    # Ogr 1-5 (Projekti)
  rep("=", n),    # Ogr 6-10 (Okviri)
  "=",            # Ogr 11 (Mora se)
  "="             # Ogr 12 (Ne smije se)
)

bvec <- c(
  rep(1, n),      # Ogr 1-5 = 1
  rep(1, n),      # Ogr 6-10 = 1
  1,              # Ogr 11 = 1
  0               # Ogr 12 = 0
)

# 3. Amat (12 x 25 matrica)
# Prvo je napunimo nulama
Amat <- matrix(0, nrow = 12, ncol = n*n)

# Ograničenja 1-5 (Projekti - zbroj redova = 1)
for (i in 1:n) {
  # Stupci za red 'i': od (i-1)*n + 1 do i*n
  cols <- ((i-1)*n + 1):(i*n)
  Amat[i, cols] <- 1
}

# Ograničenja 6-10 (Okviri - zbroj stupaca = 1)
for (j in 1:n) {
  # Stupci za stupac 'j': j, j+n, j+2n, ...
  cols <- seq(from = j, to = n*n, by = n)
  Amat[j + n, cols] <- 1
}

# Funkcija za lakše snalaženje: (Red, Stupac) -> Indeks u vektoru
x_idx <- function(i, j, num_cols) { (i - 1) * num_cols + j }

# Ograničenje 11 (Mora se): x(3,5) = 1
# (Projekt 3, Okvir 5)
idx_3_5 <- x_idx(3, 5, n) # Ovo je 15. varijabla
Amat[11, idx_3_5] <- 1

# Ograničenje 12 (Ne smije se): x(1,3) = 0
# (Projekt 1, Okvir 3)
idx_1_3 <- x_idx(1, 3, n) # Ovo je 3. varijabla
Amat[12, idx_1_3] <- 1

# 4. Rješavanje
rjesenje_prosir <- lp(direction = "min",
                      objective.in = cvec,
                      const.mat = Amat,
                      const.dir = dirvec,
                      const.rhs = bvec)

rjesenje_matrica <- matrix(rjesenje_prosir$solution, nrow = n, byrow = TRUE)
dimnames(rjesenje_matrica) <- list(paste0("Projekt ", 1:n), 
                                   paste0("Okvir ", 1:n))
rjesenje_matrica
##           Okvir 1 Okvir 2 Okvir 3 Okvir 4 Okvir 5
## Projekt 1       0       0       0       1       0
## Projekt 2       0       1       0       0       0
## Projekt 3       0       0       0       0       1
## Projekt 4       0       0       1       0       0
## Projekt 5       1       0       0       0       0


Algoritmi na mrežama


U prethodnim poglavljimo koristili smo prvenstveno linearno programiranje (LP) kao univerzalnu metodu za rješavanje optimizacijskih problema na mrežama. Iako je LP robustan, on je općenit pristup. Za mnoge specifične mrežne probleme postoje visoko specijalizirani i računski učinkovitiji algoritmi.

Ovaj pregled klasificira širi spektar algoritamskih pristupa, uključujući aproksimativne metode (heuristike) i druge fundamentalne egzaktne algoritme koji nisu eksplicitno navedeni u prethodnom tekstu.


Heuristički i metaheuristički pristupi


Za mnoge optimizacijske probleme, posebno one iz klase NP-teških (poput “problema trgovačkog putnika”), pronalaženje egzaktnog optimalnog rješenja je računski neizvedivo za instance problema velikih dimenzija. U takvim se slučajevima pribjegava aproksimativnim metodama.


Heuristike

Heuristika je algoritam ili metoda dizajnirana za rješavanje problema na brži, lakši ili učinkovitiji način u usporedbi s egzaktnim metodama. Cijena te učinkovitosti je odricanje od garancije pronalaska optimalnog rješenja. Heuristike su načelno osmišljene da pronađu “dovoljno dobro” (suboptimalno) rješenje unutar prihvatljivog vremenskog okvira.

  • Primjer: Kod problema trgovačkog putnika, heuristika “najbližeg susjeda” (engl. Nearest Neighbour) funkcionira tako da iz početnog čvora iterativno odabire sljedeći, još neposjećeni čvor koji je prostorno najbliži trenutnom. Iako je brza, ova “pohlepna” (engl. greedy) strategija rijetko rezultira optimalnom turom.


Metaheuristike

Metaheuristika predstavlja strategiju ili radni okvir više razine za dizajniranje i usmjeravanje heurističkih algoritama. One su općeniti recepti koji se mogu prilagoditi različitim problemima. Njihov je primarni cilj učinkovito pretraživanje prostora rješenja i izbjegavanje zaglavljivanja u lokalnim optimumima.

Najčešće korištene metaheuristike u mrežnoj optimizaciji uključuju:

  • Genetski algoritmi (GA): Inspirirani biološkom evolucijom, koriste koncepte selekcije, križanja i mutacije na populaciji rješenja kako bi iterativno “uzgojili” kvalitetnije rješenje.

  • Simulirano kaljenje (Simulated Annealing): Temelji se na termodinamičkom procesu hlađenja. Algoritam pretražuje prostor rješenja, pri čemu u početku (na “visokoj temperaturi”) povremeno prihvaća i lošija rješenja kako bi preskočio lokalne optime. Kako se “hladi”, postaje rigorozniji u prihvaćanju samo boljih rješenja.

  • Optimizacija kolonijom mrava (Ant Colony Optimization): Bio-inspirirani algoritam koji simulira ponašanje mrava u potrazi za hranom. Koristi virtualne “feromonske tragove” za označavanje dobrih puteva, što iterativno pojačava vjerojatnost pronalaska kvalitetnih rješenja.


Fundamentalni mrežni algoritmi

Specijalizirani paketi (poput critpath) često implementiraju klasične, visoko učinkovite algoritme, koji su brži od općenitog LP solvera za specifičan problem.


Dijkstrin algoritam

Iako je problem najkraćeg puta rješiv pomoću LP-a, standardni algoritam za njegovo rješavanje (u grafovima s nenegativnim težinama lukova) je Dijkstrin algoritam. To je “pohlepni” algoritam koji iterativno pronalazi najkraći put od početnog čvora do svih ostalih čvorova. On održava skup posjećenih čvorova i u svakom koraku odabire neposjećeni čvor s najkraćom trenutnom udaljenosti od izvora, ažurirajući udaljenosti njegovih susjeda.


Ford-Fulkersonova metoda

Za problem maksimalnog toka, temeljni pristup je Ford-Fulkersonova metoda (Ford & Fulkerson, 1956; Ford & Fulkerson, 1962). Njezin princip je iterativno pronalaženje “poboljšavajućeg puta” (engl. augmenting path) od izvora (\(s\)) do ponora (\(t\)) u rezidualnom grafu koji ima raspoloživ kapacitet. Tok se povećava duž tog puta za maksimalnu moguću vrijednost (koju definira “usko grlo” na tom putu). Postupak se ponavlja dok god takav put postoji. Kada više nijedan poboljšavajući put ne postoji, algoritam završava, a teorem Max-Flow Min-Cut jamči da je postignut maksimalni tok. Učinkovita implementacija ove metode, Edmonds-Karpov algoritam, koristi pretraživanje u širinu (BFS) za pronalaženje najkraćeg poboljšavajućeg puta, osiguravajući polinomijalnu složenost.


Problem minimalnog raspinjućeg stabla (MST)

Ovo je fundamentalni problem dizajna mreže, koji nismo eksplicitno obradili. Cilj je povezati sve čvorove u grafu u jedinstvenu komponentu (stablo) na način da je ukupni zbroj težina korištenih lukova minimalan. Primjer je dizajniranje telekomunikacijske mreže koja povezuje sve gradove uz minimalan trošak polaganja kabela.

Dva klasična “pohlepna” algoritma za rješavanje MST-a su:

  • Kruskalov algoritam: Sortira sve lukove po težini od najmanje do najveće. Iterativno dodaje najjeftiniji luk koji ne stvara ciklus u dotad formiranoj šumi (Kruskal, 1956).
  • Primov algoritam: Započinje stablo iz proizvoljnog čvora. U svakom koraku, pohlepno dodaje najjeftiniji luk koji povezuje čvor iz postojećeg stabla s čvorom izvan stabla (Prim, 1957).


Primjer


U poglavlju s najkraćim putem definirali smo spefifičan slučaj TSP-a za dostavni dron i 4 lokacije. Riješili smo ga egzaktno, koristeći LP (Miller-Tucker-Zemlin) formulaciju koja je zahtijevala 20 varijabli i 21 ograničenje. Podsjetimo se poznatih parametara problema.

  • Čvorovi: 1 (Baza), 2 (A), 3 (B), 4 (Server)

  • Matrica troškova (vrijeme u minutama):

\(c_{ij}\) 1 (Baza) 2 (A) 3 (B) 4 (Server)
1 (Baza) 999 10 15 20
2 (A) 5 999 9 10
3 (B) 6 13 999 12
4 (Server) 8 8 9 999
grViz("
digraph tsp_graph {
  layout = circo; # 'circo' je dobar za male, guste mreže
  rankdir = LR;
  node [shape = record, style=filled, fillcolor=lightblue, fontname = Helvetica];
  edge [fontname = Helvetica, fontsize = 10, color=\"#444444\"];

  # Definicija čvorova
  N1 [label = '1 (Baza)'];
  N2 [label = '2 (A)'];
  N3 [label = '3 (B)'];
  N4 [label = '4 (Server)'];

  # Definicija lukova (cijena u minutama)
  # Iz Baze (1)
  N1 -> N2 [taillabel = ' 10'];
  N1 -> N3 [taillabel = ' 15'];
  N1 -> N4 [taillabel = ' 20'];
  
  # Iz A (2)
  N2 -> N1 [label = ' 5'];
  N2 -> N3 [label = ' 9'];
  N2 -> N4 [label = ' 10'];

  # Iz B (3)
  N3 -> N1 [label = ' 6'];
  N3 -> N2 [label = ' 13'];
  N3 -> N4 [label = ' 12'];

  # Iz Servera (4)
  N4 -> N1 [label = ' 8'];
  N4 -> N2 [label = ' 8'];
  N4 -> N3 [label = ' 9'];
}
")

Primijenimo sada heuristiku najbližeg susjeda.

  • Metoda: Krećemo od baze (1). U svakom koraku, odabiremo najbliži, još neposjećeni čvor.
  • Složenost postavljanja: Minimalna. Može se brzo izračunati i ručno.


Koraci (početak iz 1): 1. Start: 1 (Baza) 2. Gledamo red 1. Najbliži susjed je 2 (A) (cijena 10). - Put: 1 -> 2 - Trošak: 10 3. Sada smo u 2 (A). Gledamo red 2. Neposjećeni su 3 i 4. - Najbliži je 3 (B) (cijena 9). - Put: 1 -> 2 -> 3 - Trošak: 10 + 9 = 19 4. Sada smo u 3 (B). Gledamo red 3. Neposjećen je samo 4 (Server) (cijena 12). - Put: 1 -> 2 -> 3 -> 4 - Trošak: 19 + 12 = 31 5. Svi su posjećeni. Vrati se u Bazu (1). - Put: 1 -> 2 -> 3 -> 4 -> 1 - Trošak: 31 + (cijena 4 \(\to\) 1) = 31 + 8 = 39 minuta.

  • Rezultat: Suboptimalan.
  • Rješenje: Tura 1 -> 2 -> 3 -> 4 -> 1
  • Ukupni trošak: 39 minuta.


Zaključak heuristike:

Pronašli smo rješenje (39 min) koje je $$10-11% lošije od optimalnog (35 min), ali smo ga pronašli gotovo trenutno, bez ikakvog LP-a. Ovo je klasičan kompromis (trade-off) brzine i optimalnosti.

U stvarnom svijetu, ne bismo koristili ni lpSolve ni ručnu heuristiku. Koristili bismo paket dizajniran za TSP, kao što je TSP. Ovaj paket implementira brze, napredne heuristike (poput “nearest insertion”) i metaheuristike (poput simuliranog kaljenja) koje daju izvrsne rezultate.

  • Metoda: TSP::solve_TSP
  • Složenost postavljanja: Niska. Samo trebamo formirati matricu troškova.


# install.packages("TSP")
library(TSP)

# Naša matrica troškova
costs_m <- matrix(c(
  999, 10, 15, 20,
  5, 999,  9, 10,
  6,  13, 999, 12,
  8,   8,   9, 999
), nrow = 4, byrow = TRUE)

# Imena čvorova
gradovi <- c("1_Baza", "2_A", "3_B", "4_Server")
colnames(costs_m) <- gradovi
rownames(costs_m) <- gradovi

# Stvaramo TSP objekt
# Moramo mu reći da matrica NIJE simetrična (c_ij != c_ji)
# tj. da rješava Asymmetric TSP (ATSP)
# koristimo 'as.ATSP()' umjesto 'TSP()'
tsp_problem <- as.ATSP(costs_m)

# Rješavamo problem. 
# solve_TSP() radi i na ATSP objektima
rjesenje_tsp_paket <- solve_TSP(tsp_problem, method = "repetitive_nn")

cat("--- Rješenje iz 'TSP' paketa ---\n")
## --- Rješenje iz 'TSP' paketa ---
# Ispisujemo imena (labele) umjesto ID-eva za bolju čitljivost
cat(paste("Pronađena tura (po imenima):", labels(rjesenje_tsp_paket), collapse = " -> "))
## Pronađena tura (po imenima): 3_B -> Pronađena tura (po imenima): 1_Baza -> Pronađena tura (po imenima): 2_A -> Pronađena tura (po imenima): 4_Server
cat(paste("\nUkupni trošak:", tour_length(rjesenje_tsp_paket), "minuta"))
## 
## Ukupni trošak: 35 minuta


Na ovom primjeru vidimo da naprednije heuristike u specifičnim slučajevima mogu biti podjednako uspješne u traženju rješenja, uz manje vremena potrebnog za provedbu.


Vizualizacija grafova u R-u


Analiza mreža (grafova) sastoji se od dva dijela: analize (izračuna) i sinteze (vizualizacije). R nudi nekoliko moćnih, ali vrlo različitih, ekosustava za crtanje grafova. Mi ćemo se ovdje pozabaviti detaljnije s tri opcije.


DiagrammeR i graphViz


Ovo je pristup koji smo do sada najčešće koristili u dosadašnjem kodu (grViz("...")).

Važno je razumjeti da graphViz nije R paket, već moćan, zaseban softver otvorenog koda koji je desetljećima standard za crtanje grafova. Paket DiagrammeR u R-u služi kao “most” koji nam omogućuje slanje naredbi graphViz-u koristeći njegov nativni jezik, DOT.

  • Kako se koristi: Pišemo DOT kod unutar grViz() funkcije. To je deklarativni jezik: mi opišemo što želimo (Čvor A poveži s Čvorom B), a graphViz sam odlučuje kako će to nacrtati (gdje će biti A, a gdje B).
  • Snaga: Njegovi algoritmi za automatski raspored (layout) su fantastični, posebno za usmjerene acikličke grafove (DAGs). Zato je bio savršen za naše CPM/PERT dijagrame, gdje smo s rankdir=LR lako mogli prikazati logiku projekta slijeva nadesno.
  • Slabost: Nije dizajniran za dubinsku analizu podataka, već za prikaz strukture.

Najvažniji DOT atributi (najčešće korišteni):

  • graph [rankdir=LR, layout=dot, splines=true, ranksep=, nodesep=]

  • node [shape=box, style=filled, fillcolor="#E3F2FD", fontsize=, width=, height=]

  • edge [color="#90A4AE", arrowsize=0.8, penwidth=1.2, label=]

  • podgrafovi / klasteri: subgraph cluster_X { label="Naziv"; ... } (vizualno grupiranje)

  • layout engine: dot (DAG/hijerarhije), neato/sfdp (neusmjerene/velike mreže), circo (kružni), twopi (radijalni)

Primjer A — CPM/PERT stil (DAG, slijeva nadesno)

library(DiagrammeR)

grViz("
digraph {
  graph [rankdir=LR, layout=dot, splines=true, ranksep=0.6, nodesep=0.4]
  node  [shape=box, style=filled, fillcolor='#E3F2FD', fontsize=11]
  edge  [color='#78909C', arrowsize=0.8, penwidth=1.1]

  A [label='Start']
  B [label='Analiza']
  C [label='Dizajn']
  D [label='Implementacija']
  E [label='Test']
  F [label='Isporuka']

  A->B->C->D->E->F
  B->E [style=dashed, label='paralelno']
}
")

Primjer B — Klasteri (grupiranje faza/odjela)

grViz("
digraph {
  graph [rankdir=LR]
  node  [shape=box, style=filled, fillcolor='white']
  edge  [arrowsize=0.8]

  subgraph cluster_eng {
    label='Inženjering'; color='#BBDEFB'
    E1 [label='Specifikacija']; E2 [label='Prototip']; E3 [label='Refaktoriranje']
  }

  subgraph cluster_biz {
    label='Biznis'; color='#C8E6C9'
    B1 [label='Analiza troška']; B2 [label='Procjena rizika']
  }

  E1->E2->E3
  B1->B2
  E2->B2 [color='#9E9E9E', style=dashed, label='review']
}
")

Primjer C — Stiliziranje rubova i čvorova

grViz("
digraph {
  graph [rankdir=TB, splines=true]
  node  [shape=ellipse, style=filled, fillcolor='#FFF3E0']
  edge  [color='#8D6E63', penwidth=1.4]

  A [label='Korisnik', shape=oval, fillcolor='#E1F5FE']
  B [label='UI']; C [label='API']; D [label='Baza']
  E [label='Cache', shape=box, fillcolor='#E8F5E9']

  A->B [label='klik', arrowsize=0.7]
  B->C [label='REST', penwidth=2]
  C->D [label='SQL', color='#5D4037']
  C->E [label='GET', style=dashed]
  E->C [label='HIT', color='#9CCC65', arrowsize=0.6]
}
")

Brzi savjeti

  • DAG? → layout=dot + rankdir=LR/TB.

  • Preglednost? → povećajte ranksep/nodesep, uključite splines=true.

  • Grupiranje logičkih dijelova → subgraph cluster_*.


ggplot2 (i ggraph)


Ovo je put kojim idete ako vam je estetika na prvom mjestu i želite potpunu, apsolutnu kontrolu nad svakim pikselom.

Sam ggplot2 ne razumije što je “mreža”. On razumije točke (x, y), linije i estetiku (boja, veličina). Da bismo premostili taj jaz, potreban nam je “prevoditelj”: paket ggraph.

  • Kako se koristi: Prvo stvorite mrežni objekt (obično igraph objekt). Zatim, ggraph preuzima taj objekt i izračunava raspored (layout)—on zapravo svakom čvoru dodjeljuje x i y koordinate.
  • Tek tada možete koristiti ggplot2 sintaksu za crtanje, koristeći posebne “geome” koje ggraph pruža, kao što su geom_node_point() (za čvorove) i geom_edge_link() (za lukove).
  • Snaga: Apsolutna kontrola i lijepe vizualizacije koje se mogu lako uklopiti u ostatak tidyverse analize.
  • Slabost: Često je to previše posla za brzu analizu. Zahtijeva dublje razumijevanje i ggplot2 i specifičnih ggraph layouta.

Najčešće opcije layouta:

  • layout = "fr" (Fruchterman–Reingold, “force-directed”), kk (Kamada–Kawai)

  • sugiyama (hijerarhijski / DAG)

  • circle (kružni), linear + geom_edge_arc (lukovi)

  • tree (stabla) — uz as.tree(...) ili orijentirani graf

Tipične estetike:

  • geom_edge_link(aes(width=weight, alpha=...)), arrow = arrow(length = unit(3, "mm"))

  • geom_node_point(aes(size=centrality, color=community))

  • geom_node_text(aes(label=name), repel=TRUE) (uz ggrepel, ali može i geom_node_label)

Setup (zajednički podatci)

library(igraph)
library(ggraph)
library(ggplot2)

edges <- data.frame(
  from = c("A","A","B","C","C","D","E","E"),
  to   = c("B","C","D","D","E","F","F","G"),
  w    = c(1,2,1,1,3,2,1,1)
)
g <- graph_from_data_frame(edges, directed = TRUE)
V(g)$group <- c("start","modul","modul","core","core","out","out","out")[match(V(g)$name,
  c("A","B","C","D","E","F","G","H")[1:length(V(g))])]


Primjer A — Force-directed pregled (fr)

set.seed(42)
ggraph(g, layout = "fr") +
  geom_edge_link(aes(width = w), alpha = 0.6) +
  geom_node_point(size = 4) +
  geom_node_text(aes(label = name), vjust = -1, size = 3) +
  guides(edge_width = "none") +
  theme_void()

Primjer B — Hijerarhijski (DAG) sa sugiyama

ggraph(g, layout = "sugiyama") +
  geom_edge_link(arrow = arrow(length = unit(3, "mm")), end_cap = circle(3, 'mm')) +
  geom_node_point(size = 4) +
  geom_node_text(aes(label = name), vjust = -1, size = 3) +
  theme_void()

Primjer C — Kružni sa zakrivljenim rubovima

ggraph(g, layout = "linear", circular = TRUE) +
  geom_edge_arc(aes(alpha = ..index..)) +
  geom_node_point(size = 3) +
  geom_node_text(aes(label = name), repel = TRUE, size = 3) +
  coord_fixed() +
  theme_void()

Primjer D - “lijepi graf”

“Lijepi” primjer s ggraph koji pokazuje: strelice, kapice na bridovima, debljinu i boju brida po tipu, “halo” oko čvora, veličinu čvora po centralnosti i boju po zajednici, s čitljivim etiketama.

library(igraph)
library(tidygraph)
library(ggraph)
library(ggplot2)

## 1) Igračka mreža s tipovima bridova i težinama
set.seed(123)
edges <- data.frame(
  from = c("A","A","A","B","B","C","C","D","E","E","F","G","H","H"),
  to   = c("B","C","D","E","C","F","G","E","F","H","H","I","I","J"),
  w    = c(3,2,1,2,1,3,1,2,2,1,3,2,1,2),
  type = c("data","control","data","data","control","control","data","data","control","data","data","control","data","data")
)
g <- graph_from_data_frame(edges, directed = TRUE)

g_tbl <- as_tbl_graph(g) |>
  activate(nodes) |>
  mutate(
    deg   = centrality_degree(mode = "all"),
    btw   = centrality_betweenness(normalized = TRUE),
    label = name
  ) |>
  # Ovdje privremeno radimo neusmjereni pogled radi Louvain-a
  morph(to_undirected) |>
  mutate(community = as.factor(group_louvain())) |>
  unmorph()  # vraća se na originalni (usmjereni) graf

# Napomena: atributi bridova (w, type) su već u grafu, nije ih potrebno posebno dodavati
set.seed(7)
ggraph(g_tbl, layout = "fr") +
  geom_edge_link(aes(width = w, colour = type),
                 alpha = 0.75, lineend = "round",
                 arrow   = arrow(length = unit(3, "mm"), type = "closed"),
                 end_cap = circle(2.5, "mm")) +
  geom_node_point(aes(size = scales::rescale(btw, to = c(5, 10))),
                  shape = 21, stroke = 0, fill = "white", alpha = 0.6) +
  geom_node_point(aes(size = scales::rescale(btw, to = c(3.5, 8)), fill = community),
                  shape = 21, colour = "grey15", stroke = 0.7) +
  geom_node_text(aes(label = label), size = 3.2, vjust = -1, repel = TRUE) +
  scale_edge_width(range = c(0.4, 2.4), guide = "none") +
  scale_edge_colour_brewer(palette = "Set2", name = "Tip brida") +
  scale_fill_brewer(palette = "Set3", name = "Zajednica") +
  guides(fill = guide_legend(override.aes = list(size = 6))) +
  theme_graph(base_size = 12) +
  theme(legend.position = "right")


igraph


Ovo je vjerojatno najvažniji paket za analizu i vizualizaciju mreža u R-u. igraph je poput švicarskog noža: on je istovremeno i analitička knjižnica i alat za vizualizaciju.

Dok su DiagrammeR i ggraph primarno fokusirani na crtanje, igraph-u je crtanje samo jedna od mnogih funkcija.


Stvaranje igraph objekta


Prije crtanja ili analize, morate imati igraph objekt. Najčešće se stvara iz postojećih podataka (npr. liste lukova):

library(igraph)

# Primjer: naša TSP mreža
# Prvo definiramo data.frame s lukovima i cijenama (težinama)
el <- data.frame(
  from = c("Baza", "Baza", "Baza", "A", "A", "A", "B", "B", "B", "Server", "Server", "Server"),
  to   = c("A", "B", "Server", "Baza", "B", "Server", "Baza", "A", "Server", "Baza", "A", "B"),
  weight = c(10, 15, 20, 5, 9, 10, 6, 13, 12, 8, 8, 9)
)

# Stvaramo igraph objekt
# graph_from_data_frame() je najčešća funkcija
g <- graph_from_data_frame(el, directed = TRUE)

# Možemo odmah vidjeti sažetak
print(g)
## IGRAPH c27a079 DNW- 4 12 -- 
## + attr: name (v/c), weight (e/n)
## + edges from c27a079 (vertex names):
##  [1] Baza  ->A      Baza  ->B      Baza  ->Server A     ->Baza   A     ->B     
##  [6] A     ->Server B     ->Baza   B     ->A      B     ->Server Server->Baza  
## [11] Server->A      Server->B


Osnovna vizualizacija i opcije


igraph koristi plot() funkciju iz baznog R-a. Funkcija je jednostavna za korištenje, a nudi brojne opcije za podešavanje.

Kako razmišljati o argumentima

  • Raspored (layout) određuje koordinate čvorova

layout = layout_with_fr, layout_in_circle, layout_as_tree, layout_on_grid, …

  • Atributi čvorova (prefiks vertex.):

vertex.size, vertex.color, vertex.label, vertex.label.cex, vertex.label.color, vertex.frame.color, vertex.shape

  • Atributi bridova (prefiks edge.):

edge.width, edge.color, edge.arrow.size, edge.arrow.mode ("->", "<-", "<->"), edge.curved, edge.lty, edge.label, edge.label.cex

  • Globalno: margin, asp, main, xlab, ylab

Atributi iz grafa:

  • Čvorovi: V(g)$ime_atributa <- … (npr. stupnjevi, zajednice)

  • Bridovi: E(g)$ime_atributa <- … (npr. težine, tip)


# Najjednostavniji plot (funkcionalan, ali ne nužno estetski zadovoljavajući)
plot(g)


# Kontroliranje izgleda
plot(g, 
     layout = layout_in_circle,       # Raspored: u krug (dobro za 4 čvora)
     vertex.label = V(g)$name,         # Koristi 'name' atribut za labele
     vertex.color = "lightblue",       # Boja čvorova
     vertex.size = 25,                 # Veličina čvorova
     edge.label = E(g)$weight,         # Koristi 'weight' za labele na luku
     edge.arrow.size = 0.5             # Veličina strelica
)


Još jedan niz primjera

1. Transportni problem (ponuda → potražnja)

Ideja: bipartitni raspored (dva stupca), čvorovi obojani po tipu, edge-label = trošak.

library(igraph)

# Podaci
supply   <- c(A=20, B=30)
demand   <- c(X=10, Y=25, Z=15)
cost_tbl <- data.frame(
  from = rep(names(supply), each = length(demand)),
  to   = rep(names(demand), times = length(supply)),
  cost = c(2, 3, 1,   5, 4, 8)  # A->X,Y,Z, B->X,Y,Z
)

# Graf
g1 <- graph_from_data_frame(cost_tbl, directed = TRUE)
V(g1)$type <- ifelse(V(g1)$name %in% names(supply), "supply", "demand")
V(g1)$label <- ifelse(V(g1)$type=="supply",
                      paste0(V(g1)$name," (", supply[V(g1)$name],")"),
                      paste0(V(g1)$name," (", demand[V(g1)$name],")"))
E(g1)$label <- E(g1)$cost

# Bipartitni layout: lijevo ponuda, desno potražnja
sup_idx <- which(V(g1)$type == "supply")
dem_idx <- which(V(g1)$type == "demand")

# vertikalni raspored unutar svake grupe (jednako razmaknuti)
ys_sup <- seq(1, 0, length.out = length(sup_idx))
ys_dem <- seq(1, 0, length.out = length(dem_idx))

# složimo layout matricu (x=0 za supply, x=1 za demand)
lay1 <- matrix(NA_real_, nrow = vcount(g1), ncol = 2)
lay1[sup_idx, ] <- cbind(0, ys_sup)
lay1[dem_idx, ] <- cbind(1, ys_dem)

# (po želji) malo jittera da se edge-labeli lakše čitaju
lay1[,2] <- lay1[,2] + c(rep(0, length(sup_idx)), rep(0, length(dem_idx)))

plot(
  g1, layout = lay1,
  vertex.color = ifelse(V(g1)$type=="supply","#8dd3c7","#bebada"),
  vertex.size = 50, vertex.label.cex = 0.9, vertex.frame.color = "grey30",
  edge.arrow.size = 0.6, edge.width = 1.5,
  edge.label = E(g1)$label, edge.label.cex = 0.9, edge.label.color = "grey20",
  main = "Transportni problem: troškovi na lukovima, zagrade = ponuda/potražnja"
)

2. Transport s posrednim čvorovima (transshipment)

Ideja: tri “reda” (ponuda – posredni – potražnja), edge-label = trošak ili kapacitet.

# Slojevi
sup <- c(S1=15, S2=25)
hub <- c(H1=Inf, H2=Inf)   # posredni (bez limita u vizualu)
dem <- c(D1=10, D2=12, D3=18)

# Lukovi s troškom
E1 <- data.frame(from=rep(names(sup), each=length(hub)),
                 to=rep(names(hub), times=length(sup)),
                 cost=c(2,3, 4,1))
E2 <- data.frame(from=rep(names(hub), each=length(dem)),
                 to=rep(names(dem), times=length(hub)),
                 cost=c(5,2,3,  4,6,1))

g2 <- graph_from_data_frame(rbind(E1,E2), directed = TRUE)

# Tipovi i labele
V(g2)$type <- ifelse(V(g2)$name %in% names(sup), "supply",
                     ifelse(V(g2)$name %in% names(dem), "demand","hub"))
V(g2)$label <- ifelse(V(g2)$type=="supply", paste0(V(g2)$name," (",sup[V(g2)$name],")"),
                      ifelse(V(g2)$type=="demand", paste0(V(g2)$name," (",dem[V(g2)$name],")"),
                             V(g2)$name))
E(g2)$label <- E(g2)$cost

# Raspored po “razinama”
x <- ifelse(V(g2)$type=="supply", 0, ifelse(V(g2)$type=="hub", 0.5, 1))
y <- numeric(vcount(g2))
y[V(g2)$type=="supply"] <- seq(1,0,length.out = sum(V(g2)$type=="supply"))
y[V(g2)$type=="hub"]    <- seq(1,0,length.out = sum(V(g2)$type=="hub"))
y[V(g2)$type=="demand"] <- seq(1,0,length.out = sum(V(g2)$type=="demand"))
lay2 <- cbind(x,y)

plot(
  g2, layout = lay2,
  vertex.color = ifelse(V(g2)$type=="supply","#8dd3c7",
                        ifelse(V(g2)$type=="hub","#ffffb3","#bebada")),
  vertex.size = 50, vertex.label.cex = 0.9, vertex.frame.color = "grey30",
  edge.arrow.size = 0.6, edge.width = 1.5,
  edge.label = E(g2)$label, edge.label.cex = 0.9, edge.label.color = "grey20",
  main = "Transshipment: 3 razine (ponuda–hub–potražnja), trošak na luku"
)

3. Najkraći put i/ili maksimalni protok

Ideja: layout s “organskim” rasporedom; oznake na bridovima = duljina (SP) ili kapacitet (MF). Za najkraći put označimo rješenje debljim/bojanjem.

# Mreža s duljinama/kapacitetima
edges <- data.frame(
  from=c("s","s","a","a","b","b","c","d","e"),
  to  =c("a","b","c","d","d","e","t","t","t"),
  w   =c(2,4,  2,5,  1,3,  2,1,  4)  # tumači se kao duljina ili kapacitet
)
g3 <- graph_from_data_frame(edges, directed=TRUE)
V(g3)$type <- ifelse(V(g3)$name %in% c("s","t"), "terminal","intermediate")
E(g3)$label <- E(g3)$w

set.seed(42)
lay3 <- layout_with_fr(g3)

# PRIKAZ NAJKRAĆEG PUTA s→t (po w kao duljini)
sp <- shortest_paths(g3, from="s", to="t", weights=E(g3)$w)$epath[[1]]
E(g3)$col <- "grey70"
E(g3)$lwd <- 1.5
E(g3)$col[sp] <- "#1f78b4"
E(g3)$lwd[sp] <- 3.5

plot(
  g3, layout=lay3,
  vertex.color = ifelse(V(g3)$type=="terminal","#fb8072","#80b1d3"),
  vertex.size=28, vertex.label.cex=0.9, vertex.frame.color="grey30",
  edge.color=E(g3)$col, edge.width=E(g3)$lwd, edge.arrow.size=0.6,
  edge.label=E(g3)$label, edge.label.cex=0.9,
  main="Najkraći put (edge label = duljina)"
)

4. Kritični put (CPM / PERT)

Ideja: DAG, layout kao stablo s lijeva nadesno, edge-label = trajanje aktivnosti.

library(igraph)

# 1) Aktivnosti (aktivnost-na-luku), sve usmjereno
acts <- data.frame(
  from = c("Start","Start","A","A","B","C","D","E"),
  to   = c("A","B","C","D","E","E","E","F"),
  dur  = c(3,2,  4,6,  5,  2,  3,  4)    # trajanja na lukovima
)

g4 <- graph_from_data_frame(acts, directed = TRUE)

# 2) Layout kao stablo s lijeva nadesno
lay4 <- layout_as_tree(g4, root = which(V(g4)$name == "Start"), mode = "out")
lay4[,1] <- scales::rescale(lay4[,1], to = c(0,1))  # malo "raširimo" po x osi

# 3) Edge label = trajanje
E(g4)$label <- E(g4)$dur

# 4) Crtanje
plot(
  g4, layout = lay4,
  vertex.color = "#ffffb3", vertex.size = 28, vertex.frame.color = "grey30",
  vertex.label.cex = 0.95,
  edge.color = E(g4)$col, edge.width = E(g4)$lwd, edge.arrow.size = 0.65,
  edge.label = E(g4)$label, edge.label.cex = 0.9,
  main = "CPM/PERT"
)

5. Problem pridruživanja (assignment)

Ideja: bipartitno (radnici – poslovi), edge-label = trošak; prikaži optimalno pridruživanje podebljano.

workers <- c("W1","W2","W3")
jobs    <- c("J1","J2","J3")
costA <- matrix(c(9,2,7,
                  6,4,3,
                  5,8,1), nrow=3, byrow=TRUE,
                dimnames=list(workers, jobs))

E5 <- do.call(rbind, lapply(seq_along(workers), function(i){
  data.frame(from = workers[i], to = jobs, cost = costA[i, ])
}))
g5 <- graph_from_data_frame(E5, directed = TRUE)

V(g5)$type <- ifelse(V(g5)$name %in% workers, "worker", "job")
E(g5)$label <- E(g5)$cost

# --- bipartitni layout (dva čista stupca) ---
idx_w <- which(V(g5)$type == "worker")
idx_j <- which(V(g5)$type == "job")
lay5  <- matrix(NA_real_, nrow = vcount(g5), ncol = 2)
lay5[idx_w,] <- cbind(0, seq(1, 0, length.out = length(idx_w)))
lay5[idx_j,] <- cbind(1, seq(1, 0, length.out = length(idx_j)))

# --- optimalni parovi (primjer: rezultat Hungarian metode) ---
opt_pairs <- data.frame(from=c("W1","W2","W3"), to=c("J2","J1","J3"))

# označimo optimalne bridove
el_names  <- ends(g5, E(g5), names = TRUE)
edge_keys <- paste0(el_names[,1], "_", el_names[,2])
opt_keys  <- paste0(opt_pairs$from, "_", opt_pairs$to)
is_opt    <- edge_keys %in% opt_keys

E(g5)$col <- ifelse(is_opt, "#1b9e77", "grey70")
E(g5)$lwd <- ifelse(is_opt, 3.5, 1.5)

# --- plot (bez strelica jer je assignment) ---
plot(
  g5, layout = lay5,
  vertex.color = ifelse(V(g5)$type=="worker","#8dd3c7","#bebada"),
  vertex.size = 28, vertex.label = V(g5)$name, vertex.label.cex = 0.95,
  vertex.frame.color = "grey30",
  edge.color = E(g5)$col, edge.width = E(g5)$lwd,
  edge.arrow.size = 0,                 # assignment: bez strelica
  edge.curved = 0.03,
  edge.label = E(g5)$label, edge.label.cex = 0.85,
  main = "Assignment: optimalne veze zadebljane (edge label = trošak)"
)


Ključni koncept: raspored (Layout)

Zadani raspored (layout_nicely) pokušava “pogoditi” što želite. Često je rezultat “zapetljana klupka” (engl. hairball). Ključ je odabir pravog layout algoritma:

  • layout_in_circle: Prikazuje čvorove u krugu.
  • layout_as_tree: Izvrsno za hijerarhije (ako nema ciklusa).
  • layout_with_fr (Fruchterman-Reingold): Klasični “fizikalni” model. Smatra čvorove objektima koji se odbijaju, a lukove oprugama. Daje organski izgled.
  • layout_on_grid: Postavlja čvorove na mrežu (korisno ako se crta transportni problem).


Algoritmi i rješenja u igraph-u

Ovdje igraph briljira i direktno zamjenjuje mnoge naše LP formulacije.

  • Problem najkraćeg puta (Dijkstra):

Umjesto da pišemo LP, igraph ima ugrađen Dijkstrin algoritam. Lukovi moraju imati atribut weight.

  • Što treba? Atribut na luku weight (cijena/duljina).

  • Tipična pogreška: zaboravimo weights = E(g)$weight, pa dobijemo netočno rješenje.


edges <- data.frame(
  from=c("s","s","a","a","b","b","c","d","e"),
  to  =c("a","b","c","d","d","e","t","t","t"),
  w   =c(2,4,  2,5,  1,3,  2,1,  4)  # tumači se kao duljina ili kapacitet
)
g3 <- graph_from_data_frame(edges, directed=TRUE)
V(g3)$type <- ifelse(V(g3)$name %in% c("s","t"), "terminal","intermediate")
E(g3)$label <- E(g3)$w

sp <- shortest_paths(g3, from = "s", to = "t", mode = "out", weights = E(g3)$w, output = "both")
sp
## $vpath
## $vpath[[1]]
## + 4/7 vertices, named, from c3023c8:
## [1] s a c t
## 
## 
## $epath
## $epath[[1]]
## + 3/9 edges from c3023c8 (vertex names):
## [1] s->a a->c c->t
## 
## 
## $predecessors
## NULL
## 
## $inbound_edges
## NULL


  • Problem maksimalnog toka (Edmonds-Karp):

Umjesto LP-a, možemo direktno pitati za maks. tok. Lukovi moraju imati atribut capacity.


max_flow(g3, source = "s", target = "t", capacity = E(g3)$w) # ranije upisane težine ovdje tretiramo kao kapacitet
## $value
## [1] 6
## 
## $flow
## [1] 2 4 2 0 1 3 2 1 3
## 
## $cut
## + 3/9 edges from c3023c8 (vertex names):
## [1] b->e c->t d->t
## 
## $partition1
## + 5/7 vertices, named, from c3023c8:
## [1] s a b c d
## 
## $partition2
## + 2/7 vertices, named, from c3023c8:
## [1] e t
## 
## $stats
## $stats$nopush
## [1] 12
## 
## $stats$norelabel
## [1] 2
## 
## $stats$nogap
## [1] 0
## 
## $stats$nogapnodes
## [1] 0
## 
## $stats$nobfs
## [1] 1


  • Problem kritičnog puta (CPM/PERT):

igraph nema ugrađenu CPM funkciju (jer CPM traži najduži put, što je NP-teško za opće grafove). Ali za acikličke grafove (DAGs) poput naših slučajeva, možemo pronaći sve puteve i izračunati njihova trajanja ili koristiti druge pakete (critpath).


  • Analiza centralnosti (Koji je čvor “važan”? Ovaj dio priče relevantniji je za kolegij SNA - Analiza društvenih mreža):


Ovdje igraph nudi ono što LP ne može. Možemo kvantificirati “važnost” čvora:

  • degree(g): Koliko lukova ulazi/izlazi iz čvora? (Jednostavna popularnost).
  • betweenness(g): Koliko često čvor leži na najkraćem putu između dva druga čvora? (Mjeri tko je “posrednik” ili “usko grlo”).
  • closeness(g): Koliko je prosječno čvor blizu svim ostalim čvorovima? (Tko može najbrže širiti informacije?).
  • page_rank(g): Tko je važan jer je povezan s drugim važnim čvorovima? (Googleov algoritam).


  • Detekcija zajednica (Community detection. Također, relevantnije za kolegij SNA - Analiza društvenih mreža):

igraph ima snažne algoritme (cluster_walktrap, cluster_louvain) koji mogu automatski pronaći “klastere” ili “zajednice” u mreži—grupe čvorova koje su gušće povezane međusobno nego s ostatkom mreže.


Primjeri za samostalnu vježbu


1. slučaj

Tri pržionice kave (Izvori) moraju opskrbiti četiri velika skladišta trgovačkih lanaca (Odredišta). Vaš posao je spriječiti nacionalnu krizu nedostatka kofeina uz minimalni trošak transporta.

  • Izvori (\(I\)): Pržionica Rovinj (I1), Pržionica Zagreb (I2), Pržionica Split (I3)
  • Odredišta (\(K\)): Skladište Kunzom (K1), Skladište Spir (K2), Skladište Land (K3), Skladište Mimmy (K4)

Podaci:

  • Ponuda (u tonama):

    • I1 (Rovinj): 1000 t
    • I2 (Zagreb): 2000 t
    • I3 (Split): 1500 t
    • (Ukupno: 4500 t)
  • Potražnja (u tonama):

    • K1 (Kunzom): 1200 t
    • K2 (Spir): 1500 t
    • K3 (Land): 1000 t
    • K4 (Mimmy): 800 t
    • (Ukupno: 4500 t)
  • Troškovi Transporta \(c_{ik}\) (EUR po toni):

(Iz \(I\)) K1 (Konzum) K2 (Spar) K3 (Kaufland) K4 (Tommy)
I1 (Rovinj) 3 4 5 9
I2 (Zagreb) 2 1 3 7
I3 (Split) 7 6 4 2


2. slučaj

Tri velika podatkovna centra (Izvori) moraju isporučiti podatkovni promet (streaminge) do četiri glavna ISP čvorišta (Odredišta) tijekom vršnog opterećenja. Trošak je cijena (u EUR) za prijenos 1 petabajta (PB) podataka.

  • Izvori (\(I\)): DC Frankfurt (I1), DC Amsterdam (I2), DC Jastrebarsko (I3)
  • Odredišta (\(K\)): Čvor Tele-A (K1), Čvor Mobi-B (K2), Čvor Inter-C (K3), Čvor Tel-D (K4)

Podaci:

  • Kapacitet (Ponuda) (u PB):

    • I1 (Frankfurt): 50 PB
    • I2 (Amsterdam): 80 PB
    • I3 (Jastrebarsko): 30 PB
    • (Ukupno: 160 PB)
  • Vršna potražnja (u PB):

    • K1: 60 PB
    • K2: 45 PB
    • K3: 35 PB
    • K4: 20 PB
    • (Ukupno: 160 PB)
  • Troškovi prijenosa \(c_{ik}\) (EUR po PB):

(Iz \(I\)) K1 K2 K3 K4
I1 (Frankfurt) 5 4 6 5
I2 (Amsterdam) 4 3 3 4
I3 (Jastrebarsko) 1 1 2 2


3. slučaj

Tri luke služe kao noćni vez za trajekte. Četiri polazne luke ujutro moraju imati trajekte da započnu linije. Cilj je minimizirati trošak goriva za premještanje praznih trajekata prije 06:00.

  • Izvori (\(I\)): Noćni vez Mali Lošinj (I1), Noćni vez Zadar (I2), Noćni vez Šibenik (I3)
  • Odredišta (\(K\)): Linija Rijeka (K1), Linija Preko (K2), Linija Supetar (K3), Linija Vis (K4)

Podaci:

  • Ponuda (Broj dostupnih trajekata):

    • I1 (M. Lošinj): 3
    • I2 (Zadar): 5
    • I3 (Šibenik): 2
    • (Ukupno: 10 trajekata)
  • Potražnja (Broj potrebnih trajekata):

    • K1 (Rijeka): 4
    • K2 (Preko): 2
    • K3 (Supetar): 3
    • K4 (Vis): 1
    • (Ukupno: 10 trajekata)
  • Troškovi goriva \(c_{ik}\) (u tisućama EUR):

(Iz \(I\)) K1 (Rijeka) K2 (Preko) K3 (Supetar) K4 (Vis)
I1 (M. Lošinj) 2 4 9 10
I2 (Zadar) 5 1 3 4
I3 (Šibenik) 8 3 2 3

Ovi problemi zahtijevaju da sav teret prođe kroz posrednički čvor (npr. centralno skladište, carinski terminal, hub).


4. slučaj

Dvije tvornice proizvode opremu. Dva carinska skladišta u EU vrše inspekciju i certificiranje. Tri regionalna skladišta u HR (odredišta) čekaju robu. Sva roba mora proći carinu.

  • Izvori (\(I\)): Tvornica Kina (I1), Tvornica Njemačka (I2)
  • Posrednici (\(J\)): Luka Rotterdam (J1), Luka Kopar (J2)
  • Odredišta (\(K\)): Skladište Zagreb (K1), Skladište Osijek (K2), Skladište Split (K3)

Podaci:

  • Ponuda (Broj kontejnera):

    • I1 (Kina): 10000
    • I2 (Njemačka): 5000
    • (Ukupno: 15000)
  • Potražnja (Broj kontejnera):

    • K1 (Zagreb): 8000
    • K2 (Osijek): 3000
    • K3 (Split): 4000
    • (Ukupno: 15000)
  • Tablica Troškova 1: \(c_{ij}\) (Izvor \(\to\) Posrednik):

(Iz \(I\)) J1 (Rotterdam) J2 (Kopar)
I1 (Kina) 250 280
I2 (Njemačka) 50 90
  • Tablica Troškova 2: \(c_{jk}\) (Posrednik \(\to\) Odredište):
(Iz \(J\)) K1 (Zagreb) K2 (Osijek) K3 (Split)
J1 (Rotterdam) 100 110 130
J2 (Kopar) 40 50 60


5. slučaj

Dvije podružnice generiraju podatkovni promet (npr. backup podataka). Sav promet mora proći kroz jedan od dva centralna vatrozida (posrednici) radi sigurnosne inspekcije, prije nego što se pohrani na tri različita cloud servera (odredišta).

  • Izvori (\(I\)): Ured Split (I1), Ured Rijeka (I2)
  • Posrednici (\(J\)): Vatrozid-A (J1), Vatrozid-B (J2)
  • Odredišta (\(K\)): ABC server (K1), BDF server (K2), ACE Cloud (K3)

Podaci:

  • Generirani promet (Ponuda) (u Gbps):

    • I1 (Split): 50 Gbps
    • I2 (Rijeka): 40 Gbps
    • (Ukupno: 90 Gbps)
  • Kapacitet pohrane (Potražnja) (u Gbps):

    • K1 (ABC): 30 Gbps
    • K2 (BDF): 40 Gbps
    • K3 (ACE): 20 Gbps
    • (Ukupno: 90 Gbps)
  • Tablica Troškova 1: \(c_{ij}\) (Latencija/Trošak Ureda \(\to\) Vatrozida):

(Iz \(I\)) J1 (Vatrozid-A) J2 (Vatrozid-B)
I1 (Split) 5 2
I2 (Rijeka) 1 3
  • Tablica Troškova 2: \(c_{jk}\) (Latencija/Trošak Vatrozida \(\to\) Clouda):
(Iz \(J\)) K1 (ABC) K2 (BDF) K3 (ACE)
J1 (Vatrozid-A) 4 2 6
J2 (Vatrozid-B) 3 3 5


6. slučaj

Agencija organizira transfere turista s kopna na manje otoke. Svi turisti prvo se prevoze brzim brodovima na dva “otoka-huba”. Tamo popiju kavu od 5 eura i presjedaju na manje brodove koji ih voze na finalne destinacije.

  • Izvori (\(I\)): Zadar (I1), Split (I2), Dubrovnik (I3)
  • Posrednici (\(J\)): Otok Hvar (J1), Otok Brač (J2)
  • Odredišta (\(K\)): Otok Vis (K1), Otok Korčula (K2), Otok Mljet (K3)

Podaci:

  • Ponuda (Broj turista - ‘pax’):

    • I1 (Zadar): 200 pax
    • I2 (Split): 300 pax
    • I3 (Dubrovnik): 150 pax
    • (Ukupno: 650 pax)
  • Potražnja (Kapacitet smještaja):

    • K1 (Vis): 250 pax
    • K2 (Korčula): 250 pax
    • K3 (Mljet): 150 pax
    • (Ukupno: 650 pax)
  • Tablica troškova 1: \(c_{ij}\) (Cijena karte Kopno \(\to\) Hub):

(Iz \(I\)) J1 (Hvar) J2 (Brač)
I1 (Zadar) 20 18
I2 (Split) 10 8
I3 (Dubrovnik) 12 16
  • Tablica troškova 2: \(c_{jk}\) (Cijena karte Hub \(\to\) Destinacija):
(Iz \(J\)) K1 (Vis) K2 (Korčula) K3 (Mljet)
J1 (Hvar) 8 5 10
J2 (Brač) 6 7 15


7. slučaj

Vi ste softverski inženjer u “Gladnom Grizliju” (lokalni Glovo/Wolt). Vaš algoritam mora izračunati najbržu rutu za dostavljača od restorana “Nonine delicije” do kupca.

Mreža gradskih ulica (većinom jednosmjernih) i prosječno vrijeme putovanja (uključujući semafore) u minutama su kako slijedi:

  • Početak (\(s\)): Restoran “Nonine delicije” (Čvor 1)
  • Kraj (\(t\)): Kupac (Čvor 7)
  • Posredni čvorovi: Raskrižja A (2), B (3), C (4), D (5), E (6)

Mreža i vremena:

  • Restoran (1) \(\to\) Raskrižje A (2): 2 min

  • Restoran (1) \(\to\) Raskrižje B (3): 5 min

  • Raskrižje A (2) \(\to\) Raskrižje C (4): 6 min

  • Raskrižje A (2) \(\to\) Raskrižje D (5): 3 min

  • Raskrižje B (3) \(\to\) Raskrižje D (5): 1 min

  • Raskrižje B (3) \(\to\) Raskrižje E (6): 8 min

  • Raskrižje C (4) \(\to\) Kupac (7): 2 min

  • Raskrižje D (5) \(\to\) Raskrižje C (4): 1 min

  • Raskrižje D (5) \(\to\) Raskrižje E (6): 4 min

  • Raskrižje D (5) \(\to\) Kupac (7): 9 min

  • Raskrižje E (6) \(\to\) Kupac (7): 3 min

Grafički prikažite problem, strukurirajte (matematički zapis modela) i utvrdite rutu s najkraćim vremenom dostave.


8. slučaj

Hitni sigurnosni patch mora se “progurati” s development servera (DEV) na produkcijski server (PROD). Put mora proći kroz razne testne servere i vatrozide.

Cilj je pronaći put s najmanjim ukupnim vremenom (prijenos + verifikacija). Trošak na svakom luku \((i, j)\) predstavlja ukupno vrijeme (u sekundama) potrebno za prijenos paketa s \(i\) na \(j\) plus vrijeme koje \(j\) treba da verificira paket.

  • Početak (\(s\)): DEV server (Čvor 1)
  • Kraj (\(t\)): PROD server (Čvor 6)
  • Posredni čvorovi: Build server (B), Test-env (T), Staging-env (STG), Vatrozid (FW)

Mreža i vremena:

  • DEV (1) \(\to\) Build Server (2): 10 s

  • DEV (1) \(\to\) Test-Env (3): 20 s

  • Build Server (2) \(\to\) Test-Env (3): 5 s

  • Test-Env (3) \(\to\) Staging-Env (4): 30 s

  • Test-Env (3) \(\to\) Vatrozid (5): 10 s

  • Staging-Env (4) \(\to\) Vatrozid (5): 5 s

  • Staging-Env (4) \(\to\) PROD Server (6): 20 s

  • Vatrozid (5) \(\to\) PROD Server (6): 15 s

Nađite najbrži put za deployment.


9. slučaj

Pokušavate oboriti rekord u igri. Morate doći od Ulaza u Tamnicu (A) do Dvorane Kraljeva (G). Svaki hodnik (luk) ima “cijenu” koja predstavlja očekivano vrijeme prolaska (uključujući borbe s manjim neprijateljima).

  • Početak (\(s\)): Ulaz (A)
  • Kraj (\(t\)): Dvorana Kraljeva (G)
  • Posredni čvorovi: Dvorana Jeke (B), Ukleti Hodnik (C), Kula (D), Oružarnica (E), Tamnica (F)

Mreža i vremena:

  • Ulaz (A) \(\to\) Dvorana Jeke (B): 3 min

  • Ulaz (A) \(\to\) Ukleti Hodnik (C): 2 min

  • Dvorana Jeke (B) \(\to\) Kula (D): 2 min

  • Dvorana Jeke (B) \(\to\) Oružarnica (E): 8 min (Čuvar!)

  • Ukleti Hodnik (C) \(\to\) Kula (D): 6 min

  • Ukleti Hodnik (C) \(\to\) Tamnica (F): 4 min

  • Kula (D) \(\to\) Oružarnica (E): 1 min (Tajni prolaz, jednosmjeran!)

  • Kula (D) \(\to\) Tamnica (F): 3 min

  • Oružarnica (E) \(\to\) Dvorana Kraljeva (G): 2 min

  • Tamnica (F) \(\to\) Dvorana Kraljeva (G): 7 min

Grafički prikažite i nađite rekordno brzi put.


10. slučaj

Vi ste inženjer sigurnosti i morate procijeniti kapacitet evakuacije nove sportske dvorane. Zanima vas maksimalan broj ljudi po minuti koji može sigurno izaći iz dvorane (Izvor) i doći do “Sigurne zone” na trgu (Ponor).

  • Izvor (\(s\)): Dvorana (Čvor 1)
  • Ponor (\(t\)): Sigurna zona (Čvor 6)
  • Posredni čvorovi: Zapadni Izlaz (2), Istočni Izlaz (3), Glavni Hodnik (4), Pomoćni Hodnik (5)

Kapaciteti lukova (\(u_{ij}\) u “osoba / minuti”):

    1. Dvorana \(\to\) (2) Zapadni Izlaz: 200 (Kapacitet glavnih vrata)
    1. Dvorana \(\to\) (3) Istočni Izlaz: 150 (Kapacitet sporednih vrata)
    1. Zapadni Izlaz \(\to\) (4) Glavni Hodnik: 180
    1. Zapadni Izlaz \(\to\) (5) Pomoćni Hodnik: 50
    1. Istočni Izlaz \(\to\) (4) Glavni Hodnik: 100
    1. Glavni Hodnik \(\to\) (6) Sigurna zona: 250
    1. Pomoćni Hodnik \(\to\) (6) Sigurna zona: 80

Koliki je maksimalni broj ljudi koji dvorana može evakuirati u jednoj minuti?


11. slučaj

Vi ste voditelj proizvodnje u tvornici poluvodiča. Sirovi silikon (Izvor) mora proći kroz niz stanica (litografija, jetkanje, testiranje) prije nego što postane gotov čip (Ponor). Svaka stanica i veza između njih ima maksimalni kapacitet (broj wafera obrađenih po satu).

  • Izvor (\(s\)): Skladište silikona (Čvor 1)
  • Ponor (\(t\)): Pakiranje (Čvor 6)
  • Posredni čvorovi: Fotolitografija (2), Jetkanje (Etch) (3), Testiranje (4), Montaža (5)

Kapaciteti lukova (\(u_{ij}\) u “wafera / sat”):

    1. Skladište \(\to\) (2) Fotolitografija: 500
    1. Skladište \(\to\) (3) Jetkanje: 400 (Druga vrsta proizvoda)
    1. Fotolitografija \(\to\) (3) Jetkanje: 300
    1. Fotolitografija \(\to\) (4) Testiranje: 150
    1. Jetkanje \(\to\) (5) Montaža: 600
    1. Testiranje \(\to\) (5) Montaža: 150
    1. Montaža \(\to\) (6) Pakiranje: 700

Koliki je stvarni maksimalni kapacitet proizvodnje (wafera po satu) cijele tvornice? Gdje je “usko grlo”?


12. slučaj

Nakon prirodne katastrofe, tim Crvenog križa mora dostaviti zalihe (hranu, vodu) iz centralnog aerodromskog skladišta (Izvor) u izolirani grad (Ponor). Mreža cesta je oštećena, a neki mostovi i tuneli imaju stroga ograničenja propusnosti.

  • Izvor (\(s\)): Aerodrom (Čvor 1)
  • Ponor (\(t\)): Grad (Čvor 7)
  • Posredni čvorovi: Kontrolna točka A (2), Selo B (3), Stari Most (4), Tunel (5), Kontrolna točka C (6)

Kapaciteti lukova (\(u_{ij}\) u “tona tereta / dan”):

    1. Aerodrom \(\to\) (2) K. Točka A: 80
    1. Aerodrom \(\to\) (3) Selo B: 70
    1. K. Točka A \(\to\) (4) Stari Most: 50
    1. Selo B \(\to\) (4) Stari Most: 40 (Konvergencija)
    1. Selo B \(\to\) (5) Tunel: 50
    1. Stari Most \(\to\) (6) K. Točka C: 60 (Most je usko grlo)
    1. Tunel \(\to\) (6) K. Točka C: 30 (Tunel je usko grlo)
    1. K. Točka C \(\to\) (7) Grad: 100 (Glavna cesta u grad)


13. slučaj

Vi ste analitičar na fakultetu i morate odrediti maksimalan broj studentskih prijava (molbi, zahtjeva) koje referada može obraditi u jednom danu. Prijave ulaze u Prijemni ured (Izvor) i moraju proći kroz razne službe (Posrednici) prije nego što stignu u Arhivu (Ponor) kao “riješene”.

  • Izvor (\(s\)): Prijemni ured (Čvor 1)
  • Ponor (\(t\)): Arhiva riješenih predmeta (Čvor 6)
  • Posredni čvorovi: Studentska služba (2), Računovodstvo (3), Pravna služba (4), IT podrška (5)

Kapaciteti službi (\(u_{ij}\) u “predmeta / dan”):

    1. Prijemni \(\to\) (2) Stud. služba: 200 (Kapacitet referenata)
    1. Prijemni \(\to\) (3) Računovodstvo: 50 (Zahtjevi koji idu direktno na plaćanje)
    1. Stud. služba \(\to\) (3) Računovodstvo: 80 (Proslijeđivanje na uplatu)
    1. Stud. služba \(\to\) (4) Pravna služba: 100 (Složeni slučajevi)
    1. Stud. služba \(\to\) (5) IT podrška: 30 (Digitalni zahtjevi)
    1. Računovodstvo \(\to\) (6) Arhiva: 120
    1. Pravna služba \(\to\) (6) Arhiva: 70
    1. IT podrška \(\to\) (6) Arhiva: 25


14. slučaj

Uprava Nacionalnog parka (npr. Plitvička jezera) želi utvrditi maksimalan broj posjetitelja koji smije ući u park po satu (\(s\)), kako bi se osiguralo da se na stazama ne stvaraju opasni “čepovi”. Sav promet ulazi na Glavni Ulaz (\(s\)) i izlazi kod Izlaza 2 (\(t\)).

  • Izvor (\(s\)): Ulaz 1 (Čvor 1)
  • Ponor (\(t\)): Izlaz 2 (Čvor 5)
  • Posredni čvorovi: Stajalište busa A (2), Okretište broda B (3), Veliki slap (4)

Kapaciteti staza (\(u_{ij}\) u “ljudi / sat”):

    1. Ulaz 1 \(\to\) (2) Stajalište A: 1000
    1. Ulaz 1 \(\to\) (3) Okretište B: 800
    1. Stajalište A \(\to\) (4) Veliki slap: 700
    1. Okretište B \(\to\) (4) Veliki slap: 900
    1. Veliki slap \(\to\) (5) Izlaz 2: 1500

Formulirajte problem uvođenjem zamišljenog luka \(x_{51}\) (Izlaz \(\to\) Ulaz) i maksimizirajte tok na njemu.


15. slučaj

U slučaju kvara glavnog servera (Izvor), morate prebaciti sve aktivne korisnike na rezervni sustav (Ponor). Zanima vas maksimalni broj istovremenih konekcija koje vaš sustav može preusmjeriti, s obzirom na kapacitete routera i linkova.

  • Izvor (\(s\)): Glavni Server (Čvor 1)
  • Ponor (\(t\)): Rezervni Server (Čvor 6)
  • Posredni čvorovi: Router A (2), Router B (3), Agregator (4), Vatrozid (5)

Kapaciteti veza (\(u_{ij}\) u “tisućama konekcija”):

    1. Glavni Server \(\to\) (2) Router A: 20
    1. Glavni Server \(\to\) (3) Router B: 30
    1. Router A \(\to\) (4) Agregator: 15
    1. Router B \(\to\) (4) Agregator: 15
    1. Router B \(\to\) (5) Vatrozid: 10
    1. Agregator \(\to\) (6) Rezervni Server: 25
    1. Vatrozid \(\to\) (6) Rezervni Server: 15

Maksimizirajte protok mreže.


16. slučaj

Organizirate studentski hackaton, “Code-a-thon”, i morate se za to pripremiti. Cilj je imati funkcionalnu web-aplikaciju (od ideje do deploymenta) u što kraćem roku. Trajanja su u satima.

Događaji (Čvorovi):

  • 1: Start (Kava)
  • 2: Tema/Ideja definirana
  • 3: Dizajn (UI/UX Mockup) gotov
  • 4: Arhitektura (Backend/DB) definirana
  • 5: Frontend kodiran (statički)
  • 6: Backend API funkcionalan
  • 7: Aplikacija integrirana (Frontend spojen na API)
  • 8: QA Testiranje završeno
  • 9: Aplikacija deployana (uživo)
Oznaka Aktivnost (Luk) Opis Trajanje (sati)
A (1) \(\to\) (2) Brainstorming i odabir teme 2
B (2) \(\to\) (3) Dizajn sučelja (Figma) 4
C (2) \(\to\) (4) Definiranje API endpointa 3
D (3) \(\to\) (5) Kodiranje Frontenda (React/Vue) 8
E (4) \(\to\) (6) Kodiranje Backenda (API) 6
F (5) \(\to\) (7) Spajanje Frontenda na API 3
G (6) \(\to\) (7) Fino podešavanje API-ja 2
H (7) \(\to\) (8) Testiranje funkcionalnosti 5
I (4) \(\to\) (5) [Fiktivna] Priprema podataka 1
J (8) \(\to\) (9) Deployment (Heroku/Vercel) 1
K (3) \(\to\) (6) [Fiktivna] Postavke Baze 2

Ključne ovisnosti:

  • Aktivnosti B i C mogu se raditi paralelno (obje nakon A).
  • Aktivnost G (podešavanje API-ja) ne može početi dok Frontend (F) nije počeo spajanje, ili… o ne, ovo je komplicirano.
  • Aktivnost F (spajanje frontenda) i Aktivnost G (podešavanje API-ja) moraju obje biti gotove prije nego što je Događaj 7 (Aplikacija integrirana) gotov.


17. slučaj

Morate postaviti mrežu za vikend turnir u studentskoj menzi. Nemate puno vremena. Trajanja su u satima.

Događaji (čvorovi):

  • 1: Start
  • 2: Oprema (kabeli, switchevi) dostavljena
  • 3: Glavni server (za igre) konfiguriran
  • 4: Stolovi i stolice postavljeni
  • 5: Kabeli provučeni po podu
  • 6: Switchevi instalirani i spojeni (Uplink)
  • 7: Server fizički montiran u rack
  • 8: Mreža testirana (DHCP, povezivost)
  • 9: Spremno za igrače
Oznaka Aktivnost (Luk) Opis Trajanje (sati)
A (1) \(\to\) (2) Nabava i dostava opreme 8
B (1) \(\to\) (3) Instalacija OS-a i servera 6
C (1) \(\to\) (4) Fizički raspored (Menza) 4
D (2) \(\to\) (6) Montaža Switcheva 3
E (4) \(\to\) (5) Provlačenje UTP kabela 5
F (5) \(\to\) (6) Krimpanje i spajanje kabela na switcheve 2
G (3) \(\to\) (7) Fizička instalacija servera 1
H (6) \(\to\) (8) Testiranje mrežne infrastrukture 2
I (7) \(\to\) (8) Spajanje servera i test 1
J (8) \(\to\) (9) Puštanje sustava u pogon 1

Ključne ovisnosti:

  • Tri grane kreću odmah (A, B, C).
  • Događaj 6 (Switchevi spojeni) ovisi o tome da su kabeli provučeni (E) i da su sami switchevi montirani (D).
  • Događaj 8 (Testiranje) ovisi o tome da je mreža (H) i server (I) spojen.


18. slučaj

Radite na projektu iz strojnog učenja. Cilj je od nule izgraditi i deployati model za filtriranje spam poruka. Trajanja su u danima.

Događaji (čvorovi):

  • 1: Start (Problem definiran)
  • 2: Podatci (emailovi) prikupljeni
  • 3: Podatci očišćeni i labelirani
  • 4: Feature Engineering (priprema značajki) gotova
  • 5: Infrastruktura (npr. Cloud GPU) postavljena
  • 6: Model istreniran
  • 7: Model evaluiran (test set)
  • 8: API za predikciju napisan
  • 9: Model deployan (integriran s API-jem)
  • 10: Kraj (filter je u produkciji)
Oznaka Aktivnost (luk) Opis Trajanje (dani)
A (1) \(\to\) (2) Pisanje skripti za prikupljanje 7
B (2) \(\to\) (3) Čišćenje i labeliranje 10
C (3) \(\to\) (4) Analiza i inženjering značajki 5
D (1) \(\to\) (5) Postavljanje infrastrukture 6
E (4) \(\to\) (6) Pisanje i pokretanje skripte za trening 8
F (5) \(\to\) (6) Fiktivna (Integ. infrastrukture) 1
G (6) \(\to\) (7) Evaluacija i analiza metrika 3
H (7) \(\to\) (8) Pisanje API-ja (Flask/FastAPI) 4
I (7) \(\to\) (9) Pakiranje modela (Pickle/ONNX) 2
J (8) \(\to\) (10) Deployment API-ja 1
K (9) \(\to\) (10) Integracija modela u API 1

Kad će projekt biti gotov?


19. slučaj

Vaš tim mora izdati hitan patch (zakrpu) za igru. Morate popraviti bugove, ali i ubaciti nove skinove koje je menadžment obećao. Vremena su u danima.

Događaji (čvorovi):

  • 1: Start (feedback primljen)
  • 2: Popis bugova (bug list) finaliziran
  • 3: Novi feature-i (npr. skinovi) dizajnirani (art)
  • 4: Core gameplay bugovi fixani (programiranje)
  • 5: Art asseti importani
  • 6: Novi UI elementi kodirani
  • 7: Verzija za Test (Build) spremna
  • 8: QA (testiranje) završeno
  • 9: Patch deployan (live)
Oznaka Aktivnost (luk) Optimistično (\(a\)) Najvjerojatnije (\(m\)) Pesimistično (\(b\))
A (1) \(\to\) (2) Triage (Pregled bugova) 1 2
B (1) \(\to\) (3) Dizajn novih skinova (Art) 3 5
C (2) \(\to\) (4) Fixanje core bugova (Program.) 2 4
D (2) \(\to\) (6) Kodiranje UI updatea 1 2
E (3) \(\to\) (5) Import asseta 1 1
F (4) \(\to\) (7) Merge (Spajanje) bug fixeva 0.5 1
G (5) \(\to\) (7) Merge (Spajanje) asseta 0.5 1
H (6) \(\to\) (7) Merge (Spajanje) UI-a 0.5 1
I (7) \(\to\) (8) QA Testiranje 2 3
J (8) \(\to\) (9) Deployment 0.5 1

Menadžment je obećao igračima da patch stiže za 15 dana. Kolika je vjerojatnost da nećete ispuniti obećanje?


20. slučaj

Nadograđujete stan IoT sustavom (pametne žarulje, termostat, server). Morate sve kupiti, instalirati i isprogramirati. Vremena su u satima.

Događaji (Čvorovi):

  • 1: Start (Ideja)
  • 2: Sva oprema naručena i dostavljena
  • 3: Mrežna infrastruktura (Router/VLAN) spremna
  • 4: Glavni server (npr. Raspberry Pi) instaliran
  • 5: Pametne žarulje i senzori montirani
  • 6: Softver (Home Assistant) instaliran na server
  • 7: Uređaji (Žarulje/Senzori) dodani u sustav
  • 8: Osnovne automatizacije (skripte) napisane
  • 9: Sustav testiran (WAF - Wife Acceptance Factor)
  • 10: Projekt gotov
Oznaka Aktivnost (luk) Optimistično (\(a\)) Najvjerojatnije (\(m\)) Pesimistično (\(b\))
A (1) \(\to\) (2) Istraživanje i dostava opreme 24.0 48.0
B (1) \(\to\) (3) Planiranje i konfiguracija mreže 1.0 2.0
C (2) \(\to\) (4) Fizička instalacija servera 0.5 1.0
D (2) \(\to\) (5) Montaža žarulja i senzora 2.0 3.0
E (3) \(\to\) (6) Provjera 0.5 1.0
F (4) \(\to\) (6) Instalacija OS-a i Home Assistanta 1.0 2.0
G (5) \(\to\) (7) Uparivanje uređaja (Zigbee/WiFi) 1.0 2.0
H (6) \(\to\) (7) Konfiguracija integracija 0.5 1.0
I (7) \(\to\) (8) Pisanje automatizacija (Node-RED) 2.0 4.0
J (8) \(\to\) (9) Testiranje (WAF) 0.5 1.0
K (9) \(\to\) (10) Dokumentacija / GOTOVO 0.5 1.0

Obećali ste da ćete sve završiti za 72 sata (3 dana) od trenutka kad je oprema stigla (Događaj 2). Kolika je vjerojatnost da ćete uspjeti (tj. da će put od 2 do 10 trajati \(\le 72\) sata)?


21. slučaj

Student informatike mora u 15 tjedana semestra savladati gradivo i predati 6 zadataka. Uspjeh na zadatcima (vježbama) je preduvjet za predaju. Struktura predmeta je složena i slijedi logiku ovisnosti samih metoda. Vremena su u tjednima.

Događaji (čvorovi):

  • 1: Početak semestra
  • 2: Savladao LP i Analizu Osjetljivosti (temelj)
  • 3: Uspješno riješene vježbe za Zadatak 1 (LP)
  • 4: Predan Zadatak 1
  • 5: Savladao Mrežne modele
  • 6: Uspješno riješene vježbe za Zadatak 2 (Mreže)
  • 7: Predan Zadatak 2
  • 8: Savladao Cjelobrojno i Pridruživanje
  • 9: Savladao Ciljno i Višeciljno programiranje
  • 10: Uspješno riješene vježbe za Zadatak 3 i 4
  • 11: Predan Zadatak 3 i 4
  • 12: Savladao Nelinearno programiranje
  • 13: Savladao Višekriterijsko odlučivanje
  • 14: Uspješno riješene vježbe za Zadatak 5 i 6
  • 15: Predan Zadatak 5 i 6
  • 16: Kraj (Ostvaren uvjet pristupa ispitu)
Ozn. Aktivnost (Luk) Opis Preduvjeti (Događaji) \(a\) \(m\) \(b\)
A (1) \(\to\) (2) Učenje: LP/Analiza Osjet. 1 1.0 2.0 4.0
B (2) \(\to\) (3) Vježba: Zadatak 1 (LP) 2 0.5 1.0 3.0
C (3) \(\to\) (4) Formatiranje/Predaja Z1 3 0.1 0.2 0.3
D (2) \(\to\) (5) Učenje: Mrežni modeli 2 (ovisi o LP) 1.0 1.5 3.0
E (5) \(\to\) (6) Vježba: Zadatak 2 (Mreže) 5 1.0 1.5 2.0
F (6) \(\to\) (7) Formatiranje/Predaja Z2 6 0.1 0.2 0.3
G (5) \(\to\) (8) Učenje: Cjelobrojno/Pridruž. 5 (ovisi o Mrežama) 1.0 2.0 3.0
H (2) \(\to\) (9) Učenje: Ciljno/Višeciljno 2 (ovisi o LP) 2.0 3.0 5.0
I (8) \(\to\) (9) [Logička veza] 8 (ovisi i o Cjelobrojnom) 0.0 0.0 0.0
J (9) \(\to\) (10) Vježba: Zadatak 3 i 4 9 1.0 2.0 4.0
K (10) \(\to\) (11) Formatiranje/Predaja Z3/4 10 0.1 0.2 0.3
L (2) \(\to\) (12) Učenje: Nelinearno 2 (ovisi o LP) 1.0 1.5 2.0
M (1) \(\to\) (13) Učenje: Višekriterijsko 1 (samo osnove) 1.0 2.0 3.0
N (12) \(\to\) (14) Vježba: Zadatak 5 i 6 12 (Nelinearno) 1.0 2.0 3.0
O (13) \(\to\) (14) [Logička veza] 13 (MCDA) 0.0 0.0 0.0
P (14) \(\to\) (15) Formatiranje/Predaja Z5/6 14 0.1 0.2 0.3
Q (4) \(\to\) (16) [Kraj] 4 0.0 0.0 0.0
R (7) \(\to\) (16) [Kraj] 7 0.0 0.0 0.0
S (11) \(\to\) (16) [Kraj] 11 0.0 0.0 0.0
T (15) \(\to\) (16) [Kraj] 15 0.0 0.0 0.0

(Napomena: Fiktivne aktivnosti s trajanjem 0 (I, O, Q, R, S, T) su nužne u ovom slučaju pri korištenju AOA metodologijie da bi se ispravno prikazale složene ovisnosti spajanja).

Kolika je vjerojatnost da student neće ostvariti uvjet pristupa ispitu (Događaj 16) unutar zadanog roka od 15 tjedana?


22. slučaj

U vašem game-dev studiju, imate 5 ključnih programera i 5 kritičnih “show-stopper” bugova koji moraju biti riješeni prije sutrašnjeg patcha. Zbog različitih vještina (jedan je ekspert za baze, drugi za UI, treći za AI), svaki programer ima različitu procijenjenu brzinu (u satima) za rješavanje svakog buga.

Cilj:

Pridružite svakog programera točno jednom bugu tako da je ukupno utrošeno vrijeme (u satima) minimalno.

Matrica troškova (procijenjeni sati \(c_{ij}\)):

Programer (\(i\)) Bug 1 (UI Glitch) Bug 2 (Server Crash) Bug 3 (AI Pathfinding) Bug 4 (Save Corrupt) Bug 5 (Network Lag)
Ana (UI Ekspert) 2 8 9 10 7
Branko (Backend) 6 3 7 5 4
Cvita (Game AI) 9 10 2 8 9
Darko (Database) 8 5 6 3 7
Ema (Network) 7 4 8 6 2


23. slučaj

Vaša studentska udruga ima 4 dizajnera volontera. Za nadolazeću konferenciju, morate im dodijeliti 4 ključna zadatka: dizajn logotipa, dizajn web stranice, dizajn mobilne aplikacije i dizajn plakata. Nakon kratkog testiranja, svaki dizajner je dobio “ocjenu efikasnosti” (na skali od 1 do 20) za svaki od zadataka.

Cilj:

Pridružite svakog dizajnera točno jednom zadatku tako da je ukupna efikasnost tima maksimalna.

Matrica efikasnosti (\(e_{ij}\)):

Dizajner (\(i\)) Logo (j=1) Web (j=2) Aplikacija (j=3) Plakat (j=4)
Marko 18 12 10 15
Ivan 10 16 14 8
Lea 12 14 17 11
Sara 15 13 11 18


24. slučaj

Organizirate LAN party i imate 5 server-mašina (fizičkih “bladeova”) u racku. Morate na njih “pridružiti” 5 dediciranih servera za igre. Mašine nisu iste – neke imaju više RAM-a (dobro za sandbox igre), neke jači CPU (dobro za pucačine).

Vaša matrica predstavlja procijenjeni “trošak” u vidu ukupnog mrežnog laga i latencije (veći broj je lošiji) kada se određena igra vrti na određenoj mašini.

Cilj: Pridružite svaku igru točno jednoj mašini tako da je ukupni lag (trošak) sustava minimalan.

Matrica troškova (Lag/Performanse \(c_{ij}\)):

Igra (\(i\)) Mašina 1 (Jak CPU) Mašina 2 (Puno RAM-a) Mašina 3 (Brzi Disk) Mašina 4 (Balansirana) Mašina 5 (Stara)
CS:GO (FPS) 3 10 8 5 20
Minecraft (Sandbox) 12 4 9 8 22
Valheim (Sandbox) 14 5 10 9 25
League of Legends 6 9 7 6 15
Call of Duty (FPS) 4 12 9 7 18


25. slučaj

Vi ste koordinator “Dana studenata” na vašem fakultetu. Imate 5 ključnih sektora (zadataka) koje treba pokriti i 5 studentskih udruga (agenata) koje su se prijavile za volontiranje.

Nakon sastanka, svaka udruga je dala procjenu “napora” (stresa, utrošenog vremena, nekompatibilnosti) za svaki zadatak, na skali od 1 (savršeno) do 20 (katastrofa).

Cilj: Pridružiti svaku udrugu točno jednom sektoru tako da je ukupan procijenjeni napor (trošak) minimalan, ali uz poštivanje dva dodatna, stroga uvjeta.

Agenti (Udruge, \(i\)): * 1: Klub Informatičara (K.I.) * 2: Erasmus Student Network (ESN) * 3: Debatni Klub (D.K.) * 4: Sportska Udruga (S.U.) * 5: Udruga Volontera (V.U.)

Zadatci (Sektori, \(j\)): * 1: Tehnika (Audio, Video, Stream) * 2: Info Pult & Gosti (Prijava) * 3: Logistika (Stolovi, Štandovi) * 4: Program (Vođenje pozornice) * 5: Catering (Hrana, Piće)

Matrica troškova (Procijenjeni napor \(c_{ij}\)):

Udruga (\(i\)) 1: Tehnika 2: Info Pult 3: Logistika 4: Program 5: Catering
1: K.I. 2 15 18 10 12
2: ESN 12 3 10 8 9
3: D.K. 10 6 14 4 11
4: S.U. 18 10 3 15 7
5: V.U. 8 4 9 7 5

Nakon sastanka s Upravom, nametnuta su dva stroga pravila:

  1. Mora se: Zbog dolaska međunarodnih gostiju, Rektorat zahtijeva da ESN (Udruga 2) mora preuzeti Info Pult (Sektor 2), jer tečno govore strane jezike.
  2. Ne smije se: Klub Informatičara (Udruga 1) je prijavio da njihova oprema vrijedi tisuće eura i da odbijaju nositi stolove. Stoga, K.I. (Udruga 1) ne smije biti dodijeljen Logistici (Sektor 3).

Riješite problem i prikažite konačni, optimalni raspored i minimalni ukupni napor.


Sažetak


Mala mapa mrežnih modela - tipični cilj, ograničenja, dual, podaci
Model Cilj Uobičajena ograničenja Tipična interpretacija duala Tipični podaci
Transportni problem (TP) Min. trošak isporuka iz izvora u odredišta Očuvanje ponude/potražnje po čvoru Potencijali (u_i, v_j); MODI/UV metoda = reducirane cijene C + ponude s_i + potražnje d_j
Transshipment Min. trošak uz tranzitne čvorove Očuvanje toka u svim čvorovima; (mogu) kapaciteti Potencijali čvorova; reducirane cijene C + b_i (ponuda/potražnja po svim čvorovima)
Najkraći put (SP) Minimizirati zbroj troškova na ruti s→t Jedinica toka; očuvanje toka u čvorovima; x_a ∈ {0,1} ili [0,1] Potencijali čvorova; korišteni lukovi imaju nultu reduciranu cijenu Matrica troškova C (∞ za zabranjene lukove) + matrica susjedstva A ∈ {0,1}
Maksimalni tok (MF) Maksimizirati protok s→t Kapaciteti na lukovima; očuvanje toka u čvorovima Min-cut (Max-flow = Min-cut); dual = rezovi koji saturiraju kapacitete Matrica kapaciteta U + A ∈ {0,1}
Kritični put (CPM, CPM/PERT) Minimizirati trajanje projekta ≡ maksimizirati najduži put od starta do cilja u DAG-u Prethodnosne (precedence) veze; bez ciklusa; vremenski odnosi ES/EF/LS/LF; slack ≥ 0 Potencijali čvorova = vremena događaja; kritične aktivnosti imaju reduciranu cijenu/slack = 0 Trajanja aktivnosti d_a; precedence graf (AON/AOA); za PERT: a, m, b; E[d]=(a+4m+b)/6
Assignment (pridruživanje) Minimizirati trošak dodjele i↔︎j (1-1) Svaki i dodijeljen jednom; svaki j dodijeljen jednom Potencijali redaka/stupaca (u, v); Hungarian = nulte reducirane cijene Matrica troškova C (∞ za zabranjene parove)
Note:
Matrica susjedstva A je boole {0,1}; matrica troškova/težina C sadrži stvarne troškove/udaljenosti. Vrijednost “∞” je isključivo konvencija u modeliranju troškova (zabranjen luk), ne svojstvo adjacency matrice.
1 Oznake: s = izvor (source) čvor, t = ponor/odredišni (sink, terminal) čvor;
a i = indeks izvora/redaka (npr. radnik, polazište), j = indeks odredišta/stupaca (npr. zadatak, odredište).


Pitanja za ponavljanje


1. Pitanje

GPS aplikacija na vašem mobitelu treba pronaći rutu od Zagreba do Splita. Korisnik je odabrao opciju “Najbrža ruta”. Aplikacija analizira stotine segmenata cesta (čvorova i lukova), pri čemu svaki luk (cesta) ima procijenjeno vrijeme putovanja (težinu).

Koje su od ovih tvrdnji TOČNE?

2. Pitanje

IT odjel želi utvrditi maksimalnu količinu podataka (u Gbps) koju može “progurati” od glavnog servera (Izvor) do servera za backup (Ponor) kroz mrežu routera. Svaka veza (luk) između routera ima poznat maksimalni kapacitet.

Koje su od ovih tvrdnji TOČNE za modeliranje ovog problema?

3. Pitanje

U kontekstu Problema maksimalnog toka, što tvrdi temeljni “Max-Flow Min-Cut” teorem?

4. Pitanje

Voditelj projekta razvoja igre (naš “Projekt Poltergeist”) ima sljedeći plan:

  • A (GDD, 4 tjedna)
  • B (Engine, 8 tjedna), ovisi o A
  • C (Grafika, 12 tjedana), ovisi o A
  • D (Integ. Mehanika, 6 tjedana), ovisi o B
  • E (Integ. Grafike, 3 tjedna), ovisi o C
  • F (Beta Test, 5 tjedana), ovisi o D i E
  • G (Popravci, 2 tjedna), ovisi o F

Koje su od ovih tvrdnji TOČNE?

5. Pitanje

U CPM (Metodi kritičnog puta), što su TOČNE karakteristike “kritičnog puta”?

6. Pitanje

Tim procjenjuje trajanje aktivnosti “Trening AI modela”. Procjene su:

  • Optimistično (\(a\)) = 10 dana
  • Najvjerojatnije (\(m\)) = 20 dana
  • Pesimistično (\(b\)) = 30 dana

Koje su TOČNE tvrdnje prema PERT metodologiji?

7. Pitanje

Koja je ključna razlika između CPM i PERT metodologije?

8. Pitanje)

PERT analizom utvrđeno je da je očekivano trajanje vašeg projekta (\(\mu\)) 40 tjedana, a standardna devijacija projekta (\(\sigma_Z\)) 3 tjedna. Menadžment želi znati status.

Koje su TOČNE tvrdnje (koristeći aproksimaciju normalne distribucije)?

9. Pitanje

Fakultet mora dodijeliti 4 profesora (A, B, C, D) na 4 kolegija (K1, K2, K3, K4). Postoji matrica “troškova” koja predstavlja koliko sati pripreme (nešto što želimo minimizirati) treba svakom profesoru za svaki kolegij. Svaki profesor preuzima točno jedan kolegij.

Koje su od ovih tvrdnji TOČNE za modeliranje ovog problema?

10. Pitanje

Koje su TOČNE tvrdnje o “Problemu pridruživanja” (Assignment Problem)?

11. Pitanje

Imamo isti problem kao u Pitanju 9 (4 profesora, 4 kolegija), ali Uprava je dodala dva stroga uvjeta: 1. Profesor A mora preuzeti Kolegij K2. 2. Profesor B ne smije preuzeti Kolegij K3.

Kako sada rješavamo ovaj problem?

12. Pitanje

Dostavni kombi kreće iz skladišta, mora obići 6 trgovina (svaku točno jednom) i vratiti se u skladište. Vozač ima matricu procijenjenog vremena putovanja između svake dvije lokacije. Cilj je pronaći rutu (turu) koja minimizira ukupno vrijeme vožnje.

Koje su od ovih tvrdnji TOČNE za modeliranje ovog problema?

13. Pitanje)

Kada rješavamo Problem trgovačkog putnika (TSP) koji je asimetričan (ATSP) – gdje put od A do B košta drugačije od B do A – koje su tvrdnje TOČNE?

14. Pitanje

Rješavate TSP za 50 gradova. Egzaktno rješenje (pomoću LP-a) traje satima. Odlučite koristiti heuristiku “Najbliži susjed” (uvijek idi u najbliži neposjećeni grad).

Koje su TOČNE tvrdnje o ovom pristupu?

15. Pitanje

Koja je razlika između heuristike i metaheuristike?

16. Pitanje

Admin želi fizički povezati 10 servera u racku. Ima matricu troškova polaganja UTP kabela između svaka dva servera. Cilj je osigurati da su svi serveri povezani (direktno ili indirektno) u jednu mrežu, uz minimalan ukupan trošak položenih kabela.

Koje su od ovih tvrdnji TOČNE?

17. Pitanje

U kontekstu vizualizacije mreža u R-u, koje su tvrdnje TOČNE?

18. Pitanje

Analizirate LP rješenje i gledate “Sjene cijene” (Duals) za ograničenja resursa.

Koje su od ovih tvrdnji TOČNE?

19. Pitanje

Morate odabrati jedan CPU (i5, i7, i9) i jedan GPU (Nvidia, AMD) za računalo. Svaka komponenta ima cijenu i “performans” bodove. Morate ostati unutar budžeta od 1500€ i želite maksimizirati ukupne performanse.

Kako biste ovo modelirali?

20. Pitanje

Koji R paketi i funkcije su ISPRAVNO upareni sa svojim primarnim problemom?


Literatura


Aggarwal, S., & Kumar, N. (2020). Path planning techniques for unmanned aerial vehicles: A review, solutions, and challenges. Computer Communications, 149, 270-299. (u tekstu je “Aggrawal”; ostavljam ispravno “Aggarwal”)

Ahuja, R. K., Magnanti, T. L., & Orlin, J. B. (1993). Network flows: Theory, algorithms, and applications. Prentice Hall. https://doi.org/10.1287/educ.1.2.110 (pregled i citat u INFORMS Education)

Anderson, D. R., Sweeney, D. J., Williams, T. A., Camm, J. D., & Cochran, J. J. (2012). Quantitative Methods for Business. Cengage Learning.

Anderson, R. D., Dennis, J. S., Thomas, A. W., Jeffrey, D. C., & Kipp, M. (2012). An Introduction to Management Science—Quantitative Approaches to Decision Making (Revised 13th Edition).

Bazaraa, M. S., Jarvis, J. J., & Sherali, H. D. (2010). Linear programming and network flows (4th ed.). Wiley.

Bellman, R. (1958). On a routing problem. Quarterly of Applied Mathematics, 16(1), 87–90. DOI: https://doi.org/10.1090/qam/102435

Bertsimas, D., & Tsitsiklis, J. N. (1997). Introduction to linear optimization. Athena Scientific. Izdavač (opis knjige): https://www.athenasc.com/linoptbook.html

Bozorgavari, S. A., Aghaei, J., Pirouzi, S., Nikoobakht, A., Farahmand, H., & Korpås, M. (2020). Robust planning of distributed battery energy storage systems in flexible smart distribution networks: A comprehensive study. Renewable and Sustainable Energy Reviews, 123, 109739. https://doi.org/10.1016/j.rser.2020.109739

Collier, Z. A., Hendrickson, D., Polmateer, T. L., & Lambert, J. H. (2018). Scenario analysis and PERT/CPM applied to strategic investment at an automated container port. ASCE-ASME Journal of Risk and Uncertainty in Engineering Systems, Part A: Civil Engineering, 4(3), 04018026. https://doi.org/10.1061/AJRUA6.0000950

Cormen, T. H., Leiserson, C. E., Rivest, R. L., & Stein, C. (2009). Introduction to algorithms (3rd ed.). MIT Press. https://mitpress.mit.edu/9780262033848/introduction-to-algorithms/

Dascioglu, B. G., & Tuzkaya, G. (2019). A literature review for hybrid vehicle routing problem. Industrial Engineering in the Big Data Era, 249-257.

Dewangan, A., & Patel, K. (2019). A review on shortest path algorithms for road network and their applications. International Journal of Engineering and Advanced Technology, 8(6), 3708–3713. https://www.ijeat.org/wp-content/uploads/papers/v8i6/F8714088619.pdf

Dewinter, M., Vandeviver, C., Vander Beken, T., & Witlox, F. (2020). Analysing the police patrol routing problem: A review. ISPRS International Journal of Geo-Information, 9(3), 157. https://www.mdpi.com/2220-9964/9/3/157

Diestel, R. (2017). Graph theory (5th ed.). Springer. DOI: https://doi.org/10.1007/978-3-662-53622-3

Dijkstra, E. W. (1959). A note on two problems in connexion with graphs. Numerische Mathematik, 1, 269–271. DOI: https://doi.org/10.1007/BF01386390

Felipe, Á., Ortuño, M. T., Righini, G., & Tirado, G. (2014). A survey of modeling and solution approaches for the electric vehicle routing problem. Transportation Research Part B: Methodological, 64, 87–110. https://doi.org/10.1016/j.trb.2014.02.008

Floyd, R. W. (1962). Algorithm 97: Shortest path. Communications of the ACM, 5(6), 345. DOI: https://doi.org/10.1145/367766.368168

Ford, L. R., Jr., & Fulkerson, D. R. (1956). Maximal flow through a network. Canadian Journal of Mathematics, 8, 399–404. DOI: https://doi.org/10.4153/CJM-1956-045-5

Ford, L. R., Jr., & Fulkerson, D. R. (1962). Flows in networks. Princeton University Press. https://press.princeton.edu/books/hardcover/9780691640451/flows-in-networks

Garey, M. R., & Johnson, D. S. (1979). Computers and intractability: A guide to the theory of NP-completeness. W. H. Freeman. https://theory.cs.princeton.edu/complexity/

Hoffman, A. J., & Kruskal, J. B. (1956). Integral boundary points of convex polyhedra. In H. W. Kuhn & A. W. Tucker (Eds.), Linear inequalities and related systems (pp. 223–246). Princeton University Press. https://press.princeton.edu/books/paperback/9780691625335/linear-inequalities-and-related-systems

Hu, W., Ardeshiricham, A., & Kastner, R. (2021). Hardware information flow tracking. ACM Computing Surveys, 54(4), Article 81. https://doi.org/10.1145/3439819

Kelley, J. E., & Walker, M. R. (1959). Critical-Path Planning and Scheduling. Proceedings of the Eastern Joint Computer Conference (IRE-AIEE-ACM), 160–173. https://doi.org/10.1145/1460299.1460318

Koopmans, T. C., & Beckmann, M. (1957). Assignment problems and the location of economic activities. Econometrica, 25(1), 53–76. DOI: https://doi.org/10.2307/1907742

Kostelić, K., Cvek, L., Draženović, M., Drezga, L., Grbac, A., & Pajković, M. (2021). Određivanje optimalne rute u mreži turističkih atrakcija grada Pule. Oeconomica Jadertina, 11(2), 77–93.

Kruskal, J. B. (1956). On the shortest spanning subtree of a graph and the traveling salesman problem. Proceedings of the American Mathematical Society, 7(1), 48–50. DOI: https://doi.org/10.1090/S0002-9939-1956-0078686-7

Kuhn, H. W. (1955). The Hungarian method for the assignment problem. Naval Research Logistics Quarterly, 2(1-2), 83–97. DOI: https://doi.org/10.1002/nav.3800020109

Malcolm, D. G., Roseboom, J. H., Clark, C. E., & Fazar, W. (1959). Application of a technique for research and development program evaluation. Operations Research, 7(5), 646–669. https://doi.org/10.1287/opre.7.5.646

Miller, C. E., Tucker, A. W., & Zemlin, R. A. (1960). Integer programming formulation of traveling salesman problems. Journal of the ACM, 7(4), 326–329. DOI: https://doi.org/10.1145/321043.321046

Montoya, A., Guéret, C., Mendoza, J. E., & Villegas, J. G. (2017). The electric vehicle routing problem with nonlinear charging functions. Transportation Research Part B: Methodological, 103, 87–110. https://doi.org/10.1016/j.trb.2017.01.002

Nahar, J., Rusyaman, E., & Putri, S. D. V. E. (2018, March). Application of improved Vogel’s approximation method in minimization of rice distribution costs of Perum BULOG. In IOP Conference Series: Materials Science and Engineering (Vol. 332, No. 1, p. 012027). IOP Publishing.

Patil, H., & Kalkhambkar, V. N. (2020). Grid integration of electric vehicles for economic benefits: A review. Journal of Modern Power Systems and Clean Energy, 9(1), 13–26. https://link.springer.com/article/10.35833/MPCE.2019.000123

Prim, R. C. (1957). Shortest connection networks and some generalizations. Bell System Technical Journal, 36(6), 1389–1401. DOI: https://doi.org/10.1002/j.1538-7305.1957.tb01515.x

Roopa, K. M., Vishwanath, M. C., BIT, B., Srinivasu, V. K., & Umesh, V. G. Review on Shortest Route Problem in Urban Network System. (2019)

Van Steen, M. (2010). Graph theory and complex networks. An introduction, 144(1).


Ključ rješenja


  1. B, C, F
    • (B je definicija, C je algoritam, F je ispravna LP formulacija za SP; E - moguća formulacija, ali nije nužno da varijable budu zadane kao binarne)
  2. B, C, D, E
    • (B je definicija, C je očuvanje toka, D je funkcija cilja, E je teorem)
  3. D
    • (To je definicija Min-Cuta, koji je jednak Max-Flowu)
  4. A, B, C, D, E, F
    • (Sve tvrdnje su točne: A=18, B=19. Početak F je 19. Slack za A-B-D je 19-18=1. Kritični put je duži (A-C-E) plus F i G. Ukupno trajanje je 19 + 5 + 2 = 26. Kašnjenje na C (kritični) odgađa sve.)
  5. A, D
    • (A je definicija, D je posljedica. B je krivo (najduži). C je krivo (može ih biti više paralelnih). E je nebitno.)
  6. A, C, D
    • (\(t_e = (10 + 4*20 + 30) / 6 = 120 / 6 = 20\). \(\sigma = (30-10) / 6 = 20/6 \approx 3.33\).)
  7. C
    • (PERT je stohastički i koristi \(t_e\) i \(\sigma^2\) za izračun vjerojatnosti.)
  8. A, C, D, E
    • (A = 1 std dev. C = 2 std dev. D = po definiciji normalne distribucije (simetrična). E = Rok (46) je \(\mu + 2\sigma\), što obuhvaća \(\approx 97.7\%\) (sve lijevo od te točke).)
  9. B, C, D, E, F
    • (Sve osim A. B je definicija. C i D su ograničenja. E je alat. F je točan broj: 16 varijabli i (4+4)=8 ograničenja.)
  10. B, C
    • (B je standardni trik \(\max(Z) = \min(-Z)\). C je ključno svojstvo totalne unimodularnosti.)
  11. C, D, E
    • (C je nužno jer lp.assign ne prima dodatna ograničenja. D i E su ispravne formulacije tih ograničenja. F je pogrešno (postavljanje na 0 bi ga učinilo poželjnim).)
  12. B, D
    • (B je definicija. C je pod-tura i nije valjano. D je nužno za LP rješenje. E je krivo, TSP je NP-težak.)
  13. A, C
    • (A je definicija asimetrije. C je točan način korištenja TSP paketa.)
  14. B, D, E
    • (B je definicija. D je svrha heuristike. E je točno, pohlepni izbori ovise o startu.)
  15. B
    • (Heuristika je pravilo/prečica, metaheuristika je strategija/okvir.)
  16. B, C, E
    • (B je definicija. C je ključno svojstvo stabla (nema ciklusa). E su standardni algoritmi za MST.)
  17. B, C
    • (A je krivo, igraph je jak analitički alat. B i C su točni opisi tih paketa.)
  18. A, C, D
    • (A = cijena 0 znači da resurs nije usko grlo (ima ga viška). C = definicija sjene cijene u \(\max\) problemu. D = definicija sjene cijene u \(\min\) problemu (povećanje resursa smanjuje trošak).)
  19. B, C, D, E
    • (A je krivo (ne možeš kupiti 0.5 CPU-a). B je točno (problem izbora 0/1). C je točno (binarne varijable). D je točno (ograničenje “odaberi točno jedan CPU”). E je točno (ograničenje budžeta).)
  20. B, C, D, E
    • (A je krivo (lp.assign je za AP). B je točno (to je heuristika). C je točno (determ=FALSE je PERT). D je točno. E je točno (to je LP formulacija za MF). F je netočno (igraph::max_flow() u praksi koristi push–relabel/Dinic-ovu varijantu (ne “Ford-Fulkerson” per se))).