operations_research/OR_HTML_04/einfuehrung.html

1431 lines
146 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 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung · 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>
Phase 8.1: Synthese-Seiten je Teil - und die gebrochene Lesekette Setzt Paket 5 aus Verbesserungen_02.md um (den Teil, der nicht zurueckgestellt wurde). Fuenf neue Dateien 19_/29_/39_/49_/52_Synthese_*.md, je eine am Ende eines Teils, mit eigener Website-Seite ueber SONDERSEITEN - sie tragen bewusst keine "# Kapitel:"-Ueberschrift, weil sie keine Kapitel sind, sondern der Rueckblick auf einen Teil. Der Entwurf musste sich abgrenzen: Die Teil-Einleitungen haben bereits Entscheidungsdiagramme. Eine zweite Matrix am Teil-Ende waere eine Dopplung gewesen. Die Synthesen leisten deshalb, was eine Einleitung nicht kann - den Vergleich ueber die Kapitel hinweg (Verfahren nebeneinander, mit der Spalte "wo es aufhoert"), eine Tabelle "was dieser Teil gemessen hat" (Behauptung gegen Messung gegen Fundstelle) und drei Fehler, die der Teil verhindert. Zitiert wird ausschliesslich, was im Buch tatsaechlich gerechnet wird. Drei Funde beim Einbau: * Teil III sagte "die drei Kapitel dieses Teils", hat aber fuenf. Phase 3 hatte Mehrziel und Predict-then-Optimize hinzugefuegt, die Einleitung blieb stehen. * 50_Praxis.md verwies auf die Projektwerkstatt mit "acht eigene Anwendungen" - sie hat elf. * Und der eigentliche Fund: Die Lesekette der Quelldateien fuehrte an ACHT Kapiteln vorbei. 12_Python_Oekosystem zeigte direkt auf 20_Lineare_Programmierung, 23_Graphen direkt auf 30_QP, 32_Dynamische direkt auf 40_Finanzdaten, 50_Praxis direkt auf die Projektwerkstatt. Wer der Kette folgte, uebersprang acht von 23 Kapiteln - darunter Metaheuristiken, Spaltengenerierung, Strukturbruecke, Supply-Chain und das ganze Testing-Kapitel. Zehn weitere Dateien hatten gar keine Navigationszeile. Zur Reichweite, damit sie nicht ueberschaetzt wird: Diese Zeilen stehen nur in den Quelldateien. entferne_navigation() streicht sie aus dem Gesamtdokument, und die Website baut ihre Vor/Zurueck-Knoepfe selbst aus DATEIEN. PDF und Website waren nie betroffen - wohl aber jeder, der die Markdown-Dateien im Repository liest, und das wird nach der Veroeffentlichung der Normalfall sein. Die Kette ist jetzt ueber alle 35 Uebergaenge geschlossen, und --check bewacht sie: Fehlt eine Zeile oder zeigt sie an der in DATEIEN folgenden Datei vorbei, ist der Lauf rot. Gegengetestet mit beiden Bruchformen. Stand: 36 Dateien, 296 Abschnitte, 815 Querverweise, 328 Indexmarken, 76 Programme (unveraendert), 33 pytest-Tests, PDF 758 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 12:10:02 +02:00
<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" open><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html" class="aktiv"><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"><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"><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 I</span> <span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="notation.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Notation & Abkürzungen</span></a><a class="prev-next-knopf prev-next-next" href="fundament.html"><span><small>Weiter</small>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="teil-grundlagen">Teil I: Grundlagen des Operations Research</h1>
<hr />
<h1 id="kap-einfuehrung">Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Was Operations Research ist, warum Ausprobieren ab einer gewissen Problemgröße hoffnungslos wird, und aus welchen vier Bausteinen <em>jedes</em> Optimierungsmodell besteht. Los geht es mit einem Problem, das Sie in fünf Minuten selbst lösen.</p>
<p><strong>Voraussetzungen:</strong> Keine. Dies ist der Einstieg.</p>
<p><strong>Danach können Sie:</strong> Ein Alltagsproblem in Entscheidungsvariablen, Parameter, Zielfunktion und Nebenbedingungen zerlegen, ein erstes Modell mit Google OR-Tools lösen — und die Daten dafür aus einer Excel-Mappe holen und das Ergebnis dorthin zurückschreiben.</p>
<p><strong>Zeitbedarf:</strong> ca. 3,5 Stunden inklusive Übungen.</p>
<p><strong>Programme:</strong><br />
<code>Bot_Allokation.py</code><br />
<code>Brute_Force_Vergleich.py</code><br />
<code>Bausteine_Vorlage.py</code><br />
<code>Excel_Bruecke.py</code></p>
<p><strong>Notebook:</strong> <a href="Notebooks_04/einfuehrung.ipynb">einfuehrung.ipynb</a><br />
<a href="https://colab.research.google.com/github/dschlueter/or-mit-python/blob/main/Notebooks_04/einfuehrung.ipynb">In Google Colab öffnen</a></p>
</blockquote>
</div>
<hr />
<h2 id="sec:einfuehrung-schnellstart">1.1 In 5 Minuten gelöst</h2>
<p>Bevor wir über Theorie sprechen, lösen Sie ein echtes Problem. Kopieren Sie den folgenden Block, führen Sie ihn aus, sehen Sie das Ergebnis — die Erklärung kommt danach.</p>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Der Wochenplan einer Schreinerei</strong></p>
<p>Eine Schreinerei fertigt Tische und Stühle. Nächste Woche stehen <strong>150 Montagestunden</strong> und <strong>240 m² Plattenmaterial</strong> zur Verfügung. Die Frage des Chefs: <em>Was sollen wir bauen?</em></p>
<table>
<thead>
<tr class="header">
<th>Produkt</th>
<th style="text-align: right;">Deckungsbeitrag</th>
<th style="text-align: right;">Montagezeit</th>
<th style="text-align: right;">Plattenmaterial</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Tisch</td>
<td style="text-align: right;">240 €</td>
<td style="text-align: right;">3,0 h</td>
<td style="text-align: right;">6,0 m²</td>
</tr>
<tr class="even">
<td>Stuhl</td>
<td style="text-align: right;">60 €</td>
<td style="text-align: right;">1,0 h</td>
<td style="text-align: right;">1,0 m²</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">from</span> ortools.linear_solver <span class="im">import</span> pywraplp</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a>produkt <span class="op">=</span> {<span class="st">&quot;Tisch&quot;</span>: (<span class="dv">240</span>, <span class="fl">3.0</span>, <span class="fl">6.0</span>), <span class="st">&quot;Stuhl&quot;</span>: (<span class="dv">60</span>, <span class="fl">1.0</span>, <span class="fl">1.0</span>)} <span class="co"># DB, Stunden, m²</span></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>vorrat <span class="op">=</span> {<span class="st">&quot;Montagestunden&quot;</span>: <span class="dv">150</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="dv">240</span>}</span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>s <span class="op">=</span> pywraplp.Solver.CreateSolver(<span class="st">&quot;GLOP&quot;</span>)</span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a>x <span class="op">=</span> {p: s.NumVar(<span class="dv">0</span>, s.infinity(), p) <span class="cf">for</span> p <span class="kw">in</span> produkt}</span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>s.Add(<span class="bu">sum</span>(x[p] <span class="op">*</span> produkt[p][<span class="dv">1</span>] <span class="cf">for</span> p <span class="kw">in</span> produkt) <span class="op">&lt;=</span> vorrat[<span class="st">&quot;Montagestunden&quot;</span>])</span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>s.Add(<span class="bu">sum</span>(x[p] <span class="op">*</span> produkt[p][<span class="dv">2</span>] <span class="cf">for</span> p <span class="kw">in</span> produkt) <span class="op">&lt;=</span> vorrat[<span class="st">&quot;Plattenmaterial&quot;</span>])</span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a>s.Maximize(<span class="bu">sum</span>(x[p] <span class="op">*</span> produkt[p][<span class="dv">0</span>] <span class="cf">for</span> p <span class="kw">in</span> produkt))</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a>s.Solve()</span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p <span class="kw">in</span> produkt:</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">:6}</span><span class="ss">: </span><span class="sc">{</span>x[p]<span class="sc">.</span>solution_value()<span class="sc">:5.0f}</span><span class="ss"> Stueck&quot;</span>)</span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Deckungsbeitrag: </span><span class="sc">{</span>s<span class="sc">.</span>Objective()<span class="sc">.</span>Value()<span class="sc">:.0f}</span><span class="ss"> EUR&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Tisch : 30 Stueck
Stuhl : 60 Stueck
Deckungsbeitrag: 10800 EUR</code></pre>
</blockquote>
</div>
<p><strong>Und jetzt der Punkt, auf den es ankommt.</strong> Fragen Sie sich ehrlich: Was hätten <em>Sie</em> geantwortet? Die naheliegende Antwort lautet „möglichst viele Tische“ — der Tisch bringt mit 240 € schließlich den vierfachen Deckungsbeitrag eines Stuhls. Rechnen wir das durch:</p>
<table>
<colgroup>
<col style="width: 30%" />
<col style="width: 30%" />
<col style="width: 40%" />
</colgroup>
<thead>
<tr class="header">
<th>Strategie</th>
<th>Menge</th>
<th style="text-align: right;">Deckungsbeitrag</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Nur Tische (Bauchgefühl)</td>
<td>40 Tische (das Plattenmaterial ist zuerst leer)</td>
<td style="text-align: right;">9 600 €</td>
</tr>
<tr class="even">
<td>Nur Stühle</td>
<td>150 Stühle (die Montagezeit ist zuerst leer)</td>
<td style="text-align: right;">9 000 €</td>
</tr>
<tr class="odd">
<td><strong>Optimum des Solvers</strong></td>
<td><strong>30 Tische + 60 Stühle</strong></td>
<td style="text-align: right;"><strong>10 800 €</strong></td>
</tr>
</tbody>
</table>
<p>Das Bauchgefühl kostet <strong>1 200 € pro Woche</strong>, rund 62 000 € im Jahr — bei einem Problem mit sage und schreibe zwei Produkten und zwei Ressourcen. Genau das ist Operations Research: nicht das Ausrechnen von Zahlen, die man ohnehin kennt, sondern das Auffinden von Entscheidungen, auf die man von allein nicht kommt.</p>
<p><strong>Warum funktioniert das?</strong> Der Solver macht sich zunutze, dass keines der beiden Produkte in <em>beiden</em> Ressourcen das bessere ist. Pro Montagestunde bringt der Tisch 80 €, der Stuhl nur 60 € — pro Quadratmeter Plattenmaterial dagegen bringt der Stuhl 60 €, der Tisch nur 40 €. Wo kein Produkt durchgehend gewinnt, gibt es eine Mischung, die beide Engpässe gleichzeitig ausschöpft. Diese Mischung von Hand zu finden ist schon bei zwei Produkten mühsam und bei zwanzig unmöglich. Wie der Solver sie findet, zeigt <a href="lp.html#kap-lp">Kapitel 5</a>; was die einzelnen Zeilen des Programms bedeuten, klärt der Rest dieses Kapitels.</p>
<hr />
<h2 id="sec:einfuehrung-lernziele">1.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… in eigenen Worten erklären, wodurch sich Operations Research von Business Intelligence und maschinellem Lernen unterscheidet.</li>
<li>… abschätzen, ab welcher Problemgröße vollständiges Durchprobieren scheitert — und diese Abschätzung selbst rechnen.</li>
<li>… die vier Bausteine eines Modells benennen und einem konkreten Problem zuordnen.</li>
<li>… entscheiden, ob eine Bedingung eine <strong>harte</strong> oder eine <strong>weiche</strong> Nebenbedingung ist.</li>
<li>… ein einfaches Modell mit <strong>Google OR-Tools</strong> aufschreiben, lösen und das Ergebnis interpretieren.</li>
</ol>
<hr />
<h2 id="sec:einfuehrung-was-ist-operations-research-wirklich">1.3 Was ist Operations Research wirklich?</h2>
<p>Im Alltag lösen wir Probleme intuitiv: durch Ausprobieren, Daumenregeln, Erfahrung. Das funktioniert erstaunlich gut — bis eine bestimmte Schwelle überschritten wird. Dann bricht die Intuition nicht langsam ein, sondern schlagartig zusammen.</p>
<p>Drei Beispiele:</p>
<ul>
<li>Ein Disponent soll 40 Lastwagen auf 300 Lieferorte verteilen. Es gibt mehr mögliche Routenkombinationen als Atome im bekannten Universum: <span class="math inline">300! \approx 3{,}06 \times 10^{614}</span>.</li>
<li>Eine Klinik muss 200 Pflegekräfte einteilen — unter Berücksichtigung von 15 gesetzlichen Arbeitszeitregeln, Urlaubsanträgen, Qualifikationsstufen und Notfallreserven.</li>
<li>Ein Fondsmanager muss 10 Millionen Euro so auf 50 Aktien aufteilen, dass bei einer erwarteten Mindestrendite von 8 % das Verlustrisiko im schlechtesten Marktszenario minimal bleibt.</li>
</ul>
<p>Ein naiver Ansatz — <em>Brute Force</em>, das vollständige Durchrechnen aller Möglichkeiten — würde selbst auf den schnellsten Supercomputern der Welt Milliarden Jahre dauern.</p>
<blockquote>
<p><strong>📖 Definition: Operations Research</strong></p>
<p><strong>Operations Research (OR)</strong>, deutsch etwa <em>Unternehmensforschung</em> oder <em>mathematische Entscheidungsoptimierung</em>, ist die wissenschaftliche Disziplin, reale Entscheidungsprobleme in formale mathematische Modelle zu überführen und mithilfe exakter oder heuristischer Algorithmen (<em>Solver</em>) die beweisbar beste (<strong>optimale</strong>) oder eine messbar hochwertige Lösung zu finden.</p>
</blockquote>
<h3 id="die-drei-stufen-der-analytik">Die drei Stufen der Analytik</h3>
<p>OR beantwortet nicht die beschreibende Frage <em>„Was ist passiert?“</em> und nicht die vorhersagende Frage <em>„Was wird passieren?“</em>, sondern die <strong>vorschreibende</strong> (präskriptive) Kernfrage:</p>
<p><span class="math display">
\textbf{„Was ist unter den gegebenen Bedingungen die beste Handlung?“}
</span></p>
<figure>
<img src="bilder_04/kap01_analytik_reifegrad.svg" alt="Abb. 1.1: Analytik-Reifegrad vom Beschreiben zum Entscheiden" />
<figcaption aria-hidden="true">Abb. 1.1: Analytik-Reifegrad vom Beschreiben zum Entscheiden</figcaption>
</figure>
<table>
<colgroup>
<col style="width: 15%" />
<col style="width: 20%" />
<col style="width: 35%" />
<col style="width: 28%" />
</colgroup>
<thead>
<tr class="header">
<th>Stufe</th>
<th>Frage</th>
<th>Typisches Werkzeug</th>
<th>Ergebnis</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Deskriptiv</strong></td>
<td>Was ist passiert?</td>
<td>Business Intelligence, Dashboards</td>
<td>Ein Bericht</td>
</tr>
<tr class="even">
<td><strong>Prädiktiv</strong></td>
<td>Was wird geschehen?</td>
<td>Statistik, maschinelles Lernen</td>
<td>Eine Prognose</td>
</tr>
<tr class="odd">
<td><strong>Präskriptiv</strong></td>
<td>Was sollen wir tun?</td>
<td><strong>Operations Research</strong></td>
<td>Eine <strong>Handlungsanweisung</strong></td>
</tr>
</tbody>
</table>
<p>Die Stufen bauen aufeinander auf, ersetzen einander aber nicht. Eine Umsatzprognose sagt Ihnen, dass die Nachfrage im März um 20 % steigt. Sie sagt Ihnen nicht, welche der sieben Maschinen Sie dafür wann mit welchem Auftrag belegen sollen. Genau dort beginnt OR — und deshalb ist eine Prognose in diesem Buch fast immer nur eine <strong>Eingangsgröße</strong> des Modells, nie sein Ergebnis.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Maschinelles Lernen sagt Ihnen, was kommt. Operations Research sagt Ihnen, was Sie deswegen tun sollen.</p>
</blockquote>
<hr />
<h2 id="sec:einfuehrung-warum-ausprobieren-scheitert-mit-eigener-rechnung">1.4 Warum Ausprobieren scheitert — mit eigener Rechnung</h2>
<p>Die Behauptung „vollständiges Durchprobieren ist unmöglich“ wird oft aufgestellt und selten belegt. Rechnen wir sie einmal selbst nach — das Ergebnis prägt sich besser ein als jede Zahl im Text.</p>
<blockquote>
<p><strong>✏️ Handrechnung 1.1: Wie schnell wächst „alle Möglichkeiten“?</strong></p>
<p>Angenommen, Sie sollen <span class="math inline">n</span> Aufträge in eine Reihenfolge bringen. Für den ersten Platz haben Sie <span class="math inline">n</span> Kandidaten, für den zweiten <span class="math inline">n-1</span>, für den dritten <span class="math inline">n-2</span> und so fort. Die Gesamtzahl der Reihenfolgen ist damit die <strong>Fakultät</strong>:</p>
<p><span class="math display">n! = n \cdot (n-1) \cdot (n-2) \cdots 2 \cdot 1</span></p>
<p>Nehmen wir großzügig an, ein Rechner prüfe <strong>eine Milliarde</strong> (<span class="math inline">10^9</span>) Reihenfolgen pro Sekunde. Wie lange dauert es?</p>
<table>
<thead>
<tr class="header">
<th><span class="math inline">n</span></th>
<th><span class="math inline">n!</span></th>
<th>Rechenzeit bei <span class="math inline">10^9</span> Prüfungen/s</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>5</td>
<td>120</td>
<td>unmessbar kurz</td>
</tr>
<tr class="even">
<td>10</td>
<td>3 628 800</td>
<td>0,004 Sekunden</td>
</tr>
<tr class="odd">
<td>15</td>
<td><span class="math inline">1{,}3 \times 10^{12}</span></td>
<td>22 Minuten</td>
</tr>
<tr class="even">
<td><strong>20</strong></td>
<td><span class="math inline">2{,}4 \times 10^{18}</span></td>
<td><strong>77 Jahre</strong></td>
</tr>
<tr class="odd">
<td>25</td>
<td><span class="math inline">1{,}6 \times 10^{25}</span></td>
<td>490 Millionen Jahre</td>
</tr>
<tr class="even">
<td>30</td>
<td><span class="math inline">2{,}7 \times 10^{32}</span></td>
<td><span class="math inline">8{,}4 \times 10^{15}</span> Jahre</td>
</tr>
</tbody>
</table>
<p><strong>Der springende Punkt:</strong> Zwischen <span class="math inline">n = 15</span> (22 Minuten, machbar) und <span class="math inline">n = 20</span> (77 Jahre, hoffnungslos) liegen nur <strong>fünf zusätzliche Aufträge</strong>. Es gibt keine sanfte Verschlechterung — es gibt eine Wand.</p>
</blockquote>
<p>Und die Rechnung ist noch geschönt: Ein schnellerer Rechner hilft praktisch nicht. Selbst eine Beschleunigung um den Faktor <strong>eine Million</strong> verschiebt die Grenze von <span class="math inline">n=20</span> nur auf etwa <span class="math inline">n=24</span>. Gegen kombinatorisches Wachstum ist Hardware machtlos.</p>
<p>Das folgende Programm macht diesen Effekt erfahrbar. Es löst dasselbe Zuordnungsproblem zweimal — einmal durch vollständiges Durchprobieren, einmal mit einem OR-Solver — und vergleicht die Laufzeiten.</p>
<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"># Brute_Force_Vergleich.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 Einfuehrung: Warum Ausprobieren scheitert.</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a><span class="co">Vergleicht vollständige Enumeration mit einem Constraint-Solver an einem</span></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">Zuordnungsproblem wachsender Größe (n Mitarbeiter auf n Aufgaben).</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> itertools</span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> math</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb3-13"><a href="#cb3-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-14"><a href="#cb3-14" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> erzeuge_kostenmatrix(n: <span class="bu">int</span>, seed: <span class="bu">int</span> <span class="op">=</span> <span class="dv">7</span>) <span class="op">-&gt;</span> np.ndarray:</span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zufällige, aber reproduzierbare Kosten: Wer bearbeitet welche Aufgabe wie teuer?&quot;&quot;&quot;</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(seed)</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> rng.integers(low<span class="op">=</span><span class="dv">10</span>, high<span class="op">=</span><span class="dv">100</span>, size<span class="op">=</span>(n, n))</span>
<span id="cb3-22"><a href="#cb3-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-23"><a href="#cb3-23" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_brute_force(kosten: np.ndarray) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">float</span>, <span class="bu">tuple</span>, <span class="bu">int</span>]:</span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a><span class="co"> Probiert ALLE n! Zuordnungen durch und behält die beste.</span></span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a><span class="co"> Rückgabe: (bester Kostenwert, beste Permutation, Anzahl geprüfter Kombinationen)</span></span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(kosten)</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> bester_wert <span class="op">=</span> math.inf</span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a> beste_zuordnung <span class="op">=</span> <span class="va">None</span></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a> geprueft <span class="op">=</span> <span class="dv">0</span></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="co"># itertools.permutations(range(n)) liefert nacheinander jede Reihenfolge</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> zuordnung <span class="kw">in</span> itertools.permutations(<span class="bu">range</span>(n)):</span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> <span class="co"># zuordnung[i] = Aufgabe, die Mitarbeiter i übernimmt</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> wert <span class="op">=</span> <span class="bu">sum</span>(kosten[i][zuordnung[i]] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n))</span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> geprueft <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> wert <span class="op">&lt;</span> bester_wert:</span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> bester_wert <span class="op">=</span> wert</span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> beste_zuordnung <span class="op">=</span> zuordnung</span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> bester_wert, beste_zuordnung, geprueft</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_mit_solver(kosten: np.ndarray) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">float</span>, <span class="bu">tuple</span>]:</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Dasselbe Problem als Constraint-Programm — der Solver probiert NICHT alles durch.&quot;&quot;&quot;</span></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(kosten)</span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</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> <span class="co"># x[i][j] = 1 &lt;=&gt; Mitarbeiter i übernimmt Aufgabe j</span></span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> [[modell.NewBoolVar(<span class="ss">f&quot;x_</span><span class="sc">{</span>i<span class="sc">}</span><span class="ss">_</span><span class="sc">{</span>j<span class="sc">}</span><span class="ss">&quot;</span>) <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="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-54"><a href="#cb3-54" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n):</span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n)) <span class="co"># jeder genau eine Aufgabe</span></span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n):</span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i][j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)) <span class="co"># jede Aufgabe genau einmal</span></span>
<span id="cb3-58"><a href="#cb3-58" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-59"><a href="#cb3-59" aria-hidden="true" tabindex="-1"></a> modell.Minimize(<span class="bu">sum</span>(<span class="bu">int</span>(kosten[i][j]) <span class="op">*</span> x[i][j] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n)))</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> <span class="fl">30.0</span></span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="st">&quot;Solver fand keine Lösung.&quot;</span>)</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> <span class="bu">tuple</span>(</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">next</span>(j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(n) <span class="cf">if</span> loeser.Value(x[i][j]) <span class="op">==</span> <span class="dv">1</span>) <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n)</span>
<span id="cb3-69"><a href="#cb3-69" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb3-70"><a href="#cb3-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.ObjectiveValue(), zuordnung</span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-73"><a href="#cb3-73" 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-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; VOLLSTÄNDIGES DURCHPROBIEREN vs. OPERATIONS RESEARCH&quot;</span>)</span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">72</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><span class="st">&#39;n&#39;</span><span class="sc">:&gt;3}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Kombinationen&#39;</span><span class="sc">:&gt;18}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Brute Force&#39;</span><span class="sc">:&gt;14}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Solver&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> | gleich?&quot;</span>)</span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">72</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="cf">for</span> n <span class="kw">in</span> [<span class="dv">4</span>, <span class="dv">6</span>, <span class="dv">8</span>, <span class="dv">9</span>, <span class="dv">10</span>]: <span class="co"># n = 11 dauert bereits ~2 Minuten</span></span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> erzeuge_kostenmatrix(n)</span>
<span id="cb3-82"><a href="#cb3-82" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-83"><a href="#cb3-83" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> wert_bf, zuord_bf, geprueft <span class="op">=</span> loese_brute_force(kosten)</span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> zeit_bf <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a> wert_or, zuord_or <span class="op">=</span> loese_mit_solver(kosten)</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a> zeit_or <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</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> gleich <span class="op">=</span> <span class="st">&quot;ja&quot;</span> <span class="cf">if</span> <span class="bu">abs</span>(wert_bf <span class="op">-</span> wert_or) <span class="op">&lt;</span> <span class="fl">1e-9</span> <span class="cf">else</span> <span class="st">&quot;NEIN!&quot;</span></span>
<span id="cb3-92"><a href="#cb3-92" 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;3}</span><span class="ss"> | </span><span class="sc">{</span>geprueft<span class="sc">:&gt;18,}</span><span class="ss"> | </span><span class="sc">{</span>zeit_bf<span class="sc">:&gt;11.4f}</span><span class="ss"> s | </span><span class="sc">{</span>zeit_or<span class="sc">:&gt;7.4f}</span><span class="ss"> s | </span><span class="sc">{</span>gleich<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-93"><a href="#cb3-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-94"><a href="#cb3-94" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span>
<span id="cb3-95"><a href="#cb3-95" aria-hidden="true" tabindex="-1"></a> <span class="co"># Hochrechnung: Wie lange bräuchte Brute Force bei n = 20?</span></span>
<span id="cb3-96"><a href="#cb3-96" aria-hidden="true" tabindex="-1"></a> kombis_20 <span class="op">=</span> math.factorial(<span class="dv">20</span>)</span>
<span id="cb3-97"><a href="#cb3-97" aria-hidden="true" tabindex="-1"></a> <span class="co"># Messbasis: Prüfungen pro Sekunde aus dem letzten Lauf schätzen</span></span>
<span id="cb3-98"><a href="#cb3-98" aria-hidden="true" tabindex="-1"></a> pro_sekunde <span class="op">=</span> geprueft <span class="op">/</span> <span class="bu">max</span>(zeit_bf, <span class="fl">1e-9</span>)</span>
<span id="cb3-99"><a href="#cb3-99" aria-hidden="true" tabindex="-1"></a> jahre <span class="op">=</span> kombis_20 <span class="op">/</span> pro_sekunde <span class="op">/</span> (<span class="dv">60</span> <span class="op">*</span> <span class="dv">60</span> <span class="op">*</span> <span class="dv">24</span> <span class="op">*</span> <span class="fl">365.25</span>)</span>
<span id="cb3-100"><a href="#cb3-100" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Hochrechnung für n = 20: </span><span class="sc">{</span>kombis_20<span class="sc">:,}</span><span class="ss"> Kombinationen&quot;</span>)</span>
<span id="cb3-101"><a href="#cb3-101" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Bei gemessenen </span><span class="sc">{</span>pro_sekunde<span class="sc">:,.0f}</span><span class="ss"> Prüfungen/s wären das </span><span class="sc">{</span>jahre<span class="sc">:,.0f}</span><span class="ss"> Jahre.&quot;</span>)</span>
<span id="cb3-102"><a href="#cb3-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Solver löst dieselbe Instanz in Sekundenbruchteilen.&quot;</span>)</span>
<span id="cb3-103"><a href="#cb3-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">72</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
<pre><code>========================================================================
VOLLSTÄNDIGES DURCHPROBIEREN vs. OPERATIONS RESEARCH
========================================================================
n | Kombinationen | Brute Force | Solver | gleich?
------------------------------------------------------------------------
4 | 24 | 0.0001 s | 0.0408 s | ja
6 | 720 | 0.0021 s | 0.0067 s | ja
8 | 40,320 | 0.1088 s | 0.0088 s | ja
9 | 362,880 | 1.0596 s | 0.0120 s | ja
10 | 3,628,800 | 12.0897 s | 0.0252 s | ja
------------------------------------------------------------------------
Hochrechnung für n = 20: 2,432,902,008,176,640,000 Kombinationen
Bei gemessenen 300,157 Prüfungen/s wären das 256,845 Jahre.
Der Solver löst dieselbe Instanz in Sekundenbruchteilen.
========================================================================</code></pre>
<p>Lesen Sie die Tabelle von oben nach unten: Bei <span class="math inline">n = 4</span> ist das Durchprobieren noch <strong>400-mal schneller</strong> als der Solver — der Solveraufruf hat einen festen Startaufwand von einigen Millisekunden, der sich bei winzigen Problemen nicht lohnt. Ab <span class="math inline">n = 8</span> kippt das Verhältnis, und ab <span class="math inline">n = 10</span> ist der Solver bereits <strong>480-mal schneller</strong>. Entscheidend ist nicht der Faktor, sondern die <em>Richtung</em>: Die Brute-Force-Zeit verzehnfacht sich mit jedem zusätzlichen Element, die Solver-Zeit wächst kaum merklich.</p>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 31%" />
<col style="width: 40%" />
</colgroup>
<thead>
<tr class="header">
<th>Zeile / Stelle</th>
<th>Was passiert</th>
<th>Warum</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>itertools.permutations(range(n))</code></td>
<td>erzeugt nacheinander alle <span class="math inline">n!</span> Reihenfolgen</td>
<td>die Definition von „alles durchprobieren“</td>
</tr>
<tr class="even">
<td><code>wert = sum(kosten[i][zuordnung[i]] ...)</code></td>
<td>bewertet eine einzelne Kombination</td>
<td>das ist die <strong>Zielfunktion</strong>, nur eben von Hand ausgewertet</td>
</tr>
<tr class="odd">
<td><code>modell.NewBoolVar(...)</code></td>
<td>legt eine Ja/Nein-<strong>Entscheidungsvariable</strong> an</td>
<td><span class="math inline">n^2</span> Variablen statt <span class="math inline">n!</span> Kombinationen</td>
</tr>
<tr class="even">
<td><code>modell.AddExactlyOne(...)</code></td>
<td>„genau eine davon ist wahr“</td>
<td>die <strong>Nebenbedingungen</strong></td>
</tr>
<tr class="odd">
<td><code>modell.Minimize(...)</code></td>
<td>die zu minimierende Summe</td>
<td>die <strong>Zielfunktion</strong>, jetzt deklariert statt ausgerechnet</td>
</tr>
<tr class="even">
<td><code>loeser.Solve(modell)</code></td>
<td>der Solver sucht — aber nicht stur</td>
<td>er nutzt Struktur (Schranken, Propagation), statt abzuzählen</td>
</tr>
</tbody>
</table>
<p><strong>Die entscheidende Beobachtung:</strong> Beide Programme liefern denselben Zielwert (Spalte „gleich?“). Der Solver rät also nicht und liefert keine Näherung — er findet dasselbe beweisbare Optimum, nur ohne alles anzusehen.</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>„Dann nehme ich eben eine schnellere Sprache.“</strong> C++ ist etwa 50-mal schneller als Python. Bei <span class="math inline">n=20</span> verkürzt das 157 Millionen Jahre auf 3 Millionen Jahre. Das Problem ist nicht die Sprache, sondern der Ansatz.</li>
<li><strong>„Ich probiere nur die plausiblen Kombinationen.“</strong> Genau das tut ein Solver — nur systematisch, mit einem Beweis, dass die weggelassenen Zweige nichts Besseres enthalten. Eine handgestrickte Auswahl hat diesen Beweis nicht.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:einfuehrung-historischer-kontext-und-evolution">1.5 Historischer Kontext und Evolution</h2>
<p>Der Begriff entstand im Vorfeld des Zweiten Weltkriegs in Großbritannien. Militärische Führungsstäbe standen vor neuartigen logistischen und strategischen Fragen: Wie platziert man die neu entwickelten Radaranlagen an der Küste optimal? Wie groß müssen Schiffskonvois sein, um U-Boot-Angriffe bei minimalem Geleitschutzaufwand abzuwehren?</p>
<p>Wissenschaftler wie <strong>George Dantzig</strong>, <strong>Patrick Blackett</strong> und <strong>John von Neumann</strong> entwickelten daraufhin mathematische Formalismen. 1947 erfand George Dantzig den <strong>Simplex-Algorithmus</strong> zur Lösung linearer Programme — ein Durchbruch, der die industrielle Planung in den 1950er-Jahren revolutionierte und den wir in <a href="lp.html#kap-lp">Kapitel 5</a> selbst programmieren werden.</p>
<p>Heute treibt OR die Kernsysteme moderner Industrien an:</p>
<ul>
<li><strong>Tech-Konzerne:</strong> Server-Scheduling, Datenrouting in Glasfasernetzen, Werbeplatzierungsauktionen.</li>
<li><strong>Luftfahrt &amp; Logistik:</strong> Crew-Scheduling, Flugzeugumlaufplanung, Paketlogistik (etwa das System <em>ORION</em> von UPS, das durch Routenoptimierung jährlich zweistellige Millionenbeträge an Treibstoff einspart).</li>
<li><strong>Energie:</strong> Kraftwerkseinsatzplanung, Netzausbau, Speicherbewirtschaftung.</li>
<li><strong>Gesundheitswesen:</strong> OP-Saal-Belegung, Dienstpläne, Rettungsmittel-Standorte.</li>
<li><strong>Finanzindustrie:</strong> Arbitrage-Freiheit, Portfolio-Optimierung, Liquiditäts- und Orderausführungsmanagement — der Gegenstand von <a href="bruecke.html#teil-anwendungen">Teil IV</a> dieses Buches.</li>
</ul>
<hr />
<h2 id="sec:einfuehrung-die-vier-universellen-bausteine-jedes-or">1.6 Die vier universellen Bausteine jedes OR-Problems</h2>
<p>Dies ist der wichtigste Abschnitt des Kapitels. Jedes Optimierungsproblem der Welt lässt sich — unabhängig von der Branche — auf vier Elemente reduzieren. Wenn Sie später vor einem neuen Problem sitzen, ist dies Ihre Checkliste.</p>
<figure>
<img src="bilder_04/kap01_vier_bausteine.svg" alt="Abb. 1.2: Die vier universellen Bausteine eines OR-Modells" />
<figcaption aria-hidden="true">Abb. 1.2: Die vier universellen Bausteine eines OR-Modells</figcaption>
</figure>
<h3 id="baustein-1-entscheidungsvariablen-x">Baustein 1 — Entscheidungsvariablen (<span class="math inline">x</span>)</h3>
<p>Die <strong>Stellschrauben</strong> des Modells: Werte, die der Algorithmus aktiv festlegen darf.</p>
<table>
<colgroup>
<col style="width: 13%" />
<col style="width: 17%" />
<col style="width: 55%" />
<col style="width: 12%" />
</colgroup>
<thead>
<tr class="header">
<th>Typ</th>
<th>Notation</th>
<th>Beispiel</th>
<th>Problemklasse</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Kontinuierlich</td>
<td><span class="math inline">x \in \mathbb{R}</span></td>
<td>Investitionsbetrag in Euro, Fördermenge in Litern</td>
<td>LP — leicht</td>
</tr>
<tr class="even">
<td>Ganzzahlig</td>
<td><span class="math inline">x \in \mathbb{Z}</span></td>
<td>Anzahl produzierter Maschinen, Anzahl Mitarbeiter</td>
<td>MILP — schwer</td>
</tr>
<tr class="odd">
<td>Binär</td>
<td><span class="math inline">x \in \{0, 1\}</span></td>
<td>„Aktie <span class="math inline">i</span> kaufen: ja/nein“, „Schicht zuweisen: ja/nein“</td>
<td>MILP — schwer</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>Die Leitfrage:</strong> <em>Worüber darf ich überhaupt entscheiden?</em> Alles, was feststeht, ist kein Variable, sondern Parameter. Die häufigste Ursache für unbrauchbare Modelle ist eine falsch gewählte Variablendefinition — nicht ein falscher Solver.</p>
</blockquote>
<h3 id="baustein-2-parameter-eingabedaten-c-a-b">Baustein 2 — Parameter / Eingabedaten (<span class="math inline">c, A, b</span>)</h3>
<p>Die <strong>unveränderlichen Fakten</strong> der Realität: historische Renditen, Maschinenstundensätze, Lagerkapazitäten, Steuerquoten, Entfernungen, Qualifikationen.</p>
<blockquote>
<p><strong>Die Leitfrage:</strong> <em>Was ist gegeben und wird nicht entschieden?</em> Parameter sind der Ort, an dem Datenqualität über Modellqualität entscheidet. <a href="finanzdaten.html#kap-finanzdaten">Kapitel 18</a> zeigt am Beispiel der Kovarianzmatrix, wie ein schlecht geschätzter Parameter ein mathematisch perfektes Modell wertlos macht.</p>
</blockquote>
<h3 id="baustein-3-zielfunktion-fx">Baustein 3 — Zielfunktion (<span class="math inline">f(x)</span>)</h3>
<p>Ein mathematischer Ausdruck, der die <strong>Güte</strong> einer Lösung zu <strong>einer einzigen Zahl</strong> verdichtet:</p>
<p><span class="math display">
\min_{x} f(x) \qquad \text{oder} \qquad \max_{x} f(x)
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong></p>
<ul>
<li><span class="math inline">f(x)</span> — die Bewertungsfunktion: „Wie gut ist die Lösung <span class="math inline">x</span>?“</li>
<li><span class="math inline">\min_x</span> — „wähle dasjenige <span class="math inline">x</span>, das <span class="math inline">f</span> so klein wie möglich macht“</li>
<li>Das <span class="math inline">x</span> unter dem <span class="math inline">\min</span> sagt, <strong>worüber</strong> minimiert wird (über die Variablen, nicht etwa über die Parameter).</li>
</ul>
<p><strong>Ohne Formel gesagt:</strong> Von allen erlaubten Handlungsmöglichkeiten suchen wir diejenige mit den geringsten Kosten (bzw. dem höchsten Gewinn).</p>
</blockquote>
<blockquote>
<p><strong>Die Leitfrage:</strong> <em>Was genau soll besser werden — und in welcher Einheit?</em> Wenn Sie die Einheit nicht nennen können (Euro, Minuten, Strafpunkte), ist die Zielfunktion noch nicht fertig. Mehrere gleichzeitige Ziele müssen entweder gewichtet in <strong>eine</strong> Zahl überführt oder hierarchisch nacheinander optimiert werden.</p>
</blockquote>
<h3 id="baustein-4-nebenbedingungen-constraints">Baustein 4 — Nebenbedingungen (<em>Constraints</em>)</h3>
<p>Regeln, die den Raum der erlaubten Lösungen einschränken:</p>
<p><span class="math display">
g_i(\mathbf{x}) \le b_i \qquad \text{bzw.} \qquad h_j(\mathbf{x}) = 0
</span></p>
<p><strong>Harte Nebenbedingungen (<em>hard constraints</em>)</strong> sind zwingend: gesetzliche Ruhezeit <span class="math inline">\ge 11</span> Stunden, Summe der Portfoliogewichte <span class="math inline">= 100\,\%</span>, maximales Verlustrisiko <span class="math inline">\le 5\,\%</span>. Wird eine einzige harte Bedingung verletzt, ist die Lösung mathematisch <strong>unzulässig</strong> (<em>infeasible</em>) — sie existiert für den Solver schlicht nicht.</p>
<p><strong>Weiche Nebenbedingungen (<em>soft constraints</em>)</strong> sind Wünsche: Mitarbeiterpräferenzen, möglichst wenig Umschichtung, gleichmäßige Lastverteilung. Sie werden über <strong>Strafkosten</strong> (<em>penalties</em>) in die Zielfunktion integriert und dürfen im Notfall verletzt werden — es kostet dann eben.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Harte Bedingung = „darf nicht“. Weiche Bedingung = „soll möglichst nicht, sonst kostet es X“. Die Entscheidung zwischen beiden ist eine der folgenreichsten im ganzen Modell: Zu viele harte Bedingungen erzeugen unlösbare Modelle (<a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>), zu wenige erzeugen Lösungen, die niemand akzeptiert.</p>
</blockquote>
<h3 id="die-vier-bausteine-als-eine-formel">Die vier Bausteine als eine Formel</h3>
<p>Setzt man die vier Bausteine zusammen, entsteht die Standardform, in der Ihnen jedes Optimierungsmodell dieses Buches begegnen wird. Sie sieht auf den ersten Blick sperrig aus — deshalb steht daneben, was jede Zeile in Alltagssprache bedeutet.</p>
<p><span class="math display">
\max_{\mathbf{x}}\ \sum_{j=1}^{n} c_j x_j
\qquad \text{u. d. N.} \qquad
\sum_{j=1}^{n} a_{ij} x_j \le b_i \quad \forall i = 1,\dots,m,
\qquad x_j \ge 0
</span></p>
<div class="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer</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">x_j</span></td>
<td>Wie viel bauen / kaufen / einplanen wir von Produkt <span class="math inline">j</span>? Das ist die Stellschraube — der Solver entscheidet darüber.</td>
</tr>
<tr class="even">
<td><span class="math inline">c_j</span></td>
<td>Was bringt eine Einheit von <span class="math inline">j</span>? Im Schreinerei-Beispiel: 240 € beim Tisch, 60 € beim Stuhl.</td>
</tr>
<tr class="odd">
<td><span class="math inline">\sum_{j} c_j x_j</span></td>
<td>„Zähle über alle Produkte zusammen: Menge mal Stückertrag.“ Das ist der Gesamtdeckungsbeitrag.</td>
</tr>
<tr class="even">
<td><span class="math inline">\max_{\mathbf{x}}</span></td>
<td>„Suche unter allen erlaubten Mengenkombinationen die mit dem höchsten Gesamtertrag.“ Das <span class="math inline">\mathbf{x}</span> darunter sagt nur, worüber entschieden wird.</td>
</tr>
<tr class="odd">
<td><span class="math inline">a_{ij}</span></td>
<td>Wie viel von Ressource <span class="math inline">i</span> verbraucht eine Einheit von Produkt <span class="math inline">j</span>? Ein Tisch verbraucht 3 Montagestunden.</td>
</tr>
<tr class="even">
<td><span class="math inline">b_i</span></td>
<td>Wie viel von Ressource <span class="math inline">i</span> ist überhaupt da? 150 Montagestunden.</td>
</tr>
<tr class="odd">
<td><span class="math inline">\sum_{j} a_{ij} x_j \le b_i</span></td>
<td>„Was alle Produkte zusammen von Ressource <span class="math inline">i</span> verbrauchen, darf den Vorrat nicht überschreiten.“</td>
</tr>
<tr class="even">
<td><span class="math inline">\forall i = 1,\dots,m</span></td>
<td>„und zwar für jede einzelne Ressource“ — aus einer geschriebenen Zeile werden im Modell <span class="math inline">m</span> Nebenbedingungen.</td>
</tr>
<tr class="odd">
<td><span class="math inline">x_j \ge 0</span></td>
<td>„Negative Mengen gibt es nicht.“ Trivial für Menschen, muss dem Solver aber gesagt werden.</td>
</tr>
</tbody>
</table>
<p><strong>Der ganze Ausdruck in einem Satz:</strong> <em>Verteile die knappen Ressourcen so auf die Produkte, dass der Gesamtertrag maximal wird und kein Vorrat überzogen wird.</em></p>
</blockquote>
</div>
<p>Das <span class="math inline">\forall</span> („für alle“) ist dabei das Zeichen mit der größten praktischen Wirkung: Es verwandelt eine Zeile Mathematik in eine <code>for</code>-Schleife und damit in beliebig viele Nebenbedingungen. Im Schreinerei-Programm oben steckt es in den zwei <code>s.Add(...)</code>-Zeilen — bei 40 Ressourcen wären es 40, geschrieben in derselben einen Schleife.</p>
<h3 id="die-bausteine-in-der-praxis-eine-vorlage">Die Bausteine in der Praxis: eine Vorlage</h3>
<p>Damit die Zerlegung zur Gewohnheit wird, hier eine Vorlage, die Sie bei jedem neuen Problem ausfüllen können — vor der ersten Codezeile.</p>
<table style="width:100%;">
<colgroup>
<col style="width: 23%" />
<col style="width: 64%" />
<col style="width: 11%" />
</colgroup>
<thead>
<tr class="header">
<th>Baustein</th>
<th>Leitfrage</th>
<th>Ihr Problem</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Entscheidungsvariablen</td>
<td>Worüber darf ich entscheiden? Welcher Typ?</td>
<td></td>
</tr>
<tr class="even">
<td>Parameter</td>
<td>Was ist gegeben? Woher kommen die Daten?</td>
<td></td>
</tr>
<tr class="odd">
<td>Zielfunktion</td>
<td>Was soll besser werden? In welcher Einheit? Min oder Max?</td>
<td></td>
</tr>
<tr class="even">
<td>Harte Bedingungen</td>
<td>Was ist <em>unter keinen Umständen</em> erlaubt?</td>
<td></td>
</tr>
<tr class="odd">
<td>Weiche Bedingungen</td>
<td>Was ist unerwünscht, aber im Notfall hinnehmbar? Wie teuer?</td>
<td></td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>✏️ Handrechnung 1.2: Zerlegen Sie dieses Problem</strong></p>
<p><em>Eine Bäckerei backt Brot und Brötchen. Ein Brot bringt 2,50 € Deckungsbeitrag und braucht 0,5 kg Mehl und 4 Minuten Ofenzeit; 10 Brötchen bringen 3,00 € und brauchen 0,6 kg Mehl und 3 Minuten Ofenzeit. Verfügbar sind 90 kg Mehl und 600 Minuten Ofenzeit. Aus Vertragsgründen müssen mindestens 40 Brote gebacken werden.</em></p>
<p>Füllen Sie die Vorlage aus, bevor Sie weiterlesen. — Auflösung:</p>
<ul>
<li><strong>Variablen:</strong> <span class="math inline">x_1</span> = Anzahl Brote, <span class="math inline">x_2</span> = Anzahl Brötchen-Zehnerpackungen; beide ganzzahlig <span class="math inline">\ge 0</span>.</li>
<li><strong>Parameter:</strong> Deckungsbeiträge <span class="math inline">(2{,}50;\ 3{,}00)</span>; Mehlbedarf <span class="math inline">(0{,}5;\ 0{,}6)</span>; Ofenzeit <span class="math inline">(4;\ 3)</span>; Kapazitäten <span class="math inline">(90;\ 600)</span>; Mindestmenge 40.</li>
<li><strong>Zielfunktion:</strong> <span class="math inline">\max\ 2{,}5x_1 + 3{,}0x_2</span> (Einheit: Euro).</li>
<li><strong>Harte Bedingungen:</strong> <span class="math inline">0{,}5x_1 + 0{,}6x_2 \le 90</span> (Mehl); <span class="math inline">4x_1 + 3x_2 \le 600</span> (Ofen); <span class="math inline">x_1 \ge 40</span> (Vertrag).</li>
<li><strong>Weiche Bedingungen:</strong> keine.</li>
</ul>
Achter Fund (Uebung-Verweise) und Phase 8.3: Dockerfile Zwei Dinge in einem Commit, weil beide den Plan abschliessen. ACHTER FUND: sieben Saetze der Bauart "Uebung 8.5 laesst Sie diese Balance untersuchen" - und sechs davon standen in alter Zaehlung. "8" war in Version 03 das QP/NLP-Kapitel, heute ist es Kapitel 11. Dieselbe Familie wie die Denkfehler-Verweise aus 6.1a, nur mit einem Wort, das keine der bestehenden Pruefungen kannte. Aufgaben haben kein eigenes Label, ein {ref:} auf eine einzelne Aufgabe ist also nicht moeglich. Verwiesen wird stattdessen auf Abschnitt plus Aufgabentitel - und der ist stabil. Jedes Ziel wurde einzeln ueber die alte Zaehlung bestimmt und am Zusammenhang geprueft. Ein Fall war knifflig: "Uebung 6.7 (Wochendienstplan)" meinte die siebte CP-SAT-Aufgabe der alten Zaehlung, also "Eigener Dienstplan" - die heute an achter Stelle steht, weil in Phase 6.3 eine Aufgabe davor eingefuegt wurde. Wer nur die Kapitelnummer angepasst haette, waere bei der falschen Aufgabe gelandet. --check kennt jetzt auch "Uebung"/"Übung". Gegengetestet. PHASE 8.3: Dockerfile, zweistufig. Die erste Stufe uebersetzt die Abhaengigkeiten in eine virtuelle Umgebung und braucht dafuer einen Compiler, die zweite kopiert nur /opt/venv. Installiert werden die Gruppen finance, large-scale, api und dev aus pyproject.toml; figures fehlt bewusst, weil es zusaetzlich Graphviz verlangt. Es wurde nicht behauptet, sondern gebaut. Ergebnis: 1,31 GB, und darin der Installationstest mit allen drei Solver-Funktionstests bestanden, die 33 pytest-Tests bestanden und alle 69 netzfreien Programme fehlerfrei - einschliesslich der drei aus 8.2, deren spawn-Isolation im Container ebenso traegt wie ausserhalb. Zwei Dinge, die der Bau gelehrt hat: libgomp1 fehlt im python:3.12-slim-Image und wird von OR-Tools und HiGHS zur Laufzeit gebraucht (sonst "libgomp.so.1: cannot open shared object file"). Und ein eigener Fehler: USER kurs stand vor dem mkdir /buch/output, /buch gehoert root, der Bau brach in der letzten Zeile ab. Beides steht jetzt als Kommentar im Dockerfile. Das Image fuehrt die Programme aus und baut das Buch nicht. Ein .dockerignore haelt Website, PDF und Notebooks aus dem Build-Kontext. Und es enthaelt ortools UND highspy, obwohl sie sich nicht gemeinsam importieren lassen - der Konflikt wird zur Laufzeit durch getrennte Prozesse geloest, nicht durch Weglassen. Damit ist Phase 8 abgeschlossen und der Plan abgearbeitet. Stand: 825 Querverweise, 76 Programme, 33 pytest-Tests, PDF 760 Seiten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 13:18:05 +02:00
<p>Dieses Problem lösen Sie in der Aufgabe <em>Die Bäckerei programmieren</em> (<a href="#sec:einfuehrung-uebungsaufgaben">Abschnitt 1.9</a>) selbst.</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>
<p>Die Vorlage lässt sich fast wörtlich in Code übersetzen — wer die fünf Zeilen ausgefüllt hat, hat den größten Teil der Modellierungsarbeit schon erledigt:</p>
<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"># Bausteine_Vorlage.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 Einfuehrung: Die Bausteine-Vorlage als ausfuellbares Python-Geruest.</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">Uebersetzt die Vier-Bausteine-Vorlage aus dem Abschnitt &#39;Die vier</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">universellen Bausteine jedes OR-Problems&#39; direkt in Code: Wer sie</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a><span class="co">auf Papier ausgefuellt hat, kann sie fast unveraendert in ein loesbares</span></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="co">Modell umsetzen. Angewendet auf das Baeckerei-Beispiel der Handrechnung.</span></span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-12"><a href="#cb5-12" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-13"><a href="#cb5-13" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dataclasses <span class="im">import</span> dataclass</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</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>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a><span class="at">@dataclass</span></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Bausteine:</span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Die vier Bausteine jedes OR-Problems - wortwoertlich aus der Vorlage</span></span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a><span class="co"> im Abschnitt &#39;Die vier universellen Bausteine&#39; uebernommen.&quot;&quot;&quot;</span></span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> variablen: <span class="bu">str</span></span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> parameter: <span class="bu">str</span></span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> zielfunktion: <span class="bu">str</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> harte_bedingungen: <span class="bu">str</span></span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> weiche_bedingungen: <span class="bu">str</span></span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a> <span class="kw">def</span> zeige(<span class="va">self</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb5-28"><a href="#cb5-28" 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="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; AUSGEFUELLTE BAUSTEINE-VORLAGE&quot;</span>)</span>
<span id="cb5-30"><a href="#cb5-30" 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="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> feld, wert <span class="kw">in</span> [</span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Entscheidungsvariablen&quot;</span>, <span class="va">self</span>.variablen),</span>
<span id="cb5-33"><a href="#cb5-33" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Parameter&quot;</span>, <span class="va">self</span>.parameter),</span>
<span id="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Zielfunktion&quot;</span>, <span class="va">self</span>.zielfunktion),</span>
<span id="cb5-35"><a href="#cb5-35" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Harte Bedingungen&quot;</span>, <span class="va">self</span>.harte_bedingungen),</span>
<span id="cb5-36"><a href="#cb5-36" aria-hidden="true" tabindex="-1"></a> (<span class="st">&quot;Weiche Bedingungen&quot;</span>, <span class="va">self</span>.weiche_bedingungen),</span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> ]:</span>
<span id="cb5-38"><a href="#cb5-38" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>feld<span class="sc">:&lt;24}</span><span class="ss">: </span><span class="sc">{</span>wert<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-39"><a href="#cb5-39" 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="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a></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 class="co"># --- Baeckerei-Beispiel aus der Handrechnung ---------------------------------</span></span>
<span id="cb5-43"><a href="#cb5-43" aria-hidden="true" tabindex="-1"></a>VORLAGE <span class="op">=</span> Bausteine(</span>
<span id="cb5-44"><a href="#cb5-44" aria-hidden="true" tabindex="-1"></a> variablen<span class="op">=</span><span class="st">&quot;x1 = Anzahl Brote, x2 = Anzahl Broetchen-Zehnerpackungen &quot;</span></span>
<span id="cb5-45"><a href="#cb5-45" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;(beide ganzzahlig &gt;= 0)&quot;</span>,</span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> parameter<span class="op">=</span><span class="st">&quot;Deckungsbeitraege (2,50; 3,00) EUR; Mehlbedarf (0,5; 0,6) kg; &quot;</span></span>
<span id="cb5-47"><a href="#cb5-47" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Ofenzeit (4; 3) min; Kapazitaeten (90 kg, 600 min); Mindestmenge 40 Brote&quot;</span>,</span>
<span id="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a> zielfunktion<span class="op">=</span><span class="st">&quot;max 2,5*x1 + 3,0*x2 (Einheit: Euro Deckungsbeitrag)&quot;</span>,</span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a> harte_bedingungen<span class="op">=</span><span class="st">&quot;0,5*x1 + 0,6*x2 &lt;= 90 (Mehl); 4*x1 + 3*x2 &lt;= 600 (Ofen); &quot;</span></span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;x1 &gt;= 40 (Vertrag)&quot;</span>,</span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> weiche_bedingungen<span class="op">=</span><span class="st">&quot;keine&quot;</span>,</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>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_baeckerei():</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Baut aus der ausgefuellten Vorlage mechanisch ein CP-SAT-Modell.&quot;&quot;&quot;</span></span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb5-58"><a href="#cb5-58" aria-hidden="true" tabindex="-1"></a> x1 <span class="op">=</span> modell.NewIntVar(<span class="dv">40</span>, <span class="dv">1000</span>, <span class="st">&quot;Brote&quot;</span>) <span class="co"># Vertrag: mindestens 40</span></span>
<span id="cb5-59"><a href="#cb5-59" aria-hidden="true" tabindex="-1"></a> x2 <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">1000</span>, <span class="st">&quot;Broetchen_Zehner&quot;</span>)</span>
<span id="cb5-60"><a href="#cb5-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-61"><a href="#cb5-61" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="dv">5</span> <span class="op">*</span> x1 <span class="op">+</span> <span class="dv">6</span> <span class="op">*</span> x2 <span class="op">&lt;=</span> <span class="dv">900</span>) <span class="co"># Mehl, x10 fuer Ganzzahligkeit</span></span>
<span id="cb5-62"><a href="#cb5-62" aria-hidden="true" tabindex="-1"></a> modell.Add(<span class="dv">4</span> <span class="op">*</span> x1 <span class="op">+</span> <span class="dv">3</span> <span class="op">*</span> x2 <span class="op">&lt;=</span> <span class="dv">600</span>) <span class="co"># Ofenzeit</span></span>
<span id="cb5-63"><a href="#cb5-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-64"><a href="#cb5-64" aria-hidden="true" tabindex="-1"></a> modell.Maximize(<span class="dv">25</span> <span class="op">*</span> x1 <span class="op">+</span> <span class="dv">30</span> <span class="op">*</span> x2) <span class="co"># Deckungsbeitrag x10</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> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb5-67"><a href="#cb5-67" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb5-68"><a href="#cb5-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb5-69"><a href="#cb5-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Keine Loesung: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-70"><a href="#cb5-70" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.Value(x1), loeser.Value(x2), loeser.ObjectiveValue() <span class="op">/</span> <span class="dv">10</span></span>
<span id="cb5-71"><a href="#cb5-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-72"><a href="#cb5-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-73"><a href="#cb5-73" 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-74"><a href="#cb5-74" aria-hidden="true" tabindex="-1"></a> VORLAGE.zeige()</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="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">--- Aus der Vorlage mechanisch abgeleitetes Modell ---&quot;</span>)</span>
<span id="cb5-77"><a href="#cb5-77" aria-hidden="true" tabindex="-1"></a> brote, broetchen, gewinn <span class="op">=</span> loese_baeckerei()</span>
<span id="cb5-78"><a href="#cb5-78" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Optimale Brote: </span><span class="sc">{</span>brote<span class="sc">}</span><span class="ss">&quot;</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;Optimale Broetchen-Zehner: </span><span class="sc">{</span>broetchen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-80"><a href="#cb5-80" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Maximaler Deckungsbeitrag: </span><span class="sc">{</span>gewinn<span class="sc">:.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb5-81"><a href="#cb5-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb5-82"><a href="#cb5-82" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Genau diese drei Codezeilen (Variablen, Add, Maximize) entstehen&quot;</span>)</span>
<span id="cb5-83"><a href="#cb5-83" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;direkt aus den fuenf Zeilen der Vorlage - das ist der ganze Trick.&quot;</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
AUSGEFUELLTE BAUSTEINE-VORLAGE
==============================================================================
Entscheidungsvariablen : x1 = Anzahl Brote, x2 = Anzahl Broetchen-Zehnerpackungen (beide ganzzahlig &gt;= 0)
Parameter : Deckungsbeitraege (2,50; 3,00) EUR; Mehlbedarf (0,5; 0,6) kg; Ofenzeit (4; 3) min; Kapazitaeten (90 kg, 600 min); Mindestmenge 40 Brote
Zielfunktion : max 2,5*x1 + 3,0*x2 (Einheit: Euro Deckungsbeitrag)
Harte Bedingungen : 0,5*x1 + 0,6*x2 &lt;= 90 (Mehl); 4*x1 + 3*x2 &lt;= 600 (Ofen); x1 &gt;= 40 (Vertrag)
Weiche Bedingungen : keine
==============================================================================
--- Aus der Vorlage mechanisch abgeleitetes Modell ---
Optimale Brote: 42
Optimale Broetchen-Zehner: 115
Maximaler Deckungsbeitrag: 450.00 EUR
Genau diese drei Codezeilen (Variablen, Add, Maximize) entstehen
direkt aus den fuenf Zeilen der Vorlage - das ist der ganze Trick.</code></pre>
<p>Mehl und Deckungsbeitrag stehen im selben Verhältnis für Brote wie für Brötchen (je 5 € pro kg Mehl) — deshalb ist die Aufteilung zwischen 42/115 nicht eindeutig, solange die 90 kg Mehl restlos verplant sind und mindestens 40 Brote gebacken werden; <strong>der maximale Deckungsbeitrag von 450 € dagegen ist es.</strong></p>
<hr />
<h2 id="sec:einfuehrung-erstes-python-vollbeispiel-ressourcenallokation-im-rechenzentrum">1.7 Erstes Python-Vollbeispiel: Ressourcenallokation im Rechenzentrum</h2>
<h3 id="szenario">Szenario</h3>
<p>Ein quantitatives Handelshaus betreibt Serverkapazitäten für zwei Algorithmentypen:</p>
<ul>
<li><strong>Algorithmus A (Arbitrage-Bot):</strong> erzeugt <span class="math inline">150\,\text{€}</span> Gewinn pro Tag, benötigt <span class="math inline">2</span> Rechenkerne (vCPUs) und <span class="math inline">4\,\text{GB}</span> Arbeitsspeicher.</li>
<li><strong>Algorithmus B (Trendfolge-Bot):</strong> erzeugt <span class="math inline">250\,\text{€}</span> Gewinn pro Tag, benötigt <span class="math inline">5</span> Rechenkerne und <span class="math inline">6\,\text{GB}</span> Arbeitsspeicher.</li>
</ul>
<p><strong>Systemressourcen:</strong> höchstens <span class="math inline">40</span> vCPUs, höchstens <span class="math inline">60\,\text{GB}</span> RAM, und aus Marktliquiditätsgründen höchstens <span class="math inline">8</span> Arbitrage-Bots.</p>
<p><strong>Frage:</strong> Wie viele Instanzen von A und B maximieren den Tagesgewinn?</p>
<h3 id="mathematische-formulierung">Mathematische Formulierung</h3>
<p><strong>Entscheidungsvariablen:</strong></p>
<p><span class="math display">
x_A \in \mathbb{N}_0,\quad x_B \in \mathbb{N}_0
</span></p>
<p><strong>Zielfunktion:</strong></p>
<p><span class="math display">
\max_{x_A, x_B} \; Z = 150\,x_A + 250\,x_B
</span></p>
<p><strong>Nebenbedingungen:</strong></p>
<p><span class="math display">
\begin{aligned}
2x_A + 5x_B &amp;\le 40 &amp;&amp; \text{(vCPU-Limit)}\\
4x_A + 6x_B &amp;\le 60 &amp;&amp; \text{(RAM-Limit)}\\
x_A &amp;\le 8 &amp;&amp; \text{(Marktliquidität)}\\
x_A,\, x_B &amp;\ge 0,\ \text{ganzzahlig}
\end{aligned}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong></p>
<ul>
<li><span class="math inline">x_A</span>, <span class="math inline">x_B</span> — Anzahl gestarteter Bots je Typ. Ganzzahlig, weil ein halber Bot nicht existiert.</li>
<li><span class="math inline">Z = 150x_A + 250x_B</span> — der Tagesgewinn in Euro: pro Arbitrage-Bot 150 €, pro Trendfolge-Bot 250 €.</li>
<li><span class="math inline">2x_A + 5x_B \le 40</span> — jeder A-Bot belegt 2 Kerne, jeder B-Bot 5; zusammen dürfen es höchstens 40 sein.</li>
</ul>
<p><strong>Ohne Formel gesagt:</strong> Starte so viele Bots wie möglich, aber verbrauche nicht mehr Rechenkerne und Arbeitsspeicher, als da sind — und nicht mehr als acht Arbitrage-Bots.</p>
</blockquote>
<blockquote>
<p><strong>✏️ Handrechnung 1.3: Das Optimum zu Fuß finden</strong></p>
<p>Bei nur zwei Variablen können wir die Kandidaten abgehen. Weil B mehr Gewinn bringt (250 € gegen 150 €), liegt die Vermutung nahe, möglichst viele B-Bots zu starten.</p>
<table>
<colgroup>
<col style="width: 27%" />
<col style="width: 30%" />
<col style="width: 28%" />
<col style="width: 13%" />
</colgroup>
<thead>
<tr class="header">
<th>Kandidat <span class="math inline">(x_A, x_B)</span></th>
<th>vCPU <span class="math inline">2x_A+5x_B \le 40</span></th>
<th>RAM <span class="math inline">4x_A+6x_B \le 60</span></th>
<th><span class="math inline">Z</span></th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><span class="math inline">(0, 8)</span></td>
<td><span class="math inline">40 \le 40</span></td>
<td><span class="math inline">48 \le 60</span></td>
<td>2000 €</td>
</tr>
<tr class="even">
<td><span class="math inline">(3, 6)</span></td>
<td><span class="math inline">36</span></td>
<td><span class="math inline">48</span></td>
<td>1950 €</td>
</tr>
<tr class="odd">
<td><span class="math inline">(5, 6)</span></td>
<td><span class="math inline">40</span></td>
<td><span class="math inline">56</span></td>
<td>2250 €</td>
</tr>
<tr class="even">
<td><span class="math inline">(8, 4)</span></td>
<td><span class="math inline">36</span></td>
<td><span class="math inline">56</span></td>
<td>2200 €</td>
</tr>
<tr class="odd">
<td><strong><span class="math inline">(7, 5)</span></strong></td>
<td><span class="math inline">39</span></td>
<td><span class="math inline">58</span></td>
<td><strong>2300 €</strong></td>
</tr>
<tr class="even">
<td><span class="math inline">(6, 6)</span></td>
<td><span class="math inline">42 &gt; 40</span></td>
<td></td>
<td>unzulässig</td>
</tr>
<tr class="odd">
<td><span class="math inline">(8, 5)</span></td>
<td><span class="math inline">41 &gt; 40</span></td>
<td></td>
<td>unzulässig</td>
</tr>
</tbody>
</table>
<p><strong>Optimum: <span class="math inline">x_A^* = 7</span>, <span class="math inline">x_B^* = 5</span>, <span class="math inline">Z^* = 2300\,\text{€}</span>.</strong></p>
<p>Beachten Sie das Überraschende: Die naive Strategie „nur den lukrativeren Bot“ (<span class="math inline">0, 8</span>) liefert <strong>2000 €</strong> — ganze 300 € weniger als die Mischung. Genau solche Effekte machen Optimierung nötig: Der beste Gesamtplan besteht selten aus lauter lokal besten Einzelentscheidungen.</p>
</blockquote>
<h3 id="umsetzung-mit-google-or-tools">Umsetzung mit Google OR-Tools</h3>
<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="co">#!/usr/bin/env python3</span></span>
<span id="cb7-2"><a href="#cb7-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-3"><a href="#cb7-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Bot_Allokation.py</span></span>
<span id="cb7-4"><a href="#cb7-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-5"><a href="#cb7-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Einfuehrung: Erstes Optimierungsmodell mit Google OR-Tools (CP-SAT).</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a><span class="co">Problem: Server-Allokation für Trading-Bots.</span></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">Modell mit expliziter Nebenbedingung statt versteckter Variablengrenze,</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">vollständiger Statusauswertung und Ergebnisprüfung.</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="co"># --- Parameter (die &quot;Fakten&quot; des Problems) ---------------------------------</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a>GEWINN_A, GEWINN_B <span class="op">=</span> <span class="dv">150</span>, <span class="dv">250</span> <span class="co"># Euro pro Bot und Tag</span></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a>CPU_A, CPU_B <span class="op">=</span> <span class="dv">2</span>, <span class="dv">5</span> <span class="co"># vCPU-Bedarf je Bot</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a>RAM_A, RAM_B <span class="op">=</span> <span class="dv">4</span>, <span class="dv">6</span> <span class="co"># GB Arbeitsspeicher je Bot</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a>CPU_GESAMT, RAM_GESAMT <span class="op">=</span> <span class="dv">40</span>, <span class="dv">60</span> <span class="co"># verfügbare Kapazitäten</span></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a>MAX_ARBITRAGE <span class="op">=</span> <span class="dv">8</span> <span class="co"># Marktliquiditätsgrenze</span></span>
<span id="cb7-20"><a href="#cb7-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-21"><a href="#cb7-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese_bot_allokation(zeitlimit_s: <span class="bu">float</span> <span class="op">=</span> <span class="fl">10.0</span>):</span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Baut das Modell, löst es und gibt eine Auswertung aus.&quot;&quot;&quot;</span></span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Modell instanziieren</span></span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Entscheidungsvariablen anlegen</span></span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a> <span class="co"># NewIntVar(untere_grenze, obere_grenze, name)</span></span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die obere Grenze ist bewusst großzügig; die echte Beschränkung</span></span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a> <span class="co"># formulieren wir unten als Nebenbedingung, damit das Modell die</span></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a> <span class="co"># mathematische Formulierung 1:1 abbildet.</span></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a> x_a <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">100</span>, <span class="st">&quot;Arbitrage_Bots&quot;</span>)</span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a> x_b <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, <span class="dv">100</span>, <span class="st">&quot;Trend_Bots&quot;</span>)</span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Nebenbedingungen definieren</span></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a> c_cpu <span class="op">=</span> modell.Add(CPU_A <span class="op">*</span> x_a <span class="op">+</span> CPU_B <span class="op">*</span> x_b <span class="op">&lt;=</span> CPU_GESAMT) <span class="co"># vCPU-Limit</span></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a> c_ram <span class="op">=</span> modell.Add(RAM_A <span class="op">*</span> x_a <span class="op">+</span> RAM_B <span class="op">*</span> x_b <span class="op">&lt;=</span> RAM_GESAMT) <span class="co"># RAM-Limit</span></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a> c_liq <span class="op">=</span> modell.Add(x_a <span class="op">&lt;=</span> MAX_ARBITRAGE) <span class="co"># Marktliquidität</span></span>
<span id="cb7-40"><a href="#cb7-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-41"><a href="#cb7-41" aria-hidden="true" tabindex="-1"></a> <span class="co"># 4. Zielfunktion: Maximiere den Tagesgewinn</span></span>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a> modell.Maximize(GEWINN_A <span class="op">*</span> x_a <span class="op">+</span> GEWINN_B <span class="op">*</span> x_b)</span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a> <span class="co"># 5. Solver konfigurieren und ausführen</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit_s</span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a> <span class="co"># 6. Status auswerten -- IMMER alle Fälle behandeln</span></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a> status_text <span class="op">=</span> loeser.StatusName(status)</span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">not</span> <span class="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Keine verwertbare Lösung. Solver-Status: </span><span class="sc">{</span>status_text<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="op">==</span> cp_model.INFEASIBLE:</span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Das Modell ist unlösbar - die Bedingungen widersprechen sich.&quot;</span>)</span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="va">None</span></span>
<span id="cb7-56"><a href="#cb7-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-57"><a href="#cb7-57" aria-hidden="true" tabindex="-1"></a> a, b <span class="op">=</span> loeser.Value(x_a), loeser.Value(x_b)</span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> gewinn <span class="op">=</span> loeser.ObjectiveValue()</span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a> <span class="co"># 7. Ergebnis ausgeben</span></span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; OPTIMALE BOT-ALLOKATION&quot;</span>)</span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb7-64"><a href="#cb7-64" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Solver-Status: </span><span class="sc">{</span>status_text<span class="sc">}</span><span class="ss">&quot;</span></span>
<span id="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39; (beweisbar optimal)&#39;</span> <span class="cf">if</span> status <span class="op">==</span> cp_model<span class="sc">.</span>OPTIMAL <span class="cf">else</span> <span class="st">&#39; (zulässig, nicht bewiesen)&#39;</span><span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Arbitrage-Bots (x_A): </span><span class="sc">{</span>a<span class="sc">}</span><span class="ss"> Instanzen&quot;</span>)</span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Trendfolge-Bots (x_B): </span><span class="sc">{</span>b<span class="sc">}</span><span class="ss"> Instanzen&quot;</span>)</span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Täglicher Max-Gewinn: </span><span class="sc">{</span>gewinn<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a> genutzt_cpu <span class="op">=</span> CPU_A <span class="op">*</span> a <span class="op">+</span> CPU_B <span class="op">*</span> b</span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a> genutzt_ram <span class="op">=</span> RAM_A <span class="op">*</span> a <span class="op">+</span> RAM_B <span class="op">*</span> b</span>
<span id="cb7-72"><a href="#cb7-72" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;CPU-Auslastung: </span><span class="sc">{</span>genutzt_cpu<span class="sc">}</span><span class="ss"> / </span><span class="sc">{</span>CPU_GESAMT<span class="sc">}</span><span class="ss"> vCPUs &quot;</span></span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>genutzt_cpu <span class="op">/</span> CPU_GESAMT <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;RAM-Auslastung: </span><span class="sc">{</span>genutzt_ram<span class="sc">}</span><span class="ss"> / </span><span class="sc">{</span>RAM_GESAMT<span class="sc">}</span><span class="ss"> GB &quot;</span></span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>genutzt_ram <span class="op">/</span> RAM_GESAMT <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Arbitrage-Limit: </span><span class="sc">{</span>a<span class="sc">}</span><span class="ss"> / </span><span class="sc">{</span>MAX_ARBITRAGE<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-78"><a href="#cb7-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-79"><a href="#cb7-79" aria-hidden="true" tabindex="-1"></a> <span class="co"># 8. Selbstkontrolle: Sind wirklich alle Bedingungen eingehalten?</span></span>
<span id="cb7-80"><a href="#cb7-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> genutzt_cpu <span class="op">&lt;=</span> CPU_GESAMT, <span class="st">&quot;vCPU-Limit verletzt!&quot;</span></span>
<span id="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> genutzt_ram <span class="op">&lt;=</span> RAM_GESAMT, <span class="st">&quot;RAM-Limit verletzt!&quot;</span></span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> a <span class="op">&lt;=</span> MAX_ARBITRAGE, <span class="st">&quot;Arbitrage-Limit verletzt!&quot;</span></span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a> <span class="co"># 9. Zum Vergleich: die naive Strategie &quot;nur der lukrativere Bot&quot;</span></span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a> naiv_b <span class="op">=</span> <span class="bu">min</span>(CPU_GESAMT <span class="op">//</span> CPU_B, RAM_GESAMT <span class="op">//</span> RAM_B)</span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a> naiv_gewinn <span class="op">=</span> GEWINN_B <span class="op">*</span> naiv_b</span>
<span id="cb7-87"><a href="#cb7-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">58</span>)</span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Naive Strategie (nur Trendfolge): </span><span class="sc">{</span>naiv_b<span class="sc">}</span><span class="ss"> Bots -&gt; </span><span class="sc">{</span>naiv_gewinn<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Vorteil der Optimierung: </span><span class="sc">{</span>gewinn <span class="op">-</span> naiv_gewinn<span class="sc">:,.2f}</span><span class="ss"> EUR pro Tag &quot;</span></span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(gewinn <span class="op">/</span> naiv_gewinn <span class="op">-</span> <span class="dv">1</span>) <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">58</span>)</span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> a, b, gewinn</span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-95"><a href="#cb7-95" 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="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a> loese_bot_allokation()</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==========================================================
OPTIMALE BOT-ALLOKATION
==========================================================
Solver-Status: OPTIMAL (beweisbar optimal)
Arbitrage-Bots (x_A): 7 Instanzen
Trendfolge-Bots (x_B): 5 Instanzen
Täglicher Max-Gewinn: 2,300.00 EUR
----------------------------------------------------------
CPU-Auslastung: 39 / 40 vCPUs (97.5 %)
RAM-Auslastung: 58 / 60 GB (96.7 %)
Arbitrage-Limit: 7 / 8
----------------------------------------------------------
Naive Strategie (nur Trendfolge): 8 Bots -&gt; 2,000.00 EUR
Vorteil der Optimierung: 300.00 EUR pro Tag (15.0 %)
==========================================================</code></pre>
<p>Das Ergebnis deckt sich mit der Handrechnung <em>Das Optimum zu Fuß finden</em> — ein gutes Zeichen. <strong>Prüfen Sie Solver-Ergebnisse wo immer möglich gegen eine unabhängige Rechnung</strong>, gerade am Anfang.</p>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 11%" />
<col style="width: 12%" />
<col style="width: 75%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Didaktischer Hinweis</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Konstanten oben</td>
<td>alle Parameter an einer Stelle</td>
<td>Nie Zahlen im Modellcode verstreuen — sonst findet man sie beim Ändern nicht wieder</td>
</tr>
<tr class="even">
<td><code>NewIntVar(0, 100, ...)</code></td>
<td>ganzzahlige Variable mit weiten Grenzen</td>
<td>Grenzen müssen endlich sein; 100 ist bewusst unkritisch groß</td>
</tr>
<tr class="odd">
<td><code>modell.Add(x_a &lt;= MAX_ARBITRAGE)</code></td>
<td>Marktlimit als <strong>Nebenbedingung</strong></td>
<td>Alternativ liesse sich dieses Limit auch in die Variablengrenze packen. Beides funktioniert — aber als Nebenbedingung bleibt das Modell zur mathematischen Formulierung deckungsgleich und man kann später den Schattenpreis abfragen</td>
</tr>
<tr class="even">
<td><code>loeser.StatusName(status)</code></td>
<td>echter Status statt fester Text</td>
<td>Ein fest codierter Text wie „OPTIMAL“ würde unabhängig vom tatsächlichen Ergebnis ausgegeben — deshalb immer den echten Solver-Status auswerten</td>
</tr>
<tr class="odd">
<td><code>assert ...</code></td>
<td>Selbstkontrolle nach dem Lösen</td>
<td>Kostet nichts und fängt Modellierungsfehler, die der Solver nicht bemerken kann</td>
</tr>
<tr class="even">
<td>Vergleich mit naiver Strategie</td>
<td>quantifiziert den Nutzen</td>
<td>Die Frage „Was bringt die Optimierung überhaupt?“ kommt in jedem Projekt — beantworten Sie sie ungefragt</td>
</tr>
</tbody>
</table>
</blockquote>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong>Variablengrenzen zu eng gewählt.</strong> <code>NewIntVar(0, 5, ...)</code> würde hier stillschweigend die Lösung <span class="math inline">x_B = 5</span> erzwingen und ein falsches „Optimum“ liefern. Grenzen sind Nebenbedingungen — behandeln Sie sie als solche.</li>
<li><strong>Status nicht geprüft.</strong> Ein Programm, das bei <code>INFEASIBLE</code> weiterläuft und <code>solver.Value(...)</code> abfragt, liefert entweder Unsinn oder stürzt ab.</li>
<li><strong>Ganzzahligkeit vergessen.</strong> Mit kontinuierlichen Variablen liefert dasselbe Modell <span class="math inline">x_A = 7{,}5</span>, <span class="math inline">x_B = 5{,}0</span> und <span class="math inline">Z = 2375</span> € — ein Wert, den es real nicht gibt. Warum man solche Werte <strong>nicht</strong> einfach runden darf, ist Thema von <a href="milp.html#kap-milp">Kapitel 6</a>.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>📎 Warum dieses Programm alles selbst macht</strong></p>
Phase 6.2: Constraint Attribution und Managementbericht Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75. Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um. Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es liegt. Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf 210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst "warum nicht?", dann "was kostet was?". Drei gemessene Befunde: * Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget Schicht 2" mit einem Schattenpreis von 0,00 EUR. * Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR). * Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen Vertrieb und Produktion. Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz. Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden, "Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die Zahl dahinter von 18 auf 58 EUR springt. Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben", bevor die Loesung geschrieben war. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe und Loesung in Anhang A. Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme, 139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:30:33 +02:00
<p>Wenige Seiten weiter, in der Excel-Brücke, steht ein Programm, das mit 96 Zeilen auskommt, weil es die immer gleichen Arbeitsschritte aus dem gemeinsamen Modul <code>or_kern.py</code> bezieht (<a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>). Warum tut <code>Bot_Allokation.py</code> das nicht auch?</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>Weil es hier nicht darum geht, <strong>wenig Code</strong> zu schreiben, sondern darum zu sehen, wie ein Modell <strong>entsteht</strong>: Variablen, Nebenbedingungen, Zielfunktion, Status, Prüfung — jeder Schritt einzeln und sichtbar. Eine Abstraktion, die diese Schritte zusammenfasst, ist genau dann ein Gewinn, wenn man sie schon einmal von Hand gegangen ist; vorher versteckt sie nur, was man verstehen will.</p>
<p>Das ist eine allgemeine Regel für eigene Projekte: <strong>Erst die Wiederholung rechtfertigt die Abstraktion.</strong> Wer sein erstes Modell gleich als Framework baut, hat ein Framework für einen Fall — und die Erfahrung, welche Fälle es tragen müsste, hat er noch nicht gemacht.</p>
</blockquote>
<hr />
<h2 id="sec:einfuehrung-excel-bruecke">1.8 Von Excel zu Python: Ihre Daten liegen schon da</h2>
<p>In den allermeisten Betrieben liegen die Zahlen, die ein Optimierungsmodell braucht, nicht in einer Datenbank, sondern in einer Tabellenkalkulation. Und viele Leserinnen und Leser kennen Optimierung bisher genau von dort: über <em>Daten → Solver</em> in Excel. Dieser Abschnitt schlägt die Brücke — in beide Richtungen.</p>
<div class="card card-excel">
<blockquote>
<p><strong>📊 Excel-Brücke: dieselben Begriffe, andere Werkzeuge</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Im Excel-Solver</th>
<th>In Python</th>
<th>Anmerkung</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>„Veränderbare Zellen“</td>
<td><code>x = solver.NumVar(...)</code></td>
<td>Der Zellbereich wird zu benannten Variablen.</td>
</tr>
<tr class="even">
<td>„Zielzelle“ + Max/Min</td>
<td><code>solver.Maximize(...)</code></td>
<td>Statt einer Formel in einer Zelle ein Ausdruck im Code.</td>
</tr>
<tr class="odd">
<td>„Nebenbedingungen“-Liste</td>
<td><code>solver.Add(...)</code></td>
<td>Eine Zeile je Regel — oder eine Schleife für 10 000 Regeln.</td>
</tr>
<tr class="even">
<td>„Ganzzahlig“ / „Binär“</td>
<td><code>NewIntVar(...)</code> / <code>NewBoolVar(...)</code></td>
<td>Gleiche Bedeutung, gleiche Konsequenz für die Laufzeit.</td>
</tr>
<tr class="odd">
<td>Simplex-LP / GRG / Evolutionär</td>
<td>GLOP / SLSQP / CP-SAT</td>
<td>In Excel drei Auswahlpunkte, in Python drei Bibliotheken (<a href="oekosystem.html#kap-oekosystem">Kapitel 3</a>).</td>
</tr>
<tr class="even">
<td>„Sensitivitätsbericht“</td>
<td><code>bedingung.dual_value()</code></td>
<td>Der Schattenpreis — <a href="lp.html#kap-lp">Kapitel 5</a> erklärt, was er bedeutet.</td>
</tr>
<tr class="odd">
<td>200 Zeilen sind das Limit</td>
<td>keine praktische Grenze</td>
<td>Der Excel-Solver ist auf 200 veränderbare Zellen begrenzt.</td>
</tr>
</tbody>
</table>
</blockquote>
</div>
<p><strong>Was Sie damit gewinnen</strong> ist weniger die Rechenleistung als die Nachvollziehbarkeit: Ein Python-Modell lässt sich versionieren, testen, in einer Schleife über hundert Szenarien laufen lassen und einem Kollegen erklären. Eine gewachsene Excel-Mappe mit Formeln quer über vier Blätter kann das alles nicht — und niemand traut sich mehr, sie anzufassen.</p>
<p>Das folgende Programm zeigt den ganzen Weg an einem Stück. Es legt die Eingabemappe beim ersten Start selbst an, damit Sie es sofort ausführen können; danach ersetzen Sie die Datei durch Ihre eigene. Solange die Spalten gleich heißen, ändert sich am Code nichts.</p>
<blockquote>
<p><strong>📎 Zum Modul <code>or_kern.py</code></strong></p>
Phase 6.2: Constraint Attribution und Managementbericht Neuer Abschnitt im Kapitel Praxisfallen plus Constraint_Attribution.py (75. Programm). Setzt den fehlenden Teil von Paket 3 aus Verbesserungen_02.md um. Die Luecke, die es schliesst: Erklaerbarkeit.py beantwortet "warum DIESE Zuweisung?", Konfliktsuche.py "warum geht es GAR NICHT?". Der haeufigste Fall der Praxis lag dazwischen und hatte kein Werkzeug - das Modell rechnet, der Plan ist zulaessig, und trotzdem enttaeuschend, nur weiss niemand woran es liegt. Gerechnet wird auf derselben Fabrik wie die Konfliktsuche, nach der Reparatur: Dort war die Lackierkapazitaet von 150 Stunden der Kern des Widerspruchs (2*40 + 3*30 = 170 > 150), eine zweite Schicht bringt sie auf 210. Damit laesst sich dieselbe Instanz ueber zwei Kapitel verfolgen - erst "warum nicht?", dann "was kostet was?". Drei gemessene Befunde: * Bindend heisst nicht teuer: Fuenf Bedingungen binden, "Lackierbudget Schicht 2" mit einem Schattenpreis von 0,00 EUR. * Der Schattenpreis ist eine Momentaufnahme: Fuer die Lackiererei verspricht die Hochrechnung 4,00 * 30 = 120 EUR, gemessen kommen 60 EUR; der Preis gilt exakt bis +15 Stunden. Die Ranglisten drehen sich dabei um - nach Schattenpreis fuehrt der Traegervertrag (18 EUR/Stueck), nach tatsaechlicher Wirkung die Montage-Sonderschicht (450 EUR). * Ein Wunsch kann unmoeglich statt teuer sein: "mindestens 55 Rahmen" ist INFEASIBLE, blockiert von drei Bedingungen gemeinsam. Mit +15 Stunden Lackierkapazitaet wird er erfuellbar und kostet dann nichts - der Plan waehlt diese Menge von sich aus. Der Konflikt bestand nie zwischen Vertrieb und Produktion. Der Managementbericht entsteht aus genau diesen Zahlen; kein Satz enthaelt eine Zahl, die nicht vorher gerechnet wurde. Voraussetzung ist der sprechende Name jeder Bedingung - aus A_ub[7] wird kein Satz. Dazu die Grenze, als Warnkasten und als Uebungsaufgabe: Der Bericht erklaert das Modell, nicht die Wirklichkeit. Steigt der Deckungsbeitrag von Deckel von 9 auf 20 EUR, kippt der Plan (Rahmen auf das Vertragsminimum 40, Deckel auf die Marktgrenze 120), "Kapazitaet Lackieren" hoert auf zu binden, "Liefervertrag Rahmen" faengt damit an - und der Satz "Teuerste Bindung ist Liefervertrag Traeger" steht woertlich unveraendert im Bericht, waehrend die Zahl dahinter von 18 auf 58 EUR springt. Die Zaehlprobe aus 6.1a hat sich sofort bewaehrt: Nach dem Einbau der Uebungsaufgabe meldete --check "6 Loesungen, das Kapitel aber 7 Aufgaben", bevor die Loesung geschrieben war. Mitgezogen: Kapitelkopf, Lernziele, Selbsttest, Zusammenfassung, Vorwort-Programmverzeichnis, Vorwaertsverweis aus Falle 2, Uebungsaufgabe und Loesung in Anhang A. Stand: 294 Abschnitte, 725 Querverweise, 328 Indexmarken, 75 Programme, 139 Aufgaben mit 139 Loesungen, 33 pytest-Tests, PDF 737 Seiten, 68 netzfreie Programme fehlerfrei. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 10:30:33 +02:00
<p>Die vier Arbeitsschritte — lesen, lösen, prüfen, schreiben — stehen nicht in diesem Programm, sondern in <code>or_kern.py</code>. Das ist ein gemeinsamer Unterbau, den alle Programme dieses Buchs benutzen können; er liegt im selben Verzeichnis und ist in <a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> vollständig abgedruckt und erklärt.</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>Sie müssen ihn jetzt noch nicht verstehen. Wichtig ist nur die Aufteilung: <strong>Hier</strong> steht, was diesen Fall ausmacht — dort steht, was jedes Optimierungsprogramm braucht. Genau deshalb ist der Code unten so kurz.</p>
</blockquote>
<div class="sourceCode" id="cb9"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb9-1"><a href="#cb9-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb9-2"><a href="#cb9-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-3"><a href="#cb9-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Excel_Bruecke.py</span></span>
<span id="cb9-4"><a href="#cb9-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-5"><a href="#cb9-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel Einfuehrung: Vom Excel-Solver zu Python - dieselbe Rechnung, dieselbe Datei.</span></span>
<span id="cb9-6"><a href="#cb9-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-7"><a href="#cb9-7" aria-hidden="true" tabindex="-1"></a><span class="co">Das Programm bildet den vollstaendigen Arbeitsweg ab, den ein Excel-Modell im</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="co">Betrieb sonst von Hand geht:</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="co"> Tabellenblatt lesen -&gt; Modell bauen -&gt; loesen -&gt; Ergebnis zurueckschreiben</span></span>
<span id="cb9-11"><a href="#cb9-11" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-12"><a href="#cb9-12" aria-hidden="true" tabindex="-1"></a><span class="co">Damit es ohne Vorbereitung laeuft, legt es die Eingabedatei beim ersten Start</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="co">selbst an. Ersetzen Sie sie durch Ihre eigene Datei - solange die Spalten</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="co">gleich heissen, aendert sich am Code nichts.</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a><span class="co">Die vier Arbeitsschritte kommen aus or_kern.py (Kapitel Praxisfallen). Was</span></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a><span class="co">hier steht, ist nur noch das, was diesen Fall ausmacht - der Rest ist</span></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="co">gemeinsamer Unterbau. Wer das Modul nicht kennt: Es ist im Buch vollstaendig</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a><span class="co">abgedruckt und liegt im selben Verzeichnis.</span></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a><span class="co">Erzeugte Dateien (im Arbeitsverzeichnis):</span></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a><span class="co"> produktionsmix.xlsx Eingabe: Blaetter &#39;Produkte&#39; und &#39;Kapazitaeten&#39;</span></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a><span class="co"> produktionsmix_ergebnis.xlsx Ausgabe: Blaetter &#39;Plan&#39; und &#39;Kennzahlen&#39;</span></span>
<span id="cb9-24"><a href="#cb9-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-25"><a href="#cb9-25" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: pandas, openpyxl, ortools, pydantic (ueber or_kern)</span></span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-27"><a href="#cb9-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-28"><a href="#cb9-28" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> os</span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> or_kern <span class="im">import</span> (lade_produktionsproblem, loese_mit_glop, pruefe_loesung,</span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a> schreibe_ergebnis)</span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a>EINGABE <span class="op">=</span> <span class="st">&quot;produktionsmix.xlsx&quot;</span></span>
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a>AUSGABE <span class="op">=</span> <span class="st">&quot;produktionsmix_ergebnis.xlsx&quot;</span></span>
<span id="cb9-39"><a href="#cb9-39" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-40"><a href="#cb9-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> lege_beispieldatei_an(pfad: <span class="bu">str</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Schreibt die Eingabemappe, wie sie auch aus dem Controlling kaeme.&quot;&quot;&quot;</span></span>
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a> produkte <span class="op">=</span> pd.DataFrame({</span>
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Produkt&quot;</span>: [<span class="st">&quot;Tisch&quot;</span>, <span class="st">&quot;Stuhl&quot;</span>, <span class="st">&quot;Regal&quot;</span>],</span>
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Deckungsbeitrag&quot;</span>: [<span class="fl">240.0</span>, <span class="fl">60.0</span>, <span class="fl">130.0</span>],</span>
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Montagestunden&quot;</span>: [<span class="fl">3.0</span>, <span class="fl">1.0</span>, <span class="fl">2.0</span>],</span>
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Plattenmaterial&quot;</span>: [<span class="fl">6.0</span>, <span class="fl">1.0</span>, <span class="fl">4.0</span>],</span>
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a> kapazitaeten <span class="op">=</span> pd.DataFrame({</span>
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Ressource&quot;</span>: [<span class="st">&quot;Montagestunden&quot;</span>, <span class="st">&quot;Plattenmaterial&quot;</span>],</span>
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Verfuegbar&quot;</span>: [<span class="fl">150.0</span>, <span class="fl">240.0</span>],</span>
<span id="cb9-52"><a href="#cb9-52" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb9-53"><a href="#cb9-53" aria-hidden="true" tabindex="-1"></a> <span class="cf">with</span> pd.ExcelWriter(pfad, engine<span class="op">=</span><span class="st">&quot;openpyxl&quot;</span>) <span class="im">as</span> mappe:</span>
<span id="cb9-54"><a href="#cb9-54" aria-hidden="true" tabindex="-1"></a> produkte.to_excel(mappe, sheet_name<span class="op">=</span><span class="st">&quot;Produkte&quot;</span>, index<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb9-55"><a href="#cb9-55" aria-hidden="true" tabindex="-1"></a> kapazitaeten.to_excel(mappe, sheet_name<span class="op">=</span><span class="st">&quot;Kapazitaeten&quot;</span>, index<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb9-56"><a href="#cb9-56" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-57"><a href="#cb9-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-58"><a href="#cb9-58" 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="cb9-59"><a href="#cb9-59" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> os.path.exists(EINGABE):</span>
<span id="cb9-60"><a href="#cb9-60" aria-hidden="true" tabindex="-1"></a> lege_beispieldatei_an(EINGABE)</span>
<span id="cb9-61"><a href="#cb9-61" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Beispiel-Eingabedatei angelegt: </span><span class="sc">{</span>EINGABE<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-62"><a href="#cb9-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-63"><a href="#cb9-63" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Lesen und pruefen. Faellt hier etwas auf, ist der Fehler noch</span></span>
<span id="cb9-64"><a href="#cb9-64" aria-hidden="true" tabindex="-1"></a> <span class="co"># zuzuordnen - spaeter waere er nur noch eine merkwuerdige Zahl.</span></span>
<span id="cb9-65"><a href="#cb9-65" aria-hidden="true" tabindex="-1"></a> problem <span class="op">=</span> lade_produktionsproblem(EINGABE)</span>
<span id="cb9-66"><a href="#cb9-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-67"><a href="#cb9-67" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Loesen. Einzige solverabhaengige Zeile des ganzen Programms -</span></span>
<span id="cb9-68"><a href="#cb9-68" aria-hidden="true" tabindex="-1"></a> <span class="co"># &#39;loese_mit_scipy&#39; waere ein Einzeiler-Wechsel.</span></span>
<span id="cb9-69"><a href="#cb9-69" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> loese_mit_glop(problem)</span>
<span id="cb9-70"><a href="#cb9-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-71"><a href="#cb9-71" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Abnahmepruefung gegen die Anforderungen, ohne den Solver zu fragen.</span></span>
<span id="cb9-72"><a href="#cb9-72" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_loesung(problem, loesung)</span>
<span id="cb9-73"><a href="#cb9-73" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beanstandungen:</span>
<span id="cb9-74"><a href="#cb9-74" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="st">&quot;Abnahmepruefung fehlgeschlagen:</span><span class="ch">\n</span><span class="st"> - &quot;</span></span>
<span id="cb9-75"><a href="#cb9-75" aria-hidden="true" tabindex="-1"></a> <span class="op">+</span> <span class="st">&quot;</span><span class="ch">\n</span><span class="st"> - &quot;</span>.join(beanstandungen))</span>
<span id="cb9-76"><a href="#cb9-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-77"><a href="#cb9-77" aria-hidden="true" tabindex="-1"></a> <span class="co"># 4. Zurueckschreiben - im Format, das die Fachabteilung ohnehin benutzt.</span></span>
<span id="cb9-78"><a href="#cb9-78" aria-hidden="true" tabindex="-1"></a> schreibe_ergebnis(AUSGABE, problem, loesung)</span>
<span id="cb9-79"><a href="#cb9-79" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-80"><a href="#cb9-80" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">66</span>)</span>
<span id="cb9-81"><a href="#cb9-81" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRODUKTIONSPLAN AUS DER EXCEL-MAPPE&quot;</span>)</span>
<span id="cb9-82"><a href="#cb9-82" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">66</span>)</span>
<span id="cb9-83"><a href="#cb9-83" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> produkt <span class="kw">in</span> problem.produkte:</span>
<span id="cb9-84"><a href="#cb9-84" aria-hidden="true" tabindex="-1"></a> menge <span class="op">=</span> loesung.werte[produkt.name]</span>
<span id="cb9-85"><a href="#cb9-85" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>produkt<span class="sc">.</span>name<span class="sc">:&lt;18}</span><span class="ss"> </span><span class="sc">{</span>menge<span class="sc">:8.1f}</span><span class="ss"> Stueck &quot;</span></span>
<span id="cb9-86"><a href="#cb9-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>menge <span class="op">*</span> produkt<span class="sc">.</span>deckungsbeitrag<span class="sc">:12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-87"><a href="#cb9-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">66</span>)</span>
<span id="cb9-88"><a href="#cb9-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(loesung.als_bericht())</span>
<span id="cb9-89"><a href="#cb9-89" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Abnahmepruefung: bestanden</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb9-90"><a href="#cb9-90" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-91"><a href="#cb9-91" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Schattenpreise beantworten die Frage, die der Plan nicht stellt:</span></span>
<span id="cb9-92"><a href="#cb9-92" aria-hidden="true" tabindex="-1"></a> <span class="co"># Was waere eine zusaetzliche Einheit dieser Ressource wert?</span></span>
<span id="cb9-93"><a href="#cb9-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> ressource, preis <span class="kw">in</span> loesung.schattenpreise.items():</span>
<span id="cb9-94"><a href="#cb9-94" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; eine Einheit </span><span class="sc">{</span>ressource<span class="sc">:&lt;18}</span><span class="ss"> mehr waere wert: </span><span class="sc">{</span>preis<span class="sc">:7.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-95"><a href="#cb9-95" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">66</span>)</span>
<span id="cb9-96"><a href="#cb9-96" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Ergebnis geschrieben nach: </span><span class="sc">{</span>AUSGABE<span class="sc">}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>Beispiel-Eingabedatei angelegt: produktionsmix.xlsx
==================================================================
PRODUKTIONSPLAN AUS DER EXCEL-MAPPE
==================================================================
Tisch 30.0 Stueck 7,200.00 EUR
Stuhl 60.0 Stueck 3,600.00 EUR
Regal 0.0 Stueck 0.00 EUR
------------------------------------------------------------------
Status: optimal | Zielwert: 10,800.00 | Gap: 0.00% | Zeit: 0.00s
Abnahmepruefung: bestanden
eine Einheit Montagestunden mehr waere wert: 40.00 EUR
eine Einheit Plattenmaterial mehr waere wert: 20.00 EUR
==================================================================
Ergebnis geschrieben nach: produktionsmix_ergebnis.xlsx</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
<th>Warum es so gebaut ist</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>lade_produktionsproblem(EINGABE)</code></td>
<td>liest die Mappe <strong>und prüft sie</strong></td>
<td>Leere Zellen, fehlende Spalten, Kapazitäten von null oder Ressourcen ohne Kapazitätsangabe scheitern hier — nicht erst beim Lösen, wo der Fehler nur noch eine merkwürdige Zahl wäre.</td>
</tr>
<tr class="even">
<td><code>loese_mit_glop(problem)</code></td>
<td>die einzige solverabhängige Zeile</td>
<td><code>loese_mit_scipy</code> wäre ein Einzeiler-Wechsel. Alles davor und danach bleibt unverändert.</td>
</tr>
<tr class="odd">
<td><code>pruefe_loesung(problem, loesung)</code></td>
<td>Abnahmeprüfung <strong>ohne</strong> Solver</td>
<td>Sie bekommt nur die ausgegebene Lösung und die Anforderungen. Eine Prüfung aus denselben Bausteinen wie das Modell prüft das Modell gegen sich selbst.</td>
</tr>
<tr class="even">
<td><code>raise SystemExit(...)</code></td>
<td>bricht bei Beanstandung ab</td>
<td>Ein Programm, das trotz durchgefallener Prüfung eine Datei schreibt, ist gefährlicher als eines, das abstürzt.</td>
</tr>
<tr class="odd">
<td><code>loesung.schattenpreise</code></td>
<td>Dualwerte aus dem Lösungsobjekt</td>
<td>Sie sind ein <strong>solverunabhängiger</strong> Begriff und stehen deshalb im Ergebnis, nicht im Solvercode.</td>
</tr>
</tbody>
</table>
</blockquote>
<p><strong>Das Ergebnis liest sich von selbst:</strong> Das Regal — obwohl mit 130 € Deckungsbeitrag scheinbar attraktiv — wird gar nicht gebaut. Die Schattenpreise sagen warum: Ein Regal verbraucht 2 Montagestunden zu je 40 € und 4 m² Material zu je 20 €, bindet also Ressourcen im Wert von <span class="math inline">2 \cdot 40 + 4 \cdot 20 = 160</span> € und bringt nur 130 € ein. Es verdient seinen Platz in der Werkstatt nicht. Diese Art von Aussage — begründet, in Euro, für den Chef nachvollziehbar — ist der eigentliche Ertrag der Optimierung.</p>
<blockquote>
<p><strong>⚠️ Typische Fehler beim Umstieg von Excel</strong></p>
<ul>
<li><strong>Zellbezüge statt Namen.</strong> Wer im Code mit <code>df.iloc[:, 3]</code> arbeitet, baut denselben Fehler nach, der in Excel-Mappen <code>=SUMME(D2:D40)</code> so gefährlich macht: Eine eingefügte Spalte verschiebt alles. Sprechen Sie Spalten <strong>über ihren Namen</strong> an.</li>
<li><strong>Einheiten mischen.</strong> Wenn eine Spalte Stunden und eine Minuten enthält, merkt das weder Excel noch der Solver — nur der Disponent, wenn der Plan nicht aufgeht.</li>
<li><strong>Das Ergebnis nur auf den Bildschirm schreiben.</strong> Was nicht in einer Datei landet, die die Fachabteilung öffnen kann, wird nicht benutzt.</li>
</ul>
</blockquote>
<hr />
<h2 id="sec:einfuehrung-uebungsaufgaben">1.9 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen zu allen Aufgaben: <a href="anhang-loesungen.html#sec:loesungen-einfuehrung">Abschnitt A.1</a>.</p>
</blockquote>
<p><strong>Aufgabe 1.1 ⭐ — Abgrenzung.</strong> Ordnen Sie jede Frage der richtigen Analytik-Stufe (deskriptiv / prädiktiv / präskriptiv) zu und begründen Sie in einem Satz: (a) „Wie viele Pakete haben wir letzten Monat zugestellt?“ (b) „Wie viele Pakete werden wir nächsten Dezember zustellen?“ (c) „Welche Tour soll Fahrzeug 3 morgen fahren?“ (d) „Welche Kunden werden voraussichtlich kündigen?“ (e) „Welchen dieser Kunden sollen wir mit unserem begrenzten Rabattbudget halten?“</p>
<p><strong>Aufgabe 1.2 ⭐ — Hart oder weich?</strong> Entscheiden Sie für jede Bedingung eines Klinik-Dienstplans, ob sie hart oder weich modelliert werden sollte, und begründen Sie: (a) Gesetzliche Ruhezeit von 11 Stunden zwischen zwei Diensten. (b) Frau Meier möchte freitags nicht arbeiten. (c) In jeder Nachtschicht muss mindestens eine examinierte Fachkraft anwesend sein. (d) Die Dienste sollen gleichmäßig über das Team verteilt sein. (e) Niemand arbeitet mehr als 10 Tage am Stück.</p>
<p><strong>Aufgabe 1.3 ⭐⭐ — Kombinatorik selbst rechnen.</strong> Ein Speditionsdisponent muss 12 Aufträge auf einen einzigen Lkw in eine Reihenfolge bringen. (a) Wie viele Reihenfolgen gibt es? (b) Der Rechner prüft 5 Millionen Reihenfolgen pro Sekunde. Wie lange dauert das vollständige Durchprobieren? (c) Wie lange dauert es bei 13 Aufträgen? Um welchen Faktor ist das mehr? (d) Wie viele Aufträge könnte man in einer Stunde noch vollständig durchprobieren?</p>
<p><strong>Aufgabe 1.4 ⭐⭐ — Modell lesen.</strong> Gegeben sei das Modell <span class="math display">\max\ 3x_1 + 5x_2 \quad \text{u. d. N.} \quad x_1 \le 4,\ \ 2x_2 \le 12,\ \ 3x_1 + 2x_2 \le 18,\ \ x_1, x_2 \ge 0.</span> (a) Benennen Sie die vier Bausteine. (b) Ist <span class="math inline">(2, 6)</span> zulässig? Ist <span class="math inline">(4, 3)</span> zulässig? Berechnen Sie jeweils <span class="math inline">Z</span>. (c) Finden Sie durch Probieren die beste ganzzahlige Lösung.</p>
<p><strong>Aufgabe 1.5 ⭐⭐⭐ — Die Bäckerei programmieren.</strong> Setzen Sie das Bäckerei-Problem der Handrechnung <em>Zerlegen Sie dieses Problem</em> mit CP-SAT um. Geben Sie die optimale Produktionsmenge, den Deckungsbeitrag und die Auslastung von Mehl und Ofen aus. Prüfen Sie Ihr Ergebnis mit <code>assert</code>-Anweisungen.</p>
<p><strong>Aufgabe 1.6 ⭐⭐⭐ — Sensitivität durch Ausprobieren.</strong> Erweitern Sie <code>Bot_Allokation.py</code> so, dass es das Modell in einer Schleife für RAM-Kapazitäten von 54 bis 72 GB (in Schritten von 2) löst und eine Tabelle <code>RAM | x_A | x_B | Gewinn | Gewinnzuwachs pro zusätzlichem GB</code> ausgibt. (a) Ab welcher RAM-Menge steigt der Gewinn nicht mehr? Warum? (b) Was sagt Ihnen der „Gewinnzuwachs pro GB“ intuitiv? (Der Begriff dafür — <em>Schattenpreis</em> — folgt in <a href="lp.html#kap-lp">Kapitel 5</a>.)</p>
<p><strong>Aufgabe 1.7 ⭐⭐⭐ — Eigenes Problem zerlegen.</strong> Wählen Sie ein Entscheidungsproblem aus Ihrem eigenen Alltag oder Beruf (Beispiele: Wochenplan für Sportkurse, Sitzordnung bei einer Feier, Aufteilung eines Budgets auf Projekte, Reihenfolge von Hausarbeiten). Füllen Sie die Vorlage aus <a href="#sec:einfuehrung-die-vier-universellen-bausteine-jedes-or">Abschnitt 1.6</a> vollständig aus. Notieren Sie außerdem: Woher kämen die Daten? Wer müsste das Ergebnis akzeptieren? — Heben Sie diese Notiz auf; sie ist der Ausgangspunkt für Ihr eigenes Projekt in der <a href="projektwerkstatt.html">Projektwerkstatt</a>.</p>
<hr />
<h2 id="sec:einfuehrung-denkfehler">1.10 Finde den Denkfehler</h2>
<p>Ein Programm, das abstürzt, ist harmlos — Sie sehen sofort, dass etwas nicht stimmt. Gefährlich sind Programme, die fehlerfrei durchlaufen, einen Status <code>OPTIMAL</code> melden und eine Zahl liefern, die betriebswirtschaftlich Unsinn ist. Genau solche Fälle üben wir hier.</p>
<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 Schreinerei verdoppelt ihren Gewinn</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 Kollegin hat das Schreinerei-Modell aus dem Kapitelanfang leicht umgebaut — sie fand die Schleife übersichtlicher als zwei einzelne <code>Add</code>-Aufrufe. Das Programm läuft, der Solver meldet <code>OPTIMAL</code>, und der Deckungsbeitrag ist plötzlich <strong>18 600 €</strong> statt 10 800 €. Der Chef ist begeistert. Sollte er das sein?</p>
<div class="sourceCode" id="cb11"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb11-1"><a href="#cb11-1" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.linear_solver <span class="im">import</span> pywraplp</span>
<span id="cb11-2"><a href="#cb11-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-3"><a href="#cb11-3" aria-hidden="true" tabindex="-1"></a>produkt <span class="op">=</span> {<span class="st">&quot;Tisch&quot;</span>: (<span class="dv">240</span>, <span class="fl">3.0</span>, <span class="fl">6.0</span>), <span class="st">&quot;Stuhl&quot;</span>: (<span class="dv">60</span>, <span class="fl">1.0</span>, <span class="fl">1.0</span>)}</span>
<span id="cb11-4"><a href="#cb11-4" aria-hidden="true" tabindex="-1"></a>vorrat <span class="op">=</span> {<span class="st">&quot;Montagestunden&quot;</span>: <span class="dv">150</span>, <span class="st">&quot;Plattenmaterial&quot;</span>: <span class="dv">240</span>}</span>
<span id="cb11-5"><a href="#cb11-5" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-6"><a href="#cb11-6" aria-hidden="true" tabindex="-1"></a>s <span class="op">=</span> pywraplp.Solver.CreateSolver(<span class="st">&quot;GLOP&quot;</span>)</span>
<span id="cb11-7"><a href="#cb11-7" aria-hidden="true" tabindex="-1"></a>x <span class="op">=</span> {p: s.NumVar(<span class="dv">0</span>, s.infinity(), p) <span class="cf">for</span> p <span class="kw">in</span> produkt}</span>
<span id="cb11-8"><a href="#cb11-8" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p <span class="kw">in</span> produkt:</span>
<span id="cb11-9"><a href="#cb11-9" aria-hidden="true" tabindex="-1"></a> s.Add(x[p] <span class="op">*</span> produkt[p][<span class="dv">1</span>] <span class="op">&lt;=</span> vorrat[<span class="st">&quot;Montagestunden&quot;</span>])</span>
<span id="cb11-10"><a href="#cb11-10" aria-hidden="true" tabindex="-1"></a> s.Add(x[p] <span class="op">*</span> produkt[p][<span class="dv">2</span>] <span class="op">&lt;=</span> vorrat[<span class="st">&quot;Plattenmaterial&quot;</span>])</span>
<span id="cb11-11"><a href="#cb11-11" aria-hidden="true" tabindex="-1"></a>s.Maximize(<span class="bu">sum</span>(x[p] <span class="op">*</span> produkt[p][<span class="dv">0</span>] <span class="cf">for</span> p <span class="kw">in</span> produkt))</span>
<span id="cb11-12"><a href="#cb11-12" aria-hidden="true" tabindex="-1"></a>s.Solve()</span>
<span id="cb11-13"><a href="#cb11-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb11-14"><a href="#cb11-14" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> p <span class="kw">in</span> produkt:</span>
<span id="cb11-15"><a href="#cb11-15" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>p<span class="sc">:6}</span><span class="ss">: </span><span class="sc">{</span>x[p]<span class="sc">.</span>solution_value()<span class="sc">:5.0f}</span><span class="ss"> Stueck&quot;</span>)</span>
<span id="cb11-16"><a href="#cb11-16" aria-hidden="true" tabindex="-1"></a><span class="bu">print</span>(<span class="ss">f&quot;Deckungsbeitrag: </span><span class="sc">{</span>s<span class="sc">.</span>Objective()<span class="sc">.</span>Value()<span class="sc">:.0f}</span><span class="ss"> EUR&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>Tisch : 40 Stueck
Stuhl : 150 Stueck
Deckungsbeitrag: 18600 EUR</code></pre>
<p><strong>Ihre Aufgabe:</strong> (a) Rechnen Sie nach, wie viele Montagestunden dieser Plan tatsächlich braucht. (b) Benennen Sie den Modellierungsfehler in einem Satz. (c) Korrigieren Sie ihn. (d) Formulieren Sie eine <code>assert</code>-Anweisung, die diesen Fehler künftig automatisch fängt.</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-einfuehrung">Abschnitt A.1</a>.</em></p>
</blockquote>
</div>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Optimierungsergebnis, das deutlich besser ausfällt als erwartet, ist zuerst einmal ein <strong>Fehlerverdacht</strong> — kein Grund zur Freude. Freuen Sie sich erst, nachdem Sie nachgerechnet haben, dass die Lösung alle Regeln einhält.</p>
</blockquote>
<hr />
<h2 id="sec:einfuehrung-quiz">1.11 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 1: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-einfuehrung">Anhang A</a>.</p>
<p><strong>1. Ein Vertriebsleiter fragt: „Welche fünf unserer 80 Kunden sollen wir mit dem Restbudget besuchen?“ Welcher Analytik-Stufe entspricht das?</strong> (a) Deskriptiv — es geht um vorhandene Kundendaten. (b) Prädiktiv — man muss den Umsatz je Kunde vorhersagen. (c) Präskriptiv — gesucht ist eine Handlungsanweisung unter einer Budgetgrenze.</p>
<p><strong>2. In einem Dienstplanmodell führt die Regel „Frau Meier möchte freitags frei haben“ als harte Nebenbedingung dazu, dass der Solver <code>INFEASIBLE</code> meldet. Was ist die fachlich richtige Reaktion?</strong> (a) Die Regel als weiche Bedingung mit Strafkosten formulieren. (b) Einen leistungsfähigeren Solver einsetzen. (c) Die Variablengrenzen erhöhen, damit mehr Lösungen möglich werden.</p>
<p><strong>3. Warum genügt es bei der Reihenfolgeplanung von 20 Aufträgen nicht, einen tausendmal schnelleren Rechner zu kaufen?</strong> (a) Weil Solver-Bibliotheken nicht parallelisiert sind. (b) Weil <span class="math inline">20!</span> so groß ist, dass Faktor 1000 die Rechenzeit von Jahrzehnten nur auf Wochen drückt — und schon 24 Aufträge wieder hoffnungslos sind. (c) Weil Fließkommazahlen bei großen Fakultäten ungenau werden.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:einfuehrung-selbsttest">1.12 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-einfuehrung">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Was unterscheidet eine Entscheidungsvariable von einem Parameter?</li>
<li>Warum hilft ein 1000-mal schnellerer Rechner bei kombinatorischer Explosion praktisch nicht?</li>
<li>Nennen Sie die vier Bausteine eines OR-Modells.</li>
<li>Was bedeutet der Solver-Status <code>INFEASIBLE</code>, und was ist die häufigste Ursache?</li>
<li>Warum ist es gefährlich, eine Beschränkung nur als Variablengrenze statt als Nebenbedingung zu formulieren?</li>
</ol>
<hr />
<h2 id="sec:einfuehrung-zusammenfassung">1.13 Zusammenfassung</h2>
<ul>
<li><strong>Operations Research ist präskriptiv:</strong> Es beantwortet nicht, was war oder was kommt, sondern was zu tun ist.</li>
<li><strong>Kombinatorische Explosion</strong> <strong>ist eine Wand, keine Steigung.</strong> Zwischen „in Minuten machbar“ und „in Jahrmillionen unmöglich“ liegen oft nur wenige zusätzliche Objekte. Solver umgehen das nicht durch Geschwindigkeit, sondern durch Struktur.</li>
<li><strong>Jedes Modell besteht aus vier Bausteinen:</strong> Entscheidungsvariablen, Parameter, Zielfunktion, Nebenbedingungen. Diese Zerlegung ist die eigentliche Arbeit; die Solverwahl ergibt sich danach fast von selbst.</li>
<li><strong>Harte und weiche Bedingungen sind eine Entwurfsentscheidung</strong> mit großen Folgen für Lösbarkeit und Akzeptanz.</li>
<li><strong>Ihre Daten liegen schon in Excel.</strong> <code>pandas.read_excel()</code> holt sie ab, der Solver rechnet, <code>to_excel()</code> gibt das Ergebnis im gewohnten Format zurück. Der Gewinn gegenüber einer Solver-Mappe ist nicht Rechenleistung, sondern Nachvollziehbarkeit: versionierbar, testbar, hundertmal in einer Schleife ausführbar.</li>
<li><strong>Prüfen Sie jedes Ergebnis</strong> — gegen eine Handrechnung, gegen <code>assert</code>-Anweisungen und gegen eine naive Vergleichsstrategie. Ein unerwartet gutes Ergebnis ist ein Fehlerverdacht, keine gute Nachricht.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="fundament.html#kap-fundament">Kapitel 2</a> liefert die Sprache, um Modelle mit hunderten Variablen kompakt aufzuschreiben: Vektoren, Matrizen und die geometrische Sicht auf den zulässigen Bereich. Dort lernen wir auch die Eigenschaft kennen, die darüber entscheidet, ob ein Problem verlässlich lösbar ist: <strong>Konvexität</strong>.</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="einfuehrung.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="notation.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Notation & Abkürzungen</span></a><a class="prev-next-knopf prev-next-next" href="fundament.html"><span><small>Weiter</small>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</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>