<aclass="brand"href="index.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-book"></use></svg><span>Optimierte Entscheidungsfindung mit Python</span></a>
<navclass="sidebar"id="sidebar"aria-label="Kapitelnavigation"><divclass="sidebar-inhalt"><detailsclass="sidebar-gruppe"><summary>Einstieg</summary><ul><lidata-kapitel="vorwort.html"><ahref="vorwort.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><lidata-kapitel="notation.html"><ahref="notation.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><lidata-kapitel="einfuehrung.html"><ahref="einfuehrung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><lidata-kapitel="fundament.html"><ahref="fundament.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><lidata-kapitel="oekosystem.html"><ahref="oekosystem.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><lidata-kapitel="modellierung.html"><ahref="modellierung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><lidata-kapitel="synthese-grundlagen.html"><ahref="synthese-grundlagen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><detailsclass="sidebar-gruppe"open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><lidata-kapitel="lp.html"><ahref="lp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><lidata-kapitel="milp.html"><ahref="milp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><lidata-kapitel="cpsat.html"><ahref="cpsat.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><lidata-kapitel="graphen.html"class="aktiv"><ahref="graphen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><lidata-kapitel="metaheuristiken.html"><ahref="metaheuristiken.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><lidata-kapitel="dekomposition.html"><ahref="dekomposition.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><lidata-kapitel="synthese-kernverfahren.html"><ahref="synthese-kernverfahren.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil II</span>›<span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="cpsat.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a><aclass="prev-next-knopf prev-next-next"href="metaheuristiken.html"><span><small>Weiter</small>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-graphen">Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</h1>
<divclass="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um Probleme, deren natürliche Sprache der <strong>Graph</strong> ist: Was fließt wohin? Wer wird wem zugeordnet? Welche Route fährt welches Fahrzeug?</p>
<p><strong>Voraussetzungen:</strong><ahref="lp.html#kap-lp">Kapitel 5</a> und <ahref="milp.html#kap-milp">Kapitel 6</a>.</p>
<p><strong>Danach können Sie:</strong> Flussprobleme modellieren, Zuordnungsprobleme effizient lösen, eine Tourenplanung mit Kapazitäten und Zeitfenstern aufsetzen — und erkennen, wann eine begrenzende Dimension im Routing-Modell fehlt.</p>
<p><strong>Zeitbedarf:</strong> ca. 6 Stunden.</p>
<p><strong>Notebook:</strong><ahref="Notebooks_04/graphen.ipynb">graphen.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<h2id="sec:graphen-schnellstart">8.1 In 5 Minuten gelöst</h2>
<divclass="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Vier Monteure, vier Einsätze</strong></p>
<p>Ein Kundendienst muss vier Monteure auf vier Einsatzorte verteilen. Die Tabelle enthält die Anfahrtszeit in Minuten. Jeder Monteur fährt genau einen Einsatz.</p>
<spanid="cb1-12"><ahref="#cb1-12"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> z, s <spanclass="kw">in</span><spanclass="bu">zip</span>(zeile, spalte):</span>
<p><strong>Eine Zeile Code, und das Problem ist beweisbar optimal gelöst</strong> — <code>linear_sum_assignment</code> ist die Ungarische Methode, ein Spezialalgorithmus, der ohne jeden Solver auskommt.</p>
<p>Interessanter ist aber, was der Algorithmus <strong>nicht</strong> tut. Der kleinste Wert der ganzen Tabelle ist die 18 bei <em>Cakir → Nord</em>. Die naheliegende Vorgehensweise — „nimm immer das günstigste noch freie Paar“ — beginnt also genau dort:</p>
<table>
<colgroup>
<colstyle="width: 30%"/>
<colstyle="width: 30%"/>
<colstyle="width: 40%"/>
</colgroup>
<thead>
<trclass="header">
<th>Vorgehen</th>
<th>Zuordnung</th>
<thstyle="text-align: right;">Gesamtzeit</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Gierig („immer das billigste freie Paar“)</td>
<p>Die optimale Lösung schickt <strong>Bauer</strong> nach Nord, obwohl er dort drei Minuten länger braucht als Cakir. Der Grund: Cakir wird im Süden gebraucht, wo er mit 29 Minuten der mit Abstand Schnellste ist. Wer die 18 zuerst greift, verbaut sich das — und zahlt am Ende 10 Minuten mehr.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Der beste erste Zug ist selten Teil der besten Gesamtlösung. Genau deshalb gibt es Operations Research: Optimierung heißt, Entscheidungen <strong>gemeinsam</strong> zu treffen statt nacheinander. Bei vier Monteuren kostet die gierige Regel 8 %; bei vierzig kostet sie regelmäßig ein Vielfaches.</p>
</blockquote>
<p><strong>Warum funktioniert das?</strong> Weil das Zuordnungsproblem eine besondere Struktur hat: Seine Nebenbedingungsmatrix ist <strong>total unimodular</strong>. Das bedeutet — wir kommen in <ahref="#sec:graphen-bipartites-matching-das-zuordnungsproblem">Abschnitt 8.4</a> darauf zurück —, dass die LP-Relaxation von ganz allein ganzzahlige Lösungen liefert. Man braucht hier also weder Branch-and-Bound noch Binärvariablen. Dieselbe Eigenschaft macht auch Flussprobleme so angenehm lösbar, und damit beginnt das Kapitel.</p>
<hr/>
<h2id="sec:graphen-lernziele">8.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<oltype="1">
<li>… ein Transportproblem als Graph mit Quellen, Senken und Kapazitäten modellieren.</li>
<li>… den <strong>Flusserhaltungssatz</strong> aufstellen und seine Bedeutung erklären.</li>
<li>… ein Vehicle Routing Problem mit Kapazitäten und Zeitfenstern mit OR-Tools lösen.</li>
<li>… einschätzen, wann ein spezialisierter Algorithmus einem allgemeinen MILP überlegen ist.</li>
<li>… begründen, warum eine gierige Zuordnung systematisch schlechter ist als eine gemeinsame Optimierung.</li>
<li>… einen Tourenplan gegen die Wirklichkeit prüfen — unabhängig von den Bausteinen, aus denen das Modell gebaut wurde.</li>
</ol>
<hr/>
<h2id="sec:graphen-graphen-als-modellsprache">8.3 Graphen als Modellsprache</h2>
<p>Viele reale Probleme in Logistik, Kommunikation und Finanzströmen sind keine flachen Ungleichungssysteme, sondern <strong>Graphen</strong><spanclass="math inline">G = (V, E)</span>:</p>
<p><strong>Minimum-Cost Network Flow Problem</strong> — deutsch: <em>kostenminimales Flussproblem</em>. Es ist das mathematische Fundament für Transportketten, Liquiditätsrouting und Datenverteilung.</p>
<figure>
<imgsrc="bilder_04/kap_graphen_min_cost_flow.svg"alt="Abb. 8.1: Das Netzwerk aus Min_Cost_Flow.py samt Lösung: 140 € für 30 Einheiten. Die billige Route über den Umschlag läuft voll (dick, amber), die Kante Werk B → Umschlag bleibt ungenutzt (gestrichelt) — und trotzdem muss die teure Direktkante Werk B → Kunde 2 für 6 € bedient werden, weil der Umschlagweg dorthin schon ausgelastet ist. An den Knoten stehen die Dualwerte der Flusserhaltung. Erzeugt von bilder_04/erzeuge_min_cost_flow.py."/>
<figcaptionaria-hidden="true">Abb. 8.1: Das Netzwerk aus <code>Min_Cost_Flow.py</code> samt Lösung: 140 € für 30 Einheiten. Die billige Route über den Umschlag läuft voll (dick, amber), die Kante Werk B → Umschlag bleibt ungenutzt (gestrichelt) — und trotzdem muss die teure Direktkante Werk B → Kunde 2 für 6 € bedient werden, weil der Umschlagweg dorthin schon ausgelastet ist. An den Knoten stehen die Dualwerte der Flusserhaltung. Erzeugt von <code>bilder_04/erzeuge_min_cost_flow.py</code>.</figcaption>
</figure>
<p>Sei <spanclass="math inline">x_{ij} \ge 0</span> der Fluss über Kante <spanclass="math inline">(i,j)</span>, <spanclass="math inline">c_{ij}</span> die Kosten je Einheit und <spanclass="math inline">u_{ij}</span> die Kapazität:</p>
<p><spanclass="math display">
\min \sum_{(i,j)\in E} c_{ij}\,x_{ij}
</span></p>
<p><spanclass="math display">
\sum_{j:(i,j)\in E} x_{ij} \;-\; \sum_{k:(k,i)\in E} x_{ki} \;=\; b_i \quad \forall i \in V
\qquad(\textbf{Flusserhaltung})
</span></p>
<p><spanclass="math display">
0 \le x_{ij} \le u_{ij} \quad \forall (i,j)\in E
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe zur Flusserhaltung</strong> * Erste Summe: alles, was aus Knoten <spanclass="math inline">i</span><strong>hinaus</strong>fließt. * Zweite Summe: alles, was in Knoten <spanclass="math inline">i</span><strong>hinein</strong>fließt. * <spanclass="math inline">b_i</span> — der Saldo des Knotens.</p>
<p><strong>Ohne Formel gesagt:</strong> Das ist die Kirchhoffsche Knotenregel aus der Elektrotechnik — nichts geht verloren, nichts entsteht aus dem Nichts. Für einen Umladeknoten heißt das wörtlich: „Was ankommt, fährt auch wieder weg.“</p>
<p><strong>Wichtig:</strong> Damit das Problem lösbar ist, muss <spanclass="math inline">\sum_i b_i = 0</span> gelten — das Gesamtangebot muss dem Gesamtbedarf entsprechen.</p>
<spanid="cb3-5"><ahref="#cb3-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Graphen: Kostenminimaler Fluss durch ein Netzwerk.</span></span>
<spanid="cb3-7"><ahref="#cb3-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Loest dasselbe Problem zweimal:</span></span>
<spanid="cb3-8"><ahref="#cb3-8"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (1) als allgemeines LP mit scipy -> zeigt die Modellstruktur</span></span>
<spanid="cb3-9"><ahref="#cb3-9"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (2) mit dem spezialisierten Netzwerk-Solver von OR-Tools -> zeigt den</span></span>
<spanid="cb3-10"><ahref="#cb3-10"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Geschwindigkeitsvorteil eines Verfahrens, das die Struktur ausnutzt</span></span>
<spanid="cb3-12"><ahref="#cb3-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">Beide laufen in eigenen Prozessen nicht noetig: scipy und ortools vertragen</span></span>
<spanid="cb3-35"><ahref="#cb3-35"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Flussproblem als allgemeines lineares Programm."""</span></span>
<spanid="cb3-37"><ahref="#cb3-37"aria-hidden="true"tabindex="-1"></a> knoten_index <spanclass="op">=</span> {k: i <spanclass="cf">for</span> i, k <spanclass="kw">in</span><spanclass="bu">enumerate</span>(KNOTEN)}</span>
<spanid="cb3-42"><ahref="#cb3-42"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Flusserhaltung als Gleichungssystem: A_eq @ x = b_eq</span></span>
<spanid="cb3-61"><ahref="#cb3-61"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" KOSTENMINIMALER FLUSS DURCH EIN TRANSPORTNETZ"</span>)</span>
<spanid="cb3-67"><ahref="#cb3-67"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="st">"Angebot und Bedarf stimmen nicht ueberein - unloesbar!"</span>)</span>
<spanid="cb3-81"><ahref="#cb3-81"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">--- Pruefung der Flusserhaltung je Knoten ---"</span>)</span>
<spanid="cb3-82"><ahref="#cb3-82"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span> KNOTEN:</span>
<spanid="cb3-83"><ahref="#cb3-83"aria-hidden="true"tabindex="-1"></a> hinaus <spanclass="op">=</span><spanclass="bu">sum</span>(fluss[e] <spanclass="cf">for</span> e, (v, n, _, _) <spanclass="kw">in</span><spanclass="bu">enumerate</span>(KANTEN) <spanclass="cf">if</span> v <spanclass="op">==</span> k)</span>
<spanid="cb3-84"><ahref="#cb3-84"aria-hidden="true"tabindex="-1"></a> hinein <spanclass="op">=</span><spanclass="bu">sum</span>(fluss[e] <spanclass="cf">for</span> e, (v, n, _, _) <spanclass="kw">in</span><spanclass="bu">enumerate</span>(KANTEN) <spanclass="cf">if</span> n <spanclass="op">==</span> k)</span>
<spanid="cb3-85"><ahref="#cb3-85"aria-hidden="true"tabindex="-1"></a> netto <spanclass="op">=</span> hinaus <spanclass="op">-</span> hinein</span>
<spanid="cb3-86"><ahref="#cb3-86"aria-hidden="true"tabindex="-1"></a> art <spanclass="op">=</span><spanclass="st">"Quelle"</span><spanclass="cf">if</span> SALDO[k] <spanclass="op">></span><spanclass="dv">0</span><spanclass="cf">else</span> (<spanclass="st">"Senke"</span><spanclass="cf">if</span> SALDO[k] <spanclass="op"><</span><spanclass="dv">0</span><spanclass="cf">else</span><spanclass="st">"Umschlag"</span>)</span>
<spanid="cb3-87"><ahref="#cb3-87"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>k<spanclass="sc">:<10}</span><spanclass="ss"> (</span><spanclass="sc">{</span>art<spanclass="sc">:<8}</span><spanclass="ss">): hinaus </span><spanclass="sc">{</span>hinaus<spanclass="sc">:5.1f}</span><spanclass="ss"> - hinein </span><spanclass="sc">{</span>hinein<spanclass="sc">:5.1f}</span><spanclass="ss">"</span></span>
<spanid="cb3-92"><ahref="#cb3-92"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">--- Knotenpreise (Dualwerte der Flusserhaltung) ---"</span>)</span>
<spanid="cb3-93"><ahref="#cb3-93"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Differenz zweier Knotenpreise = Grenzkosten einer zusaetzlichen Einheit"</span>)</span>
<spanid="cb3-94"><ahref="#cb3-94"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" auf dem guenstigsten Weg zwischen ihnen."</span>)</span>
<p><strong>Der Umschlagknoten hat Saldo 0</strong> — exakt 15 Einheiten hinein, exakt 15 hinaus. Er produziert und verbraucht nichts, sondern verteilt nur um.</p>
<p><strong>Werk B fährt nicht über den Umschlag</strong>, obwohl dieser Weg existiert: <spanclass="math inline">B \to \text{Umschlag} \to \text{Kunde 2}</span> kostet <spanclass="math inline">4 + 3 = 7</span> je Einheit, der direkte Weg nur 6. Werk A dagegen nutzt den Umschlag intensiv, weil <spanclass="math inline">2 + 1 = 3</span> nach Kunde 1 deutlich günstiger ist als der direkte Weg mit 5 — und die günstige Kante <spanclass="math inline">A \to \text{Umschlag}</span> ist deshalb bis zur Kapazitätsgrenze <strong>voll ausgelastet</strong>. Genau hier liegt der Wert der Optimierung: Sie gewichtet solche Alternativen für alle Kanten <strong>gleichzeitig</strong> ab, während man von Hand schon bei zehn Knoten den Überblick verliert.</p>
<hr/>
<h2id="sec:graphen-bipartites-matching-das-zuordnungsproblem">8.4 Bipartites Matching: das Zuordnungsproblem</h2>
<p>Wenn <spanclass="math inline">N</span> Aufgaben auf <spanclass="math inline">N</span> Ressourcen <strong>eins zu eins</strong> verteilt werden — Orders auf Broker, Schichten auf Mitarbeitende, Aufträge auf Maschinen — spricht man von <strong>bipartitem Matching</strong>.</p>
<h3id="der-satz-von-birkhoff-und-von-neumann">Der Satz von Birkhoff und von Neumann</h3>
<blockquote>
<p><strong>Satz.</strong> Die Extrempunkte der Menge aller doppelt-stochastischen Matrizen (alle Zeilensummen <spanclass="math inline">= 1</span>, alle Spaltensummen <spanclass="math inline">= 1</span>, <spanclass="math inline">x_{ij} \ge 0</span>) sind <strong>genau</strong> die Permutationsmatrizen (alle <spanclass="math inline">x_{ij} \in \{0,1\}</span>).</p>
</blockquote>
<p><strong>Warum das praktisch enorm wichtig ist:</strong> Nach dem Fundamentalsatz aus <ahref="fundament.html#kap-fundament">Kapitel 2</a> liegt das LP-Optimum in einer Ecke. Die Ecken sind hier laut Satz automatisch 0/1-wertig. Also gilt:</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Beim Zuordnungsproblem müssen Sie die Ganzzahligkeit <strong>nicht</strong> fordern — ein gewöhnlicher LP-Solver liefert von selbst eine 0/1-Lösung. Sie sparen sich damit die NP-Schwere von Branch-and-Bound vollständig.</p>
</blockquote>
<p>Der Grund dahinter heißt <strong>totale Unimodularität</strong>: Die Nebenbedingungsmatrix hat eine spezielle Struktur, bei der jede quadratische Teilmatrix die Determinante <spanclass="math inline">0</span>, <spanclass="math inline">+1</span> oder <spanclass="math inline">-1</span> hat. Dieselbe Eigenschaft besitzt übrigens auch die Flusserhaltungsmatrix aus <ahref="#sec:graphen-graphen-als-modellsprache">Abschnitt 8.3</a> — deshalb sind Netzwerkflüsse ebenfalls „von selbst“ ganzzahlig.</p>
<divclass="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer: totale Unimodularität</strong></p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><spanclass="math inline">\det(\mathbf{B}) \in \{0, +1, -1\}</span> für <strong>jede</strong> quadratische Teilmatrix <spanclass="math inline">\mathbf{B}</span> von <spanclass="math inline">\mathbf{A}</span></td>
<td>„Die Matrix ist so gebaut, dass beim Lösen nie ein echter Bruch entstehen kann.“</td>
</tr>
<trclass="even">
<td><spanclass="math inline">\mathbf{b}</span> ganzzahlig <spanclass="math inline">\Rightarrow</span> alle Ecken von <spanclass="math inline">\{\mathbf{x} : \mathbf{A}\mathbf{x} = \mathbf{b},\ \mathbf{x} \ge 0\}</span> ganzzahlig</td>
<td>„Sind Kapazitäten und Bedarfe ganze Zahlen, sind es die Ecken automatisch auch.“</td>
</tr>
<trclass="odd">
<td>zusammen mit dem Fundamentalsatz (<ahref="fundament.html#kap-fundament">Kapitel 2</a>)</td>
<td>„Das LP-Optimum liegt in einer Ecke — und die ist hier von selbst ganzzahlig.“</td>
</tr>
</tbody>
</table>
<p><strong>Die praktische Folge in einem Satz:</strong><em>Bei Zuordnungs- und Flussproblemen dürfen Sie die Ganzzahligkeit weglassen und trotzdem ganzzahlige Lösungen erwarten — Sie sparen sich die NP-Schwere von Branch-and-Bound vollständig.</em></p>
<p><strong>Und die Warnung dazu:</strong> Diese Eigenschaft ist zerbrechlich. Eine einzige zusätzliche Nebenbedingung, die nicht in das Schema passt — „höchstens drei Fahrzeuge insgesamt“, eine Fixkostenkopplung, eine Mindestabnahmemenge — zerstört die totale Unimodularität. Dann liefert die Relaxation wieder Brüche, und Sie brauchen doch ein MILP. Prüfen Sie das, bevor Sie sich auf die Struktur verlassen.</p>
<spanid="cb5-8"><ahref="#cb5-8"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (2) als LP OHNE Ganzzahligkeitsforderung -> liefert trotzdem 0/1 (Birkhoff)</span></span>
<spanid="cb5-9"><ahref="#cb5-9"aria-hidden="true"tabindex="-1"></a><spanclass="co"> (3) als MILP MIT Ganzzahligkeitsforderung -> gleiches Ergebnis, mehr Aufwand</span></span>
<spanid="cb5-11"><ahref="#cb5-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeigt damit die praktische Bedeutung der totalen Unimodularitaet.</span></span>
<spanid="cb5-31"><ahref="#cb5-31"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Gemeinsame LP-Struktur fuer Variante 2 und 3."""</span></span>
<spanid="cb5-32"><ahref="#cb5-32"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(kosten)</span>
<spanid="cb5-33"><ahref="#cb5-33"aria-hidden="true"tabindex="-1"></a> c <spanclass="op">=</span> kosten.flatten() <spanclass="co"># x_ij in Zeilenreihenfolge</span></span>
<spanid="cb5-34"><ahref="#cb5-34"aria-hidden="true"tabindex="-1"></a> A_eq <spanclass="op">=</span> np.zeros((<spanclass="dv">2</span><spanclass="op">*</span> n, n <spanclass="op">*</span> n))</span>
<spanid="cb5-35"><ahref="#cb5-35"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n): <spanclass="co"># jede Person genau eine Aufgabe</span></span>
<spanid="cb5-36"><ahref="#cb5-36"aria-hidden="true"tabindex="-1"></a> A_eq[i, i <spanclass="op">*</span> n:(i <spanclass="op">+</span><spanclass="dv">1</span>) <spanclass="op">*</span> n] <spanclass="op">=</span><spanclass="fl">1.0</span></span>
<spanid="cb5-37"><ahref="#cb5-37"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> j <spanclass="kw">in</span><spanclass="bu">range</span>(n): <spanclass="co"># jede Aufgabe genau einer Person</span></span>
<spanid="cb5-55"><ahref="#cb5-55"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ZUORDNUNGSPROBLEM: DREI WEGE ZUM SELBEN ERGEBNIS"</span>)</span>
<spanid="cb5-58"><ahref="#cb5-58"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Kleines Beispiel zum Nachvollziehen ------------------------------</span></span>
<spanid="cb5-66"><ahref="#cb5-66"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Kostenmatrix (wer bearbeitet was zu welchen Kosten?):"</span>)</span>
<spanid="cb5-67"><ahref="#cb5-67"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span><spanclass="st">''</span><spanclass="sc">:<8}</span><spanclass="ss">"</span><spanclass="op">+</span><spanclass="st">""</span>.join(<spanclass="ss">f"</span><spanclass="sc">{</span>a<spanclass="sc">:>12}</span><spanclass="ss">"</span><spanclass="cf">for</span> a <spanclass="kw">in</span> aufgaben))</span>
<spanid="cb5-68"><ahref="#cb5-68"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, name <spanclass="kw">in</span><spanclass="bu">enumerate</span>(namen):</span>
<spanid="cb5-73"><ahref="#cb5-73"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, j <spanclass="kw">in</span><spanclass="bu">enumerate</span>(zuordnung):</span>
<spanid="cb5-79"><ahref="#cb5-79"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">LP OHNE Ganzzahligkeitsforderung: Kosten </span><spanclass="sc">{</span>wert_lp<spanclass="sc">:.0f}</span><spanclass="ss">, "</span></span>
<spanid="cb5-80"><ahref="#cb5-80"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Loesung ist </span><spanclass="sc">{</span><spanclass="st">'0/1-wertig'</span><spanclass="cf">if</span> ist_binaer <spanclass="cf">else</span><spanclass="st">'GEBROCHEN'</span><spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb5-81"><ahref="#cb5-81"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" -> Satz von Birkhoff/von Neumann bestaetigt: Die Ecken sind Permutationen."</span>)</span>
<spanid="cb5-93"><ahref="#cb5-93"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> n <spanclass="op"><=</span><spanclass="dv">50</span>:</span>
<spanid="cb5-103"><ahref="#cb5-103"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Fazit: Der spezialisierte Ungarische Algorithmus ist um Groessenordnungen"</span>)</span>
<spanid="cb5-104"><ahref="#cb5-104"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"schneller. Nutzen Sie fuer reine Zuordnungen NIE einen MILP-Solver."</span>)</span>
<p><strong>💻 Code-Durchgang: die Indexakrobatik</strong></p>
<p>Die Variable <spanclass="math inline">x_{ij}</span> wird zu einem flachen Vektor der Länge <spanclass="math inline">n^2</span> aufgerollt: Position von <spanclass="math inline">x_{ij}</span> ist <spanclass="math inline">i \cdot n + j</span>. * <code>A_eq[i, i*n:(i+1)*n] = 1</code> — Zeile <spanclass="math inline">i</span>: alle Aufgaben <strong>einer</strong> Person (ein zusammenhängender Block). * <code>A_eq[n+j, j::n] = 1</code> — Zeile <spanclass="math inline">n+j</span>: alle Personen <strong>einer</strong> Aufgabe (jedes <spanclass="math inline">n</span>-te Element, deshalb die Schrittweite <code>::n</code>).</p>
<p>Diese Umrechnung zwischen Matrix- und Vektorindizes ist eine der häufigsten Fehlerquellen überhaupt. <strong>Prüfen Sie sie immer an einem winzigen Beispiel</strong>, bei dem Sie die Matrix von Hand hinschreiben können.</p>
</blockquote>
<hr/>
<h2id="sec:graphen-das-vehicle-routing-problem-mit-zeitfenstern">8.5 Das Vehicle Routing Problem mit Zeitfenstern</h2>
<p>Das <strong>Traveling Salesperson Problem (TSP)</strong>, deutsch <em>Problem des Handlungsreisenden</em>, fragt nach der kürzesten Rundreise durch <spanclass="math inline">N</span> Städte. Das <strong>Capacitated Vehicle Routing Problem with Time Windows (CVRPTW)</strong> erweitert es auf eine <strong>Flotte</strong> mit Kapazitätsgrenzen und Kundenzeitfenstern <spanclass="math inline">[e_i, l_i]</span>.</p>
<figure>
<imgsrc="bilder_04/kap_graphen_vrp_touren.svg"alt="Abb. 8.2: Die Lösung der Instanz aus VRP_Flotten_Routing.py: 16 Kunden, vier Fahrzeuge, 619 km. Beachten Sie, dass sich die Touren kreuzen. Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von bilder_04/erzeuge_vrp_touren.py."/>
<figcaptionaria-hidden="true">Abb. 8.2: Die Lösung der Instanz aus <code>VRP_Flotten_Routing.py</code>: 16 Kunden, vier Fahrzeuge, 619 km. <strong>Beachten Sie, dass sich die Touren kreuzen.</strong> Bei einem reinen Tourenproblem wäre das ein sicheres Zeichen für eine verbesserbare Lösung — hier ist es keines: Die Zeitfenster erzwingen die Reihenfolge, und wer die Kreuzungen auflöst, kommt zu spät. Erzeugt von <code>bilder_04/erzeuge_vrp_touren.py</code>.</figcaption>
<p><strong>Was Sie auf der Karte sehen — und was nicht.</strong> Die Instanz ist synthetisch: 17 zufällige Punkte auf einem Quadrat von 100 × 100 km. Für die Karte wurden sie <em>lagegetreu</em> auf eine reale Region gelegt — mit einer azimutal-äquidistanten Projektion, die alle 136 Abstände zwischen den Punkten bis auf <strong>0,001 %</strong> erhält. Die Karte zeigt also dieselbe Instanz, mit der das Modell rechnet, nur an geographischen Koordinaten statt auf einem Zahlenraster; kein Wert in diesem Kapitel ändert sich dadurch.</p>
<p>Die Touren sind aus demselben Grund <strong>Luftlinien und keine Straßenrouten</strong>: Das Modell kennt keine Straßen, es rechnet mit den Abständen der Distanzmatrix. Eine Linie, die einer echten Straße folgte, wäre hübscher und würde eine Genauigkeit vortäuschen, die das Modell nicht hat. Wer Straßenentfernungen braucht, ersetzt die Matrix — <ahref="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
<p>Ein naives Modell erlaubt <strong>Subtouren</strong>: isolierte Kreise, die das Depot nie anfahren. Die klassische Gegenmaßnahme ist die <strong>MTZ-Formulierung</strong> nach Miller, Tucker und Zemlin. Man führt Rangvariablen <spanclass="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
<p><spanclass="math display">
u_i - u_j + C \cdot x_{ij} \le C - d_j \qquad \forall i \ne j
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> Wird Kante <spanclass="math inline">(i,j)</span> benutzt (<spanclass="math inline">x_{ij} = 1</span>), erzwingt die Ungleichung <spanclass="math inline">u_j \ge u_i + d_j</span> — der Rang wächst also entlang jeder benutzten Kante streng an. In einem geschlossenen Kreis müsste der Rang aber wieder zum Ausgangswert zurückkehren, was unmöglich ist. <strong>Kreise ohne Depot werden dadurch mathematisch ausgeschlossen.</strong></p>
<p>Wird die Kante nicht benutzt (<spanclass="math inline">x_{ij} = 0</span>), reduziert sich die Ungleichung auf <spanclass="math inline">u_i - u_j \le C - d_j</span>, was durch hinreichend großes <spanclass="math inline">C</span> immer erfüllt ist — das Big-M-Muster aus <ahref="milp.html#kap-milp">Kapitel 6</a>.</p>
</blockquote>
<blockquote>
<p><strong>💡 In der Praxis: nicht selbst modellieren</strong> Die MTZ-Formulierung ist didaktisch wertvoll, aber für reale Instanzen zu schwach — die LP-Relaxation ist sehr locker, und Branch-and-Bound braucht sehr lange. Professionelle Solver verwenden stattdessen dynamisch erzeugte Subtour-Eliminationsschnitte oder, wie OR-Tools, <strong>spezialisierte Metaheuristiken</strong>. Nutzen Sie für Routing die <strong>Routing-Bibliothek</strong>, nicht ein selbstgebautes MILP.</p>
<spanid="cb6-5"><ahref="#cb6-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Graphen: Capacitated Vehicle Routing Problem with Time Windows (CVRPTW)</span></span>
<spanid="cb6-6"><ahref="#cb6-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">mit der Routing-Bibliothek von Google OR-Tools.</span></span>
<spanid="cb6-9"><ahref="#cb6-9"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Eingabedaten werden vorab auf Plausibilitaet geprueft (Kapazitaet</span></span>
<spanid="cb6-11"><ahref="#cb6-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Fahrzeit und Servicezeit werden getrennt ausgewiesen</span></span>
<spanid="cb6-12"><ahref="#cb6-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Ausgabe als lesbarer Tourenplan mit Ankunftszeiten</span></span>
<spanid="cb6-19"><ahref="#cb6-19"aria-hidden="true"tabindex="-1"></a>SERVICEZEIT <spanclass="op">=</span><spanclass="dv">10</span><spanclass="co"># Minuten je Kundenstopp</span></span>
<spanid="cb6-20"><ahref="#cb6-20"aria-hidden="true"tabindex="-1"></a>WARTEZEIT_MAX <spanclass="op">=</span><spanclass="dv">60</span><spanclass="co"># zulaessige Wartezeit bei zu frueher Ankunft</span></span>
<spanid="cb6-31"><ahref="#cb6-31"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(anzahl_orte):</span>
<spanid="cb6-59"><ahref="#cb6-59"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="st">"UNLOESBAR: Der Bedarf uebersteigt die Flottenkapazitaet."</span>)</span>
<spanid="cb6-61"><ahref="#cb6-61"aria-hidden="true"tabindex="-1"></a> d <spanclass="op">=</span> daten[<spanclass="st">"distanzmatrix"</span>]</span>
<spanid="cb6-63"><ahref="#cb6-63"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> kunde <spanclass="op">==</span><spanclass="dv">0</span>:</span>
<spanid="cb6-68"><ahref="#cb6-68"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"UNLOESBAR: Kunde </span><spanclass="sc">{</span>kunde<spanclass="sc">}</span><spanclass="ss"> ist erst nach </span><spanclass="sc">{</span>direktfahrt<spanclass="sc">}</span><spanclass="ss"> min erreichbar, "</span></span>
<spanid="cb6-69"><ahref="#cb6-69"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"sein Zeitfenster endet aber bei </span><spanclass="sc">{</span>spaetestens<spanclass="sc">}</span><spanclass="ss"> min."</span>)</span>
<spanid="cb6-70"><ahref="#cb6-70"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Vorabpruefung bestanden: Kapazitaet und Zeitfenster sind grundsaetzlich machbar."</span>)</span>
<spanid="cb6-83"><ahref="#cb6-83"aria-hidden="true"tabindex="-1"></a> von <spanclass="op">=</span> manager.IndexToNode(von_index)</span>
<spanid="cb6-84"><ahref="#cb6-84"aria-hidden="true"tabindex="-1"></a> nach <spanclass="op">=</span> manager.IndexToNode(nach_index)</span>
<spanid="cb6-85"><ahref="#cb6-85"aria-hidden="true"tabindex="-1"></a> service <spanclass="op">=</span> SERVICEZEIT <spanclass="cf">if</span> von <spanclass="op">!=</span> daten[<spanclass="st">"depot"</span>] <spanclass="cf">else</span><spanclass="dv">0</span></span>
<spanid="cb6-129"><ahref="#cb6-129"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">Fahrzeug </span><spanclass="sc">{</span>fahrzeug <spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">}</span><spanclass="ss">: nicht eingesetzt"</span>)</span>
<spanid="cb6-137"><ahref="#cb6-137"aria-hidden="true"tabindex="-1"></a> bezeichnung <spanclass="op">=</span><spanclass="st">"Depot"</span><spanclass="cf">if</span> knoten <spanclass="op">==</span><spanclass="dv">0</span><spanclass="cf">else</span><spanclass="ss">f"K</span><spanclass="sc">{</span>knoten<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb6-151"><ahref="#cb6-151"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Schichtzeit </span><spanclass="sc">{</span>endzeit<spanclass="sc">}</span><spanclass="ss"> min | Fahrstrecke </span><spanclass="sc">{</span>distanz<spanclass="sc">}</span><spanclass="ss"> km | "</span></span>
<spanid="cb6-158"><ahref="#cb6-158"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Transportierte Fracht: </span><spanclass="sc">{</span>gesamtfracht<spanclass="sc">}</span><spanclass="ss"> von </span><spanclass="sc">{</span><spanclass="bu">sum</span>(daten[<spanclass="st">'bedarfe'</span>])<spanclass="sc">}</span><spanclass="ss"> Einheiten"</span>)</span>
<spanid="cb6-159"><ahref="#cb6-159"aria-hidden="true"tabindex="-1"></a><spanclass="cf">assert</span> gesamtfracht <spanclass="op">==</span><spanclass="bu">sum</span>(daten[<spanclass="st">"bedarfe"</span>]), <spanclass="st">"Nicht alle Kunden wurden beliefert!"</span></span>
<spanid="cb6-160"><ahref="#cb6-160"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Alle Kunden wurden innerhalb ihrer Zeitfenster beliefert."</span>)</span>
<p><strong>⚠️ Typische Fehler beim VRP</strong></p>
<ul>
<li><strong><code>Min(CumulVar)</code> mit „Ankunftszeit“ verwechseln.</strong> Der Solver liefert ein <em>Intervall</em> möglicher Zeiten. <code>Min()</code> ist die früheste, <code>Max()</code> die späteste zulässige Zeit — die tatsächliche Fahrt kann irgendwo dazwischen starten.</li>
<li><strong>Servicezeit im Depot mitzählen.</strong> Beim Start am Depot fällt keine Servicezeit an, sonst verschiebt sich der ganze Plan.</li>
<li><strong>Zu enge Zeitfenster ohne Vorabprüfung.</strong> Ist ein Kunde in seinem Fenster physisch nicht erreichbar, meldet OR-Tools nur „keine Lösung“ — ohne zu sagen, welcher Kunde schuld ist. Die Funktion <code>pruefe_daten()</code> fängt genau das ab.</li>
<li><strong>Ergebnisse als exakt betrachten.</strong> Die Routing-Bibliothek nutzt <strong>Metaheuristiken</strong>. Ein längeres Zeitlimit kann eine bessere Lösung liefern; „optimal“ wird hier in der Regel nicht bewiesen. Für die Praxis genügt das fast immer — man sollte es aber wissen.</li>
<p><strong>Aufgabe 8.1 ⭐ — Flusserhaltung prüfen.</strong> Ein Knoten hat Zuflüsse 12 und 8 sowie Abflüsse 15 und 3. Welchen Saldo <spanclass="math inline">b_i</span> hat er, und um welchen Knotentyp handelt es sich?</p>
<p><strong>Aufgabe 8.2 ⭐ — Unlösbarkeit erkennen.</strong> Warum ist ein Flussproblem mit <spanclass="math inline">\sum_i b_i \ne 0</span> grundsätzlich unlösbar? Wie modelliert man den realistischen Fall „Angebot größer als Bedarf“?</p>
<p><strong>Aufgabe 8.3 ⭐⭐ — Transportproblem lösen.</strong> Drei Werke (Angebot 30, 25, 45) beliefern vier Lager (Bedarf 25, 30, 20, 25). Die Transportkosten je Einheit stehen in der Matrix <spanclass="math display">\begin{pmatrix}8&6&10&9\\9&12&13&7\\14&9&16&5\end{pmatrix}</span> (a) Stimmen Angebot und Bedarf überein? (b) Lösen Sie mit <code>linprog</code> und geben Sie den Transportplan aus. (c) Prüfen Sie, ob die Lösung ganzzahlig ist, obwohl Sie es nicht gefordert haben. Warum?</p>
<p><strong>Aufgabe 8.4 ⭐⭐ — Zuordnung mit Verboten.</strong> Erweitern Sie <code>Zuordnung_Ungarisch.py</code>: Carla darf Auftrag Y nicht bearbeiten (fehlende Zulassung). Wie modellieren Sie das? Wie ändert sich die Lösung?</p>
<p><strong>Aufgabe 8.5 ⭐⭐ — Engpass finden.</strong> Ergänzen Sie <code>Min_Cost_Flow.py</code> um eine Analyse: Welche Kante würde bei einer Kapazitätserhöhung um 1 Einheit die Gesamtkosten am stärksten senken? (Tipp: Dualwerte der Kapazitätsschranken oder schlicht neu rechnen.)</p>
<p><strong>Aufgabe 8.6 ⭐⭐⭐ — VRP variieren.</strong> Untersuchen Sie mit <code>VRP_Flotten_Routing.py</code>: (a) Wie ändert sich der Plan bei 3 statt 4 Fahrzeugen? Bei 2? (b) Ab welcher Fahrzeugzahl wird das Problem unlösbar — und warum? (c) Wie wirkt sich ein Zeitlimit von 1 s gegenüber 30 s auf die Lösungsqualität aus? (d) Verdoppeln Sie die Servicezeit. Was passiert?</p>
<p><strong>Aufgabe 8.7 ⭐⭐⭐ — TSP mit MTZ selbst bauen.</strong> Modellieren Sie ein TSP mit 8 Städten als MILP mit MTZ-Bedingungen (<code>linprog</code> mit <code>integrality</code>). Vergleichen Sie Laufzeit und Ergebnis mit der Routing-Bibliothek von OR-Tools. Was beobachten Sie ab 12 Städten?</p>
<hr/>
<h2id="sec:graphen-denkfehler">8.7 Finde den Denkfehler</h2>
<p>Eine Spedition lässt ihre Tagestouren optimieren: 16 Kunden, 4 Fahrzeuge zu je 10 Paletten, Gesamtbedarf 37 Paletten. Das erste Ergebnis begeistert alle — <strong>326 km</strong>. Nach einem Hinweis aus dem Fuhrpark wird das Modell überarbeitet; jetzt kommen <strong>572 km</strong> heraus, 75 % mehr. Der Auftraggeber ist verärgert: <em>„Ihre erste Version war doch viel besser.“</em></p>
<p>Der Unterschied zwischen beiden Fassungen ist ein einziger Codeblock:</p>
<p><strong>Ihre Aufgabe:</strong> (a) Sehen Sie sich unten die Tourenübersicht des ersten Laufs an. Was tun die Fahrzeuge 1 bis 3, und wie viel lädt Fahrzeug 4? (b) Warum hat die Routing-Bibliothek das nicht von allein verhindert — die Kapazitäten standen doch in den Daten? (c) Warum ist ausgerechnet ein <em>besser</em> aussehendes Ergebnis hier das gefährliche? (d) Welche Prüfung hätte den Fehler sofort sichtbar gemacht — und warum darf sie nicht dieselben Bausteine benutzen wie das Modell?</p>
<spanid="cb8-7"><ahref="#cb8-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die Routing-Bibliothek von OR-Tools kennt keine "Kapazitaet" von sich aus.</span></span>
<spanid="cb8-8"><ahref="#cb8-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Sie kennt nur DIMENSIONEN - benannte Groessen, die sich entlang einer Tour</span></span>
<spanid="cb8-9"><ahref="#cb8-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">aufsummieren und begrenzt werden koennen. Distanz ist eine, Zeit ist eine,</span></span>
<spanid="cb8-10"><ahref="#cb8-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ladung ist eine. Wer eine davon nicht anlegt, bekommt trotzdem eine Loesung:</span></span>
<spanid="cb8-11"><ahref="#cb8-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">eine schoene, kurze, guenstige - und unfahrbare.</span></span>
<spanid="cb8-13"><ahref="#cb8-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">Dieses Programm loest dieselbe Instanz zweimal und prueft beide Ergebnisse</span></span>
<spanid="cb8-14"><ahref="#cb8-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">gegen die tatsaechlichen Lademengen.</span></span>
<spanid="cb8-16"><ahref="#cb8-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">Instanz: 1 Depot, 16 Kunden, 4 Fahrzeuge zu je 10 Paletten.</span></span>
<spanid="cb8-17"><ahref="#cb8-17"aria-hidden="true"tabindex="-1"></a><spanclass="co">Gesamtbedarf 37 Paletten bei 40 Paletten Flottenkapazitaet - es ist also</span></span>
<spanid="cb8-18"><ahref="#cb8-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">knapp, aber machbar.</span></span>
<spanid="cb8-44"><ahref="#cb8-44"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Loest die Tourenplanung - wahlweise mit oder ohne Ladungsdimension."""</span></span>
<spanid="cb8-55"><ahref="#cb8-55"aria-hidden="true"tabindex="-1"></a><spanclass="co"># DIE entscheidende Stelle. Ohne diesen Block existiert im Modell keine</span></span>
<spanid="cb8-56"><ahref="#cb8-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ladung - die Fahrzeuge sind dann unendlich gross.</span></span>
<spanid="cb8-101"><ahref="#cb8-101"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft den Plan gegen die Wirklichkeit - unabhaengig vom Modell.</span></span>
<spanid="cb8-103"><ahref="#cb8-103"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Genau diese Trennung ist der Punkt: Die Pruefung darf nicht dieselben</span></span>
<spanid="cb8-104"><ahref="#cb8-104"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Annahmen benutzen wie das Modell, sonst prueft sie nichts.</span></span>
<spanid="cb8-131"><ahref="#cb8-131"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>i <spanclass="op">+</span><spanclass="dv">1</span><spanclass="sc">:<10}</span><spanclass="ss"></span><spanclass="sc">{</span><spanclass="bu">len</span>(tour) <spanclass="op">-</span><spanclass="dv">2</span><spanclass="sc">:>7}</span><spanclass="ss"></span><spanclass="sc">{</span>strecke<spanclass="sc">:>8}</span><spanclass="ss"> km </span><spanclass="sc">{</span>ladung<spanclass="sc">:>8}</span><spanclass="ss">"</span></span>
<spanid="cb8-137"><ahref="#cb8-137"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> text <spanclass="kw">in</span> beanstandungen:</span>
<spanid="cb8-145"><ahref="#cb8-145"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DIE VERGESSENE DIMENSION"</span>)</span>
<spanid="cb8-148"><ahref="#cb8-148"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>ANZAHL_FAHRZEUGE<spanclass="sc">}</span><spanclass="ss"> Fahrzeuge zu je </span><spanclass="sc">{</span>KAPAZITAETEN[<spanclass="dv">0</span>]<spanclass="sc">}</span><spanclass="ss">"</span></span>
<spanid="cb8-151"><ahref="#cb8-151"aria-hidden="true"tabindex="-1"></a> ohne <spanclass="op">=</span> plane(mit_kapazitaet<spanclass="op">=</span><spanclass="va">False</span>)</span>
<spanid="cb8-152"><ahref="#cb8-152"aria-hidden="true"tabindex="-1"></a> zeige(<spanclass="st">"[1] Ohne Ladungsdimension"</span>, ohne)</span>
<spanid="cb8-154"><ahref="#cb8-154"aria-hidden="true"tabindex="-1"></a> mit <spanclass="op">=</span> plane(mit_kapazitaet<spanclass="op">=</span><spanclass="va">True</span>)</span>
<spanid="cb8-155"><ahref="#cb8-155"aria-hidden="true"tabindex="-1"></a> zeige(<spanclass="st">"[2] Mit AddDimensionWithVehicleCapacity"</span>, mit)</span>
<spanid="cb8-158"><ahref="#cb8-158"aria-hidden="true"tabindex="-1"></a> mehr <spanclass="op">=</span> mit[<spanclass="st">"gesamtstrecke"</span>] <spanclass="op">-</span> ohne[<spanclass="st">"gesamtstrecke"</span>]</span>
<spanid="cb8-159"><ahref="#cb8-159"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Der korrekte Plan ist </span><spanclass="sc">{</span>mehr<spanclass="sc">}</span><spanclass="ss"> km laenger "</span></span>
<spanid="cb8-162"><ahref="#cb8-162"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Und genau darin liegt die Gefahr: Lauf [1] sieht BESSER aus. Wer"</span>)</span>
<spanid="cb8-163"><ahref="#cb8-163"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"beide Zahlen nebeneinander legt, ohne die Ladung zu pruefen, haelt"</span>)</span>
<spanid="cb8-164"><ahref="#cb8-164"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"die unfahrbare Loesung fuer die bessere Optimierung - und den"</span>)</span>
<spanid="cb8-165"><ahref="#cb8-165"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"korrekten Plan fuer schlechte Arbeit."</span>)</span>
<spanid="cb8-167"><ahref="#cb8-167"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Routing-Bibliothek kennt keine 'Kapazitaet'. Sie kennt nur"</span>)</span>
<spanid="cb8-168"><ahref="#cb8-168"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Dimensionen, die man ihr anlegt. Was nicht als Dimension existiert,"</span>)</span>
<spanid="cb8-169"><ahref="#cb8-169"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"wird nicht begrenzt - und faellt niemandem auf, weil das Ergebnis"</span>)</span>
<p><strong>🎯 Merksatz</strong> Die Routing-Bibliothek kennt keine „Kapazität“, keine „Arbeitszeit“ und kein „Gewicht“. Sie kennt nur <strong>Dimensionen</strong> — benannte Größen, die sich entlang einer Tour aufsummieren und die man begrenzen kann. Was Sie nicht als Dimension anlegen, wird nicht begrenzt. Und der Solver sagt Ihnen das nicht: Er meldet stolz eine kürzere Strecke.</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler bei Routing-Modellen</strong></p>
<ul>
<li><strong>Eine Dimension vergessen.</strong> Ladung, Lenkzeit, Kühlkette, Gewicht <em>und</em> Volumen — jede Größe, die begrenzt ist, braucht ihre eigene Dimension. Zählen Sie sie vor dem Modellieren auf einem Blatt Papier auf.</li>
<li><strong>Die Prüfung aus denselben Bausteinen bauen wie das Modell.</strong> Wer die Ladung mit <code>loesung.Value(ladungs_dimension.CumulVar(...))</code> prüft, fragt das Modell, ob es sich an sich selbst hält. Rechnen Sie stattdessen aus der <strong>ausgegebenen Tour</strong> neu nach.</li>
<li><strong>Zwei Läufe nur an der Zielfunktion vergleichen.</strong> 326 gegen 572 km sagt nichts, solange nicht feststeht, dass beide Pläne überhaupt fahrbar sind.</li>
<li><strong>Unbenutzte Fahrzeuge übersehen.</strong> Drei Fahrzeuge, die im Depot stehen, während eines alles fährt, sind fast immer ein Zeichen für eine fehlende Beschränkung.</li>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="anhang-loesungen.html#quiz-loesung-graphen">Anhang A</a>.</p>
<p><strong>1. Sie lösen ein Zuordnungsproblem (12 Monteure, 12 Aufträge) als LP — ganz ohne Binärvariablen. Das Ergebnis ist trotzdem 0/1-wertig. Warum?</strong> (a) Zufall; bei anderen Daten kämen Brüche heraus. (b) Die Nebenbedingungsmatrix ist total unimodular, deshalb sind alle Ecken des zulässigen Bereichs ganzzahlig — und in einer Ecke liegt das LP-Optimum. (c) <code>linprog</code> rundet die Lösung intern.</p>
<p><strong>2. Sie ergänzen dasselbe Zuordnungsmodell um die Regel „höchstens 4 Monteure dürfen Überstunden machen“. Was ändert sich?</strong> (a) Nichts — die Struktur bleibt total unimodular. (b) Die Kardinalitätsbedingung passt nicht ins Schema; die totale Unimodularität geht verloren, die Relaxation kann Brüche liefern und Sie brauchen ein MILP. (c) Das Problem wird unlösbar.</p>
<p><strong>3. Ein Tourenplan nutzt nur eines von vier verfügbaren Fahrzeugen und ist trotzdem der kürzeste gefundene. Was prüfen Sie zuerst?</strong> (a) Ob das Zeitlimit zu knapp war. (b) Ob eine begrenzende Dimension (Ladung, Lenkzeit) im Modell fehlt — ein einzelnes Fahrzeug, das alles fährt, ist das typische Bild einer vergessenen Beschränkung. (c) Ob die Distanzmatrix symmetrisch ist.</p>
<li><strong>Graphen</strong> sind die natürliche Sprache für Transport-, Zuordnungs- und Routenprobleme.</li>
<li><strong>Flusserhaltung</strong> ist die Kirchhoff-Regel des Operations Research: Was hineingeht, kommt heraus — abzüglich des Knotensaldos.</li>
<li><strong>Totale Unimodularität</strong> macht Fluss- und Zuordnungsprobleme „von selbst“ ganzzahlig. Wer hier <code>integrality</code> setzt, verschenkt Laufzeit ohne Gegenwert.</li>
<li><strong>Spezialisierte Algorithmen schlagen allgemeine Solver</strong> deutlich: Der Ungarische Algorithmus löst in <spanclass="math inline">O(n^3)</span>, wofür ein MILP-Solver Branch-and-Bound bräuchte.</li>
<li><strong>Für Routing gilt: Nutzen Sie die Routing-Bibliothek.</strong> Metaheuristiken liefern in Sekunden sehr gute Touren; exakte Optimalität ist bei realistischen Größen unrealistisch und praktisch entbehrlich.</li>
<li><strong>Die Routing-Bibliothek kennt nur Dimensionen.</strong> Ladung, Lenkzeit, Kühlkette, Gewicht — jede begrenzte Größe braucht ihre eigene. Was nicht als Dimension angelegt ist, wird nicht begrenzt, und der Solver meldet stolz eine kürzere Strecke.</li>
<li><strong>Totale Unimodularität ist zerbrechlich.</strong> Eine einzige zusätzliche Bedingung, die nicht ins Schema passt — Kardinalität, Fixkosten, Mindestmenge —, zerstört sie. Prüfen Sie das, bevor Sie sich auf die Struktur verlassen.</li>
<li><strong>Der beste erste Zug ist selten Teil der besten Gesamtlösung.</strong> Gierige Regeln kosten schon bei vier Zuordnungen 8 %.</li>
</ul>
<p><strong>Ausblick.</strong><ahref="qp-nlp.html#teil-nichtlinear">Teil III</a> verlässt die lineare Welt. <ahref="qp-nlp.html#kap-qp-nlp">Kapitel 11</a> führt quadratische Zielfunktionen und die KKT-Bedingungen ein — das mathematische Fundament der Portfoliooptimierung.</p>