operations_research/OR_HTML_04/graphen.html

1077 lines
157 KiB
HTML
Raw Normal View History

Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><li data-kapitel="synthese-grundlagen.html"><a href="synthese-grundlagen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html"><a href="milp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><li data-kapitel="cpsat.html"><a href="cpsat.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><li data-kapitel="graphen.html" class="aktiv"><a href="graphen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><li data-kapitel="metaheuristiken.html"><a href="metaheuristiken.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><li data-kapitel="dekomposition.html"><a href="dekomposition.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><li data-kapitel="synthese-kernverfahren.html"><a href="synthese-kernverfahren.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil II</span> <span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="cpsat.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a><a class="prev-next-knopf prev-next-next" href="metaheuristiken.html"><span><small>Weiter</small>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="kap-graphen">Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</h1>
<div class="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> <a href="lp.html#kap-lp">Kapitel 5</a> und <a href="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>Programme:</strong><br />
<code>Min_Cost_Flow.py</code><br />
<code>Zuordnung_Ungarisch.py</code><br />
<code>VRP_Flotten_Routing.py</code><br />
<code>VRP_Kapazitaetsfalle.py</code></p>
Veroeffentlichung: Lizenzen, README, Colab-Entscheidung, JupyterLab im Image Sechs Dinge, alle fuer das jetzt oeffentliche Repository. LIZENZEN. LICENSE traegt die MIT-Lizenz fuer den Programmcode (alle .py, Notebooks, Dockerfile, pyproject.toml), LICENSE-TEXT.md die CC BY-SA 4.0 fuer Buchtext, PDF, Website und Diagramme. Beide Dateien nennen ausdruecklich, was sie abdecken und was nicht - ein Codeblock im Buchtext bleibt Programmcode und steht unter MIT. Vorher hatte das Repository gar keine Lizenz, womit standardmaessig "alle Rechte vorbehalten" galt und niemand die 76 Programme haette weiterverwenden duerfen. COLAB. COLAB_BASIS_URL steht jetzt auf "". Der Platzhalter zeigte auf GitHub, das Repository liegt auf einer eigenen Forgejo-Instanz - und Colab oeffnet Notebooks NUR aus GitHub, Google Drive oder einem Upload. Die URL-Form colab.research.google.com/github/... ist fest auf GitHub verdrahtet; eine selbstgehostete Adresse dort einzusetzen ergaebe keinen Link zum eigenen Server, sondern einen toten GitHub-Link. Der Kommentar im Quelltext ging von GitHub aus und war damit selbst irrefuehrend; er ist ersetzt. Verloren geht nichts: Die 25 Notebooks liegen neben der Website und bekommen einen echten Download-Link, jetzt mit dem Hinweis, was man damit tut - "herunterladen und in Jupyter oeffnen, in Colab hochladen oder mit dem Kurs-Image starten". JUPYTERLAB IM KURS-IMAGE. Neue pyproject-Gruppe [notebook] mit jupyterlab, die Notebooks werden ins Image kopiert, und ein kleiner Startbefehl macht beide Betriebsarten ohne --entrypoint moeglich: ohne Argument JupyterLab, mit Argument ein einzelnes Programm. Gebaut und geprueft - Rucksack.py laeuft, JupyterLab antwortet mit HTTP 200 und zeigt alle 25 Notebooks. Image 1,31 -> 1,46 GB. Es laeuft ohne Token, deshalb im README die Portfreigabe an 127.0.0.1 gebunden. README KOMPLETT NEU. Es war die Bau-Anleitung eines privaten Verzeichnisses und ist jetzt die Visitenkarte eines oeffentlichen Repositorys: was das Buch ist, wo man es liest, drei Wege die Beispiele auszufuehren (Container, schlanke Installation, alles auf einmal), was hier liegt, wie man baut, die Colab-Frage, die Lizenzen und wie man mitwirkt. Alle relativen Links geprueft: 0 tot. .gitattributes. Ohne die Datei entschied core.autocrlf des jeweiligen Rechners, was beim Klonen passiert - ein Windows-Leser bekam CRLF-Rauschen in jedem Diff. Jetzt: im Repository immer LF, im Arbeitsverzeichnis passend zum System, Binaerdateien ausdruecklich ausgenommen. Der Bestand war bereits sauber (git add --renormalize aendert null Dateien). Zusaetzlich sind die erzeugten Verzeichnisse als linguist-generated markiert, sonst zaehlt die Sprachstatistik das Repository als HTML-Projekt. PROGRESS.md: Remote-Repository als erledigt markiert, der Colab-Befund festgehalten. CLAUDE.md um Veroeffentlichung, Lizenzen und die neuen Dateien ergaenzt. Geprueft: --check ohne Fehler, 0 tote README-Links, 76 Programme unveraendert, 33 pytest-Tests, pyproject baut mit acht Gruppen, PDF 759 Seiten (eine weniger - die Colab-Zeile entfaellt in 25 Kapiteln). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 14:54:04 +02:00
<p><strong>Notebook:</strong> <a href="Notebooks_04/graphen.ipynb">graphen.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
</blockquote>
</div>
<hr />
<h2 id="sec:graphen-schnellstart">8.1 In 5 Minuten gelöst</h2>
<div class="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>
<table>
<thead>
<tr class="header">
<th></th>
<th style="text-align: right;">Nord</th>
<th style="text-align: right;">Ost</th>
<th style="text-align: right;">Süd</th>
<th style="text-align: right;">West</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Bauer</strong></td>
<td style="text-align: right;">21</td>
<td style="text-align: right;">45</td>
<td style="text-align: right;">33</td>
<td style="text-align: right;">60</td>
</tr>
<tr class="even">
<td><strong>Cakir</strong></td>
<td style="text-align: right;">18</td>
<td style="text-align: right;">52</td>
<td style="text-align: right;">29</td>
<td style="text-align: right;">47</td>
</tr>
<tr class="odd">
<td><strong>Diaz</strong></td>
<td style="text-align: right;">40</td>
<td style="text-align: right;">24</td>
<td style="text-align: right;">55</td>
<td style="text-align: right;">38</td>
</tr>
<tr class="even">
<td><strong>Engel</strong></td>
<td style="text-align: right;">35</td>
<td style="text-align: right;">31</td>
<td style="text-align: right;">26</td>
<td style="text-align: right;">44</td>
</tr>
</tbody>
</table>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linear_sum_assignment</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>kosten <span class="op">=</span> np.array([[<span class="dv">21</span>, <span class="dv">45</span>, <span class="dv">33</span>, <span class="dv">60</span>], <span class="co"># Bauer</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a> [<span class="dv">18</span>, <span class="dv">52</span>, <span class="dv">29</span>, <span class="dv">47</span>], <span class="co"># Cakir</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a> [<span class="dv">40</span>, <span class="dv">24</span>, <span class="dv">55</span>, <span class="dv">38</span>], <span class="co"># Diaz</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a> [<span class="dv">35</span>, <span class="dv">31</span>, <span class="dv">26</span>, <span class="dv">44</span>]]) <span class="co"># Engel</span></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>monteur <span class="op">=</span> [<span class="st">&quot;Bauer&quot;</span>, <span class="st">&quot;Cakir&quot;</span>, <span class="st">&quot;Diaz&quot;</span>, <span class="st">&quot;Engel&quot;</span>]</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>ort <span class="op">=</span> [<span class="st">&quot;Nord&quot;</span>, <span class="st">&quot;Ost&quot;</span>, <span class="st">&quot;Sued&quot;</span>, <span class="st">&quot;West&quot;</span>]</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>zeile, spalte <span class="op">=</span> linear_sum_assignment(kosten)</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> z, s <span class="kw">in</span> <span class="bu">zip</span>(zeile, spalte):</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>monteur[z]<span class="sc">:6}</span><span class="ss"> -&gt; </span><span class="sc">{</span>ort[s]<span class="sc">:5}</span><span class="ss"> (</span><span class="sc">{</span>kosten[z, s]<span class="sc">}</span><span class="ss"> min)&quot;</span>)</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="st">&quot;Gesamtfahrzeit:&quot;</span>, kosten[zeile, spalte].<span class="bu">sum</span>(), <span class="st">&quot;min&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Bauer -&gt; Nord (21 min)
Cakir -&gt; Sued (29 min)
Diaz -&gt; Ost (24 min)
Engel -&gt; West (44 min)
Gesamtfahrzeit: 118 min</code></pre>
</blockquote>
</div>
<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>
<col style="width: 30%" />
<col style="width: 30%" />
<col style="width: 40%" />
</colgroup>
<thead>
<tr class="header">
<th>Vorgehen</th>
<th>Zuordnung</th>
<th style="text-align: right;">Gesamtzeit</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Gierig („immer das billigste freie Paar“)</td>
<td>Cakir→Nord, Diaz→Ost, Engel→Süd, Bauer→West</td>
<td style="text-align: right;"><strong>128 min</strong></td>
</tr>
<tr class="even">
<td><strong>Ungarische Methode</strong></td>
<td>Bauer→Nord, Cakir→Süd, Diaz→Ost, Engel→West</td>
<td style="text-align: right;"><strong>118 min</strong></td>
</tr>
</tbody>
</table>
<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 <a href="#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 />
<h2 id="sec:graphen-lernziele">8.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="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>… begründen, warum Zuordnungsprobleme <strong>ohne</strong> Ganzzahligkeitsbedingung ganzzahlig lösbar sind (totale Unimodularität).</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 />
<h2 id="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> <span class="math inline">G = (V, E)</span>:</p>
<ul>
<li><span class="math inline">V</span> — die <strong>Knoten</strong> (<em>vertices</em>): Server, Depots, Kunden, Konten, Lager.</li>
<li><span class="math inline">E</span> — die gerichteten <strong>Kanten</strong> (<em>edges</em>): Datenleitungen, Straßen, Überweisungswege.</li>
</ul>
<h3 id="das-minimum-cost-flow-problem-mcnfp">Das Minimum-Cost-Flow-Problem (MCNFP)</h3>
<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>
<img src="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." />
<figcaption aria-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 <span class="math inline">x_{ij} \ge 0</span> der Fluss über Kante <span class="math inline">(i,j)</span>, <span class="math inline">c_{ij}</span> die Kosten je Einheit und <span class="math inline">u_{ij}</span> die Kapazität:</p>
<p><span class="math display">
\min \sum_{(i,j)\in E} c_{ij}\,x_{ij}
</span></p>
<p><span class="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><span class="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 <span class="math inline">i</span> <strong>hinaus</strong>fließt. * Zweite Summe: alles, was in Knoten <span class="math inline">i</span> <strong>hinein</strong>fließt. * <span class="math inline">b_i</span> — der Saldo des Knotens.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th><span class="math inline">b_i</span></th>
<th>Knotentyp</th>
<th>Bedeutung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">b_i &gt; 0</span></td>
<td><strong>Quelle</strong></td>
<td>Hier entsteht Ware (Angebot)</td>
</tr>
<tr class="even">
<td><span class="math inline">b_i &lt; 0</span></td>
<td><strong>Senke</strong></td>
<td>Hier verschwindet Ware (Bedarf)</td>
</tr>
<tr class="odd">
<td><span class="math inline">b_i = 0</span></td>
<td><strong>Umladeknoten</strong> (<em>transshipment</em>)</td>
<td>Was hineingeht, muss wieder hinaus</td>
</tr>
</tbody>
</table>
<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 <span class="math inline">\sum_i b_i = 0</span> gelten — das Gesamtangebot muss dem Gesamtbedarf entsprechen.</p>
</blockquote>
<div class="sourceCode" id="cb3"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb3-1"><a href="#cb3-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb3-2"><a href="#cb3-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-3"><a href="#cb3-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Min_Cost_Flow.py</span></span>
<span id="cb3-4"><a href="#cb3-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-5"><a href="#cb3-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Graphen: Kostenminimaler Fluss durch ein Netzwerk.</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">Loest dasselbe Problem zweimal:</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co"> (1) als allgemeines LP mit scipy -&gt; zeigt die Modellstruktur</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="co"> (2) mit dem spezialisierten Netzwerk-Solver von OR-Tools -&gt; zeigt den</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="co"> Geschwindigkeitsvorteil eines Verfahrens, das die Struktur ausnutzt</span></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="co">Beide laufen in eigenen Prozessen nicht noetig: scipy und ortools vertragen</span></span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a><span class="co">sich (nur ortools + highspy kollidieren, siehe Kapitel Oekosystem).</span></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Netzwerk definieren ---------------------------------------------------</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a>KNOTEN <span class="op">=</span> [<span class="st">&quot;Werk_A&quot;</span>, <span class="st">&quot;Werk_B&quot;</span>, <span class="st">&quot;Umschlag&quot;</span>, <span class="st">&quot;Kunde_1&quot;</span>, <span class="st">&quot;Kunde_2&quot;</span>]</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a><span class="co"># (von, nach, Kosten je Einheit, Kapazitaet)</span></span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a>KANTEN <span class="op">=</span> [</span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Werk_A&quot;</span>, <span class="st">&quot;Umschlag&quot;</span>, <span class="fl">2.0</span>, <span class="dv">15</span>),</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Werk_A&quot;</span>, <span class="st">&quot;Kunde_1&quot;</span>, <span class="fl">5.0</span>, <span class="dv">10</span>),</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Werk_B&quot;</span>, <span class="st">&quot;Umschlag&quot;</span>, <span class="fl">4.0</span>, <span class="dv">10</span>),</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Werk_B&quot;</span>, <span class="st">&quot;Kunde_2&quot;</span>, <span class="fl">6.0</span>, <span class="dv">10</span>),</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Umschlag&quot;</span>, <span class="st">&quot;Kunde_1&quot;</span>, <span class="fl">1.0</span>, <span class="dv">20</span>),</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Umschlag&quot;</span>, <span class="st">&quot;Kunde_2&quot;</span>, <span class="fl">3.0</span>, <span class="dv">10</span>),</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a>]</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a><span class="co"># Angebot (+) bzw. Bedarf (-) je Knoten</span></span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a>SALDO <span class="op">=</span> {<span class="st">&quot;Werk_A&quot;</span>: <span class="dv">20</span>, <span class="st">&quot;Werk_B&quot;</span>: <span class="dv">10</span>, <span class="st">&quot;Umschlag&quot;</span>: <span class="dv">0</span>, <span class="st">&quot;Kunde_1&quot;</span>: <span class="op">-</span><span class="dv">15</span>, <span class="st">&quot;Kunde_2&quot;</span>: <span class="op">-</span><span class="dv">15</span>}</span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_als_lp():</span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Flussproblem als allgemeines lineares Programm.&quot;&quot;&quot;</span></span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> n_kanten <span class="op">=</span> <span class="bu">len</span>(KANTEN)</span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> knoten_index <span class="op">=</span> {k: i <span class="cf">for</span> i, k <span class="kw">in</span> <span class="bu">enumerate</span>(KNOTEN)}</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zielfunktion: Summe der Transportkosten</span></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> np.array([k[<span class="dv">2</span>] <span class="cf">for</span> k <span class="kw">in</span> KANTEN])</span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="co"># Flusserhaltung als Gleichungssystem: A_eq @ x = b_eq</span></span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a> A_eq <span class="op">=</span> np.zeros((<span class="bu">len</span>(KNOTEN), n_kanten))</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> e, (von, nach, _, _) <span class="kw">in</span> <span class="bu">enumerate</span>(KANTEN):</span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> A_eq[knoten_index[von], e] <span class="op">=</span> <span class="op">+</span><span class="fl">1.0</span> <span class="co"># fliesst hinaus</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> A_eq[knoten_index[nach], e] <span class="op">=</span> <span class="op">-</span><span class="fl">1.0</span> <span class="co"># fliesst hinein</span></span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a> b_eq <span class="op">=</span> np.array([SALDO[k] <span class="cf">for</span> k <span class="kw">in</span> KNOTEN], dtype<span class="op">=</span><span class="bu">float</span>)</span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a> schranken <span class="op">=</span> [(<span class="dv">0</span>, k[<span class="dv">3</span>]) <span class="cf">for</span> k <span class="kw">in</span> KANTEN] <span class="co"># 0 &lt;= x_ij &lt;= u_ij</span></span>
<span id="cb3-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=</span>kosten, A_eq<span class="op">=</span>A_eq, b_eq<span class="op">=</span>b_eq, bounds<span class="op">=</span>schranken, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> ergebnis.success:</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Nicht loesbar: </span><span class="sc">{</span>ergebnis<span class="sc">.</span>message<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> ergebnis.fun, ergebnis.x, ergebnis.eqlin.marginals</span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a> <span class="co"># Vorabpruefung: Angebot muss Bedarf entsprechen</span></span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a> gesamt <span class="op">=</span> <span class="bu">sum</span>(SALDO.values())</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; KOSTENMINIMALER FLUSS DURCH EIN TRANSPORTNETZ&quot;</span>)</span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Angebot gesamt: </span><span class="sc">{</span><span class="bu">sum</span>(v <span class="cf">for</span> v <span class="kw">in</span> SALDO.values() <span class="cf">if</span> v <span class="op">&gt;</span> <span class="dv">0</span>)<span class="sc">}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Bedarf gesamt: </span><span class="sc">{</span><span class="op">-</span><span class="bu">sum</span>(v <span class="cf">for</span> v <span class="kw">in</span> SALDO.values() <span class="cf">if</span> v <span class="op">&lt;</span> <span class="dv">0</span>)<span class="sc">}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Saldo: </span><span class="sc">{</span>gesamt<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> gesamt <span class="op">!=</span> <span class="dv">0</span>:</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="st">&quot;Angebot und Bedarf stimmen nicht ueberein - unloesbar!&quot;</span>)</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a> kosten_gesamt, fluss, knotenpreise <span class="op">=</span> loese_als_lp()</span>
<span id="cb3-70"><a href="#cb3-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Minimale Transportkosten: </span><span class="sc">{</span>kosten_gesamt<span class="sc">:,.2f}</span><span class="ss"> EUR</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Kante&#39;</span><span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Fluss&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kapazitaet&#39;</span><span class="sc">:&gt;11}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten/E&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Kosten&#39;</span><span class="sc">:&gt;9}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> e, (von, nach, c, u) <span class="kw">in</span> <span class="bu">enumerate</span>(KANTEN):</span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> menge <span class="op">=</span> fluss[e] <span class="op">+</span> <span class="fl">0.0</span> <span class="cf">if</span> <span class="bu">abs</span>(fluss[e]) <span class="op">&gt;</span> <span class="fl">1e-9</span> <span class="cf">else</span> <span class="fl">0.0</span> <span class="co"># vermeidet &quot;-0.0&quot;</span></span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> ausgelastet <span class="op">=</span> <span class="st">&quot; (VOLL)&quot;</span> <span class="cf">if</span> <span class="bu">abs</span>(menge <span class="op">-</span> u) <span class="op">&lt;</span> <span class="fl">1e-6</span> <span class="cf">else</span> <span class="st">&quot;&quot;</span></span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>von <span class="op">+</span> <span class="st">&#39; -&gt; &#39;</span> <span class="op">+</span> nach<span class="sc">:&lt;24}</span><span class="ss"> </span><span class="sc">{</span>menge<span class="sc">:&gt;7.1f}</span><span class="ss"> </span><span class="sc">{</span>u<span class="sc">:&gt;11}</span><span class="ss"> &quot;</span></span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>c<span class="sc">:&gt;9.2f}</span><span class="ss"> </span><span class="sc">{</span>menge <span class="op">*</span> c<span class="sc">:&gt;9.2f}{</span>ausgelastet<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-79"><a href="#cb3-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Flusserhaltung nachpruefen --------------------------------------</span></span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Pruefung der Flusserhaltung je Knoten ---&quot;</span>)</span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> KNOTEN:</span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a> hinaus <span class="op">=</span> <span class="bu">sum</span>(fluss[e] <span class="cf">for</span> e, (v, n, _, _) <span class="kw">in</span> <span class="bu">enumerate</span>(KANTEN) <span class="cf">if</span> v <span class="op">==</span> k)</span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> hinein <span class="op">=</span> <span class="bu">sum</span>(fluss[e] <span class="cf">for</span> e, (v, n, _, _) <span class="kw">in</span> <span class="bu">enumerate</span>(KANTEN) <span class="cf">if</span> n <span class="op">==</span> k)</span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> netto <span class="op">=</span> hinaus <span class="op">-</span> hinein</span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a> art <span class="op">=</span> <span class="st">&quot;Quelle&quot;</span> <span class="cf">if</span> SALDO[k] <span class="op">&gt;</span> <span class="dv">0</span> <span class="cf">else</span> (<span class="st">&quot;Senke&quot;</span> <span class="cf">if</span> SALDO[k] <span class="op">&lt;</span> <span class="dv">0</span> <span class="cf">else</span> <span class="st">&quot;Umschlag&quot;</span>)</span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>k<span class="sc">:&lt;10}</span><span class="ss"> (</span><span class="sc">{</span>art<span class="sc">:&lt;8}</span><span class="ss">): hinaus </span><span class="sc">{</span>hinaus<span class="sc">:5.1f}</span><span class="ss"> - hinein </span><span class="sc">{</span>hinein<span class="sc">:5.1f}</span><span class="ss"> &quot;</span></span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;= </span><span class="sc">{</span>netto<span class="sc">:+6.1f}</span><span class="ss"> (gefordert: </span><span class="sc">{</span>SALDO[k]<span class="sc">:+d}</span><span class="ss">)&quot;</span>)</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> <span class="bu">abs</span>(netto <span class="op">-</span> SALDO[k]) <span class="op">&lt;</span> <span class="fl">1e-6</span>, <span class="ss">f&quot;Flusserhaltung verletzt bei </span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">!&quot;</span></span>
<span id="cb3-90"><a href="#cb3-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-91"><a href="#cb3-91" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Knotenpreise (Dualwerte) interpretieren -------------------------</span></span>
<span id="cb3-92"><a href="#cb3-92" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Knotenpreise (Dualwerte der Flusserhaltung) ---&quot;</span>)</span>
<span id="cb3-93"><a href="#cb3-93" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Differenz zweier Knotenpreise = Grenzkosten einer zusaetzlichen Einheit&quot;</span>)</span>
<span id="cb3-94"><a href="#cb3-94" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; auf dem guenstigsten Weg zwischen ihnen.&quot;</span>)</span>
<span id="cb3-95"><a href="#cb3-95" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k, preis <span class="kw">in</span> <span class="bu">zip</span>(KNOTEN, knotenpreise):</span>
<span id="cb3-96"><a href="#cb3-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>k<span class="sc">:&lt;10}</span><span class="ss">: </span><span class="sc">{</span>preis<span class="sc">:7.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-97"><a href="#cb3-97" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
KOSTENMINIMALER FLUSS DURCH EIN TRANSPORTNETZ
==============================================================================
Angebot gesamt: 30 | Bedarf gesamt: 30 | Saldo: 0
Minimale Transportkosten: 140.00 EUR
Kante Fluss Kapazitaet Kosten/E Kosten
------------------------------------------------------------------------------
Werk_A -&gt; Umschlag 15.0 15 2.00 30.00 (VOLL)
Werk_A -&gt; Kunde_1 5.0 10 5.00 25.00
Werk_B -&gt; Umschlag 0.0 10 4.00 0.00
Werk_B -&gt; Kunde_2 10.0 10 6.00 60.00 (VOLL)
Umschlag -&gt; Kunde_1 10.0 20 1.00 10.00
Umschlag -&gt; Kunde_2 5.0 10 3.00 15.00
--- Pruefung der Flusserhaltung je Knoten ---
Werk_A (Quelle ): hinaus 20.0 - hinein 0.0 = +20.0 (gefordert: +20)
Werk_B (Quelle ): hinaus 10.0 - hinein 0.0 = +10.0 (gefordert: +10)
Umschlag (Umschlag): hinaus 15.0 - hinein 15.0 = +0.0 (gefordert: +0)
Kunde_1 (Senke ): hinaus 0.0 - hinein 15.0 = -15.0 (gefordert: -15)
Kunde_2 (Senke ): hinaus 0.0 - hinein 15.0 = -15.0 (gefordert: -15)</code></pre>
<p>Zwei Beobachtungen:</p>
<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: <span class="math inline">B \to \text{Umschlag} \to \text{Kunde 2}</span> kostet <span class="math inline">4 + 3 = 7</span> je Einheit, der direkte Weg nur 6. Werk A dagegen nutzt den Umschlag intensiv, weil <span class="math inline">2 + 1 = 3</span> nach Kunde 1 deutlich günstiger ist als der direkte Weg mit 5 — und die günstige Kante <span class="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 />
<h2 id="sec:graphen-bipartites-matching-das-zuordnungsproblem">8.4 Bipartites Matching: das Zuordnungsproblem</h2>
<p>Wenn <span class="math inline">N</span> Aufgaben auf <span class="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>
<p><span class="math display">
\min \sum_{i=1}^N \sum_{j=1}^N c_{ij}\,x_{ij}
\qquad\text{u. d. N.}\qquad
\sum_j x_{ij} = 1\ \forall i,\qquad \sum_i x_{ij} = 1\ \forall j,\qquad x_{ij}\ge0
</span></p>
<h3 id="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 <span class="math inline">= 1</span>, alle Spaltensummen <span class="math inline">= 1</span>, <span class="math inline">x_{ij} \ge 0</span>) sind <strong>genau</strong> die Permutationsmatrizen (alle <span class="math inline">x_{ij} \in \{0,1\}</span>).</p>
</blockquote>
<p><strong>Warum das praktisch enorm wichtig ist:</strong> Nach dem Fundamentalsatz aus <a href="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 <span class="math inline">0</span>, <span class="math inline">+1</span> oder <span class="math inline">-1</span> hat. Dieselbe Eigenschaft besitzt übrigens auch die Flusserhaltungsmatrix aus <a href="#sec:graphen-graphen-als-modellsprache">Abschnitt 8.3</a> — deshalb sind Netzwerkflüsse ebenfalls „von selbst“ ganzzahlig.</p>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer: totale Unimodularität</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">\det(\mathbf{B}) \in \{0, +1, -1\}</span> für <strong>jede</strong> quadratische Teilmatrix <span class="math inline">\mathbf{B}</span> von <span class="math inline">\mathbf{A}</span></td>
<td>„Die Matrix ist so gebaut, dass beim Lösen nie ein echter Bruch entstehen kann.“</td>
</tr>
<tr class="even">
<td><span class="math inline">\mathbf{b}</span> ganzzahlig <span class="math inline">\Rightarrow</span> alle Ecken von <span class="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>
<tr class="odd">
<td>zusammen mit dem Fundamentalsatz (<a href="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>
</blockquote>
</div>
<div class="sourceCode" id="cb5"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb5-1"><a href="#cb5-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb5-2"><a href="#cb5-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-3"><a href="#cb5-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Zuordnung_Ungarisch.py</span></span>
<span id="cb5-4"><a href="#cb5-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-5"><a href="#cb5-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Graphen: Das Zuordnungsproblem, dreifach geloest.</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="co"> (1) Ungarischer Algorithmus (scipy.optimize.linear_sum_assignment) - O(n^3)</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co"> (2) als LP OHNE Ganzzahligkeitsforderung -&gt; liefert trotzdem 0/1 (Birkhoff)</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="co"> (3) als MILP MIT Ganzzahligkeitsforderung -&gt; gleiches Ergebnis, mehr Aufwand</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="co">Zeigt damit die praktische Bedeutung der totalen Unimodularitaet.</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linear_sum_assignment, linprog</span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> erzeuge_kosten(n, seed<span class="op">=</span><span class="dv">11</span>):</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> rng.integers(<span class="dv">10</span>, <span class="dv">99</span>, size<span class="op">=</span>(n, n)).astype(<span class="bu">float</span>)</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_ungarisch(kosten):</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> zeilen, spalten <span class="op">=</span> linear_sum_assignment(kosten)</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> kosten[zeilen, spalten].<span class="bu">sum</span>(), spalten</span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_lp(kosten):</span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Gemeinsame LP-Struktur fuer Variante 2 und 3.&quot;&quot;&quot;</span></span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(kosten)</span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> kosten.flatten() <span class="co"># x_ij in Zeilenreihenfolge</span></span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> A_eq <span class="op">=</span> np.zeros((<span class="dv">2</span> <span class="op">*</span> n, n <span class="op">*</span> n))</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n): <span class="co"># jede Person genau eine Aufgabe</span></span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a> A_eq[i, i <span class="op">*</span> n:(i <span class="op">+</span> <span class="dv">1</span>) <span class="op">*</span> n] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n): <span class="co"># jede Aufgabe genau einer Person</span></span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a> A_eq[n <span class="op">+</span> j, j::n] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a> b_eq <span class="op">=</span> np.ones(<span class="dv">2</span> <span class="op">*</span> n)</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> c, A_eq, b_eq</span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_lp(kosten, ganzzahlig):</span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(kosten)</span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a> c, A_eq, b_eq <span class="op">=</span> baue_lp(kosten)</span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=</span>c, A_eq<span class="op">=</span>A_eq, b_eq<span class="op">=</span>b_eq, bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="dv">1</span>)] <span class="op">*</span> (n <span class="op">*</span> n),</span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>np.ones(n <span class="op">*</span> n) <span class="cf">if</span> ganzzahlig <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> ergebnis.x.reshape(n, n)</span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> ergebnis.fun, x</span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-52"><a href="#cb5-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-53"><a href="#cb5-53" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; ZUORDNUNGSPROBLEM: DREI WEGE ZUM SELBEN ERGEBNIS&quot;</span>)</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Kleines Beispiel zum Nachvollziehen ------------------------------</span></span>
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> np.array([[<span class="fl">82.</span>, <span class="fl">83.</span>, <span class="fl">69.</span>, <span class="fl">92.</span>],</span>
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a> [<span class="fl">77.</span>, <span class="fl">37.</span>, <span class="fl">49.</span>, <span class="fl">92.</span>],</span>
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> [<span class="fl">11.</span>, <span class="fl">69.</span>, <span class="fl">5.</span>, <span class="fl">86.</span>],</span>
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a> [<span class="fl">8.</span>, <span class="fl">9.</span>, <span class="fl">98.</span>, <span class="fl">23.</span>]])</span>
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a> namen <span class="op">=</span> [<span class="st">&quot;Anna&quot;</span>, <span class="st">&quot;Ben&quot;</span>, <span class="st">&quot;Carla&quot;</span>, <span class="st">&quot;David&quot;</span>]</span>
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a> aufgaben <span class="op">=</span> [<span class="st">&quot;Auftrag W&quot;</span>, <span class="st">&quot;Auftrag X&quot;</span>, <span class="st">&quot;Auftrag Y&quot;</span>, <span class="st">&quot;Auftrag Z&quot;</span>]</span>
<span id="cb5-65"><a href="#cb5-65" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-66"><a href="#cb5-66" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Kostenmatrix (wer bearbeitet was zu welchen Kosten?):&quot;</span>)</span>
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;8}</span><span class="ss">&quot;</span> <span class="op">+</span> <span class="st">&quot;&quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>a<span class="sc">:&gt;12}</span><span class="ss">&quot;</span> <span class="cf">for</span> a <span class="kw">in</span> aufgaben))</span>
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(namen):</span>
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;8}</span><span class="ss">&quot;</span> <span class="op">+</span> <span class="st">&quot;&quot;</span>.join(<span class="ss">f&quot;</span><span class="sc">{</span>kosten[i, j]<span class="sc">:&gt;12.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">4</span>)))</span>
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a> wert, zuordnung <span class="op">=</span> loese_ungarisch(kosten)</span>
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Optimale Zuordnung (Gesamtkosten </span><span class="sc">{</span>wert<span class="sc">:.0f}</span><span class="ss">):&quot;</span>)</span>
<span id="cb5-73"><a href="#cb5-73" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, j <span class="kw">in</span> <span class="bu">enumerate</span>(zuordnung):</span>
<span id="cb5-74"><a href="#cb5-74" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>namen[i]<span class="sc">:&lt;8}</span><span class="ss"> -&gt; </span><span class="sc">{</span>aufgaben[j]<span class="sc">:&lt;12}</span><span class="ss"> (</span><span class="sc">{</span>kosten[i, j]<span class="sc">:.0f}</span><span class="ss"> EUR)&quot;</span>)</span>
<span id="cb5-75"><a href="#cb5-75" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-76"><a href="#cb5-76" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Nachweis: LP ohne Ganzzahligkeit liefert trotzdem 0/1 -----------</span></span>
<span id="cb5-77"><a href="#cb5-77" aria-hidden="true" tabindex="-1"></a> wert_lp, x_lp <span class="op">=</span> loese_lp(kosten, ganzzahlig<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb5-78"><a href="#cb5-78" aria-hidden="true" tabindex="-1"></a> ist_binaer <span class="op">=</span> np.<span class="bu">all</span>((np.<span class="bu">abs</span>(x_lp) <span class="op">&lt;</span> <span class="fl">1e-9</span>) <span class="op">|</span> (np.<span class="bu">abs</span>(x_lp <span class="op">-</span> <span class="dv">1</span>) <span class="op">&lt;</span> <span class="fl">1e-9</span>))</span>
<span id="cb5-79"><a href="#cb5-79" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">LP OHNE Ganzzahligkeitsforderung: Kosten </span><span class="sc">{</span>wert_lp<span class="sc">:.0f}</span><span class="ss">, &quot;</span></span>
<span id="cb5-80"><a href="#cb5-80" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Loesung ist </span><span class="sc">{</span><span class="st">&#39;0/1-wertig&#39;</span> <span class="cf">if</span> ist_binaer <span class="cf">else</span> <span class="st">&#39;GEBROCHEN&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-81"><a href="#cb5-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; Satz von Birkhoff/von Neumann bestaetigt: Die Ecken sind Permutationen.&quot;</span>)</span>
<span id="cb5-82"><a href="#cb5-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-83"><a href="#cb5-83" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Laufzeitvergleich bei wachsender Groesse ------------------------</span></span>
<span id="cb5-84"><a href="#cb5-84" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb5-85"><a href="#cb5-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;n&#39;</span><span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Ungarisch&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;LP (kontinuierlich)&#39;</span><span class="sc">:&gt;21}</span><span class="ss"> | &quot;</span></span>
<span id="cb5-86"><a href="#cb5-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;MILP (ganzzahlig)&#39;</span><span class="sc">:&gt;19}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;gleich?&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-87"><a href="#cb5-87" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb5-88"><a href="#cb5-88" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n <span class="kw">in</span> [<span class="dv">10</span>, <span class="dv">25</span>, <span class="dv">50</span>, <span class="dv">100</span>]:</span>
<span id="cb5-89"><a href="#cb5-89" aria-hidden="true" tabindex="-1"></a> k <span class="op">=</span> erzeuge_kosten(n)</span>
<span id="cb5-90"><a href="#cb5-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-91"><a href="#cb5-91" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()<span class="op">;</span> w1, _ <span class="op">=</span> loese_ungarisch(k)<span class="op">;</span> t1 <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb5-92"><a href="#cb5-92" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()<span class="op">;</span> w2, _ <span class="op">=</span> loese_lp(k, <span class="va">False</span>)<span class="op">;</span> t2 <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb5-93"><a href="#cb5-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> n <span class="op">&lt;=</span> <span class="dv">50</span>:</span>
<span id="cb5-94"><a href="#cb5-94" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()<span class="op">;</span> w3, _ <span class="op">=</span> loese_lp(k, <span class="va">True</span>)<span class="op">;</span> t3 <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb5-95"><a href="#cb5-95" aria-hidden="true" tabindex="-1"></a> t3_text, gleich <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span>t3<span class="op">*</span><span class="dv">1000</span><span class="sc">:&gt;16.1f}</span><span class="ss"> ms&quot;</span>, <span class="bu">abs</span>(w1 <span class="op">-</span> w3) <span class="op">&lt;</span> <span class="fl">1e-6</span></span>
<span id="cb5-96"><a href="#cb5-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb5-97"><a href="#cb5-97" aria-hidden="true" tabindex="-1"></a> t3_text, gleich <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;uebersprungen&#39;</span><span class="sc">:&gt;19}</span><span class="ss">&quot;</span>, <span class="bu">abs</span>(w1 <span class="op">-</span> w2) <span class="op">&lt;</span> <span class="fl">1e-6</span></span>
<span id="cb5-98"><a href="#cb5-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-99"><a href="#cb5-99" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>n<span class="sc">:&gt;4}</span><span class="ss"> | </span><span class="sc">{</span>t1<span class="op">*</span><span class="dv">1000</span><span class="sc">:&gt;9.1f}</span><span class="ss"> ms | </span><span class="sc">{</span>t2<span class="op">*</span><span class="dv">1000</span><span class="sc">:&gt;18.1f}</span><span class="ss"> ms | </span><span class="sc">{</span>t3_text<span class="sc">}</span><span class="ss"> | &quot;</span></span>
<span id="cb5-100"><a href="#cb5-100" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;ja&#39;</span> <span class="cf">if</span> gleich <span class="cf">else</span> <span class="st">&#39;NEIN&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-101"><a href="#cb5-101" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-102"><a href="#cb5-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb5-103"><a href="#cb5-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Fazit: Der spezialisierte Ungarische Algorithmus ist um Groessenordnungen&quot;</span>)</span>
<span id="cb5-104"><a href="#cb5-104" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;schneller. Nutzen Sie fuer reine Zuordnungen NIE einen MILP-Solver.&quot;</span>)</span>
<span id="cb5-105"><a href="#cb5-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span></code></pre></div>
<blockquote>
<p><strong>💻 Code-Durchgang: die Indexakrobatik</strong></p>
<p>Die Variable <span class="math inline">x_{ij}</span> wird zu einem flachen Vektor der Länge <span class="math inline">n^2</span> aufgerollt: Position von <span class="math inline">x_{ij}</span> ist <span class="math inline">i \cdot n + j</span>. * <code>A_eq[i, i*n:(i+1)*n] = 1</code> — Zeile <span class="math inline">i</span>: alle Aufgaben <strong>einer</strong> Person (ein zusammenhängender Block). * <code>A_eq[n+j, j::n] = 1</code> — Zeile <span class="math inline">n+j</span>: alle Personen <strong>einer</strong> Aufgabe (jedes <span class="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 />
<h2 id="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 <span class="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 <span class="math inline">[e_i, l_i]</span>.</p>
<figure>
<img src="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." />
<figcaption aria-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>
</figure>
Folium-Karten: die Blockade war eine falsche Alternative Der einzige Punkt, der in diesem Projekt je als "blockiert" gefuehrt wurde, ist erledigt. Die Begruendung lautete: Die VRP-Instanz sei ein synthetisches 100 x 100-km-Raster ohne Geokoordinaten, eine echte Karte verlange also ENTWEDER eine neue Instanz (und damit zwei Dutzend neue abgedruckte Zahlen) ODER eine willkuerliche Verankerung. Es gibt einen dritten Weg, und er ist exakt. Eine azimutal-aequidistante Projektion um den Schwerpunkt des Rasters bildet die 17 Punkte so auf Laenge und Breite ab, dass die Grosskreisdistanzen mit den Rasterdistanzen uebereinstimmen: festes cos(Breite) Median 0,360 % Maximum 2,003 % cos der eigenen Breite Median 0,351 % Maximum 0,908 % azimutal-aequidistant Median 0,000 % Maximum 0,001 % Ein Meter auf hundert Kilometer. Kein abgedruckter Wert aendert sich, die Gesamtstrecke bleibt bei 619 km, das SVG ist byte-identisch geblieben. Zwei Befunde stuetzten die Entscheidung: "100 x 100 km Raster" steht nur in zwei Codekommentaren, nirgends im Fliesstext; und der didaktische Punkt der Bildunterschrift - die sich kreuzenden Touren, die KEIN Fehler sind - wird auf einer Karte deutlicher. erzeuge_vrp_touren.py schreibt jetzt SVG UND Karte aus einem Lauf. Ein zweites Skript haette erneut geloest, und die Routing-Bibliothek arbeitet heuristisch. pruefe_projektion() bricht ab, wenn die Verzerrung 0,01 % ueberschreitet - die Zahl im Buch wird bei jedem Lauf bewiesen. Neu im Build: die Marke {karte:name}, aufgeloest zu einem <iframe> (Leaflets Stylesheet soll nicht in die Buchseite greifen), im PDF zum Verweissatz - wie bei {plotly:}. --check bewacht jetzt beide Markenarten auf Existenz der Datei; gegengeprueft mit einer absichtlich falschen Marke. Zwei Vorkehrungen, die ohne Messung nicht aufgefallen waeren: * Folium vergibt jedem Element eine uuid4 - jeder Lauf erzeugte eine andere Datei. normalisiere() ersetzt sie durch fortlaufende Nummern; zwei Laeufe liefern jetzt dieselbe Pruefsumme. * Folium bindet ZEHN CDN-Ressourcen ein (jQuery, Bootstrap, Glyphicons, FontAwesome, awesome-markers ...). Gebraucht werden ZWEI. leaflet.js und leaflet.css liegen jetzt in web_04/assets/, die uebrigen acht entfallen. Nachgeprueft: Die Karte benutzt ausschliesslich L.*-Aufrufe. Ohne Netz fehlen nur die Kacheln, Marker und Touren bleiben sichtbar. Gegenrechnung an vier Strecken: Depot -> K1 misst auf der Karte 27,20 km, die Distanzmatrix sagt 27 - die Differenz stammt aus deren int(), nicht aus der Geographie. Veroeffentlicht: 8 Dateien uebertragen, alle dreizehn HTTP-Proben richtig, die Karte auf dem Server unter 200 erreichbar. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 20:38:16 +02:00
<iframe class="karte-figur" loading="lazy" src="bilder_04/karten/kap_graphen_vrp_touren.html" title="Interaktive Karte: kap_graphen_vrp_touren"></iframe>
<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 — <a href="#sec:graphen-denkfehler">Abschnitt 8.7</a> sagt, was dabei zu beachten ist.</p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<h3 id="kurzzyklen-verhindern">Kurzzyklen verhindern</h3>
<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 <span class="math inline">u_i</span> ein (die Position des Knotens in der Tour):</p>
Grafik-Upgrade Stufe 1: elf neue Diagramme Grafik_Upgrade.md verlangt "37 didaktisch wertvolle Visualisierungen". Es sind 19: Die Abschnitte "Modul 1-4" wiederholen die Grafiken 1-18 unter den Nummern 19-36, nur Grafik 37 ist zusaetzlich. Belegt an den Skriptnamen - 38 Nennungen, 19 verschiedene Dateien, erzeuge_kombinatorik_wand.py steht dreimal darin. Von den 19 betreffen neun bereits vorhandene Diagramme. Umgesetzt sind die zehn wirklich neuen plus eine elfte, die aus einem Sachfehler des Auftrags entstand: Grafik 14 sollte in Abschnitt 12.3 stehen, nannte aber Zahlen aus Kapitel 17. Beide Stellen haben jetzt ihre eigene Grafik mit ihren eigenen Zahlen. Vier Vorgaben des Auftrags wurden bewusst nicht befolgt: PNG bei 300 dpi (das Buch kennt nur SVG - die 33 verwaisten PNGs waren gerade erst geloescht worden), Matplotlib-Standardfarben (die Buchpalette steht in 33 Diagrammen), harte Kapitelnummern im Fliesstext ({ref:sec:...} stattdessen) und die fehlende Byte-Reproduzierbarkeit. Der eigentliche Ertrag waren die Selbsttests. Jeder Generator prueft seine Zahlen gegen das, was im Kapitel steht, und bricht bei Abweichung ab. Vier haben angeschlagen: * Kombinatorik-Wand: 25! ergibt 491,5 Mio. Jahre, die Tabelle nennt 490 Mio. - Rundung auf zwei signifikante Stellen, nicht Fehler des Buchs. * Laufzeit-Diagramm: Die abgedruckte Prozentspalte laesst sich aus den abgedruckten Zeiten nicht exakt nachrechnen (0,001/0,008 ergibt 12 %, gedruckt sind 14 %) - die Zeiten sind gerundet, die Prozente nicht. * Predict-then-Optimize: geratene Datenaufteilung ergab 197,35 statt 200,95 MSE. Mit TRAINING aus dem Buchprogramm stimmen alle vier Kostenwerte. * Almgren-Chriss: ein echter Fehler im Buchprogramm. DER FUND: analytische_loesung() minimiert die eigene Kostenfunktion nicht. Ihr Pfad kostet 10.860,40 EUR, der DP-Pfad 10.266,24 EUR - ein Gitterverfahren kann das kontinuierliche Optimum aber nicht unterbieten. periodenkosten() rechnet das Risiko mit lambda/2, die geschlossene Formel setzt kappa~^2 = lambda*sigma^2*P0^2/eta ohne dieses Halbe. Mit lambda/2 liefert die Formel 10.264,85 EUR und denselben Pfad wie die DP, knapp UNTER der Gitterloesung. Die Grafik zeigt bis zur Entscheidung darueber den DP-Pfad; er ist der, den das Kapitel abdruckt. Nachweise: Alle elf Diagramme sind ueber zwei Laeufe byte-identisch. Kein vorhandenes Diagramm wurde veraendert. Die Lastabwurf-Grafik reproduziert die Befundtabelle exakt (874.870/2.504.154/28 von 40 gegen 351.356/415.924/0). Zwei Abbildungen landeten beim automatischen Einfuegen im Codeblock (ihre Anker standen in abgedruckten Ausgaben) und wurden dahinter verschoben; eine Pruefung ueber alle elf zeigt null Fehlplatzierungen. Neu: bilder_04/stil_04.py buendelt Palette, rcParams und speichere(). networkx wurde entgegen der Planung nicht gebraucht - pyproject.toml bleibt unveraendert. PDF von 781 auf 788 Seiten, 44 Diagramme, 27 Generatoren. Veroeffentlicht: 25 Dateien. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-09 01:11:33 +02:00
<figure>
<img src="bilder_04/kap_graphen_subtouren.svg" alt="Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von bilder_04/erzeuge_vrp_subtours.py." />
<figcaption aria-hidden="true">Abb. 8.3: Links eine Lösung, die jede Gradbedingung erfüllt und trotzdem unbrauchbar ist; rechts dieselbe Instanz mit Kurzzyklus-Bedingungen. Erzeugt von <code>bilder_04/erzeuge_vrp_subtours.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Links wird jeder Kunde genau einmal angefahren — das Modell hat also gerechnet, was man ihm aufgeschrieben hat. Was fehlt, ist die Bedingung, an die man beim Aufschreiben nicht denkt: dass jede Tour am Depot beginnen muss.</p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<p><span class="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 <span class="math inline">(i,j)</span> benutzt (<span class="math inline">x_{ij} = 1</span>), erzwingt die Ungleichung <span class="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 (<span class="math inline">x_{ij} = 0</span>), reduziert sich die Ungleichung auf <span class="math inline">u_i - u_j \le C - d_j</span>, was durch hinreichend großes <span class="math inline">C</span> immer erfüllt ist — das Big-M-Muster aus <a href="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>
</blockquote>
<h3 id="praxisbeispiel-flotten-routing">Praxisbeispiel: Flotten-Routing</h3>
<div class="sourceCode" id="cb6"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb6-1"><a href="#cb6-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb6-2"><a href="#cb6-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-3"><a href="#cb6-3" aria-hidden="true" tabindex="-1"></a><span class="co"># VRP_Flotten_Routing.py</span></span>
<span id="cb6-4"><a href="#cb6-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb6-5"><a href="#cb6-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Graphen: Capacitated Vehicle Routing Problem with Time Windows (CVRPTW)</span></span>
<span id="cb6-6"><a href="#cb6-6" aria-hidden="true" tabindex="-1"></a><span class="co">mit der Routing-Bibliothek von Google OR-Tools.</span></span>
<span id="cb6-7"><a href="#cb6-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-8"><a href="#cb6-8" aria-hidden="true" tabindex="-1"></a><span class="co">Eigenschaften:</span></span>
<span id="cb6-9"><a href="#cb6-9" aria-hidden="true" tabindex="-1"></a><span class="co"> * Eingabedaten werden vorab auf Plausibilitaet geprueft (Kapazitaet</span></span>
<span id="cb6-10"><a href="#cb6-10" aria-hidden="true" tabindex="-1"></a><span class="co"> ausreichend? Zeitfenster erreichbar?)</span></span>
<span id="cb6-11"><a href="#cb6-11" aria-hidden="true" tabindex="-1"></a><span class="co"> * Fahrzeit und Servicezeit werden getrennt ausgewiesen</span></span>
<span id="cb6-12"><a href="#cb6-12" aria-hidden="true" tabindex="-1"></a><span class="co"> * Ausgabe als lesbarer Tourenplan mit Ankunftszeiten</span></span>
<span id="cb6-13"><a href="#cb6-13" aria-hidden="true" tabindex="-1"></a><span class="co"> * Kennzahlen: Auslastung, Leerfahrten, Wartezeit</span></span>
<span id="cb6-14"><a href="#cb6-14" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb6-15"><a href="#cb6-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-16"><a href="#cb6-16" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb6-17"><a href="#cb6-17" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.constraint_solver <span class="im">import</span> pywrapcp, routing_enums_pb2</span>
<span id="cb6-18"><a href="#cb6-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-19"><a href="#cb6-19" aria-hidden="true" tabindex="-1"></a>SERVICEZEIT <span class="op">=</span> <span class="dv">10</span> <span class="co"># Minuten je Kundenstopp</span></span>
<span id="cb6-20"><a href="#cb6-20" aria-hidden="true" tabindex="-1"></a>WARTEZEIT_MAX <span class="op">=</span> <span class="dv">60</span> <span class="co"># zulaessige Wartezeit bei zu frueher Ankunft</span></span>
<span id="cb6-21"><a href="#cb6-21" aria-hidden="true" tabindex="-1"></a>SCHICHTLAENGE <span class="op">=</span> <span class="dv">600</span> <span class="co"># Minuten</span></span>
<span id="cb6-22"><a href="#cb6-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-23"><a href="#cb6-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-24"><a href="#cb6-24" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> erzeuge_daten(seed: <span class="bu">int</span> <span class="op">=</span> <span class="dv">42</span>):</span>
<span id="cb6-25"><a href="#cb6-25" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Synthetische, aber reproduzierbare Instanz: 1 Depot + 16 Kunden.&quot;&quot;&quot;</span></span>
<span id="cb6-26"><a href="#cb6-26" aria-hidden="true" tabindex="-1"></a> anzahl_orte <span class="op">=</span> <span class="dv">17</span></span>
<span id="cb6-27"><a href="#cb6-27" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb6-28"><a href="#cb6-28" aria-hidden="true" tabindex="-1"></a> koordinaten <span class="op">=</span> rng.random((anzahl_orte, <span class="dv">2</span>)) <span class="op">*</span> <span class="dv">100</span> <span class="co"># 100 x 100 km Raster</span></span>
<span id="cb6-29"><a href="#cb6-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-30"><a href="#cb6-30" aria-hidden="true" tabindex="-1"></a> distanz <span class="op">=</span> np.zeros((anzahl_orte, anzahl_orte), dtype<span class="op">=</span><span class="bu">int</span>)</span>
<span id="cb6-31"><a href="#cb6-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(anzahl_orte):</span>
<span id="cb6-32"><a href="#cb6-32" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(anzahl_orte):</span>
<span id="cb6-33"><a href="#cb6-33" aria-hidden="true" tabindex="-1"></a> distanz[i][j] <span class="op">=</span> <span class="bu">int</span>(np.linalg.norm(koordinaten[i] <span class="op">-</span> koordinaten[j]))</span>
<span id="cb6-34"><a href="#cb6-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-35"><a href="#cb6-35" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> {</span>
<span id="cb6-36"><a href="#cb6-36" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;distanzmatrix&quot;</span>: distanz.tolist(),</span>
<span id="cb6-37"><a href="#cb6-37" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;zeitfenster&quot;</span>: [</span>
<span id="cb6-38"><a href="#cb6-38" aria-hidden="true" tabindex="-1"></a> (<span class="dv">0</span>, SCHICHTLAENGE), <span class="co"># 0: Depot</span></span>
<span id="cb6-39"><a href="#cb6-39" aria-hidden="true" tabindex="-1"></a> (<span class="dv">30</span>, <span class="dv">120</span>), (<span class="dv">60</span>, <span class="dv">180</span>), (<span class="dv">100</span>, <span class="dv">240</span>), (<span class="dv">150</span>, <span class="dv">300</span>), <span class="co"># Kunden 1-4</span></span>
<span id="cb6-40"><a href="#cb6-40" aria-hidden="true" tabindex="-1"></a> (<span class="dv">60</span>, <span class="dv">180</span>), (<span class="dv">120</span>, <span class="dv">240</span>), (<span class="dv">200</span>, <span class="dv">360</span>), (<span class="dv">300</span>, <span class="dv">450</span>), <span class="co"># Kunden 5-8</span></span>
<span id="cb6-41"><a href="#cb6-41" aria-hidden="true" tabindex="-1"></a> (<span class="dv">180</span>, <span class="dv">300</span>), (<span class="dv">240</span>, <span class="dv">360</span>), (<span class="dv">300</span>, <span class="dv">480</span>), (<span class="dv">360</span>, <span class="dv">500</span>), <span class="co"># Kunden 9-12</span></span>
<span id="cb6-42"><a href="#cb6-42" aria-hidden="true" tabindex="-1"></a> (<span class="dv">60</span>, <span class="dv">200</span>), (<span class="dv">120</span>, <span class="dv">300</span>), (<span class="dv">240</span>, <span class="dv">400</span>), (<span class="dv">300</span>, <span class="dv">550</span>), <span class="co"># Kunden 13-16</span></span>
<span id="cb6-43"><a href="#cb6-43" aria-hidden="true" tabindex="-1"></a> ],</span>
<span id="cb6-44"><a href="#cb6-44" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;bedarfe&quot;</span>: [<span class="dv">0</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">1</span>, <span class="dv">4</span>, <span class="dv">2</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">4</span>, <span class="dv">3</span>, <span class="dv">2</span>, <span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">2</span>],</span>
<span id="cb6-45"><a href="#cb6-45" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;kapazitaeten&quot;</span>: [<span class="dv">10</span>, <span class="dv">10</span>, <span class="dv">10</span>, <span class="dv">10</span>],</span>
<span id="cb6-46"><a href="#cb6-46" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;anzahl_fahrzeuge&quot;</span>: <span class="dv">4</span>,</span>
<span id="cb6-47"><a href="#cb6-47" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;depot&quot;</span>: <span class="dv">0</span>,</span>
<span id="cb6-48"><a href="#cb6-48" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb6-49"><a href="#cb6-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-50"><a href="#cb6-50" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-51"><a href="#cb6-51" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_daten(daten) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb6-52"><a href="#cb6-52" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vorabdiagnose - fangt die haeufigsten Ursachen fuer &#39;keine Loesung&#39; ab.&quot;&quot;&quot;</span></span>
<span id="cb6-53"><a href="#cb6-53" aria-hidden="true" tabindex="-1"></a> gesamtbedarf <span class="op">=</span> <span class="bu">sum</span>(daten[<span class="st">&quot;bedarfe&quot;</span>])</span>
<span id="cb6-54"><a href="#cb6-54" aria-hidden="true" tabindex="-1"></a> gesamtkapazitaet <span class="op">=</span> <span class="bu">sum</span>(daten[<span class="st">&quot;kapazitaeten&quot;</span>])</span>
<span id="cb6-55"><a href="#cb6-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Gesamtbedarf </span><span class="sc">{</span>gesamtbedarf<span class="sc">}</span><span class="ss"> Einheiten | &quot;</span></span>
<span id="cb6-56"><a href="#cb6-56" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Flottenkapazitaet </span><span class="sc">{</span>gesamtkapazitaet<span class="sc">}</span><span class="ss"> Einheiten | &quot;</span></span>
<span id="cb6-57"><a href="#cb6-57" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Auslastung </span><span class="sc">{</span>gesamtbedarf <span class="op">/</span> gesamtkapazitaet <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb6-58"><a href="#cb6-58" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> gesamtbedarf <span class="op">&gt;</span> gesamtkapazitaet:</span>
<span id="cb6-59"><a href="#cb6-59" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="st">&quot;UNLOESBAR: Der Bedarf uebersteigt die Flottenkapazitaet.&quot;</span>)</span>
<span id="cb6-60"><a href="#cb6-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-61"><a href="#cb6-61" aria-hidden="true" tabindex="-1"></a> d <span class="op">=</span> daten[<span class="st">&quot;distanzmatrix&quot;</span>]</span>
<span id="cb6-62"><a href="#cb6-62" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> kunde, (fruehestens, spaetestens) <span class="kw">in</span> <span class="bu">enumerate</span>(daten[<span class="st">&quot;zeitfenster&quot;</span>]):</span>
<span id="cb6-63"><a href="#cb6-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> kunde <span class="op">==</span> <span class="dv">0</span>:</span>
<span id="cb6-64"><a href="#cb6-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></span>
<span id="cb6-65"><a href="#cb6-65" aria-hidden="true" tabindex="-1"></a> direktfahrt <span class="op">=</span> d[<span class="dv">0</span>][kunde]</span>
<span id="cb6-66"><a href="#cb6-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> direktfahrt <span class="op">&gt;</span> spaetestens:</span>
<span id="cb6-67"><a href="#cb6-67" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(</span>
<span id="cb6-68"><a href="#cb6-68" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;UNLOESBAR: Kunde </span><span class="sc">{</span>kunde<span class="sc">}</span><span class="ss"> ist erst nach </span><span class="sc">{</span>direktfahrt<span class="sc">}</span><span class="ss"> min erreichbar, &quot;</span></span>
<span id="cb6-69"><a href="#cb6-69" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;sein Zeitfenster endet aber bei </span><span class="sc">{</span>spaetestens<span class="sc">}</span><span class="ss"> min.&quot;</span>)</span>
<span id="cb6-70"><a href="#cb6-70" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Vorabpruefung bestanden: Kapazitaet und Zeitfenster sind grundsaetzlich machbar.&quot;</span>)</span>
<span id="cb6-71"><a href="#cb6-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-72"><a href="#cb6-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-73"><a href="#cb6-73" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_cvrptw(zeitlimit_s: <span class="bu">int</span> <span class="op">=</span> <span class="dv">5</span>):</span>
<span id="cb6-74"><a href="#cb6-74" aria-hidden="true" tabindex="-1"></a> daten <span class="op">=</span> erzeuge_daten()</span>
<span id="cb6-75"><a href="#cb6-75" aria-hidden="true" tabindex="-1"></a> pruefe_daten(daten)</span>
<span id="cb6-76"><a href="#cb6-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-77"><a href="#cb6-77" aria-hidden="true" tabindex="-1"></a> manager <span class="op">=</span> pywrapcp.RoutingIndexManager(</span>
<span id="cb6-78"><a href="#cb6-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">len</span>(daten[<span class="st">&quot;distanzmatrix&quot;</span>]), daten[<span class="st">&quot;anzahl_fahrzeuge&quot;</span>], daten[<span class="st">&quot;depot&quot;</span>])</span>
<span id="cb6-79"><a href="#cb6-79" aria-hidden="true" tabindex="-1"></a> routing <span class="op">=</span> pywrapcp.RoutingModel(manager)</span>
<span id="cb6-80"><a href="#cb6-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-81"><a href="#cb6-81" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Fahrzeit + Servicezeit als Kantengewicht ------------------------</span></span>
<span id="cb6-82"><a href="#cb6-82" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> zeit_callback(von_index, nach_index):</span>
<span id="cb6-83"><a href="#cb6-83" aria-hidden="true" tabindex="-1"></a> von <span class="op">=</span> manager.IndexToNode(von_index)</span>
<span id="cb6-84"><a href="#cb6-84" aria-hidden="true" tabindex="-1"></a> nach <span class="op">=</span> manager.IndexToNode(nach_index)</span>
<span id="cb6-85"><a href="#cb6-85" aria-hidden="true" tabindex="-1"></a> service <span class="op">=</span> SERVICEZEIT <span class="cf">if</span> von <span class="op">!=</span> daten[<span class="st">&quot;depot&quot;</span>] <span class="cf">else</span> <span class="dv">0</span></span>
<span id="cb6-86"><a href="#cb6-86" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> daten[<span class="st">&quot;distanzmatrix&quot;</span>][von][nach] <span class="op">+</span> service</span>
<span id="cb6-87"><a href="#cb6-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-88"><a href="#cb6-88" aria-hidden="true" tabindex="-1"></a> zeit_index <span class="op">=</span> routing.RegisterTransitCallback(zeit_callback)</span>
<span id="cb6-89"><a href="#cb6-89" aria-hidden="true" tabindex="-1"></a> routing.SetArcCostEvaluatorOfAllVehicles(zeit_index)</span>
<span id="cb6-90"><a href="#cb6-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-91"><a href="#cb6-91" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Kapazitaetsdimension ---------------------------------------------</span></span>
<span id="cb6-92"><a href="#cb6-92" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> bedarf_callback(von_index):</span>
<span id="cb6-93"><a href="#cb6-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> daten[<span class="st">&quot;bedarfe&quot;</span>][manager.IndexToNode(von_index)]</span>
<span id="cb6-94"><a href="#cb6-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-95"><a href="#cb6-95" aria-hidden="true" tabindex="-1"></a> bedarf_index <span class="op">=</span> routing.RegisterUnaryTransitCallback(bedarf_callback)</span>
<span id="cb6-96"><a href="#cb6-96" aria-hidden="true" tabindex="-1"></a> routing.AddDimensionWithVehicleCapacity(</span>
<span id="cb6-97"><a href="#cb6-97" aria-hidden="true" tabindex="-1"></a> bedarf_index, <span class="dv">0</span>, daten[<span class="st">&quot;kapazitaeten&quot;</span>], <span class="va">True</span>, <span class="st">&quot;Kapazitaet&quot;</span>)</span>
<span id="cb6-98"><a href="#cb6-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-99"><a href="#cb6-99" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Zeitdimension mit Zeitfenstern -----------------------------------</span></span>
<span id="cb6-100"><a href="#cb6-100" aria-hidden="true" tabindex="-1"></a> routing.AddDimension(zeit_index, WARTEZEIT_MAX, SCHICHTLAENGE, <span class="va">False</span>, <span class="st">&quot;Zeit&quot;</span>)</span>
<span id="cb6-101"><a href="#cb6-101" aria-hidden="true" tabindex="-1"></a> zeit_dimension <span class="op">=</span> routing.GetDimensionOrDie(<span class="st">&quot;Zeit&quot;</span>)</span>
<span id="cb6-102"><a href="#cb6-102" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> ort, (fruehestens, spaetestens) <span class="kw">in</span> <span class="bu">enumerate</span>(daten[<span class="st">&quot;zeitfenster&quot;</span>]):</span>
<span id="cb6-103"><a href="#cb6-103" aria-hidden="true" tabindex="-1"></a> zeit_dimension.CumulVar(manager.NodeToIndex(ort)).SetRange(fruehestens, spaetestens)</span>
<span id="cb6-104"><a href="#cb6-104" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-105"><a href="#cb6-105" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Suchparameter -----------------------------------------------------</span></span>
<span id="cb6-106"><a href="#cb6-106" aria-hidden="true" tabindex="-1"></a> parameter <span class="op">=</span> pywrapcp.DefaultRoutingSearchParameters()</span>
<span id="cb6-107"><a href="#cb6-107" aria-hidden="true" tabindex="-1"></a> parameter.first_solution_strategy <span class="op">=</span> (</span>
<span id="cb6-108"><a href="#cb6-108" aria-hidden="true" tabindex="-1"></a> routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)</span>
<span id="cb6-109"><a href="#cb6-109" aria-hidden="true" tabindex="-1"></a> parameter.local_search_metaheuristic <span class="op">=</span> (</span>
<span id="cb6-110"><a href="#cb6-110" aria-hidden="true" tabindex="-1"></a> routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)</span>
<span id="cb6-111"><a href="#cb6-111" aria-hidden="true" tabindex="-1"></a> parameter.time_limit.seconds <span class="op">=</span> zeitlimit_s</span>
<span id="cb6-112"><a href="#cb6-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-113"><a href="#cb6-113" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> routing.SolveWithParameters(parameter)</span>
<span id="cb6-114"><a href="#cb6-114" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung:</span>
<span id="cb6-115"><a href="#cb6-115" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Keine zulaessige Routenfuehrung gefunden.&quot;</span>)</span>
<span id="cb6-116"><a href="#cb6-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span>
<span id="cb6-117"><a href="#cb6-117" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-118"><a href="#cb6-118" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Auswertung --------------------------------------------------------</span></span>
<span id="cb6-119"><a href="#cb6-119" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb6-120"><a href="#cb6-120" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; OPTIMIERTER TOURENPLAN (CVRPTW)&quot;</span>)</span>
<span id="cb6-121"><a href="#cb6-121" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb6-122"><a href="#cb6-122" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-123"><a href="#cb6-123" aria-hidden="true" tabindex="-1"></a> gesamtzeit <span class="op">=</span> gesamtfracht <span class="op">=</span> gesamtdistanz <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb6-124"><a href="#cb6-124" aria-hidden="true" tabindex="-1"></a> kapazitaet <span class="op">=</span> daten[<span class="st">&quot;kapazitaeten&quot;</span>]</span>
<span id="cb6-125"><a href="#cb6-125" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-126"><a href="#cb6-126" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> fahrzeug <span class="kw">in</span> <span class="bu">range</span>(daten[<span class="st">&quot;anzahl_fahrzeuge&quot;</span>]):</span>
<span id="cb6-127"><a href="#cb6-127" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> routing.Start(fahrzeug)</span>
<span id="cb6-128"><a href="#cb6-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> routing.IsEnd(loesung.Value(routing.NextVar(index))):</span>
<span id="cb6-129"><a href="#cb6-129" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Fahrzeug </span><span class="sc">{</span>fahrzeug <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss">: nicht eingesetzt&quot;</span>)</span>
<span id="cb6-130"><a href="#cb6-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></span>
<span id="cb6-131"><a href="#cb6-131" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-132"><a href="#cb6-132" aria-hidden="true" tabindex="-1"></a> stationen, fracht, distanz <span class="op">=</span> [], <span class="dv">0</span>, <span class="dv">0</span></span>
<span id="cb6-133"><a href="#cb6-133" aria-hidden="true" tabindex="-1"></a> <span class="cf">while</span> <span class="kw">not</span> routing.IsEnd(index):</span>
<span id="cb6-134"><a href="#cb6-134" aria-hidden="true" tabindex="-1"></a> knoten <span class="op">=</span> manager.IndexToNode(index)</span>
<span id="cb6-135"><a href="#cb6-135" aria-hidden="true" tabindex="-1"></a> ankunft <span class="op">=</span> loesung.Min(zeit_dimension.CumulVar(index))</span>
<span id="cb6-136"><a href="#cb6-136" aria-hidden="true" tabindex="-1"></a> fracht <span class="op">+=</span> daten[<span class="st">&quot;bedarfe&quot;</span>][knoten]</span>
<span id="cb6-137"><a href="#cb6-137" aria-hidden="true" tabindex="-1"></a> bezeichnung <span class="op">=</span> <span class="st">&quot;Depot&quot;</span> <span class="cf">if</span> knoten <span class="op">==</span> <span class="dv">0</span> <span class="cf">else</span> <span class="ss">f&quot;K</span><span class="sc">{</span>knoten<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb6-138"><a href="#cb6-138" aria-hidden="true" tabindex="-1"></a> stationen.append(<span class="ss">f&quot;</span><span class="sc">{</span>bezeichnung<span class="sc">}</span><span class="ss">@</span><span class="sc">{</span>ankunft<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-139"><a href="#cb6-139" aria-hidden="true" tabindex="-1"></a> naechster <span class="op">=</span> loesung.Value(routing.NextVar(index))</span>
<span id="cb6-140"><a href="#cb6-140" aria-hidden="true" tabindex="-1"></a> distanz <span class="op">+=</span> daten[<span class="st">&quot;distanzmatrix&quot;</span>][knoten][manager.IndexToNode(naechster)]</span>
<span id="cb6-141"><a href="#cb6-141" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> naechster</span>
<span id="cb6-142"><a href="#cb6-142" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-143"><a href="#cb6-143" aria-hidden="true" tabindex="-1"></a> endzeit <span class="op">=</span> loesung.Min(zeit_dimension.CumulVar(index))</span>
<span id="cb6-144"><a href="#cb6-144" aria-hidden="true" tabindex="-1"></a> stationen.append(<span class="ss">f&quot;Depot@</span><span class="sc">{</span>endzeit<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb6-145"><a href="#cb6-145" aria-hidden="true" tabindex="-1"></a> gesamtzeit <span class="op">+=</span> endzeit</span>
<span id="cb6-146"><a href="#cb6-146" aria-hidden="true" tabindex="-1"></a> gesamtfracht <span class="op">+=</span> fracht</span>
<span id="cb6-147"><a href="#cb6-147" aria-hidden="true" tabindex="-1"></a> gesamtdistanz <span class="op">+=</span> distanz</span>
<span id="cb6-148"><a href="#cb6-148" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-149"><a href="#cb6-149" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">Fahrzeug </span><span class="sc">{</span>fahrzeug <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss">:&quot;</span>)</span>
<span id="cb6-150"><a href="#cb6-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; &quot;</span> <span class="op">+</span> <span class="st">&quot; -&gt; &quot;</span>.join(stationen))</span>
<span id="cb6-151"><a href="#cb6-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Schichtzeit </span><span class="sc">{</span>endzeit<span class="sc">}</span><span class="ss"> min | Fahrstrecke </span><span class="sc">{</span>distanz<span class="sc">}</span><span class="ss"> km | &quot;</span></span>
<span id="cb6-152"><a href="#cb6-152" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Fracht </span><span class="sc">{</span>fracht<span class="sc">}</span><span class="ss">/</span><span class="sc">{</span>kapazitaet[fahrzeug]<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb6-153"><a href="#cb6-153" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>fracht <span class="op">/</span> kapazitaet[fahrzeug] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.0f}</span><span class="ss"> % Auslastung)&quot;</span>)</span>
<span id="cb6-154"><a href="#cb6-154" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-155"><a href="#cb6-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb6-156"><a href="#cb6-156" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Summe Schichtzeiten: </span><span class="sc">{</span>gesamtzeit<span class="sc">}</span><span class="ss"> min&quot;</span>)</span>
<span id="cb6-157"><a href="#cb6-157" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Summe Fahrstrecken: </span><span class="sc">{</span>gesamtdistanz<span class="sc">}</span><span class="ss"> km&quot;</span>)</span>
<span id="cb6-158"><a href="#cb6-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Transportierte Fracht: </span><span class="sc">{</span>gesamtfracht<span class="sc">}</span><span class="ss"> von </span><span class="sc">{</span><span class="bu">sum</span>(daten[<span class="st">&#39;bedarfe&#39;</span>])<span class="sc">}</span><span class="ss"> Einheiten&quot;</span>)</span>
<span id="cb6-159"><a href="#cb6-159" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> gesamtfracht <span class="op">==</span> <span class="bu">sum</span>(daten[<span class="st">&quot;bedarfe&quot;</span>]), <span class="st">&quot;Nicht alle Kunden wurden beliefert!&quot;</span></span>
<span id="cb6-160"><a href="#cb6-160" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Alle Kunden wurden innerhalb ihrer Zeitfenster beliefert.&quot;</span>)</span>
<span id="cb6-161"><a href="#cb6-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">84</span>)</span>
<span id="cb6-162"><a href="#cb6-162" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-163"><a href="#cb6-163" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb6-164"><a href="#cb6-164" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb6-165"><a href="#cb6-165" aria-hidden="true" tabindex="-1"></a> loese_cvrptw()</span></code></pre></div>
<blockquote>
<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>
</ul>
</blockquote>
<hr />
<h2 id="sec:graphen-uebungsaufgaben">8.6 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-graphen">Abschnitt A.8</a>.</p>
</blockquote>
<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 <span class="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 <span class="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 <span class="math display">\begin{pmatrix}8&amp;6&amp;10&amp;9\\9&amp;12&amp;13&amp;7\\14&amp;9&amp;16&amp;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 />
<h2 id="sec:graphen-denkfehler">8.7 Finde den Denkfehler</h2>
<div class="card card-denkfehler">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>🐛 Finde den Denkfehler: Die vergessene Dimension</strong></p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<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>
<div class="sourceCode" id="cb7"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb7-1"><a href="#cb7-1" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> bedarf(index):</span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> BEDARFE[manager.IndexToNode(index)]</span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a>bedarf_id <span class="op">=</span> routing.RegisterUnaryTransitCallback(bedarf)</span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a>routing.AddDimensionWithVehicleCapacity(</span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a> bedarf_id, <span class="dv">0</span>, KAPAZITAETEN, <span class="va">True</span>, <span class="st">&quot;Ladung&quot;</span>)</span></code></pre></div>
<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>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-graphen">Abschnitt A.8</a>.</em></p>
</blockquote>
</div>
<div class="sourceCode" id="cb8"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb8-1"><a href="#cb8-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb8-2"><a href="#cb8-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-3"><a href="#cb8-3" aria-hidden="true" tabindex="-1"></a><span class="co"># VRP_Kapazitaetsfalle.py</span></span>
<span id="cb8-4"><a href="#cb8-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-5"><a href="#cb8-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Graphen: Die vergessene Dimension.</span></span>
<span id="cb8-6"><a href="#cb8-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-7"><a href="#cb8-7" aria-hidden="true" tabindex="-1"></a><span class="co">Die Routing-Bibliothek von OR-Tools kennt keine &quot;Kapazitaet&quot; von sich aus.</span></span>
<span id="cb8-8"><a href="#cb8-8" aria-hidden="true" tabindex="-1"></a><span class="co">Sie kennt nur DIMENSIONEN - benannte Groessen, die sich entlang einer Tour</span></span>
<span id="cb8-9"><a href="#cb8-9" aria-hidden="true" tabindex="-1"></a><span class="co">aufsummieren und begrenzt werden koennen. Distanz ist eine, Zeit ist eine,</span></span>
<span id="cb8-10"><a href="#cb8-10" aria-hidden="true" tabindex="-1"></a><span class="co">Ladung ist eine. Wer eine davon nicht anlegt, bekommt trotzdem eine Loesung:</span></span>
<span id="cb8-11"><a href="#cb8-11" aria-hidden="true" tabindex="-1"></a><span class="co">eine schoene, kurze, guenstige - und unfahrbare.</span></span>
<span id="cb8-12"><a href="#cb8-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-13"><a href="#cb8-13" aria-hidden="true" tabindex="-1"></a><span class="co">Dieses Programm loest dieselbe Instanz zweimal und prueft beide Ergebnisse</span></span>
<span id="cb8-14"><a href="#cb8-14" aria-hidden="true" tabindex="-1"></a><span class="co">gegen die tatsaechlichen Lademengen.</span></span>
<span id="cb8-15"><a href="#cb8-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-16"><a href="#cb8-16" aria-hidden="true" tabindex="-1"></a><span class="co">Instanz: 1 Depot, 16 Kunden, 4 Fahrzeuge zu je 10 Paletten.</span></span>
<span id="cb8-17"><a href="#cb8-17" aria-hidden="true" tabindex="-1"></a><span class="co">Gesamtbedarf 37 Paletten bei 40 Paletten Flottenkapazitaet - es ist also</span></span>
<span id="cb8-18"><a href="#cb8-18" aria-hidden="true" tabindex="-1"></a><span class="co">knapp, aber machbar.</span></span>
<span id="cb8-19"><a href="#cb8-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-20"><a href="#cb8-20" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb8-21"><a href="#cb8-21" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb8-22"><a href="#cb8-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-23"><a href="#cb8-23" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb8-24"><a href="#cb8-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-25"><a href="#cb8-25" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb8-26"><a href="#cb8-26" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.constraint_solver <span class="im">import</span> pywrapcp, routing_enums_pb2</span>
<span id="cb8-27"><a href="#cb8-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-28"><a href="#cb8-28" aria-hidden="true" tabindex="-1"></a><span class="co"># Dieselbe Instanz wie VRP_Flotten_Routing.py</span></span>
<span id="cb8-29"><a href="#cb8-29" aria-hidden="true" tabindex="-1"></a>BEDARFE <span class="op">=</span> [<span class="dv">0</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">1</span>, <span class="dv">4</span>, <span class="dv">2</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">4</span>, <span class="dv">3</span>, <span class="dv">2</span>, <span class="dv">1</span>, <span class="dv">2</span>, <span class="dv">3</span>, <span class="dv">2</span>]</span>
<span id="cb8-30"><a href="#cb8-30" aria-hidden="true" tabindex="-1"></a>KAPAZITAETEN <span class="op">=</span> [<span class="dv">10</span>, <span class="dv">10</span>, <span class="dv">10</span>, <span class="dv">10</span>]</span>
<span id="cb8-31"><a href="#cb8-31" aria-hidden="true" tabindex="-1"></a>ANZAHL_FAHRZEUGE <span class="op">=</span> <span class="dv">4</span></span>
<span id="cb8-32"><a href="#cb8-32" aria-hidden="true" tabindex="-1"></a>DEPOT <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb8-33"><a href="#cb8-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-34"><a href="#cb8-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-35"><a href="#cb8-35" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> distanzmatrix(seed: <span class="bu">int</span> <span class="op">=</span> <span class="dv">42</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">list</span>[<span class="bu">int</span>]]:</span>
<span id="cb8-36"><a href="#cb8-36" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb8-37"><a href="#cb8-37" aria-hidden="true" tabindex="-1"></a> koordinaten <span class="op">=</span> rng.random((<span class="bu">len</span>(BEDARFE), <span class="dv">2</span>)) <span class="op">*</span> <span class="dv">100</span> <span class="co"># 100 x 100 km</span></span>
<span id="cb8-38"><a href="#cb8-38" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(BEDARFE)</span>
<span id="cb8-39"><a href="#cb8-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> [[<span class="bu">int</span>(np.linalg.norm(koordinaten[i] <span class="op">-</span> koordinaten[j]))</span>
<span id="cb8-40"><a href="#cb8-40" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n)] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)]</span>
<span id="cb8-41"><a href="#cb8-41" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-42"><a href="#cb8-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-43"><a href="#cb8-43" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> plane(mit_kapazitaet: <span class="bu">bool</span>, zeitlimit: <span class="bu">int</span> <span class="op">=</span> <span class="dv">5</span>) <span class="op">-&gt;</span> <span class="bu">dict</span>:</span>
<span id="cb8-44"><a href="#cb8-44" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Loest die Tourenplanung - wahlweise mit oder ohne Ladungsdimension.&quot;&quot;&quot;</span></span>
<span id="cb8-45"><a href="#cb8-45" aria-hidden="true" tabindex="-1"></a> distanz <span class="op">=</span> distanzmatrix()</span>
<span id="cb8-46"><a href="#cb8-46" aria-hidden="true" tabindex="-1"></a> manager <span class="op">=</span> pywrapcp.RoutingIndexManager(<span class="bu">len</span>(distanz), ANZAHL_FAHRZEUGE, DEPOT)</span>
<span id="cb8-47"><a href="#cb8-47" aria-hidden="true" tabindex="-1"></a> routing <span class="op">=</span> pywrapcp.RoutingModel(manager)</span>
<span id="cb8-48"><a href="#cb8-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-49"><a href="#cb8-49" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> entfernung(von_index, nach_index):</span>
<span id="cb8-50"><a href="#cb8-50" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> distanz[manager.IndexToNode(von_index)][manager.IndexToNode(nach_index)]</span>
<span id="cb8-51"><a href="#cb8-51" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-52"><a href="#cb8-52" aria-hidden="true" tabindex="-1"></a> kosten_id <span class="op">=</span> routing.RegisterTransitCallback(entfernung)</span>
<span id="cb8-53"><a href="#cb8-53" aria-hidden="true" tabindex="-1"></a> routing.SetArcCostEvaluatorOfAllVehicles(kosten_id)</span>
<span id="cb8-54"><a href="#cb8-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-55"><a href="#cb8-55" aria-hidden="true" tabindex="-1"></a> <span class="co"># DIE entscheidende Stelle. Ohne diesen Block existiert im Modell keine</span></span>
<span id="cb8-56"><a href="#cb8-56" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ladung - die Fahrzeuge sind dann unendlich gross.</span></span>
<span id="cb8-57"><a href="#cb8-57" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> mit_kapazitaet:</span>
<span id="cb8-58"><a href="#cb8-58" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> bedarf(index):</span>
<span id="cb8-59"><a href="#cb8-59" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> BEDARFE[manager.IndexToNode(index)]</span>
<span id="cb8-60"><a href="#cb8-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-61"><a href="#cb8-61" aria-hidden="true" tabindex="-1"></a> bedarf_id <span class="op">=</span> routing.RegisterUnaryTransitCallback(bedarf)</span>
<span id="cb8-62"><a href="#cb8-62" aria-hidden="true" tabindex="-1"></a> routing.AddDimensionWithVehicleCapacity(</span>
<span id="cb8-63"><a href="#cb8-63" aria-hidden="true" tabindex="-1"></a> bedarf_id,</span>
<span id="cb8-64"><a href="#cb8-64" aria-hidden="true" tabindex="-1"></a> <span class="dv">0</span>, <span class="co"># kein Zwischenpuffer</span></span>
<span id="cb8-65"><a href="#cb8-65" aria-hidden="true" tabindex="-1"></a> KAPAZITAETEN, <span class="co"># Obergrenze je Fahrzeug</span></span>
<span id="cb8-66"><a href="#cb8-66" aria-hidden="true" tabindex="-1"></a> <span class="va">True</span>, <span class="co"># Ladung startet bei 0</span></span>
<span id="cb8-67"><a href="#cb8-67" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Ladung&quot;</span>)</span>
<span id="cb8-68"><a href="#cb8-68" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-69"><a href="#cb8-69" aria-hidden="true" tabindex="-1"></a> parameter <span class="op">=</span> pywrapcp.DefaultRoutingSearchParameters()</span>
<span id="cb8-70"><a href="#cb8-70" aria-hidden="true" tabindex="-1"></a> parameter.first_solution_strategy <span class="op">=</span> (</span>
<span id="cb8-71"><a href="#cb8-71" aria-hidden="true" tabindex="-1"></a> routing_enums_pb2.FirstSolutionStrategy.PATH_CHEAPEST_ARC)</span>
<span id="cb8-72"><a href="#cb8-72" aria-hidden="true" tabindex="-1"></a> parameter.local_search_metaheuristic <span class="op">=</span> (</span>
<span id="cb8-73"><a href="#cb8-73" aria-hidden="true" tabindex="-1"></a> routing_enums_pb2.LocalSearchMetaheuristic.GUIDED_LOCAL_SEARCH)</span>
<span id="cb8-74"><a href="#cb8-74" aria-hidden="true" tabindex="-1"></a> parameter.time_limit.FromSeconds(zeitlimit)</span>
<span id="cb8-75"><a href="#cb8-75" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-76"><a href="#cb8-76" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> routing.SolveWithParameters(parameter)</span>
<span id="cb8-77"><a href="#cb8-77" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung <span class="kw">is</span> <span class="va">None</span>:</span>
<span id="cb8-78"><a href="#cb8-78" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;Keine Loesung gefunden&quot;</span>)</span>
<span id="cb8-79"><a href="#cb8-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-80"><a href="#cb8-80" aria-hidden="true" tabindex="-1"></a> touren, strecken, ladungen <span class="op">=</span> [], [], []</span>
<span id="cb8-81"><a href="#cb8-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> fahrzeug <span class="kw">in</span> <span class="bu">range</span>(ANZAHL_FAHRZEUGE):</span>
<span id="cb8-82"><a href="#cb8-82" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> routing.Start(fahrzeug)</span>
<span id="cb8-83"><a href="#cb8-83" aria-hidden="true" tabindex="-1"></a> tour, strecke, ladung <span class="op">=</span> [], <span class="dv">0</span>, <span class="dv">0</span></span>
<span id="cb8-84"><a href="#cb8-84" aria-hidden="true" tabindex="-1"></a> <span class="cf">while</span> <span class="kw">not</span> routing.IsEnd(index):</span>
<span id="cb8-85"><a href="#cb8-85" aria-hidden="true" tabindex="-1"></a> knoten <span class="op">=</span> manager.IndexToNode(index)</span>
<span id="cb8-86"><a href="#cb8-86" aria-hidden="true" tabindex="-1"></a> tour.append(knoten)</span>
<span id="cb8-87"><a href="#cb8-87" aria-hidden="true" tabindex="-1"></a> ladung <span class="op">+=</span> BEDARFE[knoten]</span>
<span id="cb8-88"><a href="#cb8-88" aria-hidden="true" tabindex="-1"></a> vorher <span class="op">=</span> index</span>
<span id="cb8-89"><a href="#cb8-89" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> loesung.Value(routing.NextVar(index))</span>
<span id="cb8-90"><a href="#cb8-90" aria-hidden="true" tabindex="-1"></a> strecke <span class="op">+=</span> routing.GetArcCostForVehicle(vorher, index, fahrzeug)</span>
<span id="cb8-91"><a href="#cb8-91" aria-hidden="true" tabindex="-1"></a> tour.append(manager.IndexToNode(index))</span>
<span id="cb8-92"><a href="#cb8-92" aria-hidden="true" tabindex="-1"></a> touren.append(tour)</span>
<span id="cb8-93"><a href="#cb8-93" aria-hidden="true" tabindex="-1"></a> strecken.append(strecke)</span>
<span id="cb8-94"><a href="#cb8-94" aria-hidden="true" tabindex="-1"></a> ladungen.append(ladung)</span>
<span id="cb8-95"><a href="#cb8-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-96"><a href="#cb8-96" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> {<span class="st">&quot;touren&quot;</span>: touren, <span class="st">&quot;strecken&quot;</span>: strecken, <span class="st">&quot;ladungen&quot;</span>: ladungen,</span>
<span id="cb8-97"><a href="#cb8-97" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;gesamtstrecke&quot;</span>: <span class="bu">sum</span>(strecken)}</span>
<span id="cb8-98"><a href="#cb8-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-99"><a href="#cb8-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-100"><a href="#cb8-100" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe(ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb8-101"><a href="#cb8-101" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft den Plan gegen die Wirklichkeit - unabhaengig vom Modell.</span></span>
<span id="cb8-102"><a href="#cb8-102" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-103"><a href="#cb8-103" aria-hidden="true" tabindex="-1"></a><span class="co"> Genau diese Trennung ist der Punkt: Die Pruefung darf nicht dieselben</span></span>
<span id="cb8-104"><a href="#cb8-104" aria-hidden="true" tabindex="-1"></a><span class="co"> Annahmen benutzen wie das Modell, sonst prueft sie nichts.</span></span>
<span id="cb8-105"><a href="#cb8-105" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb8-106"><a href="#cb8-106" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> []</span>
<span id="cb8-107"><a href="#cb8-107" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> fahrzeug, (ladung, kapazitaet) <span class="kw">in</span> <span class="bu">enumerate</span>(</span>
<span id="cb8-108"><a href="#cb8-108" aria-hidden="true" tabindex="-1"></a> <span class="bu">zip</span>(ergebnis[<span class="st">&quot;ladungen&quot;</span>], KAPAZITAETEN)):</span>
<span id="cb8-109"><a href="#cb8-109" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> ladung <span class="op">&gt;</span> kapazitaet:</span>
<span id="cb8-110"><a href="#cb8-110" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb8-111"><a href="#cb8-111" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Fahrzeug </span><span class="sc">{</span>fahrzeug <span class="op">+</span> <span class="dv">1</span><span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>ladung<span class="sc">}</span><span class="ss"> Paletten geladen, &quot;</span></span>
<span id="cb8-112"><a href="#cb8-112" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Kapazitaet </span><span class="sc">{</span>kapazitaet<span class="sc">}</span><span class="ss"> (</span><span class="sc">{</span>ladung <span class="op">-</span> kapazitaet<span class="sc">}</span><span class="ss"> zu viel)&quot;</span>)</span>
<span id="cb8-113"><a href="#cb8-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-114"><a href="#cb8-114" aria-hidden="true" tabindex="-1"></a> beliefert <span class="op">=</span> <span class="bu">sorted</span>(k <span class="cf">for</span> tour <span class="kw">in</span> ergebnis[<span class="st">&quot;touren&quot;</span>] <span class="cf">for</span> k <span class="kw">in</span> tour[<span class="dv">1</span>:<span class="op">-</span><span class="dv">1</span>])</span>
<span id="cb8-115"><a href="#cb8-115" aria-hidden="true" tabindex="-1"></a> erwartet <span class="op">=</span> <span class="bu">list</span>(<span class="bu">range</span>(<span class="dv">1</span>, <span class="bu">len</span>(BEDARFE)))</span>
<span id="cb8-116"><a href="#cb8-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beliefert <span class="op">!=</span> erwartet:</span>
<span id="cb8-117"><a href="#cb8-117" aria-hidden="true" tabindex="-1"></a> fehlend <span class="op">=</span> <span class="bu">set</span>(erwartet) <span class="op">-</span> <span class="bu">set</span>(beliefert)</span>
<span id="cb8-118"><a href="#cb8-118" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> fehlend:</span>
<span id="cb8-119"><a href="#cb8-119" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;nicht beliefert: </span><span class="sc">{</span><span class="bu">sorted</span>(fehlend)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-120"><a href="#cb8-120" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb8-121"><a href="#cb8-121" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-122"><a href="#cb8-122" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-123"><a href="#cb8-123" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb8-124"><a href="#cb8-124" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-125"><a href="#cb8-125" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Gesamtstrecke </span><span class="sc">{</span>ergebnis[<span class="st">&#39;gesamtstrecke&#39;</span>]<span class="sc">}</span><span class="ss"> km&quot;</span>)</span>
<span id="cb8-126"><a href="#cb8-126" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span><span class="st">&#39;Fahrzeug&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Stopps&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Strecke&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Ladung&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> &quot;</span></span>
<span id="cb8-127"><a href="#cb8-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Kapazitaet&#39;</span><span class="sc">:&gt;11}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-128"><a href="#cb8-128" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, (tour, strecke, ladung) <span class="kw">in</span> <span class="bu">enumerate</span>(</span>
<span id="cb8-129"><a href="#cb8-129" aria-hidden="true" tabindex="-1"></a> <span class="bu">zip</span>(ergebnis[<span class="st">&quot;touren&quot;</span>], ergebnis[<span class="st">&quot;strecken&quot;</span>], ergebnis[<span class="st">&quot;ladungen&quot;</span>])):</span>
<span id="cb8-130"><a href="#cb8-130" aria-hidden="true" tabindex="-1"></a> markierung <span class="op">=</span> <span class="st">&quot; &lt;-- ueberladen&quot;</span> <span class="cf">if</span> ladung <span class="op">&gt;</span> KAPAZITAETEN[i] <span class="cf">else</span> <span class="st">&quot;&quot;</span></span>
<span id="cb8-131"><a href="#cb8-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; </span><span class="sc">{</span>i <span class="op">+</span> <span class="dv">1</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">len</span>(tour) <span class="op">-</span> <span class="dv">2</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span>strecke<span class="sc">:&gt;8}</span><span class="ss"> km </span><span class="sc">{</span>ladung<span class="sc">:&gt;8}</span><span class="ss"> &quot;</span></span>
<span id="cb8-132"><a href="#cb8-132" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>KAPAZITAETEN[i]<span class="sc">:&gt;11}{</span>markierung<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-133"><a href="#cb8-133" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-134"><a href="#cb8-134" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe(ergebnis)</span>
<span id="cb8-135"><a href="#cb8-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beanstandungen:</span>
<span id="cb8-136"><a href="#cb8-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: DURCHGEFALLEN&quot;</span>)</span>
<span id="cb8-137"><a href="#cb8-137" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> text <span class="kw">in</span> beanstandungen:</span>
<span id="cb8-138"><a href="#cb8-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; - </span><span class="sc">{</span>text<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb8-139"><a href="#cb8-139" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb8-140"><a href="#cb8-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: bestanden&quot;</span>)</span>
<span id="cb8-141"><a href="#cb8-141" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-142"><a href="#cb8-142" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-143"><a href="#cb8-143" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb8-144"><a href="#cb8-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb8-145"><a href="#cb8-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DIE VERGESSENE DIMENSION&quot;</span>)</span>
<span id="cb8-146"><a href="#cb8-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb8-147"><a href="#cb8-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;16 Kunden, Gesamtbedarf </span><span class="sc">{</span><span class="bu">sum</span>(BEDARFE)<span class="sc">}</span><span class="ss"> Paletten, &quot;</span></span>
<span id="cb8-148"><a href="#cb8-148" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>ANZAHL_FAHRZEUGE<span class="sc">}</span><span class="ss"> Fahrzeuge zu je </span><span class="sc">{</span>KAPAZITAETEN[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb8-149"><a href="#cb8-149" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;= </span><span class="sc">{</span><span class="bu">sum</span>(KAPAZITAETEN)<span class="sc">}</span><span class="ss"> Paletten Flottenkapazitaet.&quot;</span>)</span>
<span id="cb8-150"><a href="#cb8-150" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-151"><a href="#cb8-151" aria-hidden="true" tabindex="-1"></a> ohne <span class="op">=</span> plane(mit_kapazitaet<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb8-152"><a href="#cb8-152" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] Ohne Ladungsdimension&quot;</span>, ohne)</span>
<span id="cb8-153"><a href="#cb8-153" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-154"><a href="#cb8-154" aria-hidden="true" tabindex="-1"></a> mit <span class="op">=</span> plane(mit_kapazitaet<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb8-155"><a href="#cb8-155" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] Mit AddDimensionWithVehicleCapacity&quot;</span>, mit)</span>
<span id="cb8-156"><a href="#cb8-156" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb8-157"><a href="#cb8-157" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb8-158"><a href="#cb8-158" aria-hidden="true" tabindex="-1"></a> mehr <span class="op">=</span> mit[<span class="st">&quot;gesamtstrecke&quot;</span>] <span class="op">-</span> ohne[<span class="st">&quot;gesamtstrecke&quot;</span>]</span>
<span id="cb8-159"><a href="#cb8-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Der korrekte Plan ist </span><span class="sc">{</span>mehr<span class="sc">}</span><span class="ss"> km laenger &quot;</span></span>
<span id="cb8-160"><a href="#cb8-160" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>mehr <span class="op">/</span> ohne[<span class="st">&#39;gesamtstrecke&#39;</span>] <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %).&quot;</span>)</span>
<span id="cb8-161"><a href="#cb8-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb8-162"><a href="#cb8-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Und genau darin liegt die Gefahr: Lauf [1] sieht BESSER aus. Wer&quot;</span>)</span>
<span id="cb8-163"><a href="#cb8-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;beide Zahlen nebeneinander legt, ohne die Ladung zu pruefen, haelt&quot;</span>)</span>
<span id="cb8-164"><a href="#cb8-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;die unfahrbare Loesung fuer die bessere Optimierung - und den&quot;</span>)</span>
<span id="cb8-165"><a href="#cb8-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;korrekten Plan fuer schlechte Arbeit.&quot;</span>)</span>
<span id="cb8-166"><a href="#cb8-166" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb8-167"><a href="#cb8-167" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Die Routing-Bibliothek kennt keine &#39;Kapazitaet&#39;. Sie kennt nur&quot;</span>)</span>
<span id="cb8-168"><a href="#cb8-168" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Dimensionen, die man ihr anlegt. Was nicht als Dimension existiert,&quot;</span>)</span>
<span id="cb8-169"><a href="#cb8-169" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;wird nicht begrenzt - und faellt niemandem auf, weil das Ergebnis&quot;</span>)</span>
<span id="cb8-170"><a href="#cb8-170" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;plausibel aussieht.&quot;</span>)</span>
<span id="cb8-171"><a href="#cb8-171" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
DIE VERGESSENE DIMENSION
==============================================================================
16 Kunden, Gesamtbedarf 37 Paletten, 4 Fahrzeuge zu je 10 = 40 Paletten Flottenkapazitaet.
[1] Ohne Ladungsdimension
Gesamtstrecke 326 km
Fahrzeug Stopps Strecke Ladung Kapazitaet
1 0 0 km 0 10
2 0 0 km 0 10
3 0 0 km 0 10
4 16 326 km 37 10 &lt;-- ueberladen
PRUEFUNG: DURCHGEFALLEN
- Fahrzeug 4: 37 Paletten geladen, Kapazitaet 10 (27 zu viel)
[2] Mit AddDimensionWithVehicleCapacity
Gesamtstrecke 572 km
Fahrzeug Stopps Strecke Ladung Kapazitaet
1 4 124 km 10 10
2 3 142 km 9 10
3 5 198 km 10 10
4 4 108 km 8 10
PRUEFUNG: bestanden
==============================================================================
Der korrekte Plan ist 246 km laenger (75.5 %).
Und genau darin liegt die Gefahr: Lauf [1] sieht BESSER aus. Wer
beide Zahlen nebeneinander legt, ohne die Ladung zu pruefen, haelt
die unfahrbare Loesung fuer die bessere Optimierung - und den
korrekten Plan fuer schlechte Arbeit.
Die Routing-Bibliothek kennt keine &#39;Kapazitaet&#39;. Sie kennt nur
Dimensionen, die man ihr anlegt. Was nicht als Dimension existiert,
wird nicht begrenzt - und faellt niemandem auf, weil das Ergebnis
plausibel aussieht.
==============================================================================</code></pre>
<blockquote>
<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>
</ul>
</blockquote>
<hr />
<h2 id="sec:graphen-quiz">8.8 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
Sechster und siebter Fund: die letzten handgeschriebenen Nummern Der Loesungsanhang trug 98 Marken der Bauart "**9.2 — Ansatz waehlen.**", saemtlich aus Version 03, wo Unsicherheit Kapitel 9 war. Heute ist es Kapitel 12, und die Aufgabe wird korrekt als "Aufgabe 12.2" gesetzt. Das war keine Schoenheitsfrage: "Aufgabe 9.2" existiert wirklich - sie gehoert zum Kapitel Metaheuristiken ("Zuggroesse und Temperatur"). Wer die Loesung zu 9.2 nachschlug, landete beim falschen Thema. Beide bisherigen Pruefungen liefen vorbei: Vor der Zahl steht kein Schluesselwort, und es ist keine Tabellenzelle. Zwei Messungen machten die Reparatur billig. In allen 15 nummerierten Abschnitten gab es exakt so viele Loesungen wie Aufgaben, lueckenlos 1..n - nur der Kapitelteil war falsch. Und die acht in Phase 3 ergaenzten Kapitel machten es laengst richtig (**Titel.** ohne Nummer); der Anhang wurde also nicht auf etwas Neues umgestellt, sondern auf das, was seine neueren Teile schon taten. nummeriere_marken() vergibt die Marken jetzt selbst. Der Praefix kommt NICHT aus "# Anhang A:" - der Anhang ist eine Ueberschrift, seine Abschnitte gehoeren aber zu 23 verschiedenen Kapiteln. Er kommt aus {#sec:loesungen-X} -> {#kap:X}; diese Zuordnung gilt geprueft fuer alle 23. Im Quelltext steht "**{loesung} — Titel.**". Siebter Fund unterwegs: neun Verweise auf Denkfehler-Nummern. Saetze wie "siehe Denkfehler 8.1" standen in alter Zaehlung - "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Graphen. Eine blosse Umnummerierung haette sie stillschweigend woanders hin zeigen lassen; das Ziel wurde deshalb fuer jeden einzelnen aus dem Zusammenhang bestimmt und auf {ref:sec:<kapitel>-denkfehler} umgestellt. Einer steckte in einem Programm-Docstring und bekam nach Regel 12 den Kapitelnamen statt eines Verweises. Eine bewusste Abweichung vom Plan: Der Denkfehler bekommt GAR KEINE Nummer. Es gibt je Kapitel genau einen, die zweite Stelle waere immer .1, und er steht ohnehin unter einer nummerierten Ueberschrift. Eine Nummer, die nie variiert, holte nur die Fragilitaet zurueck, die hier beseitigt wird. Auch das folgt den acht neuen Kapiteln, die ihren Denkfehler schon vorher nur mit dem Titel des Raetsels ankuendigen. --check bewacht jetzt alle Familien (Denkfehler N.M, Micro-Quiz N, **N.M — im Anhang) und zaehlt zusaetzlich ab, dass jeder Loesungsabschnitt so viele {loesung}-Marken hat wie sein Kapitel Aufgaben - das faengt eine vergessene oder doppelte Loesung, was keine Textsuche leisten kann. Gegengetestet mit vier kuenstlichen Fehlern auf einmal: alle vier gemeldet, nach dem Rueckbau null. Gegenprobe der Umstellung: Gesamtdokument vorher gesichert, nachher verglichen. 168 geaenderte Zeilen, restlos einer Familie zuzuordnen - 118 Loesungsmarken (138 gesamt minus 20, die in den Kapiteln 1 bis 3 zufaellig schon stimmten), 15 Anhang-Ueberschriften, 15 Kapitel-Callouts, 12 Micro-Quiz, 7 Zeilen mit Verweisen, 1 Docstring. Keine verschobene oder inhaltlich veraenderte Zeile. Ergebnis: 138 Aufgaben, 138 Loesungen, keine ohne Gegenstueck. Micro-Quiz tragen die echten Kapitelnummern. 293 Abschnitte, 720 Querverweise, 327 Indexmarken, PDF unveraendert 725 Seiten, 33 pytest-Tests, 67 netzfreie Programme fehlerfrei (die vier yfinance-Programme scheiterten am Rate-Limit des Anbieters, nicht an dieser Aenderung). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 09:52:41 +02:00
<p><strong>❓ Micro-Quiz 8: Drei Fragen zum Selbstcheck</strong></p>
Version 04 als eigenes Repository Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 01:20:09 +02:00
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="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>
</blockquote>
</div>
<hr />
<h2 id="sec:graphen-selbsttest">8.9 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-graphen">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Was besagt der Flusserhaltungssatz, und welchem physikalischen Gesetz entspricht er?</li>
<li>Warum liefert ein LP-Solver beim Zuordnungsproblem automatisch 0/1-Lösungen?</li>
<li>Was sind Subtouren, und wie verhindert die MTZ-Formulierung sie?</li>
<li>Warum sollte man ein reales VRP nicht als selbstgebautes MILP lösen?</li>
<li>Ein VRP meldet „keine Lösung“. Nennen Sie drei mögliche Ursachen und je eine Prüfung.</li>
</ol>
<hr />
<h2 id="sec:graphen-zusammenfassung">8.10 Zusammenfassung</h2>
<ul>
<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 <span class="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> <a href="qp-nlp.html#teil-nichtlinear">Teil III</a> verlässt die lineare Welt. <a href="qp-nlp.html#kap-qp-nlp">Kapitel 11</a> führt quadratische Zielfunktionen und die KKT-Bedingungen ein — das mathematische Fundament der Portfoliooptimierung.</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="graphen.html"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg> <span>Als gelesen markieren</span></button>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="cpsat.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a><a class="prev-next-knopf prev-next-next" href="metaheuristiken.html"><span><small>Weiter</small>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
</main>
</div>
<footer class="site-footer">
<p>© Dieter Schlüter · <a href="gesamtdokument.html">Gesamtdokument</a> ·
<a href="programme.html">Beispielprogramme</a></p>
</footer>
<script defer src="assets/search-index.js"></script>
<script defer src="assets/programme.js"></script>
<script defer src="assets/site.js"></script>
</body>
</html>