Skip to content

Grafy a grafové algoritmy

octicon-noteNote

Reprezentace grafů. Souvislost grafu, rovinné grafy. Prohledávání grafu do šířky a do hloubky, nejkratší vzdálenosti, kostry, toky v sítích. Algoritmy: Bellman-Ford, Dijkstra, Ford-Fulkerson, Push-Relabel, maximální párování v bipartitních grafech.
> IB000, IB002, IV003

octicon-tipTip

Tahle otázka má solidní překryv s bakalářskými otázkami Grafy a Grafové problémy.

  • Graf
    Dvojice G=(V,E)G = (V, E) kde:

    • VV je množina vrcholů; V=n\lvert V \rvert = n,
    • EE je množina hran; E=m\lvert E \rvert = m,
    • hrana eEe \in E je dvojice vrcholů e=(u,v)e = (u, v).
  • Váha grafu
    Váha grafu je součet vah hran grafu GG.

    w(G)=eE(G)w(e)w(G) = \sum_{e \in E(G)} w(e)
  • Bipartitní graf
    Graf jehož vrcholy lze rozdělit do dvou disjunktních množin tak, že všechny hrany vedou z jedné množiny do druhé.

    Example of bipartite graph without cycles by Watchduck

    width=400

  • (Silná) souvislost grafu / (strongly) connected graph
    Graf GG je souvislý, pokud pro každé dva vrcholy u,vV(G)u, v \in V(G) existuje cesta z uu do vv.

  • Slabá souvislost grafu / weakly connected graph
    Graf GG je slabě souvislý, pokud je souvislý jeho podgraf GG' vzniklý odebráním orientace hran.

    Je souvislý alespoň, pokud zapomeneme, že hrany mají směr?

  • Silně souvislá komponenta / strongly connected component
    Silně souvislá komponenta grafu GG je jeho maximální podgraf GG' takový, že je silně souvislý. Jinými slovy pro každé dva vrcholy u,vV(G)u, v \in V(G') existuje cesta z uu do vv.

  • Planární / rovinný graf
    Graf GG je planární, pokud se dá nakreslit do roviny tak, že se žádné dvě hrany nekříží.

    Platí v nich Eulerova formule:

    VE+F=2\lvert V \rvert - \lvert E \rvert + \lvert F \rvert = 2

    Kde F\lvert F \rvert je počet stěn — oblastí ohraničených hranami.

    Vrcholy planárního grafu lze vždy obarvit 4 barvami tak, že žádné dva sousední vrcholy nebudou mít stejnou barvu.

  • (Hranový) řez / (edge) cut
    Množina hran CE(G)C \subseteq E(G) taková, že po odebrání hran CC se graf GG rozpadne na více komponent — G=(V,EC)G' = (V, E \setminus C) není souvislý.

    Analogicky se definuje i vrcholový řez / vertex cut.

  • Seznam následníků / adjacency list
    Pro každý vrchol vVv \in V máme seznam (např. dynamic array nebo linked list) N(v)N(v) jeho následníků.

    Zabírá Θ(V+E)\Theta(\lvert V \rvert + \lvert E \rvert) paměti.

  • Matice sousednosti / adjacency matrix
    Máme matici velikosti V×V\lvert V \rvert \times \lvert V \rvert kde Au,v=1A_{u,v} = 1 pokud existuje hrana mezi uu a vv, jinak Au,v=0A_{u,v} = 0.

    Dá se pěkně použít k uložení vah.

  • Matice incidence / incidence matrix
    Máme matici velikosti V×E\lvert V \rvert \times \lvert E \rvert kde Au,e=1A_{u,e} = 1 pokud uu je vrcholem hrany ee, jinak Au,e=0A_{u,e} = 0.

    Dá se z ní pěkně určit stupeň vrcholu.

Prohlédávání do šířky / breadth-first search (BFS)

Section titled “Prohlédávání do šířky / breadth-first search (BFS)”

Od zadaného vrcholu navštíví nejprve vrcholy vzdálené 1 hranou, poté vrcholy vzdálené 2 hranami, atd.

  • Prohledávání po “vrstvách”.
  • Je implementovaný pomocí fronty (queue / FIFO).
  • Časová složitost je O(V+E)\mathcal{O}(\lvert V \rvert + \lvert E \rvert).
def dfs(graph: List[List[bool]], stamps: List[int], vertex: int) -> None:
if stamps[vertex] == -1:
stamps[vertex] = 0
stamp = stamps[vertex]
for i in range(0, len(graph)):
if graph[vertex][i] and stamps[i] != -1:
stamps[i] = stamp + 1
dfs(graph, stamps, i)

Prohlédávání do hloubky / depth-first search (DFS)

Section titled “Prohlédávání do hloubky / depth-first search (DFS)”

Od zadaného vrcholu rekurzivně navštěvuje jeho nenavštívené následníky.

  • Prohledání po “slepých uličkách”.
  • Vynořuje se teprve ve chvíli, kdy nemá kam dál (backtrackuje).
  • Je implementovaný pomocí zásobníku (stack / LIFO).
  • Časová složitost je O(V+E)\mathcal{O}(\lvert V \rvert + \lvert E \rvert).
def bfs(graph: List[List[bool]], stamps: List[int], vertex: int) -> None:
stamp = 0
queue = deque()
queue.append(vertex)
while len(queue) > 0:
current = queue.popleft()
stamps[current] = stamp
stamp += 1
for i in range(0, len(graph)):
if graph[current][i] and stamps[i] == -1:
queue.append(i)

Problém nalezení buď nejkratší cesty mezi dvěma vrcholy nebo nejkratší cesty z jednoho vrcholu do všech ostatních.

  • Relaxace hrany (u,v)(u, v)
    Zkrácení vzdálenosti k vrcholu vv průchodem přes vrchol uu. Musí platit u.distance+w(u,v)<v.distanceu\text{.distance} + w(u, v) < v\text{.distance}. Hrana (u,v)(u, v) je v takovém případě napjatá.

Hledá nejkratší cesty z jednoho vrcholu do všech ostatních.

  • Využívá relaxaci hran.
  • Funguje i na grafech se zápornými hranami.
  • Má časovou složitost O(VE)\mathcal{O}(\lvert V \rvert \cdot \lvert E \rvert).
  • Udělá max V1V-1 iterací, protože nejdelší cesta může mít max V1V-1 hran (přes žádný vertex nepůjdeš dvakrát).
  • Modifikace: Po V1V-1 by už nejkratší cesty měly být nalezeny. Udělejme ještě jednu iteraci - pokud se hodnoty změní, pak nejkratší cesta obsahuje negativní cyklus. Viz python kód dole.
  • Modifikace: Early stop - pokud se 2 interace po sobě hodnoty vrcholů nezmění, pak jsme našli nejkratší cesty.
def bellmanford(graph: List[List[Tuple[int, int]]], s: int) \
-> Tuple[bool, List[int], List[int]]:
# graph is an adjacency list of tuples (dst, weight)
distance = [float('inf') for i in range(0, len(graph))]
distance[s] = 0
parent = [-1 for i in range(0, len(graph))]
# relax all edges |V| - 1 times
for _ in range(1, len(graph)):
for u in range(0, len(graph)):
for edge in graph[u]:
(v, w) = edge
if distance[u] + w < distance[v]:
distance[v] = distance[u] + w
parent[v] = u
# check for negative cycles
for u in range(0, len(graph)):
for edge in graph[u]:
(v, w) = edge
if distance[u] + w < distance[v]:
return (False, None, None)
return (True, distance, parent)

Hledá nejkratší cesty z jednoho vrcholu do všech ostatních.

  • Je podobný BFS, ale používá prioritní frontu.
  • Funguje pouze na grafech bez záporných hran.

octicon-tipTip

Složitost závisí na implementaci prioritní fronty. Je to Θ(V)\Theta(V) insertů, Θ(V)\Theta(V) hledání nejmenšího prvku, Θ(E)\Theta(E) snížení priority.

octicon-noteNote

Implementace níže používá pole (resp. Pythoní list), tedy složitost je Θ(V2)\Theta(V^2), jelikož hledání minima je lineární.

def dijkstra(graph: List[List[Tuple[int, int]]], s: int) \
-> Tuple[List[int], List[int]]:
# graph is an adjacency list of tuples (dst, weight)
distance = [float('inf') for i in range(0, len(graph))]
distance[s] = 0
parent = [-1 for i in range(0, len(graph))]
queue = list(range(0, len(graph)))
while len(queue) > 0:
u = min(queue, lambda v: distance[v])
queue.remove(u)
for edge in graph[current]:
(v, w) = edge
if distance[u] + w < distance[v]:
distance[v] = distance[u] + w
parent[v] = u
return (distance, parent)

V binární haldě by to bylo Θ(VlogV+ElogV)\Theta(V \log V + E \log V) a ve Fibonacciho haldě Θ(VlogV+E)\Theta(V \log V + E).

Dijkstrův algoritmus lze optimalizovat, pokud nás zajímá jen nejkratší cesta mezi dvěma konkrétními vrcholy:

  • Funkce vrátí výsledek, jakmile je cílový vrchol vytažen z fronty.

  • Můžeme hledat zároveň ze začátku a konce pomocí dvou front a skončit, jakmile se někde potkají.

  • Můžeme přidat potenciál — dodatečnou heuristickou váhu.

    octicon-importantImportant

    Téhle variantě se říká A* (A star). Věnuje se mu část otázky Umělá inteligence v počítačových hrách.

  • Spanning tree / kostra
    Kostra grafu G=(V,E)G = (V, E) je podgraf TGT \sube G takový, že V(T)=V(G)V(T) = V(G) je TT je strom.

    width=400

  • Minimum spanning tree (MST) / minimální kostra
    Je kostra MM grafu GG s nejmenší možnou váhou. Tedy pro každou kostru TT grafu GG:

    w(M)w(T)w(M) \le w(T)
  • Fundamental cycle
    Fundamental cycle je cyklus CC v grafu GG takový, že odebráním libovolné hrany eCe \in C získáme kostru.

  • Fundamental cutset / řez
    Fundamental cutset je množina hran DD v grafu GG taková, že přidáním libovolné hrany eDe \in D získáme kostru.

  • Red rule
    Najdi cyklus bez červených hran, vyber v něm neobarvenou hranu s nejvyšší cenou a obarvi ji červeně. Červená hrana znamená odebrání z finální kostry.

  • Blue rule
    Najdi řez bez modrých hran, vyber v něm neobarvenou hranu s nejmenší cenou a obarvi ji modře. Modrá hrana znamená přidání do finální kostry.

  • Greedy algoritmus
    Nedeterministicky aplikuj red rule a blue rule, dokud to jde (stačí n1n-1 iterací). Modré hrany tvoří MST.

  • Jarníkův / Primův algoritmus
    Speciální případ greedy algoritmu, kdy aplikujeme pouze blue rule. Princip:

    1. Vyber libovolný vrchol vv a přidej ho do kostry SS.
    2. Opakuj n1n-1 krát:
      1. Vyber hranu ee s nejmenší cenou, která má právě jeden vrchol v SS.
      2. Přidej druhý vrchol ee do SS.

    Složitost: použijeme binární haldu

    • Inicializace (\infty jako cena hrany mezi prázdnou kostrou a každým vrcholem): O(V)\mathcal{O}( \lvert V \rvert )
    • Odstranění minima z binární haldy pro každý vrchol ve VV: O(VlogV)\mathcal{O}( \lvert V \rvert \log \lvert V \rvert )
    • Procházení každé hrany z EE a snižování ceny: O(ElogV)\mathcal{O}( \lvert E \rvert \log \lvert V \rvert )
    • Celková složitost: O(ElogV)\mathcal{O}( \lvert E \rvert \log \lvert V \rvert )
    • S Fibonacciho haldou jde zlepšit na: O(E+VlogV)\mathcal{O}( \lvert E \rvert + \lvert V \rvert \log \lvert V \rvert )
  • Kruskalův algoritmus
    Princip: Seřaď hrany podle ceny vzestupně. Postupně přidávej hrany do kostry, vynechej ty, které by vytvořily cyklus.

    1. Seřad hrany podle ceny vzestupně.
    2. Použij union-find na udržování komponent grafu.
    3. Procházej hrany postupně. Pokud oba konce hrany patří do různých množin, přidej ji.

    Je to speciální případ greedy algoritmu.

    Složitost:

    • Inicializace union-findu: O(V)\mathcal{O}( \lvert V \rvert )
    • Seřazení hran: O(ElogE)\mathcal{O}( \lvert E \rvert \log \lvert E \rvert )
    • Pro každou hranu provádíme dvakrát find (O(logV)\mathcal{O}(\log \lvert V \rvert )) a eventuálně union (O(logV)\mathcal{O}(\log \lvert V \rvert )): O(ElogV)\mathcal{O}( \lvert E \rvert \log \lvert V \rvert )
    • Celková složitost: O(ElogV)\mathcal{O}( \lvert E \rvert \log \lvert V \rvert )
  • Borůvkův algoritmus
    Je “paralelní”. Buduje modré stromy ze všech vrcholů naráz.

    1. Pro každý vrchol inicializuj modrý strom.
    2. Dokud nemáš jen jeden modrý strom, opakuj fázi:
      1. Pro každý modrý strom najdi nejlevnější odchozí hranu a přidej ji (propojíš tak dva stromy).

    Je to speciální případ greedy algoritmu, který spamuje jen blue rule.

    Složitost:

    • Počet komponent v první fázi: V\lvert V \rvert.
    • V každé fázi se zmenší počet komponent na polovin. Tím pádem bude logV\log \lvert V \rvert fází.
    • Každá fáze zabere O(E)\mathcal{O}( \lvert E \rvert ) času, protože procházíme všechny hrany.
    • Celková složitost: O(ElogV)\mathcal{O}( \lvert E \rvert \log \lvert V \rvert )

    octicon-tipTip

    Kruskal sice taky buduje stromy na více místech najednou, ale není “paralelní”, protože minimalita kostry spoléhá na to, že hrany jsou seřazené. Borůvka takový požadavek nemá, a proto je paralelizovatelnější.

Složitosti algoritmů

AlgoritmusČasová složitostProstorová složitost
Jarník (Prim) s prioritní frontouO(ElogV)\mathcal{O}(\lvert E \rvert \log \lvert V \rvert )O(V)\mathcal{O}( \lvert V \rvert )
Jarník (Prim) s Fibonacciho haldouO(E+VlogV)\mathcal{O}(\lvert E \rvert + \lvert V \rvert \log \lvert V \rvert )O(V)\mathcal{O}( \lvert V \rvert )
KruskalO(ElogV)\mathcal{O}(\lvert E \rvert \log \lvert V \rvert )O(V)\mathcal{O}( \lvert V \rvert )
BorůvkaO(ElogV)\mathcal{O}(\lvert E \rvert \log \lvert V \rvert )O(V)\mathcal{O}( \lvert V \rvert )
  • Síť toků / flow network
    Je čtveřice (G,s,t,c)(G, s, t, c), kde:

    • G=(V,E)G = (V, E) je orientovaný graf,
    • sVs \in V je zdroj / source,
    • tVt \in V je cíl / stok / sink; sts \neq t,
    • c:ER+c: E \rightarrow \mathbb{R}^+ je funkce udávající kapacitu hran.
  • Network flow / tok
    Je funkce f:ER+f: E \rightarrow \mathbb{R}^+, která splňuje:

    • podmínku kapacity: (eE)(f(e)0f(e)c(e))(\forall e \in E)(f(e) \ge 0 \land f(e) \leq c(e))
      • tok hranou je nezáporný a nepřevyšuje povolenou kapacitu
    • podmínku kontinuity: (vV{s,t})(eδ+(v)f(e)=eδ(v)f(e))(\forall v \in V \setminus \{s, t\})(\sum_{e \in \delta^+(v)} f(e) = \sum_{e \in \delta^-(v)} f(e))
      • tok do vrcholu je stejný jako tok z vrcholu
  • Hodnota toku ff

    f=(s,v)Ef(s,v)=(w,t)Ef(w,t)\lvert f \rvert = \sum_{(s, v) \in E} f(s, v) = \sum_{(w, t) \in E} f(w, t)
  • Residual network
    Síť, která vzniká, když je už část kapacity hrany využívána tokem ff. Umožnuje algoritmům změnit přechozí rozhodnutí a získat využitou kapacitu zpět.

    Je to pětice Gf=(V,Ef,s,t,cf)G_f = (V, E_f, s, t, c_f), kde

    • Ef={eE:f(e)<c(e)}{eR:f(e)>0}E_f = \{ e \in E : f(e) < c(e) \} \cup \{ e^R : f(e) > 0 \},
    • pokud e=(u,v)Ee = (u, v) \in E, eR=(v,u)e^R = (v, u),
    • cf(e)={c(e)f(e) pokud eEf(e) pokud eREc_f(e) = \begin{cases} c(e) - f(e) & \text{ pokud } e \in E \\ f(e) & \text{ pokud } e^R \in E \end{cases}
  • Augmenting path PP
    Jednoduchá sts \rightsquigarrow t cesta v residuální síti GfG_f. Cesty hledáš buď BFS nebo DFS - každou iteraci si prostě nějakou vybereš.

    octicon-noteNote

    T.j. cesta která může jít i proti směru toku ff.

    Bottleneck kapacita je nejmenší kapacita hran v augmenting path PP.

    To krásné na augmenting cestách je, že pro flow ff a augmenting path PP v grafu GfG_f, existuje tok ff' takový, že val(f)=val(f)+bottleneck(Gf,P)\text{val}(f') = \text{val}(f) + \text{bottleneck}(G_f, P). Nový tok ff' lze získat takto:

    *Augment*(f, c, P)
    {
    delta = bottleneck(P)
    *foreach*(e in P)
    {
    *if*(e in E)
    {
    f[e] = f[e] + delta
    }
    *else*
    {
    f[reverse(e)] = f[reverse(e)] - delta
    }
    }
    *return* f
    }
  • Algoritmus Ford-Fulkerson
    Hledá maximální tok. Augmentuje cestu v residuální síti dokud to jde.

    1. f(e)=0f(e) = 0 pro každou eEe \in E.
    2. Najdi sts \rightsquigarrow t cestu PP v reziduální síti GfG_f.
    3. Augmentuj tok podél PP.
    4. Opakuj dokud se nezasekneš.
    *Ford-Fulkerson*(G)
    {
    *foreach* (e in E)
    {
    f(e) = 0
    }
    G_f = reziduální síť vzniklá z G vzhledem k toku f
    *while* (existuje s ~> t cesta v G_f)
    {
    f = Augment(f, c, P)
    Updatuj G_f
    }
    *return* f
    }
  • Pre-flow
    Ne-tak-docela tok.

    Funkce ff taková, že

    • platí kapacitní podmínka: (eE)(0f(e)c(e))(\forall e \in E)(0 \le f(e) \le c(e)),
    • platí relaxováné zachování toku: (vV{s,t})(e do vf(e)e ven z vf(e))(\forall v \in V - \{ s, t \})(\sum_{e \text{ do } v} f(e) \ge \sum_{e \text{ ven z } v} f(e))
  • Overflowing vertex
    Takový vertex vV{s,t}v \in V - \{ s, t \}, do kterého více přitéká než odtéká.

    e do vf(e)>e ven z vf(e)\sum_{e \text{ do } v} f(e) > \sum_{e \text{ ven z } v} f(e)
  • Excess flow
    To, co je v overflowing vertexu navíc.

    ef(v)=e do vf(e)e ven z vf(e)e_f(v) = \sum_{e \text{ do } v} f(e) - \sum_{e \text{ ven z } v} f(e)
  • Height function
    Funkce h:VN0h : V \to \N_0. Řekneme, že hh je kompatibilní s preflow ff, právě když

    • source: h(s)=V=nh(s) = |V| = n,

    • sink: h(t)=0h(t) = 0,

    • height difference: ((v,w)EGf)(h(v)h(w)+1)(\forall (v, w) \in E_{G_f})(h(v) \le h(w) + 1).

      octicon-noteNote

      Pokud mezi dvěma vrcholy (v,w)(v, w) v reziduální síti vede hrana, pak je vv nejvýše o jednu úroveň výš než ww.

  • Push operace
    Pro (reziduálně-grafovou) hranu (v,w)(v, w) se pokusí přesunout excess flow z vv do ww, aniž by porušil (reziduální) kapacitu (v,w)(v, w).

    // Assumptions: e_f[v] > 0, c_f( (v, w) > 0) > 0, h[v] > h[w]
    *Push*(f, h, v, w)
    {
    delta_f = min(e_f[v], c_f(v, w))
    *if*( (v, w) in E)
    f[(v, w)] += delta_f
    *else*
    f[(w, v)] -= delta_f
    e_f[v] -= delta_f
    e_f[w] += delta_f
    }
  • Relabel operace
    Zvýší výšku h(v)h(v) natolik, aby neporušil kompatibilitu hh s ff.

    // Assumptions:
    // - v is overflowing: e_f[v] > 0
    // - all residual neighbors of v the same height or higher:
    // forall (v, w) in E_f: h[v] \<= h[w]
    *Relabel*(f, h, v)
    {
    h[v] = 1 + min(h[w] | (v, w) in E_f)
    }
  • Algoritmus Push-Relabel (Goldberg-Tarjan)
    Hledá maximální tok.

    Princip: Pokud je nějaký vrchol overflowing, tak ho pushni nebo relabeluj. Pokud ne, tak jsi našel maximální tok.

    *Push-Relabel*(V, E, s, t, c)
    {
    // initialize preflow -- default values
    *for*(v in V)
    {
    h[v] = 0 // height function
    e_f[v] = 0 // excess flow
    }
    n = |V|
    h[s] = n
    *for*(e in E)
    {
    f[e] = 0 // (pre)flow
    }
    // initialize preflow -- saturate connections from s
    *for*( (s, v) in E)
    {
    f[(s, v)] = c(s, v) // preflow maxes out all capacity
    e_f[v] = c(s, v) // presume all of it excess
    e_f[s] -= c(s, v) // yes, it will be negative
    }
    // the juicy part
    *while*(_any vertex is overflowing_)
    {
    v = _an overflowing vertex_ (has e_f[v] > 0)
    *if*(v _has a neighbor_ w _in_ G_f _such that_ h(v) > h(w))
    {
    *Push*(f, h, v, w)
    }
    else
    {
    *Relabel*(f, h, v)
    }
    }
    *return* f
    }

    Korektnost: V průběhu výpočtu platí:

    • Výška vrcholu nikdy neklesá.
    • Pre-flow a výšková funkce jsou kompatibilní.

    Složitost:

    • Nejvýše 2n2^n Relabelů.
    • 2nm2nm saturujících Push.
    • 4n2m4n^2m nesaturujících Push.
    • Relabel i Push jsou v O(1)\mathcal{O}(1).
    • Celkem: O(n2m)O(n^2m).

Srovnání algoritmů Ford-Fulkerson a Push-Relabel

Ford-FulkersonPush-Relabel (Goldberg)
global characterlocal character
update flow along an augmenting pathupdate flow on edges
flow conservationpreflow

Maximální párování v bipartitních grafech

Section titled “Maximální párování v bipartitních grafech”
  • Párování / matching
    Množina MEM \sube E taková, že žádné dvě hrany v MM nemají společný vrchol. 1

    Prázdná množina je párováním na každém grafu. Graf bez hran má pouze prázdné párování.

    Příklad párování, které je zároveň maximální by RRPPGG

    width=400

  • Maximální párování
    Takové párování, které má nejvyšší počet hran. Graf může mít několik maximálních párování.

  • Perfektní párování
    Takové párování, které páruje všechny vrcholy grafu. Každé perfektní párování je zároveň maximální.

  • Maximum cardinality matching (MCM) v bipartitním grafu
    Problém nalezení maximálního párování v grafu. Ve speciálním případě, kdy graf je bipartitní, se tento problém dá převést na problém nalezení maximálního toku v síti: 2

    1. Mejmě bipartitní graf G=(X+Y,E)G=(X+Y,E).

      width=150

    2. Přidej zdroj ss a hranu (s,v)(s, v) pro každý vrchol vv z XX.

    3. Přidej stok tt a ranu (v,t)(v, t) pro každý vrchol vv z YY.

      width=300

    4. Každé hraně dej kapacitu 1.

    5. Spusť algoritmus Ford-Fulkerson.

      width=300

  1. Wikipedia: Párování grafu

  2. Wikipedia: Maximum cardinality matching