====== std.graph — Graphalgorithmen ====== Gewichtete Graphen in CSR-Darstellung: BFS/DFS, topologische Sortierung, Dijkstra und Bellman-Ford, minimaler Spannbaum, Zusammenhangskomponenten, starke Komponenten, Zweifärbbarkeit und Max-Flow nach Dinic mit minimalem Schnitt. → [[lyx_-_programmiersprache:units:mathematik|Mathematik-Units]] · [[lyx_-_programmiersprache:units:grid|std.grid]] · [[lyx_-_programmiersprache:units:graphdb|Graphdatenbank]] import std.graph; Schicht 0, hängt an ''std.math'' und ''std.alloc''. 45 Funktionen. Alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt. ---- ===== Warum das Präfix ''Gph'' heißt ===== **''Graph*'' war belegt.** Die 65 ''Graph*''-Funktionen im Bestand gehören zur [[lyx_-_programmiersprache:units:graphdb|Graphdatenbank]] — Knoten, Kanten, Attribute, Zeit- und Typindizes, Musterabfragen. Das ist ein **Wissensgraph**, kein Algorithmenpaket: dort gibt es keinen Dijkstra, keine topologische Sortierung, keinen Spannbaum. ''std.grid'' konnte Pfadfindung, aber nur auf Gittern. Straßennetze, Abhängigkeitsgraphen und Ablaufpläne sind keine Gitter — diese Lücke schließt ''std.graph''. ---- ===== Aufbau: erst füllen, dann bauen ===== Ein Graph wird in zwei Schritten benutzt. ''GphAddEdge'' sammelt die Kanten, ''GphBuild'' überführt sie in die CSR-Darstellung. **Vor ''GphBuild'' liefert kein Algorithmus ein Ergebnis**, danach nimmt der Graph keine Kanten mehr an. unit main; import std.io; import std.string; import std.alloc; import std.graph; fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); } fn ZI(t: pchar, v: int64): void { Print(t); PrintLn(IntToStr(v)); } fn main(): int64 { var g: int64 := GphNew(5, 8, true); // 5 Knoten, Platz für 8 Kanten, gerichtet GphAddEdge(g, 0, 1, 4.0); GphAddEdge(g, 0, 2, 1.0); GphAddEdge(g, 2, 1, 2.0); GphAddEdge(g, 1, 3, 5.0); GphAddEdge(g, 2, 3, 8.0); GphAddEdge(g, 3, 4, 3.0); GphBuild(g); ZI("Knoten : ", GphNodeCount(g)); ZI("Kanten : ", GphEdgeCount(g)); ZI("Grad(0) : ", GphDegree(g, 0)); ZI("HasCycle : ", GphHasCycle(g) as int64); var dist: int64 := alloc(5 * 8); var prev: int64 := alloc(5 * 8); GphDijkstra(g, 0, dist, prev); ZF("dist[1] : ", peekf64(dist + 8)); ZF("dist[3] : ", peekf64(dist + 24)); ZF("dist[4] : ", peekf64(dist + 32)); var order: int64 := alloc(5 * 8); var cnt: int64 := GphTopoSort(g, order); Print("TopoSort : "); var i: int64 := 0; while (i < cnt) { Print(IntToStr(peek64(order + i * 8))); Print(" "); i := i + 1; } PrintLn(""); var path: int64 := alloc(5 * 8); var plen: int64 := GphPathFrom(prev, 4, path, 5); Print("Pfad 0->4: "); i := 0; while (i < plen) { Print(IntToStr(peek64(path + i * 8))); Print(" "); i := i + 1; } PrintLn(""); GphFree(g); return 0; } Knoten : 5 Kanten : 6 Grad(0) : 2 HasCycle : 0 dist[1] : 3.000000 dist[3] : 8.000000 dist[4] : 11.000000 TopoSort : 0 2 1 3 4 Pfad 0->4: 0 2 1 3 4 ''dist[1]'' ist **3**, nicht 4: der Weg über Knoten 2 (1+2) ist kürzer als die direkte Kante (4). ''GphPathFrom'' setzt aus dem ''prev''-Feld den Weg zusammen — es braucht die Höchstlänge als Argument und liefert die tatsächliche zurück. **''GphFree'' gehört dazu.** ''GphNew'' reserviert Speicher, den kein Aufräumen von selbst zurückgibt. ---- ===== Kürzeste Wege: die Wahl ist keine Geschmacksfrage ===== unit main; import std.io; import std.string; import std.alloc; import std.graph; fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); } fn main(): int64 { // 0->1 = 2 ; 0->2 = 5 ; 2->1 = -4 richtig ist dist[1] = 1 var g: int64 := GphNew(3, 4, true); GphAddEdge(g, 0, 1, 2.0); GphAddEdge(g, 0, 2, 5.0); GphAddEdge(g, 2, 1, -4.0); GphBuild(g); var d1: int64 := alloc(3 * 8); var p1: int64 := alloc(3 * 8); GphDijkstra(g, 0, d1, p1); ZF("Dijkstra dist[1] : ", peekf64(d1 + 8)); var d2: int64 := alloc(3 * 8); var p2: int64 := alloc(3 * 8); var rc: int64 := GphBellmanFord(g, 0, d2, p2); ZF("BellmanFord dist[1] : ", peekf64(d2 + 8)); Print("Rueckgabe BellmanFord: "); PrintLn(IntToStr(rc)); GphFree(g); return 0; } Dijkstra dist[1] : 2.000000 BellmanFord dist[1] : 1.000000 Rueckgabe BellmanFord: 1 **Dijkstra liefert hier ein falsches Ergebnis — stillschweigend.** Der richtige Weg ist ''0 → 2 → 1'' mit 5 − 4 = **1**; Dijkstra meldet 2. Es gibt keine Fehleranzeige, das Ergebnis sieht gültig aus. Der Grund: Dijkstra erklärt einen Knoten für abgeschlossen, sobald er ihn zum ersten Mal erreicht, und kommt darauf nicht zurück. Knoten 1 wird über die direkte Kante mit 2 abgeschlossen, **bevor** der billigere Umweg über Knoten 2 sichtbar wird. **Bei negativen Kantengewichten gehört Bellman-Ford hierher, ohne Ausnahme.** > **Dieses Gegenbeispiel ist sorgfältig gewählt.** Eine negative Kante allein genügt **nicht**, um Dijkstra zu Fall zu bringen — beim ersten Versuch lieferte er zufällig das richtige Ergebnis, weil der Zielknoten noch nicht abgeschlossen war. Der Fehler tritt nur auf, wenn eine negative Kante einen **bereits abgeschlossenen** Knoten betrifft. Wer das beim Prüfen nicht trifft, hat einen grünen Test ohne Aussage. ''GphBellmanFord'' gibt **1** zurück, wenn alles in Ordnung ist, und **0**, wenn ein **negativer Zyklus** gefunden wurde — dann gibt es keine kürzesten Wege, und die Distanzen sind bedeutungslos. Unerreichbare Knoten trägt es als **−1** ein, nicht als Unendlich. ^ Verfahren ^ Voraussetzung ^ Aufwand ^ | ''GphBfs'' | ungewichtet (jede Kante zählt 1) | linear | | ''GphDijkstra'' | **keine negativen Gewichte** | O(m log n) | | ''GphBellmanFord'' | beliebige Gewichte, erkennt negative Zyklen | O(n·m) | ---- ===== Struktur ===== ^ Funktion ^ Liefert ^ | ''GphTopoSort(g, out)'' | Reihenfolge; **weniger als ''n'' Knoten heißt: der Graph hat einen Zyklus** | | ''GphHasCycle(g)'' | direkt gefragt | | ''GphComponents(g, outComp)'' | Zusammenhangskomponenten, Anzahl als Rückgabe | | ''GphScc(g, outComp)'' | **stark** zusammenhängende Komponenten (gerichtet) | | ''GphIsConnected(g)'' | ein Stück? | | ''GphIsBipartite(g, outColor)'' | Zweifärbbarkeit, Färbung in ''outColor'' | | ''GphReachable(g, s)'' | Anzahl von ''s'' aus erreichbarer Knoten | | ''GphDfsOrder(g, s, outOrder)'' | Tiefensuche-Reihenfolge | Der Rückgabewert von ''GphTopoSort'' ist die eigentliche Zyklenprüfung: eine topologische Sortierung existiert genau dann, wenn der Graph kreisfrei ist. Kommen weniger als ''GphNodeCount'' Knoten zurück, steckt der Rest in einem Zyklus. ==== Union-Find und Spannbaum ==== ''GphMstKruskal(g, outEdges, outWeight)'' liefert den minimalen Spannbaum. Die zugrunde liegende Union-Find-Struktur ist einzeln nutzbar: ''GphUfNew'', ''GphUfFind'', ''GphUfUnion'', ''GphUfSame'', ''GphUfSize'', ''GphUfFree''. ---- ===== Fluss und Schnitt ===== Flussnetze sind eine **eigene** Struktur — ''GphFlowNew'' statt ''GphNew'', mit eigenem ''Build'' und eigenem ''Free'': unit main; import std.io; import std.string; import std.alloc; import std.graph; fn ZF(t: pchar, v: f64): void { Print(t); PrintF64(v); } fn main(): int64 { var f: int64 := GphFlowNew(4, 5); GphFlowAddEdge(f, 0, 1, 3.0); GphFlowAddEdge(f, 0, 2, 2.0); GphFlowAddEdge(f, 1, 2, 1.0); GphFlowAddEdge(f, 1, 3, 2.0); GphFlowAddEdge(f, 2, 3, 3.0); GphFlowBuild(f); ZF("MaxFlow 0->3 : ", GphMaxFlow(f, 0, 3)); var side: int64 := alloc(4 * 8); var cnt: int64 := GphMinCut(f, 0, side); Print("Knoten auf der Quellseite: "); PrintLn(IntToStr(cnt)); Print("und zwar : "); var i: int64 := 0; while (i < 4) { if (peek64(side + i * 8) == 1) { Print(IntToStr(i)); Print(" "); } i := i + 1; } PrintLn(""); GphFlowFree(f); return 0; } MaxFlow 0->3 : 5.000000 Knoten auf der Quellseite: 1 und zwar : 0 **5, nicht 4** — neben ''0→1→3'' (2) und ''0→2→3'' (2) trägt der Weg ''0→1→2→3'' noch eine weitere Einheit. > **''GphMinCut'' gibt die Zahl der Knoten auf der Quellseite zurück, nicht die der Schnittkanten.** ''outSide'' bekommt je Knoten 1 oder 0. Hier bleibt nur Knoten 0 übrig: beide Kanten aus der Quelle sind ausgeschöpft (3+2 = 5), im Restnetz ist nichts mehr erreichbar. Das ist der Satz von Ford und Fulkerson in Zahlen — der größte Fluss ist genau so groß wie der kleinste Schnitt. ^ Funktion ^ Zweck ^ | ''GphFlowAddEdge(f, u, v, cap)'' | gerichtete Kante, gibt den Kantenindex zurück | | ''GphFlowAddUndirected'' | Kapazität in beide Richtungen | | ''GphMaxFlow(f, s, t)'' | Dinic | | ''GphMinCut(f, s, outSide)'' | Schnitt aus dem Restnetz | | ''GphFlowOn(f, k)'' | tatsächlicher Fluss über Kante ''k'' | | ''GphFlowResidual(f, i)'' | verbleibende Kapazität | | ''GphBipartiteMatch(g, nLeft, outMatch)'' | größte Zuordnung im bipartiten Graphen | ''GphFlowOn'' ist der Weg zur **Lösung** statt nur zum Wert: nach ''GphMaxFlow'' steht dort je Kante, wie viel tatsächlich fließt. ---- ===== Grenzen ===== * ''GphNew(n, maxEdges, directed)'' legt die Kantenzahl **im Voraus** fest — mehr Kanten nimmt ''GphAddEdge'' nicht an. * Kein Floyd-Warshall (alle Paare) und kein A* auf allgemeinen Graphen. A* auf Gittern hat [[lyx_-_programmiersprache:units:grid|std.grid]]. * Knoten sind Zahlen von ''0'' bis ''n−1''. Wer Namen braucht, führt die Zuordnung selbst. ---- Letzte Aktualisierung: 2026-08-16 · alle Beispiele mit ''lyxc 1.0.21A'' übersetzt und ausgeführt; das Dijkstra-Gegenbeispiel und die Rückgabekonvention von ''GphMinCut'' einzeln nachgemessen bzw. aus der Quelle abgelesen.