Základní pojmy teorie grafů & grafové algoritmy
Definice grafů, procházení do šířky, komponenty, topologické uspořádání, minimální kostra, Dijkstrův algoritmus.
🔵 Co je graf?
Proč to tak je? Grafy modelují reálné vztahy: mapy ulic (vrcholy = křižovatky, hrany = ulice), sítě přátel, závislosti modulů při kompilaci, schémata zapojení.
Klíčová terminologie
- Stupeň vrcholu deg(v) = počet hran obsahujících v; v orientovaném grafu rozlišujeme vstupní a výstupní stupeň.
- Sousedé N(v) = množina vrcholů spojených s v hranou. deg(v) = |N(v)|.
- Princip sudosti: Σ deg(v) = 2|E| – každá hrana přispěje ke stupni dvou vrcholů.
- Izolovaný vrchol: deg(v) = 0, nemá žádné sousedy.
- Regulární graf: všechny vrcholy mají stejný stupeň.
Speciální grafy
- Úplný graf Kn: každé dva vrcholy jsou spojeny hranou. |E| = n(n-1)/2.
- Bipartitní graf: vrcholy dělíme na dvě množiny A, B; hrany vedou pouze mezi A a B, nikdy uvnitř A nebo B. Úplný bipartitní Kn1,n2 má n1·n2 hran.
- Cesta Pn: posloupnost vrcholů v1, v2, …, vn bez opakování.
- Kružnice Cn: uzavřená cesta, n ≥ 3.
- Klika: podmnožina vrcholů, kde každé dva jsou sousední.
Seznam sousedů: Pro každý vrchol seznam jeho sousedů. Paměť O(n+m). Výpis sousedů O(deg(v)). Preferovaná reprezentace pro řídké grafy!
🛤️ Sled, cesta, vzdálenost
Cesta je sled, kde se neopakují vrcholy (ani hrany).
Vzdálenost d(s, t) = délka nejkratší s-t-cesty (počet hran). Pokud cesta neexistuje, d = ∞.
Klíčové pozorování: Existuje-li sled z u do v, existuje i cesta (zkrácením cyklů).
🌲 Strom, les, kostra
Charakterizace stromů – pro graf G = (V, E) jsou ekvivalentní:
- G je strom (acyklický + souvislý).
- Pro každé dva vrcholy existuje právě jedna cesta.
- G je souvislý a odebrání libovolné hrany ho odpojí.
- G je souvislý a |E| = |V| − 1.
Věta o existenci listů: Každý strom s ≥ 2 vrcholy má alespoň 2 listy. (Důkaz: nejdelší cesta má listové konce.)
Věta o trhání listů: Strom G − v (po odebrání listu) je opět strom.
🌊 BFS – Prohledávání do šířky
Motivace: Najít nejkratší cestu v neohodnoceném grafu (všechny hrany mají délku 1). Myšlenka: vlnový algoritmus – postupně „zaplavujeme" vrcholy ve vzdálenosti 0, 1, 2, …
Výstup: Pole vzdáleností D (D[v] = délka nejkratší s-v-cesty), pole předchůdců P (P[v] = předchůdce v na nejkratší cestě).
Klíčové vlastnosti BFS
- Stavy vrcholů: nenalezený → otevřený (v Q) → uzavřený. Každý vrchol přejde nejvýše jednou.
- BFS hladiny Hi: H0 = {s}, Hi = vrcholy otevřené ve fázi Fi-1. Vrcholy v Hi mají vzdálenost i od s.
- Správnost: Po skončení D[v] = d(s,v) pro všechny dosažitelné vrcholy. Klíčový argument: dva sousední vrcholy leží v sousedních hladinách – nemohou přeskočit.
- Časová složitost: O(|V| + |E|) při reprezentaci seznamem sousedů. Každý vrchol vyjmeme z Q jednou, každou hranu procházíme dvakrát (jednou z každé strany).
BFS pro celý graf (zjišťování komponent): Opakovaně spouštíme BFS z nových nenalezených vrcholů. Komplexita celku zůstává O(|V| + |E|).
BFS na orientovaném grafu: Totéž, ale procházíme pouze hrany ve směru (následníky, ne předchůdce).
🧩 Souvislost a komponenty
Souvislá komponenta: maximální (v inkluzi) souvislý podgraf. Binární relace „existuje cesta" je ekvivalence – třídy jsou komponenty.
Graf je souvislý právě tehdy, když má jedinou komponentu. BFS spuštěný z vrcholu s uzavře přesně komponentu obsahující s.
Slabá a silná souvislost orientovaných grafů
- Slabě souvislý: symetrizace (ignorování orientace hran) je souvislá.
- Silně souvislý: pro každé u, v existuje orient. cesta z u do v i z v do u. Silnou souvislost lze otestovat pomocí DFS/BFS (viz BI-AG2).
DFS – Prohledávání do hloubky
Alternativa k BFS, přirozeně rekurzivní. Neudává nejkratší cesty, ale je elegantní pro detekci cyklů a topologické uspořádání.
Časová složitost DFS: O(|V| + |E|). Používá zásobník (rekurzi nebo explicitní zásobník).
📋 Topologické uspořádání (TopSort)
Motivace: Nástroj make – pořadí kompilace modulů podle závislostí. Pokud A závisí na B, musíme B zkompilovat dříve než A.
Existence TU: Orientovaný graf má TU právě tehdy, když je acyklický (DAG – Directed Acyclic Graph). Pokud obsahuje orientovanou kružnici, TU neexistuje.
Zdroj: vrchol s nulovým vstupním stupněm (žádná hrana nevede dovnitř). Stok: vrchol s nulovým výstupním stupněm.
Věta: Každý DAG obsahuje alespoň jeden zdroj a jeden stok. (Důkaz sporem: bez zdroje jdeme zpět po hranách, nutně narazíme na cyklus.)
Algoritmus TopSort (Kahnův algoritmus)
Časová složitost: O(|V| + |E|) – inicializace O(E), vkládání do Q O(V), zpracování hran O(E).
Alternativa přes DFS: TU = vrcholy v pořadí klesajícího uzavírací doby DFS.
🌳 Minimální kostra (MST)
Minimální kostra (MST – Minimum Spanning Tree): kostra s nejmenší celkovou váhou hran.
Motivace: Propojit síťové uzly s minimálním celkovým kabelem; silniční sítě; návrh elektrických obvodů.
Klíčový nástroj: Elementární řez
Elementární řez R určený podmnožinou A ⊆ V: množina všech hran s jedním vrcholem v A a druhým v V\A.
Jarníkův (Primův) algoritmus
Myšlenka (hladový přístup): Začni s jedním vrcholem, postupně přidávej nejlehčí hranu vedoucí z dosud vytvořeného stromu do zbytku grafu.
Proč funguje: V každém kroku přidáváme nejlehčí hranu elementárního řezu (T vs. V\T) → vždy leží v MST. Indukčně: celý strom je podgraf MST → je MST.
| Implementace | Časová složitost |
|---|---|
| Naivní (průchod všech hran) | O(|V|·|E|) |
| S binární haldou (jako Dijkstra) | O(|E| log |V|) |
| S Fibonacciho haldou | O(|E| + |V| log |V|) |
Jarník s binární haldou: Udržuj haldu vrcholů s klíčem d[v] = váha nejlehčí hrany z T do v. Při přidání vrcholu přepoč klíče sousedů (DecreaseKey). Složitost: O(|E| log |V|).
Kruskalův algoritmus
Myšlenka: Seřaď hrany vzestupně, postupně přidávej – přeskakuj hrany, které by vytvořily cyklus.
Proč funguje: Každá přidaná hrana je nejlehčí v elementárním řezu oddělujícím její komponentu od zbytku – leží v MST.
Union-Find (Disjoint Set Union): Struktura pro správu disjunktních množin. Operace Init, Find, Union. Keříková implementace: Find = jdi do kořene, Union = připoj mělčí pod hlubší. Hloubka keříku ≤ log n → Find/Union trvá O(log n). Celková složitost Kruskala: O(|E| log |V|).
⚡ Dijkstrův algoritmus – nejkratší cesty v ohodnoceném grafu
Motivace: GPS navigace, routování síťových paketů, sociální sítě (6 stupňů odloučení).
Proč nestačí BFS? BFS předpokládá jednotkové délky. U ohodnocených hran je nejkratší cesta hledána podle součtu vah, ne počtu hran.
Klíčová myšlenka (relaxace): Udržuj pro každý vrchol w odhad h[w] vzdálenosti od v0. Při uzavření vrcholu v aktualizuj odhady sousedů: h[w] = min(h[w], h[v] + ℓ(v,w)).
Čtyři klíčové vlastnosti (invarianty) Dijkstry
- Vlastnost O (ohodnocení): h[v] vždy odpovídá délce nějakého sledu z v0 do v; nikdy neroste.
- Vlastnost M (monotonie): h[uzavřený] ≤ h[otevřený]. Uzavřený vrchol se neotevře znovu – proto kladné délky hran jsou nutné!
- Vlastnost D (dosažitelnost): Po skončení jsou uzavřené právě dosažitelné vrcholy.
- Vlastnost V (vzdálenost): Po skončení h[v] = d(v0, v) pro všechny v.
| Implementace | Celková složitost |
|---|---|
| Pole (hledáme min ručně) | O(|V|²) |
| Binární halda (ExtractMin + DecreaseKey) | O(|E| log |V|) |
| Fibonacciho halda | O(|E| + |V| log |V|) |
Dijkstra s binární haldou: Vlož všechny vrcholy do haldy s klíčem h. ExtractMin vybere v s nejmenším h. DecreaseKey aktualizuje klíče sousedů. O(|V|) ExtractMin + O(|E|) DecreaseKey = O(|E| log |V|).
Vztah k Jarníkovi: Oba algoritmy jsou velmi podobné! Jarník přidává nejlehčí hranu do stromu; Dijkstra přidává nejbližší vrchol podle kumulativní vzdálenosti. Implementace pomocí haldy je identická, liší se jen klíč.
📝 Shrnutí Okruhu 1
- Graf = (V, E); neorientovaný má neuspořádané hrany, orientovaný uspořádané. Strom = acyklický + souvislý, |E| = |V|-1.
- BFS hledá nejkratší cesty v neohodnoceném grafu v čase O(V+E). Hladiny = vzdálenosti od startu.
- TopSort existuje iff G je DAG; Kahnův algoritmus odebírá zdroje v čase O(V+E).
- MST: Jarník (Prim) i Kruskal dávají O(E log V) pomocí haldy resp. Union-Find. Oba jsou hladové algoritmy korektní díky lemmatu o řezech.
- Dijkstra: nejkratší cesty v ohodnoceném grafu s kladnými délkami; O(E log V) s binární haldou. Relaxace = aktualizace odhadu vzdálenosti.
❓ Kontrolní otázky ke státnicím (Okruh 1)
Binární haldy, binomiální haldy, BVS, AVL, hešování
Datové struktury pro efektivní správu dynamických množin: prioritní fronty, vyvážené stromy, rozptylovací tabulky.
🏔️ Binární minimová halda
(1) Tvar haldy: Všechny hladiny jsou plné kromě poslední, která je zaplněna zleva.
(2) Haldové uspořádání: Klíč každého vrcholu ≤ klíče jeho synů.
Důsledek: Kořen vždy obsahuje globální minimum. Na každé cestě z vrcholu do kořene jsou klíče nerostoucí.
Podpora operací a jejich složitosti
| Operace | Složitost | Princip |
|---|---|---|
| HeapFindMin | O(1) | Vrátí klíč kořene |
| HeapInsert | O(log n) | Vlož jako nový list, BubbleUp |
| HeapExtractMin | O(log n) | Odstraň kořen, nahraď posledním listem, BubbleDown |
| HeapBuild | O(n) | BubbleDown od ⌊n/2⌋ dolů |
| HeapDecreaseKey | O(log n) | Sniž klíč, BubbleUp |
HeapInsert (BubbleUp)
Přidej nový vrchol na první volnou pozici v poslední hladině. Pokud je klíč menší než klíč otce, prohoď je a pokračuj výše – až do kořene nebo do chvíle, kdy haldové uspořádání platí.
HeapExtractMin (BubbleDown)
Prohoď klíč kořene s klíčem posledního listu (H.last). Odstraň poslední list. BubbleDown: opakovaně prohoď klíč vrcholu s menším ze synů, dokud je to potřeba.
HeapBuild v čase O(n)
Naivní přístup: n volání HeapInsert = O(n log n). Lepší: vlož prvky do pole libovolně, pak od ⌊n/2⌋ do 1 volej BubbleDown. Listy (⌈n/2⌉ jich) jsou triviální haldy. Analýza: součet prací BubbleDown přes všechny hladiny = O(n) (geometrická řada Σ i/2ⁱ konverguje).
Reprezentace v poli
Číslujeme vrcholy po hladinách (kořen = 1). Vrchol s indexem i: levý syn = 2i, pravý syn = 2i+1, otec = ⌊i/2⌋. Tím halda = jednoduché pole bez ukazatelů!
Aplikace: HeapSort
HeapBuild(pole) v O(n) + n×ExtractMin v O(n log n) = O(n log n) celkem. Je in-place (výsledky zapisujeme do uvolněné části pole). Nevýhoda: špatná cache lokalita oproti QuickSortu.
🔗 Binomiální halda
Motivace: Binární halda neumí efektivně slučovat dvě haldy. Binomiální halda přidává operaci BHMerge v O(log n).
Vlastnosti Bk: Počet vrcholů = 2k. Počet hladin = k+1. Stupeň kořene = k. Počet vrcholů na i-té hladině = C(k,i) (binomiální koeficient – proto název).
– Každý strom Ti splňuje haldové uspořádání.
– Každý řád k se vyskytuje nejvýše jednou.
– Strukturu řádů určuje binární zápis čísla n: Bk ∈ BH iff k-tý bit n je 1.
Operace BHMergeTree
Sloučení dvou stromů stejného řádu k → strom řádu k+1 v O(1): kořen s menším klíčem se stane kořenem výsledku, druhý kořen se připojí jako jeho nejpravější syn. Zachovává haldové uspořádání.
Operace BHMerge
Simulujeme binární sčítání čísel a a b: procházíme řády od nejnižšího, slučujeme stromy stejného řádu (přenos), výsledné stromy tvoří novou BH. Časová složitost O(log n).
| Operace | BH složitost | Binární halda |
|---|---|---|
| Insert | O*(1), O(log n) worst | O(log n) |
| FindMin | O(1) s ukazatelem | O(1) |
| ExtractMin | O(log n) | O(log n) |
| Merge dvou hald | O(log n) ✓ | O(n) (HeapBuild) |
| DecreaseKey | O(log n) | O(log n) |
| Delete | O(log n) | O(log n) |
BHExtractMin
Najdi strom T s minimálním kořenem (O(log n)). Odtrhni kořen T. Synové kořene (stromy B0, B1, …, Bk-1) tvoří novou BH H'. Proveď BHMerge(H, H'). Celkem O(log n).
Paměťová reprezentace
Každý prvek si pamatuje: klíč, ukazatel na otce, ukazatel na levého sourozence, ukazatel na nejpravějšího syna. Stromy jsou uloženy ve spojovém seznamu podle řádu.
🔍 Binární vyhledávací stromy (BVS)
– Všechny klíče v levém podstromu L(v) jsou < k(v).
– Všechny klíče v pravém podstromu R(v) jsou > k(v).
Klíčová vlastnost: InOrder průchod (Levý podstrom → Kořen → Pravý podstrom) dává vzestupně seřazenou posloupnost.
Operace BVS
| Operace | Princip | Složitost |
|---|---|---|
| BVSFind(v, x) | Porovnej x s k(v), jdi do L nebo R podstromu | O(h) |
| BVSMin(v) | Jdi vždy doleva, až na list | O(h) |
| BVSMax(v) | Jdi vždy doprava | O(h) |
| BVSPred(v, w) | Max(L(w)) nebo první předek, kam vstoupíme zprava | O(h) |
| BVSInsert(v, x) | Find → vlož jako list na nalezenou pozici | O(h) |
| BVSDelete(v, x) | 3 případy: list, 1 syn, 2 synové (nahraď následníkem) | O(h) |
Kde h = hloubka stromu. V nejlepším případě h = O(log n), v nejhorším (seřazené vkládání) h = O(n).
Mazání vrcholu se dvěma syny
Nachycení u se dvěma syny: Najdi in-order následník w = BVSMin(R(u)). w má nejvýše jednoho syna. Nahraď k(u) klíčem k(w) a odstraň w (případ 1 nebo 2).
Proč nestačí dokonalé vyvážení?
Dokonale vyvážený BVS má h = O(log n), ale udržet ho po každém Insert/Delete je drahé – příkladem jsou střídající se Insert/Delete operace, které nutí přeorganizovat Ω(n) vrcholů v nejhorším případě.
⚖️ AVL stromy – hloubkové vyvážení
Znaménko vrcholu δ(v) = h(R(v)) − h(L(v)) ∈ {-1, 0, +1}.
Rotace pro obnovení vyváženosti
Rotace mění tvar stromu ale zachovávají vlastnost BVS (InOrder pořadí klíčů).
| Rotace | Kdy použít | Výsledek |
|---|---|---|
| Jednoduchá doprava (R) | δ(x) = -2, δ(y=ℓ(x)) = -1 | δ(x) = δ(y) = 0 |
| Jednoduchá doleva (L) | δ(x) = +2, δ(y=r(x)) = +1 | δ(x) = δ(y) = 0 |
| Dvojitá LR | δ(x) = -2, δ(y=ℓ(x)) = +1 | Překoření za z=r(y) |
| Dvojitá RL | δ(x) = +2, δ(y=r(x)) = -1 | Překoření za z=ℓ(y) |
AVLInsert
Vlož nový vrchol jako list (standardní BVSInsert). Propaguj nahoru informaci o prohloubení. V každém vrcholu x rozeznáváme:
- δ(x) = +1 → změní se na 0, propagování STOP (hloubka T(x) se nezměnila).
- δ(x) = 0 → změní se na -1, propagování POKRAČUJE (hloubka T(x) vzrostla).
- δ(x) = -1 → δ(x) by bylo -2, nutno vyvažovat rotací (R, L, LR nebo RL). Po rotaci STOP.
AVLDelete
Odstraň vrchol standardně (BVSDelete). Propaguj nahoru informaci o zmenšení. V každém vrcholu x:
- δ(x) = -1 → změní se na 0, propagování POKRAČUJE.
- δ(x) = 0 → změní se na +1, propagování STOP.
- δ(x) = +1 → δ(x) by bylo +2, rotace. Po rotaci může nebo nemusí pokračovat.
Praktická poznámka: Červeno-černé stromy (std::map v C++, TreeMap v Javě) jsou jiný typ vyváženého BST s podobnými složitostmi, ale méně rotacemi v praxi. AVL stromy jsou teoreticky jednodušší na analýzu.
🗂️ Hešovací tabulky (hash tables)
Hešovací tabulka: Pole P = {0, …, m-1} přihrádek + hešovací funkce h : U → P. Prvek s klíčem k ukládáme do přihrádky h(k).
Proč hešovat? Přímé adresování (pole indexované klíčem) má paměť O(|U|), což je obrovské. Heš přiřadí klíče do malého pole m přihrádek a průměrně dosahuje O(1) pro Find/Insert/Delete při m = Θ(n).
Kolize
Kolize nastane, když h(k1) = h(k2) pro různé klíče k1 ≠ k2. Protože m ≪ |U|, kolizím se nelze úplně vyhnout (holubníkový princip).
Příklady hešovacích funkcí
- Lineární kongruence: h(k) = (ak) mod m, kde m je prvočíslo a a je vhodná konstanta.
- Vyšší bity součinu: h(k) = ⌊(ak mod 2w) / 2w-ℓ⌋ – stačí násobení + bitový posun, vhodné pro w-bitové klíče.
- Polynom pro řetězce: h(k0, …, kd-1) = Σ aⁱkᵢ mod m.
Faktor naplnění: α = n/m. Vhodné je udržovat α ≤ 0.7–0.8. Při překročení prahu → přehešování: zdvojnásobíme m, nová hešovací funkce, přeložíme vše. Amortizovaně O*(1).
Řešení kolizí: Řetězení (chaining)
Každá přihrádka obsahuje spojový seznam všech klíčů zahešovaných do ní. Find, Insert, Delete = výpočet h(k) + průchod řetízku délky ⌈n/m⌉ (průměrně). Při α = Θ(1) jsou operace průměrně O(1). Paměť: O(n + m).
Řešení kolizí: Otevřená adresace
Všechny prvky jsou přímo v tabulce. Pro klíč k je definována vyhledávací posloupnost h(k, 0), h(k, 1), …, h(k, m-1). Find/Insert prochází posloupnost dokud nenalezne prázdnou přihrádku.
- Lineární přidávání: h(k, i) = (f(k) + i) mod m. Jednoduché, cache-friendly, ale trpí shlukováním (clustering) → degradace pro vyšší α.
- Dvojité hešování: h(k, i) = (f(k) + i·g(k)) mod m, m prvočíslo. Lepší distribuce, méně shlukování. Každá přihrádka navštívena právě jednou (m je prvočíslo ⟹ g(k) je s m nesoudělné).
Mazání při otevřené adresaci
Smazaný prvek nelze prostě odstranit – přerušilo by to vyhledávací posloupnost! Místo toho použijeme náhrobek † (tombstone): přihrádka je označena jako „smazáno". Find ignoruje náhrobky (prochází dál), Insert může náhrobek přepsat.
Nevýhoda: Hromadění náhrobků. Po překročení prahu → přehešování (odstraní náhrobky).
| Metoda | Find průměr | Paměť | Poznámky |
|---|---|---|---|
| Řetězení | O(1 + α) | O(n+m) | Flexibilní; extra paměť pro seznamy |
| Lin. přidávání | O(1/(1-α)²) | O(m) | Cache-friendly, shlukování |
| Dvojité hešování | O(1/(1-α)) | O(m) | Lepší distribuce, 2 hešovací funkce |
Nafukovací (dynamická) hešovací tabulka
Analogie nafukovacího pole: na počátku malá tabulka, při α > Z zdvojnásobíme m a přehešujeme. Amortizovaná složitost Insert = O*(1). (Přehešování O(n) nastane jen logaritmicky mnohokrát.)
📝 Shrnutí Okruhu 2
- Binární halda: kompletní binární strom + haldové uspořádání. FindMin O(1), Insert/ExtractMin O(log n), Build O(n). Reprezentace v poli bez ukazatelů.
- Binomiální halda: sada binomiálních stromů, struktura daná binárním zápisem n. Klíčová výhoda: BHMerge v O(log n), BHInsert amortizovaně O*(1).
- BVS: BST vlastnost zaručuje, že Find/Insert/Delete jsou O(h). Nevyvážený strom může mít h = O(n).
- AVL stromy: hloubkové vyvážení (|h(L) - h(R)| ≤ 1) garantuje h = O(log n). Oprava rotacemi (R, L, LR, RL). Insert/Delete = O(log n).
- Hešovací tabulky: průměrně O(1) pro Find/Insert/Delete při vhodném α. Řetězení vs. otevřená adresace. Klíčový problém: kolize a jejich řešení.