operations_research/OR_HTML_04/milp.html
dschlueter 99cb85907c Stichwortregister: automatische Markierung aller Glossarbegriffe (988 statt 456 Marker)
Neues Skript erzeuge_stichwortregister_04.py:
- Sucht alle Glossarbegriffe (Anzeigename) in den Kapiteldateien
  und markiert die erste Fundstelle je Datei mit {idx:Indexmarke}
- Termliste aus glossar_eintraege_04.py + vorhandenen {idx:...}-Markern
- Idempotent: erkennt vorhandene Marker und überspringt sie
- Überspringt Codeblöcke, Inline-Code und Math
- --check und --bericht Modi
- 605 Marker eingefügt (2 Durchläufe: 597 + 8)

Zusätzlich:
- titeltexte_04.py _saeubern(): entfernt {idx:...}-Marker vor
  Titeltext-Vergleich (pruefe_titeltexte schlug sonst fehl)
2026-09-10 00:07:01 +02:00

1807 lines
250 KiB
HTML
Generated
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!doctype html>
<html lang="de">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound} · Optimierte Entscheidungsfindung mit Python</title>
<script>
(function () {
try {
var t = localStorage.getItem("or-theme");
if (t) document.documentElement.setAttribute("data-theme", t);
} catch (e) {}
})();
</script>
<link rel="stylesheet" href="assets/highlight.css" />
<link rel="stylesheet" href="katex/katex.min.css" />
<script defer="" src="katex/katex.min.js"></script>
<script>document.addEventListener("DOMContentLoaded", function () {
var mathElements = document.getElementsByClassName("math");
var macros = [];
for (var i = 0; i < mathElements.length; i++) {
var texText = mathElements[i].firstChild;
if (mathElements[i].tagName == "SPAN") {
katex.render(texText.data, mathElements[i], {
displayMode: mathElements[i].classList.contains('display'),
throwOnError: false,
macros: macros,
fleqn: false
});
}}
// Der Browser springt zu einem #anker in der URL schon beim ersten Rendern
// an, BEVOR die KaTeX-Formeln oben im Text ihre finale Hoehe bekommen -
// durch den Reflow landet der Anker danach zu weit unten. Nach dem
// Formel-Rendering hier erneut zum Anker springen, das behebt es.
if (location.hash) {
var ziel = document.getElementById(decodeURIComponent(location.hash.slice(1)));
if (ziel) ziel.scrollIntoView({behavior: "instant", block: "start"});
}
});
</script>
<link rel="stylesheet" href="assets/site.css" />
</head>
<body>
<svg style="display:none" aria-hidden="true"><symbol id="icon-menu" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<line x1="3" y1="6" x2="21" y2="6"/><line x1="3" y1="12" x2="21" y2="12"/><line x1="3" y1="18" x2="21" y2="18"/>
</symbol>
<symbol id="icon-search" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="11" cy="11" r="7"/><line x1="21" y1="21" x2="16.2" y2="16.2"/>
</symbol>
<symbol id="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round">
<circle cx="12" cy="12" r="4.5"/>
<line x1="12" y1="1.5" x2="12" y2="4"/><line x1="12" y1="20" x2="12" y2="22.5"/>
<line x1="1.5" y1="12" x2="4" y2="12"/><line x1="20" y1="12" x2="22.5" y2="12"/>
<line x1="4.5" y1="4.5" x2="6.2" y2="6.2"/><line x1="17.8" y1="17.8" x2="19.5" y2="19.5"/>
<line x1="19.5" y1="4.5" x2="17.8" y2="6.2"/><line x1="6.2" y1="17.8" x2="4.5" y2="19.5"/>
</symbol>
<symbol id="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M20 14.5A8.5 8.5 0 1 1 9.5 4a6.8 6.8 0 0 0 10.5 10.5z"/>
</symbol>
<symbol id="icon-chevron-left" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="15 4 7 12 15 20"/>
</symbol>
<symbol id="icon-chevron-right" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="9 4 17 12 9 20"/>
</symbol>
<symbol id="icon-check" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<polyline points="4 13 9.5 18.5 20 6"/>
</symbol>
<symbol id="icon-external-link" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M18 13.5V19a1.5 1.5 0 0 1-1.5 1.5H5A1.5 1.5 0 0 1 3.5 19V7A1.5 1.5 0 0 1 5 5.5h5.5"/>
<polyline points="14.5 3.5 20.5 3.5 20.5 9.5"/><line x1="11" y1="13" x2="20" y2="4"/>
</symbol>
<symbol id="icon-book" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M4 5.5A2 2 0 0 1 6 4h6v16H6a2 2 0 0 0-2 1.5z"/>
<path d="M20 5.5A2 2 0 0 0 18 4h-6v16h6a2 2 0 0 1 2 1.5z"/>
</symbol>
<symbol id="icon-copy" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="9" y="9" width="13" height="13" rx="2"/><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"/>
</symbol>
<symbol id="icon-download" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M12 3v12"/><polyline points="7 10 12 15 17 10"/><path d="M4 19.5h16"/>
</symbol></svg>
<header class="site-header">
<button type="button" class="icon-btn" id="sidebar-toggle" aria-label="Menü öffnen"><svg class="icon" aria-hidden="true"><use href="#icon-menu"></use></svg></button>
<a class="brand" href="index.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> <span>Optimierte Entscheidungsfindung mit Python</span></a>
<div class="site-search">
<input id="suche-eingabe" type="search" placeholder="Suchen …" aria-label="Suche" autocomplete="off" />
<svg class="icon such-icon" aria-hidden="true"><use href="#icon-search"></use></svg>
<div id="suche-ergebnisse" class="suche-ergebnisse" hidden></div>
</div>
<button type="button" class="icon-btn" id="theme-toggle" aria-label="Darstellung umschalten">
<svg class="icon icon-sun" aria-hidden="true"><use href="#icon-sun"></use></svg><svg class="icon icon-moon" aria-hidden="true"><use href="#icon-moon"></use></svg>
</button>
</header>
<div class="site-body">
<div class="sidebar-overlay" id="sidebar-overlay" hidden></div>
<nav class="sidebar" id="sidebar" aria-label="Kapitelnavigation"><div class="sidebar-inhalt"><details class="sidebar-gruppe"><summary>Einstieg</summary><ul><li data-kapitel="vorwort.html"><a href="vorwort.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><li data-kapitel="notation.html"><a href="notation.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><li data-kapitel="einfuehrung.html"><a href="einfuehrung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><li data-kapitel="fundament.html"><a href="fundament.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität{idx:Konvexität}</span></a></li><li data-kapitel="oekosystem.html"><a href="oekosystem.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><li data-kapitel="modellierung.html"><a href="modellierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><li data-kapitel="synthese-grundlagen.html"><a href="synthese-grundlagen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><details class="sidebar-gruppe" open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><li data-kapitel="lp.html"><a href="lp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><li data-kapitel="milp.html" class="aktiv"><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{idx: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{idx: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 und mehrperiodige Dynamik</summary><ul><li data-kapitel="qp-nlp.html"><a href="qp-nlp.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 11: Quadratische und nichtlineare Optimierung — KKT, Lagrange, Konvexität</span></a></li><li data-kapitel="unsicherheit.html"><a href="unsicherheit.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 12: Optimierung unter Unsicherheit — Monte-Carlo, Stochastik, Robustheit</span></a></li><li data-kapitel="dynamische-programmierung.html"><a href="dynamische-programmierung.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 13: Dynamische Programmierung — Die Bellman-Gleichung{idx:Bellman-Gleichung} und Order-Execution</span></a></li><li data-kapitel="mehrziel.html"><a href="mehrziel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 14: Mehrere Ziele — Pareto-Fronten statt Gewichte</span></a></li><li data-kapitel="prognose.html"><a href="prognose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 15: Predict-then-Optimize{idx:Predict-then-Optimize} — die bessere Prognose, die schlechtere Entscheidung</span></a></li><li data-kapitel="synthese-nichtlinear.html"><a href="synthese-nichtlinear.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil III</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil IV: Anwendungen — Energiewirtschaft und Finanzmärkte</summary><ul><li data-kapitel="bruecke.html"><a href="bruecke.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 16: Die Strukturbrücke — dieselbe Mathematik, zwei Welten</span></a></li><li data-kapitel="supplychain.html"><a href="supplychain.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 17: Supply-Chain und Energieeinsatz unter Unsicherheit</span></a></li><li data-kapitel="finanzdaten.html"><a href="finanzdaten.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 18: Finanzdaten-Modellierung — Renditen, Kovarianz und Shrinkage</span></a></li><li data-kapitel="markowitz.html"><a href="markowitz.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 19: Die moderne Portfoliotheorie nach Markowitz</span></a></li><li data-kapitel="cvar.html"><a href="cvar.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 20: Tail-Risiko, CVaR und Transaktionskosten{idx:Transaktionskosten}</span></a></li><li data-kapitel="handelsmaschine.html"><a href="handelsmaschine.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 21: Die vollständige quantitative Handelsmaschine</span></a></li><li data-kapitel="synthese-anwendungen.html"><a href="synthese-anwendungen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil IV</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Teil V: Praxis</summary><ul><li data-kapitel="praxisfallen.html"><a href="praxisfallen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 22: Praxisfallen und der Weg zum produktiven Einsatz</span></a></li><li data-kapitel="testing.html"><a href="testing.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Kapitel 23: Testen, Messen, Ausliefern</span></a></li><li data-kapitel="synthese-praxis.html"><a href="synthese-praxis.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Synthese Teil V</span></a></li><li data-kapitel="projektwerkstatt.html"><a href="projektwerkstatt.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Projektwerkstatt</span></a></li></ul></details><details class="sidebar-gruppe"><summary>Anhänge</summary><ul><li data-kapitel="anhang-loesungen.html"><a href="anhang-loesungen.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang A: Lösungen zu allen Übungsaufgaben</span></a></li><li data-kapitel="anhang-modellierungsmuster.html"><a href="anhang-modellierungsmuster.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang B: Katalog der Modellierungsmuster</span></a></li><li data-kapitel="anhang-fehlerdiagnose.html"><a href="anhang-fehlerdiagnose.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang C: Fehlerdiagnose-Handbuch</span></a></li><li data-kapitel="anhang-spickzettel.html"><a href="anhang-spickzettel.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang D: Spickzettel der Solver</span></a></li><li data-kapitel="anhang-glossar.html"><a href="anhang-glossar.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang E: Glossar</span></a></li><li data-kapitel="anhang-literatur.html"><a href="anhang-literatur.html"><span class="fortschritt-haken"><svg class="icon" aria-hidden="true"><use href="#icon-check"></use></svg></span><span>Anhang F: Literaturverzeichnis</span></a></li></ul></details><ul class="sidebar-extra"><li><a href="programme.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Beispielprogramme</a></li><li><a href="notebooks.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Notebooks</a></li><li><a href="Notebooks_04.zip" download><svg class="icon" aria-hidden="true"><use href="#icon-download"></use></svg> Download Notebooks als ZIP</a></li><li><a href="anhang-glossar.html"><svg class="icon" aria-hidden="true"><use href="#icon-book"></use></svg> Glossar</a></li><li><a href="stichwortverzeichnis.html"><svg class="icon" aria-hidden="true"><use href="#icon-search"></use></svg> Stichwortverzeichnis</a></li><li><a href="gesamtdokument.html"><svg class="icon" aria-hidden="true"><use href="#icon-external-link"></use></svg> Gesamtdokument (eine HTML-Seite)</a></li><li><a href="Operations_Research_mit_Python_Version_04.pdf" download><svg class="icon" aria-hidden="true"><use href="#icon-download"></use></svg> Download Buch als PDF</a></li></ul></div></nav>
<main class="content">
<nav class="breadcrumb" aria-label="Breadcrumb"><a href="index.html">Start</a> <span>Teil II</span> <span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound{idx:Branch-and-Bound}</span></nav>
<nav class="prev-next"><a class="prev-next-knopf prev-next-prev" href="lp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a><a class="prev-next-knopf prev-next-next" href="cpsat.html"><span><small>Weiter</small>Kapitel 7: Constraint Programming mit CP-SAT{idx:CP-SAT} — Logik, Scheduling und Zuweisung</span><svg class="icon" aria-hidden="true"><use href="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1 id="kap-milp">Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</h1>
<div class="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um Entscheidungen, die nicht teilbar sind: ganze Maschinen, Ja/Nein, „höchstens fünf davon“. Wir zeigen mit einem konkreten Gegenbeispiel, warum Runden <strong>falsch</strong> ist, und lernen die Modellierungstricks, mit denen sich logische Regeln in lineare Ungleichungen übersetzen lassen.</p>
<p><strong>Voraussetzungen:</strong> <a href="lp.html#kap-lp">Kapitel 5</a> (LP, Relaxation, Schranken).</p>
<p><strong>Danach können Sie:</strong> Fixkosten, Entweder-Oder, Wenn-Dann und Kardinalitätsgrenzen modellieren, ein MILP mit CP-SAT oder HiGHS lösen, den MIP-Gap als Zusage an den Auftraggeber lesen — und ein Ergebnis erkennen, das nur deshalb gut aussieht, weil das Big-M zu groß gewählt war.</p>
<p><strong>Zeitbedarf:</strong> ca. 6,5 Stunden.</p>
<p><strong>Programme:</strong><br />
<code>Runden_Gegenbeispiel.py</code><br />
<code>Rucksack.py</code><br />
<code>MILP_Portfolio_Fixgebuehren.py</code><br />
<code>Solverstatus_und_Gap.py</code><br />
<code>Warmstart_Effekt.py</code><br />
<code>Big_M_Falle.py</code></p>
<p><strong>Notebook:</strong> <a href="Notebooks_04/milp.ipynb">milp.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
</blockquote>
</div>
<hr />
<h2 id="sec:milp-schnellstart">6.1 In 5 Minuten gelöst</h2>
<div class="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Welches Lager eröffnen wir?</strong></p>
<p>Ein Händler muss 100 Paletten pro Woche ausliefern und kann dafür bis zu drei Lager eröffnen. Jedes verursacht Fixkosten, egal wie viel darüber läuft.</p>
<table>
<thead>
<tr class="header">
<th>Lager</th>
<th style="text-align: right;">Fixkosten/Woche</th>
<th style="text-align: right;">Kapazität</th>
<th style="text-align: right;">Kosten je Palette</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Nord</td>
<td style="text-align: right;">180 €</td>
<td style="text-align: right;">40</td>
<td style="text-align: right;">6,00 €</td>
</tr>
<tr class="even">
<td>Mitte</td>
<td style="text-align: right;">260 €</td>
<td style="text-align: right;">70</td>
<td style="text-align: right;">4,50 €</td>
</tr>
<tr class="odd">
<td>Süd</td>
<td style="text-align: right;">300 €</td>
<td style="text-align: right;">90</td>
<td style="text-align: right;">3,80 €</td>
</tr>
</tbody>
</table>
<div class="sourceCode" id="cb1"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb1-1"><a href="#cb1-1" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb1-2"><a href="#cb1-2" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> milp, LinearConstraint, Bounds</span>
<span id="cb1-3"><a href="#cb1-3" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-4"><a href="#cb1-4" aria-hidden="true" tabindex="-1"></a>fix <span class="op">=</span> np.array([<span class="fl">180.0</span>, <span class="fl">260.0</span>, <span class="fl">300.0</span>]) <span class="co"># Fixkosten je Lager</span></span>
<span id="cb1-5"><a href="#cb1-5" aria-hidden="true" tabindex="-1"></a>kap <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">70.0</span>, <span class="fl">90.0</span>]) <span class="co"># Kapazitaet je Lager</span></span>
<span id="cb1-6"><a href="#cb1-6" aria-hidden="true" tabindex="-1"></a>var <span class="op">=</span> np.array([<span class="fl">6.0</span>, <span class="fl">4.5</span>, <span class="fl">3.8</span>]) <span class="co"># variable Kosten je Palette</span></span>
<span id="cb1-7"><a href="#cb1-7" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-8"><a href="#cb1-8" aria-hidden="true" tabindex="-1"></a>A <span class="op">=</span> np.zeros((<span class="dv">4</span>, <span class="dv">6</span>)) <span class="co"># Variablen: 3x Menge, 3x Schalter</span></span>
<span id="cb1-9"><a href="#cb1-9" aria-hidden="true" tabindex="-1"></a>A[<span class="dv">0</span>, :<span class="dv">3</span>] <span class="op">=</span> <span class="fl">1.0</span> <span class="co"># Summe der Mengen = 100</span></span>
<span id="cb1-10"><a href="#cb1-10" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">3</span>):</span>
<span id="cb1-11"><a href="#cb1-11" aria-hidden="true" tabindex="-1"></a> A[i <span class="op">+</span> <span class="dv">1</span>, i], A[i <span class="op">+</span> <span class="dv">1</span>, <span class="dv">3</span> <span class="op">+</span> i] <span class="op">=</span> <span class="fl">1.0</span>, <span class="op">-</span>kap[i] <span class="co"># Menge &lt;= Kapazitaet * Schalter</span></span>
<span id="cb1-12"><a href="#cb1-12" aria-hidden="true" tabindex="-1"></a>nb <span class="op">=</span> LinearConstraint(A, [<span class="dv">100</span>, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf, <span class="op">-</span>np.inf], [<span class="dv">100</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>])</span>
<span id="cb1-13"><a href="#cb1-13" aria-hidden="true" tabindex="-1"></a>grenzen <span class="op">=</span> Bounds([<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>, <span class="bu">list</span>(kap) <span class="op">+</span> [<span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])</span>
<span id="cb1-14"><a href="#cb1-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb1-15"><a href="#cb1-15" aria-hidden="true" tabindex="-1"></a><span class="cf">for</span> name, ganzzahlig <span class="kw">in</span> [(<span class="st">&quot;LP-Relaxation&quot;</span>, [<span class="dv">0</span>] <span class="op">*</span> <span class="dv">6</span>), (<span class="st">&quot;MILP&quot;</span>, [<span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">0</span>, <span class="dv">1</span>, <span class="dv">1</span>, <span class="dv">1</span>])]:</span>
<span id="cb1-16"><a href="#cb1-16" aria-hidden="true" tabindex="-1"></a> r <span class="op">=</span> milp(c<span class="op">=</span>np.concatenate([var, fix]), constraints<span class="op">=</span>[nb],</span>
<span id="cb1-17"><a href="#cb1-17" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>ganzzahlig, bounds<span class="op">=</span>grenzen)</span>
<span id="cb1-18"><a href="#cb1-18" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:14}</span><span class="ss"> </span><span class="sc">{</span>r<span class="sc">.</span>fun<span class="sc">:7.2f}</span><span class="ss"> EUR Mengen </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(r.x[:<span class="dv">3</span>], <span class="dv">1</span>)<span class="sc">}</span><span class="ss"> &quot;</span></span>
<span id="cb1-19"><a href="#cb1-19" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Schalter </span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">round</span>(r.x[<span class="dv">3</span>:], <span class="dv">3</span>)<span class="sc">}</span><span class="ss">&quot;</span>)</span></code></pre></div>
<p><strong>Ausgabe:</strong></p>
<pre><code>LP-Relaxation 724.14 EUR Mengen [ 0. 10. 90.] Schalter [-0. 0.143 1. ]
MILP 882.00 EUR Mengen [10. 0. 90.] Schalter [ 1. -0. 1. ]</code></pre>
</blockquote>
</div>
<p><strong>Sehen Sie sich die Schalter der ersten Zeile an: 0,143.</strong> Die LP-Relaxation eröffnet Lager Mitte zu 14,3 % — und zahlt dafür nur 14,3 % der Fixkosten. So etwas gibt es in der Wirklichkeit nicht. Ein Lager ist offen oder zu.</p>
<p>Und jetzt der Punkt, um den es in diesem ganzen Kapitel geht — <strong>was passiert, wenn man diese 0,143 rundet?</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Vorgehen</th>
<th>Ergebnis</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>Abrunden auf <span class="math inline">y = (0, 0, 1)</span></td>
<td>Nur Süd offen: 90 Paletten Kapazität für 100 Paletten Bedarf → <strong>unzulässig</strong></td>
</tr>
<tr class="even">
<td>Aufrunden auf <span class="math inline">y = (0, 1, 1)</span></td>
<td>Zulässig, aber <strong>947 €</strong> — 65 € teurer als nötig</td>
</tr>
<tr class="odd">
<td><strong>MILP-Optimum <span class="math inline">y = (1, 0, 1)</span></strong></td>
<td><strong>882 €</strong> — und es öffnet <strong>Nord</strong>, ein Lager, das die Relaxation gar nicht vorgeschlagen hatte</td>
</tr>
</tbody>
</table>
<p>Runden führt hier also in beide Richtungen in die Irre: einmal in die Unzulässigkeit, einmal in unnötige Kosten. Und der entscheidende Punkt ist der dritte: Die ganzzahlige Lösung ist <strong>strukturell anders</strong>. Sie öffnet ein anderes Lager. Keine noch so geschickte Rundung der LP-Lösung hätte darauf kommen können.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Die LP-Relaxation ist keine ungefähre Antwort, die man nur noch glattziehen muss. Sie ist eine Antwort auf eine <strong>andere Frage</strong> — nämlich die, bei der man Lager auch zu 14,3 % eröffnen darf. Ihr Wert liegt nicht in ihrer Lösung, sondern in ihrer <strong>Schranke</strong>: Weniger als 724,14 € kann die richtige Antwort nicht kosten.</p>
</blockquote>
<p>Genau diese Schranke ist der Hebel, mit dem Branch-and-Bound arbeitet. Wie, zeigt der Abschnitt <a href="#sec:milp-branch-and-bound">Abschnitt 6.4</a>.</p>
<hr />
<h2 id="sec:milp-lernziele">6.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<ol type="1">
<li>… an einem Zahlenbeispiel belegen, warum Runden einer LP-Lösung scheitert.</li>
<li>… den Ablauf von Branch-and-Bound erklären und einen kleinen Suchbaum von Hand zeichnen.</li>
<li>… die vier klassischen Big-M-Muster anwenden (Fixkosten, Entweder-Oder, Wenn-Dann, Kardinalität).</li>
<li>… begründen, warum <span class="math inline">M</span> so klein wie möglich gewählt werden muss.</li>
<li>… ein Portfolioproblem mit Ordergebühren und Höchstzahl an Positionen lösen.</li>
<li><strong>Incumbent, Schranke und MIP-Gap</strong> unterscheiden und aus einem abgebrochenen Solverlauf eine belastbare Aussage ableiten.</li>
<li>… alle Solver-Statusfälle explizit behandeln, statt <code>OPTIMAL</code> vorauszusetzen.</li>
<li>… einen <strong>Warm-Start</strong> setzen — und messen, ob er auf Ihrer Problemklasse überhaupt etwas bringt.</li>
</ol>
<hr />
<h2 id="sec:milp-warum-runden-fundamental-scheitert">6.3 Warum Runden fundamental scheitert</h2>
<p>In der realen Welt sind viele Entscheidungen nicht teilbar. Man kann nicht 0,47 Flugzeuge kaufen, keine halbe Lagerhalle bauen, keinen Mitarbeiter zu 38 % einstellen. Noch wichtiger: Logische Schalter — <em>„Wenn Fabrik A gebaut wird, muss auch Lager B gebaut werden“</em> — brauchen diskrete Zustände.</p>
<p><strong>Mixed-Integer Linear Programming (MILP)</strong>, deutsch <em>gemischt-ganzzahlige lineare Optimierung</em>, erweitert das LP um ganzzahlige (<span class="math inline">\mathbb{Z}</span>) und binäre (<span class="math inline">\{0,1\}</span>) Variablen.</p>
<p>Der verlockende Gedanke lautet: <em>„Wir lösen das Problem kontinuierlich und runden.“</em> Eine verbreitete, aber unbelegte Behauptung dazu lautet, man verliere dadurch „oft 2050 % des Gewinns“. Rechnen wir es nach, statt es zu behaupten.</p>
<figure>
<img src="bilder_04/kap_milp_runden.svg" alt="Abb. 6.1: Links die Handrechnung von unten als Bild: Die Zielfunktion läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf (1;1) führt aus dem zulässigen Bereich heraus, Abrunden auf (0;0) auf den Wert null. Rechts die Messung aus Runden_Gegenbeispiel.py über je 200 Zufallsinstanzen. Erzeugt von bilder_04/erzeuge_runden.py." />
<figcaption aria-hidden="true">Abb. 6.1: Links die Handrechnung von unten als Bild: Die Zielfunktion läuft parallel zur Restriktion, also ist die ganze Kante optimal. Aufrunden auf <span class="math inline">(1;1)</span> führt aus dem zulässigen Bereich heraus, Abrunden auf <span class="math inline">(0;0)</span> auf den Wert null. Rechts die Messung aus <code>Runden_Gegenbeispiel.py</code> über je 200 Zufallsinstanzen. Erzeugt von <code>bilder_04/erzeuge_runden.py</code>.</figcaption>
</figure>
<blockquote>
<p><strong>✏️ Handrechnung 6.1: Ein Gegenbeispiel, das Sie im Kopf prüfen können</strong></p>
<p><span class="math display">\max\ Z = x_1 + x_2 \quad\text{u. d. N.}\quad 2x_1 + 2x_2 \le 3,\quad x_1,x_2 \in \{0,1,2,\dots\}</span></p>
<p><strong>LP-Relaxation</strong> (Ganzzahligkeit weggelassen): Jede Kombination mit <span class="math inline">x_1 + x_2 = 1{,}5</span> ist optimal, z. B. <span class="math inline">x_1 = x_2 = 0{,}75</span> mit <span class="math inline">Z_{LP} = 1{,}5</span>.</p>
<p><strong>Aufrunden</strong> auf <span class="math inline">(1, 1)</span>: <span class="math inline">2\cdot1 + 2\cdot1 = 4 &gt; 3</span><strong>unzulässig</strong>. <strong>Abrunden</strong> auf <span class="math inline">(0, 0)</span>: zulässig, aber <span class="math inline">Z = 0</span>. <strong>Wahres ganzzahliges Optimum:</strong> <span class="math inline">(1, 0)</span> oder <span class="math inline">(0, 1)</span> mit <span class="math inline">Z_{IP} = 1</span>.</p>
<p>Das Abrunden verliert hier <strong>100 %</strong> des erreichbaren Werts. Das ist kein exotischer Sonderfall, sondern typisch, sobald die Zahlen klein sind — und bei Ja/Nein-Entscheidungen sind sie immer klein.</p>
</blockquote>
<p>Die drei Gründe systematisch:</p>
<ol type="1">
<li><strong>Verletzung von Nebenbedingungen.</strong> Aufrunden überschreitet Kapazitätsgrenzen — die Lösung wird unzulässig.</li>
<li><strong>Suboptimalität.</strong> Abrunden verschenkt Kapazität. Der Verlust ist umso größer, je kleiner die Zahlen sind.</li>
<li><strong>Distanz im Raum.</strong> In hohen Dimensionen liegt der beste ganzzahlige Punkt oft <strong>weit entfernt</strong> von der LP-Lösung — er ist durch Runden gar nicht erreichbar.</li>
</ol>
<p>Das folgende Programm quantifiziert den Effekt über viele Zufallsinstanzen.</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"># Runden_Gegenbeispiel.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 MILP: Wie schlecht ist Runden wirklich?</span></span>
<span id="cb3-6"><a href="#cb3-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-7"><a href="#cb3-7" aria-hidden="true" tabindex="-1"></a><span class="co">Ersetzt die unbelegte Behauptung &quot;20-50 % Verlust&quot; durch eine Messung ueber</span></span>
<span id="cb3-8"><a href="#cb3-8" aria-hidden="true" tabindex="-1"></a><span class="co">viele Zufallsinstanzen.</span></span>
<span id="cb3-9"><a href="#cb3-9" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb3-10"><a href="#cb3-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-11"><a href="#cb3-11" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb3-12"><a href="#cb3-12" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</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>
<span id="cb3-15"><a href="#cb3-15" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> erzeuge_instanz(n, m, rng):</span>
<span id="cb3-16"><a href="#cb3-16" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Zufaelliges Rucksack-aehnliches MILP mit kleinen Zahlen.&quot;&quot;&quot;</span></span>
<span id="cb3-17"><a href="#cb3-17" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> rng.integers(<span class="dv">3</span>, <span class="dv">20</span>, size<span class="op">=</span>n).astype(<span class="bu">float</span>) <span class="co"># Ertraege</span></span>
<span id="cb3-18"><a href="#cb3-18" aria-hidden="true" tabindex="-1"></a> A <span class="op">=</span> rng.integers(<span class="dv">1</span>, <span class="dv">9</span>, size<span class="op">=</span>(m, n)).astype(<span class="bu">float</span>) <span class="co"># Verbraeuche</span></span>
<span id="cb3-19"><a href="#cb3-19" aria-hidden="true" tabindex="-1"></a> b <span class="op">=</span> (A.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">*</span> rng.uniform(<span class="fl">0.25</span>, <span class="fl">0.45</span>)).<span class="bu">round</span>() <span class="co"># knappe Kapazitaeten</span></span>
<span id="cb3-20"><a href="#cb3-20" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> c, A, b</span>
<span id="cb3-21"><a href="#cb3-21" aria-hidden="true" tabindex="-1"></a></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 class="kw">def</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">False</span>):</span>
<span id="cb3-24"><a href="#cb3-24" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;LP-Relaxation oder exaktes MILP ueber HiGHS.&quot;&quot;&quot;</span></span>
<span id="cb3-25"><a href="#cb3-25" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(c)</span>
<span id="cb3-26"><a href="#cb3-26" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(</span>
<span id="cb3-27"><a href="#cb3-27" aria-hidden="true" tabindex="-1"></a> c<span class="op">=-</span>c, A_ub<span class="op">=</span>A, b_ub<span class="op">=</span>b, bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> n,</span>
<span id="cb3-28"><a href="#cb3-28" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>np.ones(n) <span class="cf">if</span> ganzzahlig <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb3-29"><a href="#cb3-29" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb3-30"><a href="#cb3-30" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> (<span class="op">-</span>ergebnis.fun, ergebnis.x) <span class="cf">if</span> ergebnis.success <span class="cf">else</span> (<span class="va">None</span>, <span class="va">None</span>)</span>
<span id="cb3-31"><a href="#cb3-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-32"><a href="#cb3-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-33"><a href="#cb3-33" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> abrunden_und_reparieren(x_lp, c, A, b):</span>
<span id="cb3-34"><a href="#cb3-34" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Naive Strategie: abrunden, dann gierig auffuellen, solange zulaessig.&quot;&quot;&quot;</span></span>
<span id="cb3-35"><a href="#cb3-35" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.floor(x_lp <span class="op">+</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-36"><a href="#cb3-36" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">True</span></span>
<span id="cb3-37"><a href="#cb3-37" aria-hidden="true" tabindex="-1"></a> <span class="cf">while</span> verbessert: <span class="co"># gierig auffuellen</span></span>
<span id="cb3-38"><a href="#cb3-38" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">False</span></span>
<span id="cb3-39"><a href="#cb3-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> np.argsort(<span class="op">-</span>c): <span class="co"># bester Ertrag zuerst</span></span>
<span id="cb3-40"><a href="#cb3-40" aria-hidden="true" tabindex="-1"></a> kandidat <span class="op">=</span> x.copy()</span>
<span id="cb3-41"><a href="#cb3-41" aria-hidden="true" tabindex="-1"></a> kandidat[j] <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-42"><a href="#cb3-42" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">all</span>(A <span class="op">@</span> kandidat <span class="op">&lt;=</span> b <span class="op">+</span> <span class="fl">1e-9</span>):</span>
<span id="cb3-43"><a href="#cb3-43" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> kandidat</span>
<span id="cb3-44"><a href="#cb3-44" aria-hidden="true" tabindex="-1"></a> verbessert <span class="op">=</span> <span class="va">True</span></span>
<span id="cb3-45"><a href="#cb3-45" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb3-46"><a href="#cb3-46" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> c <span class="op">@</span> x, x</span>
<span id="cb3-47"><a href="#cb3-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-48"><a href="#cb3-48" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-49"><a href="#cb3-49" aria-hidden="true" tabindex="-1"></a><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-50"><a href="#cb3-50" aria-hidden="true" tabindex="-1"></a> rng <span class="op">=</span> np.random.default_rng(<span class="dv">2026</span>)</span>
<span id="cb3-51"><a href="#cb3-51" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-52"><a href="#cb3-52" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)&quot;</span>)</span>
<span id="cb3-53"><a href="#cb3-53" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-54"><a href="#cb3-54" 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 x m&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Aufrunden unzul.&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Abrunden: mittl.&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | &quot;</span></span>
<span id="cb3-55"><a href="#cb3-55" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;schlimmster&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;gierig&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-56"><a href="#cb3-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&gt;8}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Verlust&#39;</span><span class="sc">:&gt;17}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Fall&#39;</span><span class="sc">:&gt;12}</span><span class="ss"> | </span><span class="sc">{</span><span class="st">&#39;Verlust&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb3-57"><a href="#cb3-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">82</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> <span class="cf">for</span> n, m <span class="kw">in</span> [(<span class="dv">5</span>, <span class="dv">2</span>), (<span class="dv">10</span>, <span class="dv">3</span>), (<span class="dv">20</span>, <span class="dv">5</span>), (<span class="dv">40</span>, <span class="dv">8</span>)]:</span>
<span id="cb3-60"><a href="#cb3-60" aria-hidden="true" tabindex="-1"></a> unzulaessig <span class="op">=</span> <span class="dv">0</span></span>
<span id="cb3-61"><a href="#cb3-61" aria-hidden="true" tabindex="-1"></a> verluste_ab, verluste_gierig <span class="op">=</span> [], []</span>
<span id="cb3-62"><a href="#cb3-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-63"><a href="#cb3-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> _ <span class="kw">in</span> <span class="bu">range</span>(<span class="dv">200</span>):</span>
<span id="cb3-64"><a href="#cb3-64" aria-hidden="true" tabindex="-1"></a> c, A, b <span class="op">=</span> erzeuge_instanz(n, m, rng)</span>
<span id="cb3-65"><a href="#cb3-65" aria-hidden="true" tabindex="-1"></a> z_lp, x_lp <span class="op">=</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb3-66"><a href="#cb3-66" aria-hidden="true" tabindex="-1"></a> z_ip, _ <span class="op">=</span> loese_lp(c, A, b, ganzzahlig<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb3-67"><a href="#cb3-67" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> z_lp <span class="kw">is</span> <span class="va">None</span> <span class="kw">or</span> z_ip <span class="kw">is</span> <span class="va">None</span> <span class="kw">or</span> z_ip <span class="op">&lt;=</span> <span class="dv">0</span>:</span>
<span id="cb3-68"><a href="#cb3-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">continue</span></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="co"># Variante 1: aufrunden</span></span>
<span id="cb3-71"><a href="#cb3-71" aria-hidden="true" tabindex="-1"></a> x_auf <span class="op">=</span> np.ceil(x_lp <span class="op">-</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-72"><a href="#cb3-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> np.<span class="bu">any</span>(A <span class="op">@</span> x_auf <span class="op">&gt;</span> b <span class="op">+</span> <span class="fl">1e-9</span>):</span>
<span id="cb3-73"><a href="#cb3-73" aria-hidden="true" tabindex="-1"></a> unzulaessig <span class="op">+=</span> <span class="dv">1</span></span>
<span id="cb3-74"><a href="#cb3-74" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-75"><a href="#cb3-75" aria-hidden="true" tabindex="-1"></a> <span class="co"># Variante 2: abrunden</span></span>
<span id="cb3-76"><a href="#cb3-76" aria-hidden="true" tabindex="-1"></a> x_ab <span class="op">=</span> np.floor(x_lp <span class="op">+</span> <span class="fl">1e-9</span>)</span>
<span id="cb3-77"><a href="#cb3-77" aria-hidden="true" tabindex="-1"></a> verluste_ab.append(<span class="fl">1.0</span> <span class="op">-</span> (c <span class="op">@</span> x_ab) <span class="op">/</span> z_ip)</span>
<span id="cb3-78"><a href="#cb3-78" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-79"><a href="#cb3-79" aria-hidden="true" tabindex="-1"></a> <span class="co"># Variante 3: abrunden + gierig auffuellen</span></span>
<span id="cb3-80"><a href="#cb3-80" aria-hidden="true" tabindex="-1"></a> z_gierig, _ <span class="op">=</span> abrunden_und_reparieren(x_lp, c, A, b)</span>
<span id="cb3-81"><a href="#cb3-81" aria-hidden="true" tabindex="-1"></a> verluste_gierig.append(<span class="fl">1.0</span> <span class="op">-</span> z_gierig <span class="op">/</span> z_ip)</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> <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"> x </span><span class="sc">{</span>m<span class="sc">:&lt;3}</span><span class="ss"> | </span><span class="sc">{</span>unzulaessig<span class="op">/</span><span class="dv">2</span><span class="sc">:&gt;15.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-84"><a href="#cb3-84" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span>mean(verluste_ab)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;15.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-85"><a href="#cb3-85" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span><span class="bu">max</span>(verluste_ab)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;10.1f}</span><span class="ss"> % | &quot;</span></span>
<span id="cb3-86"><a href="#cb3-86" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>np<span class="sc">.</span>mean(verluste_gierig)<span class="op">*</span><span class="dv">100</span><span class="sc">:&gt;6.1f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb3-87"><a href="#cb3-87" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb3-88"><a href="#cb3-88" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">82</span>)</span>
<span id="cb3-89"><a href="#cb3-89" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Lesart: &#39;Aufrunden unzul.&#39; = Anteil der Faelle, in denen die aufgerundete&quot;</span>)</span>
<span id="cb3-90"><a href="#cb3-90" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Optimum.&quot;</span>)</span>
<span id="cb3-91"><a href="#cb3-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">82</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==================================================================================
WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)
==================================================================================
n x m | Aufrunden unzul. | Abrunden: mittl. | schlimmster | gierig
| | Verlust | Fall | Verlust
----------------------------------------------------------------------------------
5 x 2 | 82.0 % | 17.9 % | 100.0 % | 3.1 %
10 x 3 | 90.0 % | 17.2 % | 100.0 % | 3.0 %
20 x 5 | 97.5 % | 17.8 % | 50.0 % | 2.9 %
40 x 8 | 100.0 % | 15.6 % | 35.7 % | 3.6 %
----------------------------------------------------------------------------------
Lesart: &#39;Aufrunden unzul.&#39; = Anteil der Faelle, in denen die aufgerundete
Loesung eine Nebenbedingung verletzt. &#39;Verlust&#39; = Abstand zum exakten Optimum.
==================================================================================</code></pre>
<p><strong>Was die Zahlen sagen:</strong></p>
<ul>
<li><strong>Aufrunden ist fast immer unzulässig</strong> (82 % bei kleinen, 100 % bei größeren Instanzen). Es ist keine Strategie, sondern ein Fehler — und zwar einer, der mit der Problemgröße <em>zunimmt</em>, weil mehr Variablen mehr Gelegenheiten bieten, eine Kapazität zu sprengen.</li>
<li><strong>Abrunden verliert im Mittel 1618 %</strong>, in den schlimmsten gemessenen Fällen <strong>100 %</strong> (dann bleibt das Ergebnis bei null). Die eingangs zitierte Behauptung „2050 %“ lag zu hoch; der gemessene Mittelwert liegt darunter, die Extremfälle darüber. Beides ist erst durch die Messung sichtbar.</li>
<li><strong>Abrunden plus gieriges Auffüllen</strong> ist deutlich besser (rund 3 % Verlust) — aber weiterhin ohne jede Garantie und mit zusätzlichem Implementierungsaufwand. Ein exakter MILP-Solver liefert die Optimalität gratis und dazu einen <strong>Beweis</strong> dafür.</li>
</ul>
<blockquote>
<p><strong>⚠️ Zur Ehrlichkeit von Messungen</strong> Diese Zahlen gelten für <strong>diese</strong> Instanzfamilie (kleine ganzzahlige Erträge, knappe Kapazitäten). Bei großen Stückzahlen — etwa 4 700 statt 4,7 produzierten Einheiten — ist der relative Rundungsfehler viel kleiner, und Runden wird zu einer brauchbaren Heuristik. <strong>Die Faustregel lautet deshalb nicht „Runden ist immer schlecht“, sondern: Je kleiner die Zahlen und je knapper die Kapazitäten, desto teurer das Runden.</strong> Bei Ja/Nein-Variablen — dem häufigsten Fall — sind die Zahlen maximal klein, und Runden ist sinnlos.</p>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Runden ist kein Näherungsverfahren mit kontrollierbarem Fehler, sondern ein Verfahren ohne jede Garantie. Wenn Ganzzahligkeit zum Problem gehört, gehört sie ins Modell.</p>
</blockquote>
<hr />
<h2 id="sec:milp-branch-and-bound">6.4 Branch-and-Bound</h2>
<p>MILP-Probleme sind <strong>NP-schwer</strong>. Der Standardansatz moderner Solver (HiGHS, SCIP, Gurobi) heißt <strong>Branch-and-Cut</strong> — Branch-and-Bound plus Schnittebenen.</p>
<figure>
<img src="bilder_04/kap_milp_suchbaum.svg" alt="Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von bilder_04/erzeuge_branch_and_bound.py." />
<figcaption aria-hidden="true">Abb. 6.2: Der Suchbaum der Handrechnung weiter unten — nicht gezeichnet, sondern gerechnet: Das Skript löst die LP-Relaxationen und kappt nach denselben drei Regeln, die oben stehen. Ast B wird verworfen, weil sein Wert 18 den Incumbent 20 aus Ast A nicht schlägt. Erzeugt von <code>bilder_04/erzeuge_branch_and_bound.py</code>.</figcaption>
</figure>
<h3 id="die-drei-phasen">Die drei Phasen</h3>
<ol type="1">
<li><strong>LP-Relaxation.</strong> Ganzzahligkeit vorübergehend weglassen und das LP lösen. Der Wert ist eine <strong>obere Schranke</strong> (bei Maximierung): Besser als das kann kein ganzzahliger Punkt sein, denn jeder ganzzahlige Punkt ist auch ein zulässiger LP-Punkt.</li>
<li><strong>Branching.</strong> Hat eine ganzzahlige Variable einen gebrochenen Wert, etwa <span class="math inline">x_1 = 2{,}7</span>, wird das Problem in zwei disjunkte Teilprobleme zerlegt: <span class="math display">\text{Ast 1: } x_1 \le 2 \qquad\text{und}\qquad \text{Ast 2: } x_1 \ge 3</span> Der Wert <span class="math inline">2{,}7</span> selbst wird dadurch ausgeschlossen — kein ganzzahliger Punkt geht verloren.</li>
<li><strong>Bounding und Pruning.</strong> Ein Ast wird abgeschnitten (<em>pruned</em>), wenn:
<ul>
<li>das Teilproblem unzulässig ist,</li>
<li>seine LP-Schranke schlechter ist als die beste bereits gefundene ganzzahlige Lösung (der <strong>Incumbent</strong>),</li>
<li>oder die LP-Lösung bereits ganzzahlig ist (dann ist dieser Ast fertig).</li>
</ul></li>
</ol>
<figure>
<img src="bilder_04/kap_milp_bnb_geometrie.svg" alt="Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von bilder_04/erzeuge_bnb_geometrie.py." />
<figcaption aria-hidden="true">Abb. 6.3: Was der erste Verzweigungsschritt des Suchbaums geometrisch bedeutet. Graue Punkte sind die 13 ganzzahligen zulässigen Punkte. Erzeugt von <code>bilder_04/erzeuge_bnb_geometrie.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Der rote Streifen ist genau das, was Phase 2 wegnimmt — und in ihm liegt kein einziger grauer Punkt. Deshalb ist Verzweigen verlustfrei. Weggenommen wird trotzdem etwas Wertvolles: das LP-Optimum (3; 1,5) selbst. Genau darum sinkt die Schranke von 21 auf 20,67, und genau dieses Sinken ist der Fortschritt, den der Suchbaum Knoten für Knoten protokolliert.</p>
<p>Zusätzlich fügen Solver <strong>Schnittebenen</strong> (<em>cutting planes</em>, z. B. Gomory-Cuts) hinzu: gültige Ungleichungen, die gebrochene Bereiche wegschneiden, ohne einen einzigen zulässigen ganzzahligen Punkt zu entfernen.</p>
<blockquote>
<p><strong>✏️ Handrechnung 6.2: Ein Suchbaum von Hand</strong></p>
<p><span class="math display">\max\ 5x_1 + 4x_2 \quad\text{u. d. N.}\quad 6x_1+4x_2\le24,\quad x_1+2x_2\le6,\quad x_1,x_2\in\mathbb{Z}_{\ge0}</span></p>
<p><strong>Wurzel <span class="math inline">P_0</span>:</strong> LP-Lösung <span class="math inline">x = (3;\ 1{,}5)</span>, <span class="math inline">Z_{LP} = 21</span>. → obere Schranke 21. <span class="math inline">x_2</span> ist gebrochen → verzweigen über <span class="math inline">x_2</span>.</p>
<p><strong>Ast A (<span class="math inline">x_2 \le 1</span>):</strong> LP liefert <span class="math inline">x = (3{,}33;\ 1)</span>, <span class="math inline">Z = 20{,}67</span>. Noch gebrochen (<span class="math inline">x_1</span>) → weiter verzweigen. * <strong>A1 (<span class="math inline">x_2\le1</span>, <span class="math inline">x_1\le3</span>):</strong> <span class="math inline">x = (3;\ 1)</span>, <span class="math inline">Z = 19</span><strong>ganzzahlig!</strong> Incumbent <span class="math inline">= 19</span>. * <strong>A2 (<span class="math inline">x_2\le1</span>, <span class="math inline">x_1\ge4</span>):</strong> <span class="math inline">x = (4;\ 0)</span>, <span class="math inline">Z = 20</span><strong>ganzzahlig!</strong> Incumbent verbessert auf <span class="math inline">20</span>.</p>
<p><strong>Ast B (<span class="math inline">x_2 \ge 2</span>):</strong> LP liefert <span class="math inline">x = (2{,}67;\ 2)</span>, <span class="math inline">Z = 21{,}33</span>… halt: Prüfen wir <span class="math inline">6x_1+4x_2\le24</span> mit <span class="math inline">x_2=2</span>: <span class="math inline">6x_1 \le 16</span>, also <span class="math inline">x_1 \le 2{,}67</span>; und <span class="math inline">x_1+2x_2\le6</span> gibt <span class="math inline">x_1 \le 2</span>. Also <span class="math inline">x_1 = 2</span>, <span class="math inline">Z = 5\cdot2+4\cdot2 = 18</span> → ganzzahlig, aber <strong>schlechter</strong> als der Incumbent 20 → verworfen.</p>
<p><strong>Ergebnis:</strong> <span class="math inline">x^* = (4;\ 0)</span>, <span class="math inline">Z^* = 20</span>. Wir haben <strong>fünf</strong> LPs gelöst — <span class="math inline">P_0</span>, A, A1, A2 und B — statt alle ganzzahligen Punkte aufzuzählen. Das Skript <code>bilder_04/erzeuge_branch_and_bound.py</code> rechnet genau diesen Baum nach und zählt mit.</p>
<p>Beachten Sie: Der Wert <span class="math inline">Z_{LP} = 21</span> der Wurzel ist die <strong>Optimalitätslücke</strong>-Referenz. Nach dem Finden von 20 weiß der Solver: Das Optimum liegt zwischen 20 und 21 — eine Lücke von 5 %. In der Praxis bricht man oft genau hier ab (siehe <a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>).</p>
</blockquote>
<hr />
<h2 id="sec:milp-modellierungstricks-big-m-und-logische-bedingungen">6.5 Modellierungstricks: Big-M und logische Bedingungen</h2>
<p>Hier kommt das Handwerkszeug, das MILP so mächtig macht: Man übersetzt Geschäftsregeln in lineare Ungleichungen — mithilfe binärer Hilfsvariablen <span class="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <span class="math inline">M</span>.</p>
<figure>
<img src="bilder_04/kap_milp_big_m.svg" alt="Abb. 6.4: Dieselbe Bedingung x \le M\,y, zwei Zahlen für M. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor y ganzzahlig wird. Erzeugt von bilder_04/erzeuge_big_m_vergleich.py." />
<figcaption aria-hidden="true">Abb. 6.4: Dieselbe Bedingung <span class="math inline">x \le M\,y</span>, zwei Zahlen für <span class="math inline">M</span>. Gezeigt ist die LP-Relaxation — also das, was der Solver sieht, bevor <span class="math inline">y</span> ganzzahlig wird. Erzeugt von <code>bilder_04/erzeuge_big_m_vergleich.py</code>.</figcaption>
</figure>
<p><strong>Was Sie in der Abbildung sehen.</strong> Für <span class="math inline">y \in \{0,1\}</span> sind beide Modelle gleichwertig; die beiden blauen Punkte stimmen überein. Verschieden ist nur, was dazwischen erlaubt ist — und rechts genügt bereits <span class="math inline">y = 0{,}08</span>, um die volle Kapazität freizugeben.</p>
<h3 id="muster-1-fixkosten-aktivierungsschalter">Muster 1 — Fixkosten / Aktivierungsschalter</h3>
<p><em>Regel:</em> Wird Maschine <span class="math inline">j</span> genutzt (<span class="math inline">x_j &gt; 0</span>), fallen Rüstkosten <span class="math inline">F_j</span> an.</p>
<p><span class="math display">
x_j \le M \cdot y_j, \qquad y_j \in \{0,1\},\ x_j \ge 0
</span></p>
<ul>
<li>Ist <span class="math inline">y_j = 0</span>: erzwingt <span class="math inline">x_j \le 0</span>, zusammen mit <span class="math inline">x_j \ge 0</span> also <span class="math inline">x_j = 0</span>.</li>
<li>Ist <span class="math inline">y_j = 1</span>: gilt nur <span class="math inline">x_j \le M</span> — die Kapazität ist freigegeben.</li>
</ul>
<p>In der Zielfunktion erscheint dann <span class="math inline">+F_j y_j</span> als Kostenterm.</p>
<h3 id="muster-2-entweder-oder-disjunctive-constraints">Muster 2 — Entweder-Oder (<em>disjunctive constraints</em>)</h3>
<p><em>Regel:</em> Es muss <strong>entweder</strong> <span class="math inline">f(\mathbf{x}) \le b_1</span> <strong>oder</strong> <span class="math inline">g(\mathbf{x}) \le b_2</span> gelten.</p>
<p><span class="math display">
f(\mathbf{x}) \le b_1 + M(1-y), \qquad g(\mathbf{x}) \le b_2 + M y, \qquad y\in\{0,1\}
</span></p>
<ul>
<li><span class="math inline">y=1</span>: erste Bedingung aktiv, zweite durch <span class="math inline">+M</span> praktisch außer Kraft.</li>
<li><span class="math inline">y=0</span>: umgekehrt.</li>
</ul>
<h3 id="muster-3-wenn-dann-implikation">Muster 3 — Wenn-Dann (Implikation)</h3>
<p><em>Regel:</em> Wenn <span class="math inline">y_1 = 1</span>, dann muss auch <span class="math inline">y_2 = 1</span> sein.</p>
<p><span class="math display">
y_1 \le y_2
</span></p>
<p>Kein Big-M nötig — das ist die eleganteste Formulierung überhaupt. Prüfen Sie die vier Fälle: <span class="math inline">(0,0)</span> ✓, <span class="math inline">(0,1)</span> ✓, <span class="math inline">(1,1)</span> ✓, <span class="math inline">(1,0)</span> ✗ — genau die verbotene Kombination wird ausgeschlossen.</p>
<h3 id="muster-4-kardinalität-höchstens-k-aus-n">Muster 4 — Kardinalität („höchstens <span class="math inline">K</span> aus <span class="math inline">N</span>“)</h3>
<p><span class="math display">
\sum_{j=1}^N y_j \le K
</span></p>
<h3 id="muster-5-semikontinuierlich-entweder-0-oder-mindestens-l">Muster 5 — Semikontinuierlich („entweder 0 oder mindestens <span class="math inline">L</span>“)</h3>
<p><em>Regel:</em> Eine Position ist entweder gar nicht besetzt oder mit mindestens <span class="math inline">L</span> Euro.</p>
<p><span class="math display">
L\,y_j \le x_j \le U\,y_j
</span></p>
<p>Dieses Muster brauchen wir gleich für die Mindestordergröße.</p>
<blockquote>
<p><strong>⚠️ Die Big-M-Falle: <span class="math inline">M</span> so klein wie möglich!</strong></p>
<p><span class="math inline">M</span> muss groß genug sein, um die Bedingung wirklich außer Kraft zu setzen — aber <strong>jedes Übermaß kostet Laufzeit</strong>. Der Grund liegt in der LP-Relaxation: Mit <span class="math inline">M = 10^9</span> und <span class="math inline">x_j \le 10^9 y_j</span> genügt schon <span class="math inline">y_j = 10^{-9} \cdot x_j</span>, um die Bedingung zu erfüllen. Die Relaxation ist dann extrem schwach, die obere Schranke nutzlos, und Branch-and-Bound muss praktisch alles durchsuchen.</p>
<p><strong>Regel:</strong> Wählen Sie <span class="math inline">M</span> als kleinste Zahl, die nachweislich nie bindet — meist eine ohnehin vorhandene Kapazitätsgrenze. Im Portfoliobeispiel unten ist das die Obergrenze pro Position (40 000 €), <strong>nicht</strong> eine willkürliche Million.</p>
<p>Gemessen an der Standortplanung aus <code>Big_M_Falle.py</code> (12 Lager, 40 Kunden, HiGHS):</p>
<table>
<thead>
<tr class="header">
<th><span class="math inline">M</span></th>
<th style="text-align: right;">Knoten im Suchbaum</th>
<th>Zielwert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td>= Lagerkapazität (knappstmöglich)</td>
<td style="text-align: right;"><strong>13</strong></td>
<td>richtig</td>
</tr>
<tr class="even">
<td><span class="math inline">10\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
<tr class="odd">
<td><span class="math inline">10^4\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
<tr class="even">
<td><span class="math inline">10^7\times</span> zu groß</td>
<td style="text-align: right;">51</td>
<td>richtig</td>
</tr>
</tbody>
</table>
<p>Zwei Dinge sind daran bemerkenswert. Erstens: Das knappe <span class="math inline">M</span> braucht <strong>viermal weniger Knoten</strong> — der Effekt ist real. Zweitens: Ab dem Zehnfachen wird es nicht mehr schlimmer, weil HiGHS <em>Presolve</em> das übergroße <span class="math inline">M</span> selbst auf die implizit vorhandene Schranke zurechtstutzt. Verlassen Sie sich darauf <strong>nicht</strong>: Presolve kann das nur, wenn eine solche Schranke im Modell überhaupt herleitbar ist. Und wenn nicht, wird es richtig unangenehm — siehe <a href="#sec:milp-denkfehler">Abschnitt 6.10</a>.</p>
</blockquote>
<hr />
<h2 id="sec:milp-beispiel-das-rucksackproblem">6.6 Beispiel: Das Rucksackproblem</h2>
<p>Bevor wir zum Portfolio kommen, das klassische Einstiegsproblem — kurz, verständlich und überall wiederzuerkennen.</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"># Rucksack.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 MILP: Das Rucksackproblem (Knapsack).</span></span>
<span id="cb5-6"><a href="#cb5-6" aria-hidden="true" tabindex="-1"></a><span class="co">Zeigt LP-Relaxation, Branch-and-Bound-Ergebnis und den Preis der Ganzzahligkeit</span></span>
<span id="cb5-7"><a href="#cb5-7" aria-hidden="true" tabindex="-1"></a><span class="co">an einem Beispiel, das man vollstaendig im Kopf nachvollziehen kann.</span></span>
<span id="cb5-8"><a href="#cb5-8" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb5-9"><a href="#cb5-9" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-10"><a href="#cb5-10" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb5-11"><a href="#cb5-11" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</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>GEGENSTAENDE <span class="op">=</span> [<span class="st">&quot;Zelt&quot;</span>, <span class="st">&quot;Schlafsack&quot;</span>, <span class="st">&quot;Kocher&quot;</span>, <span class="st">&quot;Kamera&quot;</span>, <span class="st">&quot;Buch&quot;</span>, <span class="st">&quot;Wasserfilter&quot;</span>, <span class="st">&quot;Seil&quot;</span>]</span>
<span id="cb5-14"><a href="#cb5-14" aria-hidden="true" tabindex="-1"></a>NUTZEN <span class="op">=</span> np.array([<span class="fl">40.0</span>, <span class="fl">35.0</span>, <span class="fl">20.0</span>, <span class="fl">30.0</span>, <span class="fl">8.0</span>, <span class="fl">25.0</span>, <span class="fl">12.0</span>])</span>
<span id="cb5-15"><a href="#cb5-15" aria-hidden="true" tabindex="-1"></a>GEWICHT <span class="op">=</span> np.array([ <span class="fl">6.0</span>, <span class="fl">4.0</span>, <span class="fl">3.0</span>, <span class="fl">2.0</span>, <span class="fl">1.0</span>, <span class="fl">2.0</span>, <span class="fl">3.0</span>])</span>
<span id="cb5-16"><a href="#cb5-16" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> <span class="fl">11.0</span> <span class="co"># kg</span></span>
<span id="cb5-17"><a href="#cb5-17" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-18"><a href="#cb5-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-19"><a href="#cb5-19" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(ganzzahlig: <span class="bu">bool</span>):</span>
<span id="cb5-20"><a href="#cb5-20" aria-hidden="true" tabindex="-1"></a> n <span class="op">=</span> <span class="bu">len</span>(NUTZEN)</span>
<span id="cb5-21"><a href="#cb5-21" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(</span>
<span id="cb5-22"><a href="#cb5-22" aria-hidden="true" tabindex="-1"></a> c<span class="op">=-</span>NUTZEN, A_ub<span class="op">=</span>[GEWICHT], b_ub<span class="op">=</span>[KAPAZITAET],</span>
<span id="cb5-23"><a href="#cb5-23" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="dv">1</span>)] <span class="op">*</span> n, <span class="co"># jedes Teil hoechstens einmal</span></span>
<span id="cb5-24"><a href="#cb5-24" aria-hidden="true" tabindex="-1"></a> integrality<span class="op">=</span>np.ones(n) <span class="cf">if</span> ganzzahlig <span class="cf">else</span> <span class="va">None</span>,</span>
<span id="cb5-25"><a href="#cb5-25" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb5-26"><a href="#cb5-26" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="op">-</span>ergebnis.fun, ergebnis.x</span>
<span id="cb5-27"><a href="#cb5-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-28"><a href="#cb5-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-29"><a href="#cb5-29" aria-hidden="true" tabindex="-1"></a><span class="cf">if</span> <span class="va">__name__</span> <span class="op">==</span> <span class="st">&quot;__main__&quot;</span>:</span>
<span id="cb5-30"><a href="#cb5-30" aria-hidden="true" tabindex="-1"></a> z_lp, x_lp <span class="op">=</span> loese(ganzzahlig<span class="op">=</span><span class="va">False</span>)</span>
<span id="cb5-31"><a href="#cb5-31" aria-hidden="true" tabindex="-1"></a> z_ip, x_ip <span class="op">=</span> loese(ganzzahlig<span class="op">=</span><span class="va">True</span>)</span>
<span id="cb5-32"><a href="#cb5-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb5-33"><a href="#cb5-33" 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="cb5-34"><a href="#cb5-34" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; RUCKSACKPROBLEM (Kapazitaet </span><span class="sc">{</span>KAPAZITAET<span class="sc">:.0f}</span><span class="ss"> kg)&quot;</span>)</span>
<span id="cb5-35"><a href="#cb5-35" 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="cb5-36"><a href="#cb5-36" 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;Gegenstand&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Nutzen&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;kg&#39;</span><span class="sc">:&gt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Nutzen/kg&#39;</span><span class="sc">:&gt;10}</span><span class="ss"> &quot;</span></span>
<span id="cb5-37"><a href="#cb5-37" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;LP&#39;</span><span class="sc">:&gt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;MILP&#39;</span><span class="sc">:&gt;6}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-38"><a href="#cb5-38" 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="cb5-39"><a href="#cb5-39" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(GEGENSTAENDE):</span>
<span id="cb5-40"><a href="#cb5-40" aria-hidden="true" tabindex="-1"></a> <span class="co"># int(round(...)) statt Format &quot;%.0f&quot;: vermeidet die Ausgabe &quot;-0&quot;</span></span>
<span id="cb5-41"><a href="#cb5-41" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span>NUTZEN[i]<span class="sc">:&gt;7.0f}</span><span class="ss"> </span><span class="sc">{</span>GEWICHT[i]<span class="sc">:&gt;5.0f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-42"><a href="#cb5-42" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>NUTZEN[i]<span class="op">/</span>GEWICHT[i]<span class="sc">:&gt;10.2f}</span><span class="ss"> </span><span class="sc">{</span>x_lp[i]<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span><span class="bu">int</span>(<span class="bu">round</span>(x_ip[i]))<span class="sc">:&gt;6d}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-43"><a href="#cb5-43" 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="cb5-44"><a href="#cb5-44" 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;Gesamtnutzen&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> </span><span class="sc">{</span>z_lp<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span>z_ip<span class="sc">:&gt;6.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-45"><a href="#cb5-45" 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;Gesamtgewicht&#39;</span><span class="sc">:&lt;14}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;7}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;5}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;10}</span><span class="ss"> &quot;</span></span>
<span id="cb5-46"><a href="#cb5-46" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>GEWICHT <span class="op">@</span> x_lp<span class="sc">:&gt;7.2f}</span><span class="ss"> </span><span class="sc">{</span>GEWICHT <span class="op">@</span> x_ip<span class="sc">:&gt;6.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-47"><a href="#cb5-47" 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="cb5-48"><a href="#cb5-48" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Obere Schranke aus der LP-Relaxation: </span><span class="sc">{</span>z_lp<span class="sc">:.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-49"><a href="#cb5-49" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Bestes ganzzahliges Ergebnis: </span><span class="sc">{</span>z_ip<span class="sc">:.0f}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-50"><a href="#cb5-50" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Preis der Ganzzahligkeit: </span><span class="sc">{</span>z_lp <span class="op">-</span> z_ip<span class="sc">:.2f}</span><span class="ss"> &quot;</span></span>
<span id="cb5-51"><a href="#cb5-51" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> z_ip<span class="op">/</span>z_lp)<span class="op">*</span><span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> %)&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> gebrochen <span class="op">=</span> [GEGENSTAENDE[i] <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(<span class="bu">len</span>(NUTZEN)) <span class="cf">if</span> <span class="fl">1e-6</span> <span class="op">&lt;</span> x_lp[i] <span class="op">&lt;</span> <span class="dv">1</span> <span class="op">-</span> <span class="fl">1e-6</span>]</span>
<span id="cb5-54"><a href="#cb5-54" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="ss">In der LP-Loesung nur teilweise eingepackt: </span><span class="sc">{</span>gebrochen<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb5-55"><a href="#cb5-55" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Genau hier wuerde Branch-and-Bound verzweigen:&quot;</span>)</span>
<span id="cb5-56"><a href="#cb5-56" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Ast 1: </span><span class="sc">{</span>gebrochen[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> bleibt ganz zuhause (x=0)&quot;</span>)</span>
<span id="cb5-57"><a href="#cb5-57" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Ast 2: </span><span class="sc">{</span>gebrochen[<span class="dv">0</span>]<span class="sc">}</span><span class="ss"> kommt ganz mit (x=1)&quot;</span>)</span>
<span id="cb5-58"><a href="#cb5-58" 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:</strong></p>
<pre><code>========================================================================
RUCKSACKPROBLEM (Kapazitaet 11 kg)
========================================================================
Gegenstand Nutzen kg Nutzen/kg LP MILP
------------------------------------------------------------------------
Zelt 40 6 6.67 0.00 0
Schlafsack 35 4 8.75 1.00 1
Kocher 20 3 6.67 0.67 1
Kamera 30 2 15.00 1.00 1
Buch 8 1 8.00 1.00 0
Wasserfilter 25 2 12.50 1.00 1
Seil 12 3 4.00 0.00 0
------------------------------------------------------------------------
Gesamtnutzen 111.33 110
Gesamtgewicht 11.00 11
------------------------------------------------------------------------
Obere Schranke aus der LP-Relaxation: 111.33
Bestes ganzzahliges Ergebnis: 110
Preis der Ganzzahligkeit: 1.33 (1.2 %)
In der LP-Loesung nur teilweise eingepackt: [&#39;Kocher&#39;]
Genau hier wuerde Branch-and-Bound verzweigen:
Ast 1: Kocher bleibt ganz zuhause (x=0)
Ast 2: Kocher kommt ganz mit (x=1)
========================================================================</code></pre>
<p>Drei Beobachtungen, die den Kern des Kapitels illustrieren:</p>
<ol type="1">
<li><strong>Die LP-Relaxation packt zwei Drittel Kocher ein</strong> — physikalisch unsinnig, als <strong>Schranke</strong> aber wertvoll: „Mehr als 111,33 ist unmöglich.“</li>
<li><strong>Runden hätte hier versagt.</strong> Abrunden des Kochers auf 0 ergäbe <span class="math inline">35+30+8+25 = 98</span> bei nur 9 kg — zwölf Punkte schlechter als das Optimum, und drei Kilo Kapazität bleiben ungenutzt. Aufrunden auf 1 ergäbe 12 kg und wäre <strong>unzulässig</strong>. Das exakte Optimum tauscht stattdessen das <em>Buch</em> gegen den <em>Kocher</em> — eine Umschichtung, auf die kein Rundungsverfahren kommt, weil sie eine bereits „fertige“ Variable wieder verändert.</li>
<li><strong>Die Lücke ist klein</strong> (1,2 %). Ein Solver, der frühzeitig bei 110 abbricht, kann beweisen, höchstens 1,2 % vom Optimum entfernt zu sein — ohne alle Äste zu durchsuchen. Genau das ist der MIP-Gap aus <a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>.</li>
</ol>
<hr />
<h2 id="sec:milp-praxisfall-portfolio-mit-ordergebuehren-und-kardinalitaetsgrenze">6.7 Praxisfall: Portfolio mit Ordergebühren und Kardinalitätsgrenze</h2>
<p><strong>Szenario.</strong> Ein Investor verteilt <span class="math inline">100\,000\,\text{€}</span> auf sechs Anlageklassen.</p>
<ul>
<li>Jede Transaktion kostet <strong>50 € Fixgebühr</strong>, unabhängig vom Betrag.</li>
<li>Höchstens <strong>3 Positionen</strong> gleichzeitig (<em>Kardinalitätsbeschränkung</em>).</li>
<li>Wird eine Position eröffnet, dann mit <strong>mindestens 10 000 €</strong> (Mindestordergröße).</li>
<li>Höchstens <strong>40 000 €</strong> pro Position.</li>
</ul>
<p><strong>Modell.</strong> Für jede Anlage <span class="math inline">i</span>: <span class="math inline">x_i \ge 0</span> (Euro-Betrag) und <span class="math inline">y_i \in \{0,1\}</span> (Position aktiv).</p>
<p><span class="math display">
\max \sum_{i=1}^n \mu_i x_i - F\sum_{i=1}^n y_i
</span></p>
<p><span class="math display">
\begin{aligned}
\sum_i x_i &amp;= B &amp;&amp; \text{(Budget vollständig investiert)}\\
\sum_i y_i &amp;\le K &amp;&amp; \text{(höchstens $K$ Positionen)}\\
L\,y_i \le x_i &amp;\le U\,y_i &amp;&amp; \forall i \quad \text{(semikontinuierlich, Muster 5)}
\end{aligned}
</span></p>
<blockquote>
<p><strong>📐 Formel-Lesehilfe</strong> * <span class="math inline">\mu_i</span> — erwartete Jahresrendite der Anlage <span class="math inline">i</span> (z. B. 0,11 = 11 %). * <span class="math inline">\mu_i x_i</span> — erwarteter Ertrag in Euro. * <span class="math inline">F\sum y_i</span> — Summe der Ordergebühren: 50 € je aktivierter Position. * <span class="math inline">L y_i \le x_i \le U y_i</span> — die Doppelungleichung erledigt <strong>beides</strong> auf einmal: Ist <span class="math inline">y_i=0</span>, folgt <span class="math inline">0 \le x_i \le 0</span>, also <span class="math inline">x_i=0</span>. Ist <span class="math inline">y_i=1</span>, folgt <span class="math inline">10\,000 \le x_i \le 40\,000</span>.</p>
<p><strong>Ohne Formel gesagt:</strong> „Investiere das ganze Budget in höchstens drei Töpfe, jeweils zwischen 10 000 und 40 000 Euro, und ziehe für jeden benutzten Topf 50 Euro Gebühr ab.“</p>
</blockquote>
<blockquote>
<p><strong>⚠️ Eine ökonomische Unsauberkeit, die Sie kennen sollten</strong></p>
<p>Das Modell verrechnet einen <strong>einmaligen</strong> Gebührenbetrag (50 €) mit einem <strong>jährlichen</strong> Ertrag (<span class="math inline">\mu_i x_i</span>). Streng genommen mischt das Einheiten — bei einer Haltedauer von einem Jahr geht es auf, bei zehn Jahren wären die Gebühren zehnfach zu schwer gewichtet. Für das Beispiel ist der Effekt klein (150 € gegen ~11 000 € Ertrag), aber in einem echten Modell würde man entweder die Gebühr annualisieren oder mit Barwerten rechnen.</p>
</blockquote>
<blockquote>
<p><strong>📎 Zwei Bausteine aus <code>or_kern.py</code></strong></p>
<p>Das Programm wertet den Solver nicht über <code>ergebnis.success</code> aus, sondern über <code>status_von_scipy()</code> und das <code>Loesung</code>-Objekt aus dem gemeinsamen Unterbau (<a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>). Der Unterschied ist kein Schönheitsfehler:</p>
<ul>
<li><code>success</code> ist <strong>ein Bit</strong>. Es unterscheidet nicht zwischen „es gibt keine Lösung“ (Modellfehler — das Modell muss geändert werden) und „die Zeit war um“ (Rechenproblem — mehr Zeit oder ein besserer Startwert hilft). Das sind zwei völlig verschiedene Nachrichten an völlig verschiedene Adressaten.</li>
<li>Das <code>Loesung</code>-Objekt führt neben dem Zielwert die <strong>Schranke</strong> mit und rechnet daraus den Gap aus. Erst dadurch steht in der Ausgabe <code>Gap: 0.00%</code> — die Zusage, dass hier wirklich das Optimum gefunden <em>und bewiesen</em> wurde und nicht bloß irgendetwas.</li>
</ul>
<p>Genau die Unterscheidung, um die es im nächsten Abschnitt geht — hier schon einmal angewandt.</p>
</blockquote>
<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"># MILP_Portfolio_Fixgebuehren.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 MILP: MILP-Portfolio-Selektion mit Fixkosten und Kardinalitaet.</span></span>
<span id="cb7-6"><a href="#cb7-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-7"><a href="#cb7-7" aria-hidden="true" tabindex="-1"></a><span class="co">CP-SAT-freie, gut lesbare Formulierung ueber scipy/HiGHS (kein manueller</span></span>
<span id="cb7-8"><a href="#cb7-8" aria-hidden="true" tabindex="-1"></a><span class="co">CSR-Matrixaufbau, deutlich leichter nachvollziehbar), mit Vergleich gegen die</span></span>
<span id="cb7-9"><a href="#cb7-9" aria-hidden="true" tabindex="-1"></a><span class="co">Loesung OHNE Restriktionen.</span></span>
<span id="cb7-10"><a href="#cb7-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-11"><a href="#cb7-11" aria-hidden="true" tabindex="-1"></a><span class="co">Die Auswertung laeuft ueber SolverStatus und das Loesung-Objekt aus</span></span>
<span id="cb7-12"><a href="#cb7-12" aria-hidden="true" tabindex="-1"></a><span class="co">or_kern.py: Alle Statusfaelle werden behandelt, und der MIP-Gap steht im</span></span>
<span id="cb7-13"><a href="#cb7-13" aria-hidden="true" tabindex="-1"></a><span class="co">Bericht - statt eines blossen &#39;success&#39;, das nicht verraet, ob der Solver</span></span>
<span id="cb7-14"><a href="#cb7-14" aria-hidden="true" tabindex="-1"></a><span class="co">fertig geworden ist oder nur aufgegeben hat.</span></span>
<span id="cb7-15"><a href="#cb7-15" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-16"><a href="#cb7-16" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, pandas, scipy, pydantic (ueber or_kern)</span></span>
<span id="cb7-17"><a href="#cb7-17" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-18"><a href="#cb7-18" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-19"><a href="#cb7-19" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</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 class="im">import</span> time</span>
<span id="cb7-22"><a href="#cb7-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-23"><a href="#cb7-23" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb7-24"><a href="#cb7-24" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> pandas <span class="im">as</span> pd</span>
<span id="cb7-25"><a href="#cb7-25" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> scipy.optimize <span class="im">import</span> linprog</span>
<span id="cb7-26"><a href="#cb7-26" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-27"><a href="#cb7-27" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> or_kern <span class="im">import</span> Loesung, SolverStatus, status_von_scipy</span>
<span id="cb7-28"><a href="#cb7-28" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-29"><a href="#cb7-29" aria-hidden="true" tabindex="-1"></a>ANLAGEN <span class="op">=</span> [<span class="st">&quot;US-Aktien&quot;</span>, <span class="st">&quot;EU-Aktien&quot;</span>, <span class="st">&quot;Emerging-Markets&quot;</span>,</span>
<span id="cb7-30"><a href="#cb7-30" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Staatsanleihen&quot;</span>, <span class="st">&quot;Unternehmensanleihen&quot;</span>, <span class="st">&quot;Rohstoffe&quot;</span>]</span>
<span id="cb7-31"><a href="#cb7-31" aria-hidden="true" tabindex="-1"></a>RENDITE <span class="op">=</span> np.array([<span class="fl">0.11</span>, <span class="fl">0.08</span>, <span class="fl">0.13</span>, <span class="fl">0.03</span>, <span class="fl">0.05</span>, <span class="fl">0.07</span>]) <span class="co"># erwartet, p.a.</span></span>
<span id="cb7-32"><a href="#cb7-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-33"><a href="#cb7-33" aria-hidden="true" tabindex="-1"></a>BUDGET <span class="op">=</span> <span class="fl">100_000.0</span></span>
<span id="cb7-34"><a href="#cb7-34" aria-hidden="true" tabindex="-1"></a>MIN_POSITION <span class="op">=</span> <span class="fl">10_000.0</span> <span class="co"># L</span></span>
<span id="cb7-35"><a href="#cb7-35" aria-hidden="true" tabindex="-1"></a>MAX_POSITION <span class="op">=</span> <span class="fl">40_000.0</span> <span class="co"># U (dient zugleich als Big-M!)</span></span>
<span id="cb7-36"><a href="#cb7-36" aria-hidden="true" tabindex="-1"></a>GEBUEHR <span class="op">=</span> <span class="fl">50.0</span> <span class="co"># F, je aktivierter Position</span></span>
<span id="cb7-37"><a href="#cb7-37" aria-hidden="true" tabindex="-1"></a>MAX_POSITIONEN <span class="op">=</span> <span class="dv">3</span> <span class="co"># K</span></span>
<span id="cb7-38"><a href="#cb7-38" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-39"><a href="#cb7-39" aria-hidden="true" tabindex="-1"></a>N <span class="op">=</span> <span class="bu">len</span>(ANLAGEN)</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>
<span id="cb7-42"><a href="#cb7-42" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> baue_und_loese(zeitlimit: <span class="bu">float</span> <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> Loesung:</span>
<span id="cb7-43"><a href="#cb7-43" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;</span></span>
<span id="cb7-44"><a href="#cb7-44" aria-hidden="true" tabindex="-1"></a><span class="co"> Variablenreihenfolge: [x_0..x_{N-1}, y_0..y_{N-1}]</span></span>
<span id="cb7-45"><a href="#cb7-45" aria-hidden="true" tabindex="-1"></a><span class="co"> Zielfunktion (Maximierung -&gt; fuer linprog negiert):</span></span>
<span id="cb7-46"><a href="#cb7-46" aria-hidden="true" tabindex="-1"></a><span class="co"> max sum(rendite_i * x_i) - GEBUEHR * sum(y_i)</span></span>
<span id="cb7-47"><a href="#cb7-47" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-48"><a href="#cb7-48" aria-hidden="true" tabindex="-1"></a> c <span class="op">=</span> np.concatenate([<span class="op">-</span>RENDITE, np.full(N, GEBUEHR)]) <span class="co"># negiert = Minimierung</span></span>
<span id="cb7-49"><a href="#cb7-49" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-50"><a href="#cb7-50" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Gleichungsnebenbedingung: Budget vollstaendig investiert -----------</span></span>
<span id="cb7-51"><a href="#cb7-51" aria-hidden="true" tabindex="-1"></a> A_eq <span class="op">=</span> np.zeros((<span class="dv">1</span>, <span class="dv">2</span> <span class="op">*</span> N))</span>
<span id="cb7-52"><a href="#cb7-52" aria-hidden="true" tabindex="-1"></a> A_eq[<span class="dv">0</span>, :N] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-53"><a href="#cb7-53" aria-hidden="true" tabindex="-1"></a> b_eq <span class="op">=</span> np.array([BUDGET])</span>
<span id="cb7-54"><a href="#cb7-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-55"><a href="#cb7-55" aria-hidden="true" tabindex="-1"></a> zeilen, grenzen <span class="op">=</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> <span class="co"># --- Kardinalitaet: sum(y_i) &lt;= K --------------------------------------</span></span>
<span id="cb7-58"><a href="#cb7-58" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-59"><a href="#cb7-59" aria-hidden="true" tabindex="-1"></a> zeile[N:] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-60"><a href="#cb7-60" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-61"><a href="#cb7-61" aria-hidden="true" tabindex="-1"></a> grenzen.append(MAX_POSITIONEN)</span>
<span id="cb7-62"><a href="#cb7-62" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-63"><a href="#cb7-63" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Obergrenze (Big-M): x_i - U*y_i &lt;= 0 -----------------------------</span></span>
<span id="cb7-64"><a href="#cb7-64" 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="cb7-65"><a href="#cb7-65" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-66"><a href="#cb7-66" aria-hidden="true" tabindex="-1"></a> zeile[i] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb7-67"><a href="#cb7-67" aria-hidden="true" tabindex="-1"></a> zeile[N <span class="op">+</span> i] <span class="op">=</span> <span class="op">-</span>MAX_POSITION</span>
<span id="cb7-68"><a href="#cb7-68" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-69"><a href="#cb7-69" aria-hidden="true" tabindex="-1"></a> grenzen.append(<span class="fl">0.0</span>)</span>
<span id="cb7-70"><a href="#cb7-70" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-71"><a href="#cb7-71" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Untergrenze: L*y_i - x_i &lt;= 0 (entspricht x_i &gt;= L*y_i) --------</span></span>
<span id="cb7-72"><a href="#cb7-72" 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="cb7-73"><a href="#cb7-73" aria-hidden="true" tabindex="-1"></a> zeile <span class="op">=</span> np.zeros(<span class="dv">2</span> <span class="op">*</span> N)</span>
<span id="cb7-74"><a href="#cb7-74" aria-hidden="true" tabindex="-1"></a> zeile[i] <span class="op">=</span> <span class="op">-</span><span class="fl">1.0</span></span>
<span id="cb7-75"><a href="#cb7-75" aria-hidden="true" tabindex="-1"></a> zeile[N <span class="op">+</span> i] <span class="op">=</span> MIN_POSITION</span>
<span id="cb7-76"><a href="#cb7-76" aria-hidden="true" tabindex="-1"></a> zeilen.append(zeile)</span>
<span id="cb7-77"><a href="#cb7-77" aria-hidden="true" tabindex="-1"></a> grenzen.append(<span class="fl">0.0</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> A_ub <span class="op">=</span> np.array(zeilen)</span>
<span id="cb7-80"><a href="#cb7-80" aria-hidden="true" tabindex="-1"></a> b_ub <span class="op">=</span> np.array(grenzen)</span>
<span id="cb7-81"><a href="#cb7-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-82"><a href="#cb7-82" aria-hidden="true" tabindex="-1"></a> schranken <span class="op">=</span> [(<span class="fl">0.0</span>, MAX_POSITION)] <span class="op">*</span> N <span class="op">+</span> [(<span class="fl">0.0</span>, <span class="fl">1.0</span>)] <span class="op">*</span> N</span>
<span id="cb7-83"><a href="#cb7-83" aria-hidden="true" tabindex="-1"></a> ganzzahligkeit <span class="op">=</span> np.concatenate([np.zeros(N), np.ones(N)]) <span class="co"># y binaer</span></span>
<span id="cb7-84"><a href="#cb7-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-85"><a href="#cb7-85" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb7-86"><a href="#cb7-86" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=</span>c, A_ub<span class="op">=</span>A_ub, b_ub<span class="op">=</span>b_ub, A_eq<span class="op">=</span>A_eq, b_eq<span class="op">=</span>b_eq,</span>
<span id="cb7-87"><a href="#cb7-87" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>schranken, integrality<span class="op">=</span>ganzzahligkeit,</span>
<span id="cb7-88"><a href="#cb7-88" aria-hidden="true" tabindex="-1"></a> method<span class="op">=</span><span class="st">&quot;highs&quot;</span>,</span>
<span id="cb7-89"><a href="#cb7-89" aria-hidden="true" tabindex="-1"></a> options<span class="op">=</span>{<span class="st">&quot;time_limit&quot;</span>: zeitlimit} <span class="cf">if</span> zeitlimit <span class="cf">else</span> <span class="va">None</span>)</span>
<span id="cb7-90"><a href="#cb7-90" aria-hidden="true" tabindex="-1"></a> laufzeit <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb7-91"><a href="#cb7-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-92"><a href="#cb7-92" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_scipy(ergebnis)</span>
<span id="cb7-93"><a href="#cb7-93" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-94"><a href="#cb7-94" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(status<span class="op">=</span>status, laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-95"><a href="#cb7-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-96"><a href="#cb7-96" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zurueck in die Maximierungswelt: Zielwert UND Schranke negieren.</span></span>
<span id="cb7-97"><a href="#cb7-97" aria-hidden="true" tabindex="-1"></a> <span class="co"># Aus beiden zusammen rechnet das Loesung-Objekt den MIP-Gap aus.</span></span>
<span id="cb7-98"><a href="#cb7-98" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis.x[:N], np.<span class="bu">round</span>(ergebnis.x[N:])</span>
<span id="cb7-99"><a href="#cb7-99" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Loesung(</span>
<span id="cb7-100"><a href="#cb7-100" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb7-101"><a href="#cb7-101" aria-hidden="true" tabindex="-1"></a> werte<span class="op">=</span>{<span class="op">**</span>{name: <span class="bu">float</span>(w) <span class="cf">for</span> name, w <span class="kw">in</span> <span class="bu">zip</span>(ANLAGEN, x)},</span>
<span id="cb7-102"><a href="#cb7-102" aria-hidden="true" tabindex="-1"></a> <span class="op">**</span>{<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>: <span class="bu">float</span>(w) <span class="cf">for</span> name, w <span class="kw">in</span> <span class="bu">zip</span>(ANLAGEN, y)}},</span>
<span id="cb7-103"><a href="#cb7-103" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span><span class="bu">float</span>(<span class="op">-</span>ergebnis.fun),</span>
<span id="cb7-104"><a href="#cb7-104" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span><span class="bu">float</span>(<span class="op">-</span>ergebnis.mip_dual_bound),</span>
<span id="cb7-105"><a href="#cb7-105" aria-hidden="true" tabindex="-1"></a> laufzeit<span class="op">=</span>laufzeit)</span>
<span id="cb7-106"><a href="#cb7-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-107"><a href="#cb7-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-108"><a href="#cb7-108" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> ohne_restriktionen() <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">float</span>, np.ndarray]:</span>
<span id="cb7-109"><a href="#cb7-109" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Vergleichsfall: nur Budget, keine Gebuehren/Kardinalitaet/Mindestgroesse.&quot;&quot;&quot;</span></span>
<span id="cb7-110"><a href="#cb7-110" aria-hidden="true" tabindex="-1"></a> ergebnis <span class="op">=</span> linprog(c<span class="op">=-</span>RENDITE, A_eq<span class="op">=</span>[np.ones(N)], b_eq<span class="op">=</span>[BUDGET],</span>
<span id="cb7-111"><a href="#cb7-111" aria-hidden="true" tabindex="-1"></a> bounds<span class="op">=</span>[(<span class="dv">0</span>, <span class="va">None</span>)] <span class="op">*</span> N, method<span class="op">=</span><span class="st">&quot;highs&quot;</span>)</span>
<span id="cb7-112"><a href="#cb7-112" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> status_von_scipy(ergebnis)</span>
<span id="cb7-113"><a href="#cb7-113" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> status.brauchbar:</span>
<span id="cb7-114"><a href="#cb7-114" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Vergleichsfall nicht loesbar: </span><span class="sc">{</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-115"><a href="#cb7-115" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="op">-</span>ergebnis.fun, ergebnis.x</span>
<span id="cb7-116"><a href="#cb7-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-117"><a href="#cb7-117" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-118"><a href="#cb7-118" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe_portfolio(loesung: Loesung, toleranz: <span class="bu">float</span> <span class="op">=</span> <span class="fl">1e-6</span>) <span class="op">-&gt;</span> <span class="bu">list</span>[<span class="bu">str</span>]:</span>
<span id="cb7-119"><a href="#cb7-119" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Prueft die Loesung gegen die Anforderungen - ohne den Solver zu fragen.</span></span>
<span id="cb7-120"><a href="#cb7-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-121"><a href="#cb7-121" aria-hidden="true" tabindex="-1"></a><span class="co"> Bewusst kein assert: Eine Beanstandungsliste laesst sich protokollieren,</span></span>
<span id="cb7-122"><a href="#cb7-122" aria-hidden="true" tabindex="-1"></a><span class="co"> weiterreichen und testen. Ein assert verschwindet ausserdem, sobald</span></span>
<span id="cb7-123"><a href="#cb7-123" aria-hidden="true" tabindex="-1"></a><span class="co"> jemand Python mit -O startet.</span></span>
<span id="cb7-124"><a href="#cb7-124" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb7-125"><a href="#cb7-125" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.array([loesung.werte[name] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-126"><a href="#cb7-126" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.array([loesung.werte[<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-127"><a href="#cb7-127" aria-hidden="true" tabindex="-1"></a> beanstandungen: <span class="bu">list</span>[<span class="bu">str</span>] <span class="op">=</span> []</span>
<span id="cb7-128"><a href="#cb7-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-129"><a href="#cb7-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(x.<span class="bu">sum</span>() <span class="op">-</span> BUDGET) <span class="op">&gt;</span> <span class="fl">1e-4</span>:</span>
<span id="cb7-130"><a href="#cb7-130" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;Budget nicht exakt investiert: </span><span class="sc">{</span>x<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-131"><a href="#cb7-131" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> y.<span class="bu">sum</span>() <span class="op">&gt;</span> MAX_POSITIONEN <span class="op">+</span> toleranz:</span>
<span id="cb7-132"><a href="#cb7-132" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>y<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:.0f}</span><span class="ss"> Positionen statt hoechstens &quot;</span></span>
<span id="cb7-133"><a href="#cb7-133" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>MAX_POSITIONEN<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-134"><a href="#cb7-134" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i, name <span class="kw">in</span> <span class="bu">enumerate</span>(ANLAGEN):</span>
<span id="cb7-135"><a href="#cb7-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(y[i] <span class="op">-</span> <span class="bu">round</span>(y[i])) <span class="op">&gt;</span> toleranz:</span>
<span id="cb7-136"><a href="#cb7-136" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: y = </span><span class="sc">{</span>y[i]<span class="sc">!r}</span><span class="ss"> ist nicht ganzzahlig&quot;</span>)</span>
<span id="cb7-137"><a href="#cb7-137" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> y[i] <span class="kw">and</span> <span class="kw">not</span> (MIN_POSITION <span class="op">-</span> toleranz <span class="op">&lt;=</span> x[i]</span>
<span id="cb7-138"><a href="#cb7-138" aria-hidden="true" tabindex="-1"></a> <span class="op">&lt;=</span> MAX_POSITION <span class="op">+</span> toleranz):</span>
<span id="cb7-139"><a href="#cb7-139" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: </span><span class="sc">{</span>x[i]<span class="sc">:,.2f}</span><span class="ss"> EUR verletzt die &quot;</span></span>
<span id="cb7-140"><a href="#cb7-140" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Groessengrenzen&quot;</span>)</span>
<span id="cb7-141"><a href="#cb7-141" aria-hidden="true" tabindex="-1"></a> <span class="cf">elif</span> <span class="kw">not</span> y[i] <span class="kw">and</span> x[i] <span class="op">&gt;</span> toleranz:</span>
<span id="cb7-142"><a href="#cb7-142" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(<span class="ss">f&quot;</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">: inaktiv, aber </span><span class="sc">{</span>x[i]<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb7-143"><a href="#cb7-143" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;investiert&quot;</span>)</span>
<span id="cb7-144"><a href="#cb7-144" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> beanstandungen</span>
<span id="cb7-145"><a href="#cb7-145" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-146"><a href="#cb7-146" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-147"><a href="#cb7-147" 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-148"><a href="#cb7-148" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> baue_und_loese()</span>
<span id="cb7-149"><a href="#cb7-149" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-150"><a href="#cb7-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-151"><a href="#cb7-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; OPTIMALE MILP-PORTFOLIO-ALLOKATION MIT FIXGEBUEHREN&quot;</span>)</span>
<span id="cb7-152"><a href="#cb7-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-153"><a href="#cb7-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Budget: </span><span class="sc">{</span>BUDGET<span class="sc">:,.0f}</span><span class="ss"> EUR | max. </span><span class="sc">{</span>MAX_POSITIONEN<span class="sc">}</span><span class="ss"> Positionen | &quot;</span></span>
<span id="cb7-154"><a href="#cb7-154" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;je </span><span class="sc">{</span>MIN_POSITION<span class="sc">:,.0f}</span><span class="ss">-</span><span class="sc">{</span>MAX_POSITION<span class="sc">:,.0f}</span><span class="ss"> EUR | Gebuehr </span><span class="sc">{</span>GEBUEHR<span class="sc">:.0f}</span><span class="ss"> EUR</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb7-155"><a href="#cb7-155" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-156"><a href="#cb7-156" aria-hidden="true" tabindex="-1"></a> <span class="co"># Zuerst der Status - erst danach interessieren die Zahlen.</span></span>
<span id="cb7-157"><a href="#cb7-157" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.status.modellfehler:</span>
<span id="cb7-158"><a href="#cb7-158" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Das Modell ist nicht loesbar (</span><span class="sc">{</span>loesung<span class="sc">.</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">). &quot;</span></span>
<span id="cb7-159"><a href="#cb7-159" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Naechster Schritt: Anhang Fehlerdiagnose.&quot;</span>)</span>
<span id="cb7-160"><a href="#cb7-160" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> loesung.status.brauchbar:</span>
<span id="cb7-161"><a href="#cb7-161" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">SystemExit</span>(<span class="ss">f&quot;Keine Loesung erhalten (</span><span class="sc">{</span>loesung<span class="sc">.</span>status<span class="sc">.</span>value<span class="sc">}</span><span class="ss">). &quot;</span></span>
<span id="cb7-162"><a href="#cb7-162" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zeitlimit erhoehen oder Modell vereinfachen.&quot;</span>)</span>
<span id="cb7-163"><a href="#cb7-163" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> loesung.status <span class="kw">is</span> SolverStatus.ZULAESSIG:</span>
<span id="cb7-164"><a href="#cb7-164" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;ACHTUNG: nicht beweisbar optimal - Gap </span><span class="sc">{</span>loesung<span class="sc">.</span>gap<span class="sc">:.2%}</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb7-165"><a href="#cb7-165" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-166"><a href="#cb7-166" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.array([loesung.werte[name] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN])</span>
<span id="cb7-167"><a href="#cb7-167" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.array([loesung.werte[<span class="ss">f&quot;aktiv:</span><span class="sc">{</span>name<span class="sc">}</span><span class="ss">&quot;</span>] <span class="cf">for</span> name <span class="kw">in</span> ANLAGEN], dtype<span class="op">=</span><span class="bu">int</span>)</span>
<span id="cb7-168"><a href="#cb7-168" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-169"><a href="#cb7-169" aria-hidden="true" tabindex="-1"></a> tabelle <span class="op">=</span> pd.DataFrame({</span>
<span id="cb7-170"><a href="#cb7-170" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Anlage&quot;</span>: ANLAGEN,</span>
<span id="cb7-171"><a href="#cb7-171" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Aktiv&quot;</span>: [<span class="st">&quot;JA&quot;</span> <span class="cf">if</span> y[i] <span class="cf">else</span> <span class="st">&quot;-&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-172"><a href="#cb7-172" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Investition (EUR)&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="sc">:,.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-173"><a href="#cb7-173" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Anteil&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="op">/</span>BUDGET<span class="op">*</span><span class="dv">100</span><span class="sc">:5.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-174"><a href="#cb7-174" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Erw. Rendite&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>RENDITE[i]<span class="op">*</span><span class="dv">100</span><span class="sc">:4.1f}</span><span class="ss"> %&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-175"><a href="#cb7-175" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Erw. Ertrag (EUR)&quot;</span>: [<span class="ss">f&quot;</span><span class="sc">{</span>x[i]<span class="op">*</span>RENDITE[i]<span class="sc">:,.0f}</span><span class="ss">&quot;</span> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N)],</span>
<span id="cb7-176"><a href="#cb7-176" aria-hidden="true" tabindex="-1"></a> })</span>
<span id="cb7-177"><a href="#cb7-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(tabelle.to_string(index<span class="op">=</span><span class="va">False</span>))</span>
<span id="cb7-178"><a href="#cb7-178" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-179"><a href="#cb7-179" aria-hidden="true" tabindex="-1"></a> brutto <span class="op">=</span> <span class="bu">float</span>(RENDITE <span class="op">@</span> x)</span>
<span id="cb7-180"><a href="#cb7-180" aria-hidden="true" tabindex="-1"></a> gebuehren <span class="op">=</span> <span class="bu">float</span>(GEBUEHR <span class="op">*</span> y.<span class="bu">sum</span>())</span>
<span id="cb7-181"><a href="#cb7-181" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-182"><a href="#cb7-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Erwarteter Bruttoertrag: </span><span class="sc">{</span>brutto<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-183"><a href="#cb7-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Ordergebuehren: </span><span class="sc">{</span><span class="op">-</span>gebuehren<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR (</span><span class="sc">{</span>y<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">}</span><span class="ss"> Positionen)&quot;</span>)</span>
<span id="cb7-184"><a href="#cb7-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Netto-Erwartungswert: </span><span class="sc">{</span>loesung<span class="sc">.</span>zielwert<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-185"><a href="#cb7-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>loesung<span class="sc">.</span>als_bericht()<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb7-186"><a href="#cb7-186" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-187"><a href="#cb7-187" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Vergleich mit dem unbeschraenkten Fall ---------------------------</span></span>
<span id="cb7-188"><a href="#cb7-188" aria-hidden="true" tabindex="-1"></a> z_frei, x_frei <span class="op">=</span> ohne_restriktionen()</span>
<span id="cb7-189"><a href="#cb7-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-190"><a href="#cb7-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Zum Vergleich ohne jede Restriktion (alles in den Bestwert): &quot;</span></span>
<span id="cb7-191"><a href="#cb7-191" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>z_frei<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb7-192"><a href="#cb7-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Kosten der Realitaet (Gebuehren, Streuung, Mindestgroessen): &quot;</span></span>
<span id="cb7-193"><a href="#cb7-193" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>z_frei <span class="op">-</span> loesung<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb7-194"><a href="#cb7-194" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>(<span class="dv">1</span> <span class="op">-</span> loesung.zielwert<span class="op">/</span>z_frei)<span class="op">*</span><span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb7-195"><a href="#cb7-195" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb7-196"><a href="#cb7-196" aria-hidden="true" tabindex="-1"></a> <span class="co"># --- Alle Nebenbedingungen nachpruefen --------------------------------</span></span>
<span id="cb7-197"><a href="#cb7-197" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> pruefe_portfolio(loesung)</span>
<span id="cb7-198"><a href="#cb7-198" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;-&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span>
<span id="cb7-199"><a href="#cb7-199" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> beanstandungen:</span>
<span id="cb7-200"><a href="#cb7-200" 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="cb7-201"><a href="#cb7-201" 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="cb7-202"><a href="#cb7-202" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.&quot;</span>)</span>
<span id="cb7-203"><a href="#cb7-203" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">88</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>========================================================================================
OPTIMALE MILP-PORTFOLIO-ALLOKATION MIT FIXGEBUEHREN
========================================================================================
Budget: 100,000 EUR | max. 3 Positionen | je 10,000-40,000 EUR | Gebuehr 50 EUR
Anlage Aktiv Investition (EUR) Anteil Erw. Rendite Erw. Ertrag (EUR)
US-Aktien JA 40,000 40.0 % 11.0 % 4,400
EU-Aktien JA 20,000 20.0 % 8.0 % 1,600
Emerging-Markets JA 40,000 40.0 % 13.0 % 5,200
Staatsanleihen - 0 0.0 % 3.0 % 0
Unternehmensanleihen - 0 0.0 % 5.0 % 0
Rohstoffe - 0 0.0 % 7.0 % 0
----------------------------------------------------------------------------------------
Erwarteter Bruttoertrag: 11,200.00 EUR
Ordergebuehren: -150.00 EUR (3 Positionen)
Netto-Erwartungswert: 11,050.00 EUR
Status: optimal | Zielwert: 11,050.00 | Gap: 0.00% | Zeit: 0.01s
----------------------------------------------------------------------------------------
Zum Vergleich ohne jede Restriktion (alles in den Bestwert): 13,000.00 EUR
Kosten der Realitaet (Gebuehren, Streuung, Mindestgroessen): 1,950.00 EUR (15.00 %)
----------------------------------------------------------------------------------------
Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten.
========================================================================================</code></pre>
<blockquote>
<p><strong>💻 Code-Durchgang</strong></p>
<table>
<colgroup>
<col style="width: 50%" />
<col style="width: 50%" />
</colgroup>
<thead>
<tr class="header">
<th>Stelle</th>
<th>Was passiert</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>c = concat([-RENDITE, full(N, GEBUEHR)])</code></td>
<td>Zielfunktion über <strong>beide</strong> Variablenblöcke: Erträge negativ (weil <code>linprog</code> minimiert), Gebühren positiv (sie sollen ja gedrückt werden)</td>
</tr>
<tr class="even">
<td><code>A_eq[0, :N] = 1.0</code></td>
<td>Budgetgleichung betrifft nur die <span class="math inline">x</span>-Variablen</td>
</tr>
<tr class="odd">
<td><code>zeile[i]=1; zeile[N+i]=-MAX_POSITION</code></td>
<td>Muster 1: <span class="math inline">x_i - U y_i \le 0</span>. <strong><span class="math inline">M = U = 40\,000</span></strong> — die kleinstmögliche gültige Wahl</td>
</tr>
<tr class="even">
<td><code>zeile[i]=-1; zeile[N+i]=MIN_POSITION</code></td>
<td>Muster 5 unten: <span class="math inline">L y_i - x_i \le 0</span></td>
</tr>
<tr class="odd">
<td><code>integrality=concat([zeros(N), ones(N)])</code></td>
<td>nur die <span class="math inline">y</span> sind ganzzahlig — das ist das „mixed“ in MILP</td>
</tr>
<tr class="even">
<td><code>status = status_von_scipy(ergebnis)</code></td>
<td>übersetzt den SciPy-Rückgabewert in die gemeinsame Sprache aus <a href="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a> — die drei <code>if</code>-Zweige darunter behandeln Modellfehler, Abbruch ohne Lösung und „zulässig, aber unbewiesen“ getrennt</td>
</tr>
<tr class="odd">
<td><code>schranke=-ergebnis.mip_dual_bound</code></td>
<td>die zweite Zahl aus <a href="#sec:milp-gap">Abschnitt 6.8</a>. Erst mit ihr kann das <code>Loesung</code>-Objekt den Gap ausrechnen — und der Bericht sagt <code>Gap: 0.00%</code>, also <em>beweisbar</em> optimal</td>
</tr>
<tr class="even">
<td><code>pruefe_portfolio(...)</code></td>
<td>prüft <strong>jede</strong> modellierte Regel einzeln nach und liefert eine Liste von Beanstandungen</td>
</tr>
</tbody>
</table>
<p><strong>Warum das Ergebnis wirtschaftlich Sinn ergibt:</strong> Der Solver wählt die drei renditestärksten Anlagen (13 %, 11 %, 8 %) und füllt sie in dieser Reihenfolge bis zur Obergrenze von 40 000 €. Die drittbeste Position (EU-Aktien) erhält nur den Rest von 20 000 € — sie liegt über der Mindestordergröße von 10 000 €, ist also zulässig. Ohne Kardinalitätsgrenze läge alles im Bestwert (13 %); die Restriktionen kosten 15 % des theoretischen Ertrags. Genau diese Zahl braucht man, wenn man mit dem Risikomanagement über die Sinnhaftigkeit einer Regel diskutiert: „Die Obergrenze von 40 % je Position kostet uns 1 950 € pro Jahr — ist uns die Diversifikation das wert?“ Das ist eine beantwortbare Frage; „wir sollten breiter streuen“ ist es nicht.</p>
</blockquote>
<hr />
<h2 id="sec:milp-gap">6.8 Wenn der Solver nicht fertig wird: Gap, Zeitlimit und Warm-Start</h2>
<p>Bei einem LP gibt es zwei Ausgänge: eine optimale Lösung oder eine klare Absage. Bei einem MILP gibt es einen dritten, und im Betrieb ist er der häufigste:</p>
<blockquote>
<p><em>„Ich habe eine Lösung. Ob sie die beste ist, weiß ich nicht. Die Zeit ist um.“</em></p>
</blockquote>
<p>Dieser Abschnitt handelt davon, wie man damit professionell umgeht — statt so zu tun, als käme immer <code>OPTIMAL</code> zurück.</p>
<h3 id="die-zwei-zahlen-die-zählen">Die zwei Zahlen, die zählen</h3>
<p>Branch-and-Bound führt zu jedem Zeitpunkt zwei Werte mit:</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Begriff</th>
<th>Was er bedeutet</th>
<th>Wie er sich entwickelt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><strong>Incumbent</strong> (beste gefundene Lösung)</td>
<td>ein Plan, den man tatsächlich ausführen könnte</td>
<td>wird im Lauf der Suche immer besser</td>
</tr>
<tr class="even">
<td><strong>Schranke</strong> (<em>dual bound</em>)</td>
<td>der beste Wert, den es überhaupt geben <em>könnte</em></td>
<td>wird im Lauf der Suche immer schlechter</td>
</tr>
</tbody>
</table>
<p>Beide laufen aufeinander zu. Ihr Abstand ist der <strong>MIP-Gap</strong>:</p>
<p><span class="math display">
\text{Gap} = \frac{\lvert\, z_{\text{incumbent}} - z_{\text{schranke}}\,\rvert}
{\lvert\, z_{\text{incumbent}}\,\rvert}
</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">z_{\text{incumbent}}</span></td>
<td>„Das ist der beste Plan, den ich bisher tatsächlich in der Hand habe.“</td>
</tr>
<tr class="even">
<td><span class="math inline">z_{\text{schranke}}</span></td>
<td>„Besser als das kann es rechnerisch nicht werden — bewiesen.“</td>
</tr>
<tr class="odd">
<td>Gap <span class="math inline">= 0</span></td>
<td>„Beide sind gleich: Der Plan ist beweisbar optimal.“</td>
</tr>
<tr class="even">
<td>Gap <span class="math inline">= 0{,}09</span></td>
<td>„Mein Plan ist höchstens 9 % schlechter als das theoretische Beste. <strong>Garantiert.</strong></td>
</tr>
</tbody>
</table>
<p>Der Gap ist damit keine Schätzung und keine Fehlerbalken-Angabe, sondern eine <strong>Zusage</strong>. Das macht ihn zu der Zahl, die man ins Managementgespräch mitnimmt: „Wir liegen höchstens 2 % vom Optimum entfernt“ ist eine belastbare Aussage. „Der Solver hat lange gerechnet“ ist keine.</p>
</blockquote>
</div>
<h3 id="alle-statusfälle-behandeln">Alle Statusfälle behandeln</h3>
<p>Der zweite Teil der Professionalität ist unspektakulär, aber entscheidend: <strong>jeden</strong> Rückgabewert auswerten, nicht nur <code>OPTIMAL</code>.</p>
<table>
<colgroup>
<col style="width: 33%" />
<col style="width: 33%" />
<col style="width: 33%" />
</colgroup>
<thead>
<tr class="header">
<th>Status</th>
<th>Bedeutung</th>
<th>Was Sie tun</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>Optimal</code></td>
<td>beweisbar bestmöglich</td>
<td>ausführen</td>
</tr>
<tr class="even">
<td><code>Time limit reached</code> (mit Lösung)</td>
<td>zulässig, nicht bewiesen optimal</td>
<td>Gap prüfen, dann entscheiden</td>
</tr>
<tr class="odd">
<td><code>Time limit reached</code> (ohne Lösung)</td>
<td>nichts gefunden</td>
<td>Modell vereinfachen, Heuristik als Startwert, Limit erhöhen</td>
</tr>
<tr class="even">
<td><code>Infeasible</code></td>
<td>kein zulässiger Plan existiert</td>
<td>harte Bedingungen prüfen (<a href="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>, Anhang C)</td>
</tr>
<tr class="odd">
<td><code>Unbounded</code></td>
<td>Ziel wächst unbegrenzt</td>
<td>fehlende Schranke — fast immer ein Modellierungsfehler</td>
</tr>
</tbody>
</table>
<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"># Solverstatus_und_Gap.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 MILP: Was tun, wenn der Solver nicht fertig wird?</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">Bei einem LP kommt entweder eine optimale Loesung oder eine klare Absage.</span></span>
<span id="cb9-8"><a href="#cb9-8" aria-hidden="true" tabindex="-1"></a><span class="co">Bei einem MILP ist der haeufigste Ausgang im Betrieb ein dritter: &quot;Ich habe</span></span>
<span id="cb9-9"><a href="#cb9-9" aria-hidden="true" tabindex="-1"></a><span class="co">eine Loesung, ich weiss aber nicht, ob sie die beste ist - und die Zeit ist</span></span>
<span id="cb9-10"><a href="#cb9-10" aria-hidden="true" tabindex="-1"></a><span class="co">um.&quot; Dieses Programm zeigt, wie man mit diesem Fall umgeht.</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"> 1. Alle Statusfaelle explizit behandeln, statt OPTIMAL vorauszusetzen.</span></span>
<span id="cb9-13"><a href="#cb9-13" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. Den MIP-Gap lesen: Wie weit kann ich hoechstens danebenliegen?</span></span>
<span id="cb9-14"><a href="#cb9-14" aria-hidden="true" tabindex="-1"></a><span class="co"> 3. Messen, was zusaetzliche Rechenzeit ueberhaupt noch bringt.</span></span>
<span id="cb9-15"><a href="#cb9-15" aria-hidden="true" tabindex="-1"></a><span class="co"> 4. Warm-Start ausprobieren - und ehrlich messen, ob er etwas bringt.</span></span>
<span id="cb9-16"><a href="#cb9-16" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-17"><a href="#cb9-17" aria-hidden="true" tabindex="-1"></a><span class="co">Beispiel: Standortplanung, 45 moegliche Lager, 120 Kunden (5445 Variablen,</span></span>
<span id="cb9-18"><a href="#cb9-18" aria-hidden="true" tabindex="-1"></a><span class="co">davon 45 binaer).</span></span>
<span id="cb9-19"><a href="#cb9-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-20"><a href="#cb9-20" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, highspy</span></span>
<span id="cb9-21"><a href="#cb9-21" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb9-22"><a href="#cb9-22" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-23"><a href="#cb9-23" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</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="im">import</span> time</span>
<span id="cb9-26"><a href="#cb9-26" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> dataclasses <span class="im">import</span> dataclass</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">import</span> numpy <span class="im">as</span> np</span>
<span id="cb9-29"><a href="#cb9-29" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> highspy</span>
<span id="cb9-30"><a href="#cb9-30" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-31"><a href="#cb9-31" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">7</span>)</span>
<span id="cb9-32"><a href="#cb9-32" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-33"><a href="#cb9-33" aria-hidden="true" tabindex="-1"></a>N_LAGER, N_KUNDE <span class="op">=</span> <span class="dv">45</span>, <span class="dv">120</span></span>
<span id="cb9-34"><a href="#cb9-34" aria-hidden="true" tabindex="-1"></a>FIXKOSTEN <span class="op">=</span> RNG.uniform(<span class="dv">3000</span>, <span class="dv">9000</span>, N_LAGER)</span>
<span id="cb9-35"><a href="#cb9-35" aria-hidden="true" tabindex="-1"></a>TRANSPORT <span class="op">=</span> RNG.uniform(<span class="dv">5</span>, <span class="dv">60</span>, (N_LAGER, N_KUNDE))</span>
<span id="cb9-36"><a href="#cb9-36" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> RNG.uniform(<span class="dv">10</span>, <span class="dv">60</span>, N_KUNDE)</span>
<span id="cb9-37"><a href="#cb9-37" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> np.full(N_LAGER, BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">0.22</span>)</span>
<span id="cb9-38"><a href="#cb9-38" aria-hidden="true" tabindex="-1"></a></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 class="at">@dataclass</span></span>
<span id="cb9-41"><a href="#cb9-41" aria-hidden="true" tabindex="-1"></a><span class="kw">class</span> Ergebnis:</span>
<span id="cb9-42"><a href="#cb9-42" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Alles, was nach einem Solverlauf ausgewertet werden muss - nicht nur</span></span>
<span id="cb9-43"><a href="#cb9-43" aria-hidden="true" tabindex="-1"></a><span class="co"> der Zielwert.&quot;&quot;&quot;</span></span>
<span id="cb9-44"><a href="#cb9-44" aria-hidden="true" tabindex="-1"></a> status: <span class="bu">str</span></span>
<span id="cb9-45"><a href="#cb9-45" aria-hidden="true" tabindex="-1"></a> brauchbar: <span class="bu">bool</span> <span class="co"># Gibt es ueberhaupt eine zulaessige Loesung?</span></span>
<span id="cb9-46"><a href="#cb9-46" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal: <span class="bu">bool</span></span>
<span id="cb9-47"><a href="#cb9-47" aria-hidden="true" tabindex="-1"></a> zielwert: <span class="bu">float</span> <span class="co"># bester gefundener Wert (Incumbent)</span></span>
<span id="cb9-48"><a href="#cb9-48" aria-hidden="true" tabindex="-1"></a> schranke: <span class="bu">float</span> <span class="co"># beste bewiesene Schranke (Dual Bound)</span></span>
<span id="cb9-49"><a href="#cb9-49" aria-hidden="true" tabindex="-1"></a> gap: <span class="bu">float</span> <span class="co"># relativer Abstand zwischen beiden</span></span>
<span id="cb9-50"><a href="#cb9-50" aria-hidden="true" tabindex="-1"></a> knoten: <span class="bu">int</span></span>
<span id="cb9-51"><a href="#cb9-51" aria-hidden="true" tabindex="-1"></a> dauer: <span class="bu">float</span></span>
<span id="cb9-52"><a href="#cb9-52" aria-hidden="true" tabindex="-1"></a> loesung: np.ndarray</span>
<span id="cb9-53"><a href="#cb9-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-54"><a href="#cb9-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-55"><a href="#cb9-55" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(zeitlimit: <span class="bu">float</span>, startloesung: np.ndarray <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> Ergebnis:</span>
<span id="cb9-56"><a href="#cb9-56" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Loest das Standortmodell mit Zeitlimit und wertet ALLE Statusfaelle aus.&quot;&quot;&quot;</span></span>
<span id="cb9-57"><a href="#cb9-57" aria-hidden="true" tabindex="-1"></a> hochschule <span class="op">=</span> highspy.Highs()</span>
<span id="cb9-58"><a href="#cb9-58" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb9-59"><a href="#cb9-59" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, zeitlimit)</span>
<span id="cb9-60"><a href="#cb9-60" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-61"><a href="#cb9-61" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> N_LAGER <span class="op">*</span> N_KUNDE</span>
<span id="cb9-62"><a href="#cb9-62" aria-hidden="true" tabindex="-1"></a> unendlich <span class="op">=</span> highspy.kHighsInf</span>
<span id="cb9-63"><a href="#cb9-63" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-64"><a href="#cb9-64" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich))</span>
<span id="cb9-65"><a href="#cb9-65" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(N_LAGER, np.zeros(N_LAGER), np.ones(N_LAGER))</span>
<span id="cb9-66"><a href="#cb9-66" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb9-67"><a href="#cb9-67" aria-hidden="true" tabindex="-1"></a> hochschule.changeColIntegrality(anzahl_x <span class="op">+</span> i, highspy.HighsVarType.kInteger)</span>
<span id="cb9-68"><a href="#cb9-68" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(anzahl_x <span class="op">+</span> i, FIXKOSTEN[i])</span>
<span id="cb9-69"><a href="#cb9-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb9-70"><a href="#cb9-70" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(i <span class="op">*</span> N_KUNDE <span class="op">+</span> j, TRANSPORT[i, j])</span>
<span id="cb9-71"><a href="#cb9-71" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-72"><a href="#cb9-72" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb9-73"><a href="#cb9-73" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER)], dtype<span class="op">=</span>np.int32)</span>
<span id="cb9-74"><a href="#cb9-74" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(BEDARF[j], BEDARF[j], <span class="bu">len</span>(index), index, np.ones(<span class="bu">len</span>(index)))</span>
<span id="cb9-75"><a href="#cb9-75" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-76"><a href="#cb9-76" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb9-77"><a href="#cb9-77" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE)] <span class="op">+</span> [anzahl_x <span class="op">+</span> i],</span>
<span id="cb9-78"><a href="#cb9-78" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32)</span>
<span id="cb9-79"><a href="#cb9-79" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.ones(N_KUNDE), [<span class="op">-</span>KAPAZITAET[i]]])</span>
<span id="cb9-80"><a href="#cb9-80" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(<span class="op">-</span>unendlich, <span class="fl">0.0</span>, <span class="bu">len</span>(index), index, werte)</span>
<span id="cb9-81"><a href="#cb9-81" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-82"><a href="#cb9-82" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> startloesung <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb9-83"><a href="#cb9-83" aria-hidden="true" tabindex="-1"></a> hochschule.setSolution(<span class="bu">len</span>(startloesung),</span>
<span id="cb9-84"><a href="#cb9-84" aria-hidden="true" tabindex="-1"></a> np.arange(<span class="bu">len</span>(startloesung), dtype<span class="op">=</span>np.int32),</span>
<span id="cb9-85"><a href="#cb9-85" aria-hidden="true" tabindex="-1"></a> startloesung)</span>
<span id="cb9-86"><a href="#cb9-86" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-87"><a href="#cb9-87" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb9-88"><a href="#cb9-88" aria-hidden="true" tabindex="-1"></a> hochschule.run()</span>
<span id="cb9-89"><a href="#cb9-89" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</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> status <span class="op">=</span> hochschule.modelStatusToString(hochschule.getModelStatus())</span>
<span id="cb9-92"><a href="#cb9-92" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> hochschule.getInfo()</span>
<span id="cb9-93"><a href="#cb9-93" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-94"><a href="#cb9-94" aria-hidden="true" tabindex="-1"></a> <span class="co"># Der Kern der Sache: Aus dem Status folgt, WAS man mit dem Ergebnis</span></span>
<span id="cb9-95"><a href="#cb9-95" aria-hidden="true" tabindex="-1"></a> <span class="co"># ueberhaupt anfangen darf.</span></span>
<span id="cb9-96"><a href="#cb9-96" aria-hidden="true" tabindex="-1"></a> brauchbar <span class="op">=</span> status <span class="kw">in</span> (<span class="st">&quot;Optimal&quot;</span>, <span class="st">&quot;Time limit reached&quot;</span>, <span class="st">&quot;Solution limit reached&quot;</span>)</span>
<span id="cb9-97"><a href="#cb9-97" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal <span class="op">=</span> status <span class="op">==</span> <span class="st">&quot;Optimal&quot;</span></span>
<span id="cb9-98"><a href="#cb9-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> status <span class="kw">in</span> (<span class="st">&quot;Infeasible&quot;</span>, <span class="st">&quot;Unbounded&quot;</span>, <span class="st">&quot;Primal infeasible or unbounded&quot;</span>):</span>
<span id="cb9-99"><a href="#cb9-99" aria-hidden="true" tabindex="-1"></a> brauchbar <span class="op">=</span> <span class="va">False</span></span>
<span id="cb9-100"><a href="#cb9-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-101"><a href="#cb9-101" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> Ergebnis(</span>
<span id="cb9-102"><a href="#cb9-102" aria-hidden="true" tabindex="-1"></a> status<span class="op">=</span>status,</span>
<span id="cb9-103"><a href="#cb9-103" aria-hidden="true" tabindex="-1"></a> brauchbar<span class="op">=</span>brauchbar <span class="kw">and</span> info.objective_function_value <span class="op">&lt;</span> unendlich,</span>
<span id="cb9-104"><a href="#cb9-104" aria-hidden="true" tabindex="-1"></a> beweisbar_optimal<span class="op">=</span>beweisbar_optimal,</span>
<span id="cb9-105"><a href="#cb9-105" aria-hidden="true" tabindex="-1"></a> zielwert<span class="op">=</span>info.objective_function_value,</span>
<span id="cb9-106"><a href="#cb9-106" aria-hidden="true" tabindex="-1"></a> schranke<span class="op">=</span>info.mip_dual_bound,</span>
<span id="cb9-107"><a href="#cb9-107" aria-hidden="true" tabindex="-1"></a> gap<span class="op">=</span>info.mip_gap,</span>
<span id="cb9-108"><a href="#cb9-108" aria-hidden="true" tabindex="-1"></a> knoten<span class="op">=</span>info.mip_node_count,</span>
<span id="cb9-109"><a href="#cb9-109" aria-hidden="true" tabindex="-1"></a> dauer<span class="op">=</span>dauer,</span>
<span id="cb9-110"><a href="#cb9-110" aria-hidden="true" tabindex="-1"></a> loesung<span class="op">=</span>np.array(hochschule.getSolution().col_value),</span>
<span id="cb9-111"><a href="#cb9-111" aria-hidden="true" tabindex="-1"></a> )</span>
<span id="cb9-112"><a href="#cb9-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-113"><a href="#cb9-113" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-114"><a href="#cb9-114" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> gieriger_startplan() <span class="op">-&gt;</span> <span class="bu">tuple</span>[np.ndarray, <span class="bu">float</span>]:</span>
<span id="cb9-115"><a href="#cb9-115" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Eine Faustregel-Loesung, wie sie ein Disponent von Hand erstellen wuerde:</span></span>
<span id="cb9-116"><a href="#cb9-116" aria-hidden="true" tabindex="-1"></a><span class="co"> die guenstigsten Lager oeffnen (Fixkosten je Kapazitaetseinheit), dann</span></span>
<span id="cb9-117"><a href="#cb9-117" aria-hidden="true" tabindex="-1"></a><span class="co"> jeden Kunden dem naechstgelegenen offenen Lager mit Restkapazitaet</span></span>
<span id="cb9-118"><a href="#cb9-118" aria-hidden="true" tabindex="-1"></a><span class="co"> zuordnen. Kein Solver noetig - und in Sekunden fertig.</span></span>
<span id="cb9-119"><a href="#cb9-119" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb9-120"><a href="#cb9-120" aria-hidden="true" tabindex="-1"></a> reihenfolge <span class="op">=</span> np.argsort(FIXKOSTEN <span class="op">/</span> KAPAZITAET)</span>
<span id="cb9-121"><a href="#cb9-121" aria-hidden="true" tabindex="-1"></a> offen: <span class="bu">list</span>[<span class="bu">int</span>] <span class="op">=</span> []</span>
<span id="cb9-122"><a href="#cb9-122" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> reihenfolge:</span>
<span id="cb9-123"><a href="#cb9-123" aria-hidden="true" tabindex="-1"></a> offen.append(<span class="bu">int</span>(i))</span>
<span id="cb9-124"><a href="#cb9-124" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> KAPAZITAET[offen].<span class="bu">sum</span>() <span class="op">&gt;=</span> BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">1.05</span>:</span>
<span id="cb9-125"><a href="#cb9-125" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb9-126"><a href="#cb9-126" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-127"><a href="#cb9-127" aria-hidden="true" tabindex="-1"></a> rest <span class="op">=</span> KAPAZITAET.copy()</span>
<span id="cb9-128"><a href="#cb9-128" aria-hidden="true" tabindex="-1"></a> x <span class="op">=</span> np.zeros((N_LAGER, N_KUNDE))</span>
<span id="cb9-129"><a href="#cb9-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> np.argsort(<span class="op">-</span>BEDARF): <span class="co"># groesste Kunden zuerst</span></span>
<span id="cb9-130"><a href="#cb9-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">sorted</span>(offen, key<span class="op">=</span><span class="kw">lambda</span> i: TRANSPORT[i, j]):</span>
<span id="cb9-131"><a href="#cb9-131" aria-hidden="true" tabindex="-1"></a> menge <span class="op">=</span> <span class="bu">min</span>(rest[i], BEDARF[j] <span class="op">-</span> x[:, j].<span class="bu">sum</span>())</span>
<span id="cb9-132"><a href="#cb9-132" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> menge <span class="op">&gt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb9-133"><a href="#cb9-133" aria-hidden="true" tabindex="-1"></a> x[i, j] <span class="op">+=</span> menge</span>
<span id="cb9-134"><a href="#cb9-134" aria-hidden="true" tabindex="-1"></a> rest[i] <span class="op">-=</span> menge</span>
<span id="cb9-135"><a href="#cb9-135" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(x[:, j].<span class="bu">sum</span>() <span class="op">-</span> BEDARF[j]) <span class="op">&lt;</span> <span class="fl">1e-9</span>:</span>
<span id="cb9-136"><a href="#cb9-136" aria-hidden="true" tabindex="-1"></a> <span class="cf">break</span></span>
<span id="cb9-137"><a href="#cb9-137" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-138"><a href="#cb9-138" aria-hidden="true" tabindex="-1"></a> y <span class="op">=</span> np.zeros(N_LAGER)</span>
<span id="cb9-139"><a href="#cb9-139" aria-hidden="true" tabindex="-1"></a> y[offen] <span class="op">=</span> <span class="fl">1.0</span></span>
<span id="cb9-140"><a href="#cb9-140" aria-hidden="true" tabindex="-1"></a> kosten <span class="op">=</span> <span class="bu">float</span>((FIXKOSTEN <span class="op">*</span> y).<span class="bu">sum</span>() <span class="op">+</span> (TRANSPORT <span class="op">*</span> x).<span class="bu">sum</span>())</span>
<span id="cb9-141"><a href="#cb9-141" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> np.concatenate([x.ravel(), y]), kosten</span>
<span id="cb9-142"><a href="#cb9-142" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-143"><a href="#cb9-143" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-144"><a href="#cb9-144" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, e: Ergebnis) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb9-145"><a href="#cb9-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-146"><a href="#cb9-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Status </span><span class="sc">{</span>e<span class="sc">.</span>status<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-147"><a href="#cb9-147" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="kw">not</span> e.brauchbar:</span>
<span id="cb9-148"><a href="#cb9-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; KEINE verwertbare Loesung. Nicht weiterrechnen!&quot;</span>)</span>
<span id="cb9-149"><a href="#cb9-149" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span></span>
<span id="cb9-150"><a href="#cb9-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; bester Plan (Incumbent) </span><span class="sc">{</span>e<span class="sc">.</span>zielwert<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-151"><a href="#cb9-151" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; bewiesene Schranke </span><span class="sc">{</span>e<span class="sc">.</span>schranke<span class="sc">:&gt;12,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-152"><a href="#cb9-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; MIP-Gap </span><span class="sc">{</span>e<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:&gt;12.3f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb9-153"><a href="#cb9-153" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Knoten / Zeit </span><span class="sc">{</span>e<span class="sc">.</span>knoten<span class="sc">:&gt;12,}</span><span class="ss"> / </span><span class="sc">{</span>e<span class="sc">.</span>dauer<span class="sc">:.2f}</span><span class="ss"> s&quot;</span>)</span>
<span id="cb9-154"><a href="#cb9-154" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> e.beweisbar_optimal:</span>
<span id="cb9-155"><a href="#cb9-155" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; -&gt; beweisbar optimal&quot;</span>)</span>
<span id="cb9-156"><a href="#cb9-156" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb9-157"><a href="#cb9-157" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; -&gt; zulaessig, aber nicht bewiesen optimal. Der wahre Bestwert&quot;</span>)</span>
<span id="cb9-158"><a href="#cb9-158" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; liegt zwischen </span><span class="sc">{</span>e<span class="sc">.</span>schranke<span class="sc">:,.2f}</span><span class="ss"> und </span><span class="sc">{</span>e<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR.&quot;</span>)</span>
<span id="cb9-159"><a href="#cb9-159" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-160"><a href="#cb9-160" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-161"><a href="#cb9-161" 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-162"><a href="#cb9-162" 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="cb9-163"><a href="#cb9-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; MIP-GAP UND ZEITLIMIT: STANDORTPLANUNG, 45 LAGER, 120 KUNDEN&quot;</span>)</span>
<span id="cb9-164"><a href="#cb9-164" 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="cb9-165"><a href="#cb9-165" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>N_LAGER <span class="op">*</span> N_KUNDE <span class="op">+</span> N_LAGER<span class="sc">:,}</span><span class="ss"> Variablen, davon </span><span class="sc">{</span>N_LAGER<span class="sc">}</span><span class="ss"> binaer.&quot;</span>)</span>
<span id="cb9-166"><a href="#cb9-166" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-167"><a href="#cb9-167" aria-hidden="true" tabindex="-1"></a> kurz <span class="op">=</span> loese(<span class="fl">2.0</span>)</span>
<span id="cb9-168"><a href="#cb9-168" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] Zeitlimit 2 Sekunden&quot;</span>, kurz)</span>
<span id="cb9-169"><a href="#cb9-169" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-170"><a href="#cb9-170" aria-hidden="true" tabindex="-1"></a> lang <span class="op">=</span> loese(<span class="fl">60.0</span>)</span>
<span id="cb9-171"><a href="#cb9-171" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] Zeitlimit 60 Sekunden&quot;</span>, lang)</span>
<span id="cb9-172"><a href="#cb9-172" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-173"><a href="#cb9-173" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb9-174"><a href="#cb9-174" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS BRINGT MEHR RECHENZEIT?&quot;</span>)</span>
<span id="cb9-175"><a href="#cb9-175" 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="cb9-176"><a href="#cb9-176" aria-hidden="true" tabindex="-1"></a> verbesserung <span class="op">=</span> kurz.zielwert <span class="op">-</span> lang.zielwert</span>
<span id="cb9-177"><a href="#cb9-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Nach 2 Sekunden: </span><span class="sc">{</span>kurz<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR, Gap </span><span class="sc">{</span>kurz<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %&quot;</span>)</span>
<span id="cb9-178"><a href="#cb9-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Nach </span><span class="sc">{</span>lang<span class="sc">.</span>dauer<span class="sc">:.1f}</span><span class="ss"> Sekunden: </span><span class="sc">{</span>lang<span class="sc">.</span>zielwert<span class="sc">:,.2f}</span><span class="ss"> EUR, bewiesen optimal&quot;</span>)</span>
<span id="cb9-179"><a href="#cb9-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Gewinn durch </span><span class="sc">{</span>lang<span class="sc">.</span>dauer <span class="op">-</span> kurz<span class="sc">.</span>dauer<span class="sc">:.1f}</span><span class="ss"> Sekunden mehr Rechenzeit: &quot;</span></span>
<span id="cb9-180"><a href="#cb9-180" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>verbesserung<span class="sc">:,.2f}</span><span class="ss"> EUR &quot;</span></span>
<span id="cb9-181"><a href="#cb9-181" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;(</span><span class="sc">{</span>verbesserung <span class="op">/</span> kurz<span class="sc">.</span>zielwert <span class="op">*</span> <span class="dv">100</span><span class="sc">:.2f}</span><span class="ss"> %)&quot;</span>)</span>
<span id="cb9-182"><a href="#cb9-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-183"><a href="#cb9-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Das ist die Frage, die im Betrieb wirklich zaehlt: Der Gap von&quot;</span>)</span>
<span id="cb9-184"><a href="#cb9-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>kurz<span class="sc">.</span>gap <span class="op">*</span> <span class="dv">100</span><span class="sc">:.1f}</span><span class="ss"> % nach 2 Sekunden ist eine GARANTIE - schlechter als&quot;</span>)</span>
<span id="cb9-185"><a href="#cb9-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;dieser Wert kann die Loesung nicht sein. Ob sich die restliche&quot;</span>)</span>
<span id="cb9-186"><a href="#cb9-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Rechenzeit lohnt, entscheidet nicht der Solver, sondern die Anwendung:&quot;</span>)</span>
<span id="cb9-187"><a href="#cb9-187" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ein naechtlicher Tourenplan darf eine Stunde rechnen, eine Umplanung&quot;</span>)</span>
<span id="cb9-188"><a href="#cb9-188" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;bei Maschinenausfall hat 30 Sekunden.&quot;</span>)</span>
<span id="cb9-189"><a href="#cb9-189" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-190"><a href="#cb9-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb9-191"><a href="#cb9-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; BRINGT EIN WARM-START ETWAS?&quot;</span>)</span>
<span id="cb9-192"><a href="#cb9-192" 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="cb9-193"><a href="#cb9-193" aria-hidden="true" tabindex="-1"></a> start, start_kosten <span class="op">=</span> gieriger_startplan()</span>
<span id="cb9-194"><a href="#cb9-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Faustregel-Startplan (ohne Solver): </span><span class="sc">{</span>start_kosten<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb9-195"><a href="#cb9-195" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Das sind </span><span class="sc">{</span>(start_kosten <span class="op">/</span> lang.zielwert <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"> % ueber dem Optimum.</span><span class="ch">\n</span><span class="ss">&quot;</span>)</span>
<span id="cb9-196"><a href="#cb9-196" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb9-197"><a href="#cb9-197" aria-hidden="true" tabindex="-1"></a> warm <span class="op">=</span> loese(<span class="fl">60.0</span>, startloesung<span class="op">=</span>start)</span>
<span id="cb9-198"><a href="#cb9-198" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:26}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zeit&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Knoten&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Ziel&#39;</span><span class="sc">:&gt;13}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-199"><a href="#cb9-199" 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="cb9-200"><a href="#cb9-200" 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;ohne Warm-Start&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>lang<span class="sc">.</span>dauer<span class="sc">:&gt;8.2f}</span><span class="ss">s </span><span class="sc">{</span>lang<span class="sc">.</span>knoten<span class="sc">:&gt;9,}</span><span class="ss"> &quot;</span></span>
<span id="cb9-201"><a href="#cb9-201" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>lang<span class="sc">.</span>zielwert<span class="sc">:&gt;13,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-202"><a href="#cb9-202" 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;mit Warm-Start&#39;</span><span class="sc">:&lt;26}</span><span class="ss"> </span><span class="sc">{</span>warm<span class="sc">.</span>dauer<span class="sc">:&gt;8.2f}</span><span class="ss">s </span><span class="sc">{</span>warm<span class="sc">.</span>knoten<span class="sc">:&gt;9,}</span><span class="ss"> &quot;</span></span>
<span id="cb9-203"><a href="#cb9-203" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>warm<span class="sc">.</span>zielwert<span class="sc">:&gt;13,.2f}</span><span class="ss">&quot;</span>)</span>
<span id="cb9-204"><a href="#cb9-204" 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="cb9-205"><a href="#cb9-205" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Ergebnis: praktisch kein Unterschied. Der Grund ist nicht, dass&quot;</span>)</span>
<span id="cb9-206"><a href="#cb9-206" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Warm-Starts nichts taugen - sondern dass HiGHS&#39; eigene Heuristiken&quot;</span>)</span>
<span id="cb9-207"><a href="#cb9-207" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;innerhalb der ersten Sekunde bereits eine BESSERE Loesung finden als&quot;</span>)</span>
<span id="cb9-208"><a href="#cb9-208" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;unsere Faustregel. Ein Startwert hilft nur, wenn er besser ist als&quot;</span>)</span>
<span id="cb9-209"><a href="#cb9-209" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;das, was der Solver von allein in derselben Zeit findet.&quot;</span>)</span>
<span id="cb9-210"><a href="#cb9-210" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb9-211"><a href="#cb9-211" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Warm-Starts lohnen sich damit vor allem in zwei Faellen:&quot;</span>)</span>
<span id="cb9-212"><a href="#cb9-212" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * Sie haben Domaenenwissen, das der Solver nicht hat (siehe&quot;</span>)</span>
<span id="cb9-213"><a href="#cb9-213" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Warmstart_Effekt.py - dort halbiert ein Heuristik-Hinweis die Zeit).&quot;</span>)</span>
<span id="cb9-214"><a href="#cb9-214" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; * Sie planen laufend neu und der gestrige Plan ist fast noch gueltig.&quot;</span>)</span>
<span id="cb9-215"><a href="#cb9-215" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;In beiden Faellen gilt: MESSEN, nicht glauben.&quot;</span>)</span>
<span id="cb9-216"><a href="#cb9-216" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
<pre><code>==============================================================================
MIP-GAP UND ZEITLIMIT: STANDORTPLANUNG, 45 LAGER, 120 KUNDEN
==============================================================================
5,445 Variablen, davon 45 binaer.
[1] Zeitlimit 2 Sekunden
Status Time limit reached
bester Plan (Incumbent) 69,255.12 EUR
bewiesene Schranke 62,982.37 EUR
MIP-Gap 9.057 %
Knoten / Zeit 2 / 2.01 s
-&gt; zulaessig, aber nicht bewiesen optimal. Der wahre Bestwert
liegt zwischen 62,982.37 und 69,255.12 EUR.
[2] Zeitlimit 60 Sekunden
Status Optimal
bester Plan (Incumbent) 69,134.14 EUR
bewiesene Schranke 69,128.19 EUR
MIP-Gap 0.009 %
Knoten / Zeit 150 / 8.98 s
-&gt; beweisbar optimal
==============================================================================
WAS BRINGT MEHR RECHENZEIT?
==============================================================================
Nach 2 Sekunden: 69,255.12 EUR, Gap 9.06 %
Nach 9.0 Sekunden: 69,134.14 EUR, bewiesen optimal
Gewinn durch 7.0 Sekunden mehr Rechenzeit: 120.98 EUR (0.17 %)</code></pre>
<p><strong>Diese letzte Zeile ist die betriebswirtschaftlich interessante.</strong> Nach 2 Sekunden liegt ein Plan vor, der garantiert höchstens 9 % vom Optimum entfernt ist. Die restlichen 7 Sekunden Rechenzeit verbessern ihn um <strong>0,17 %</strong> — sie werden fast vollständig dafür verbraucht, die <em>Optimalität zu beweisen</em>, nicht den Plan zu verbessern.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Bei einem MILP kostet der Beweis der Optimalität meist ein Vielfaches dessen, was das Finden der optimalen Lösung kostet. Fragen Sie deshalb nicht „wie lange bis optimal?“, sondern „welchen Gap kann ich mir leisten?“. Ein nächtlicher Tourenplan darf eine Stunde rechnen; die Umplanung bei einem Maschinenausfall hat dreißig Sekunden.</p>
</blockquote>
<h3 id="warm-starts-was-sie-können-und-was-nicht">Warm-Starts: was sie können und was nicht</h3>
<p>Ein <strong>Warm-Start</strong> gibt dem Solver eine bekannte Lösung als Startpunkt mit. Die Idee ist verlockend: Wer schon eine brauchbare Lösung hat, muss nicht bei null anfangen.</p>
<p>In der Literatur wird das oft als sicherer Beschleuniger dargestellt. Das ist es <strong>nicht</strong> — und der ehrlichste Weg, das zu zeigen, ist eine Messung, die beide Seiten enthält. Der letzte Teil von <code>Solverstatus_und_Gap.py</code> füttert HiGHS mit einem Faustregel-Startplan:</p>
<pre><code>Faustregel-Startplan (ohne Solver): 75,091.59 EUR
Das sind 8.6 % ueber dem Optimum.
Zeit Knoten Ziel
------------------------------------------------------------------------------
ohne Warm-Start 8.98s 150 69,134.14
mit Warm-Start 8.44s 150 69,134.14</code></pre>
<p><strong>Kein nennenswerter Unterschied.</strong> Der Grund ist nicht, dass Warm-Starts nichts taugen, sondern dass HiGHS eigene Heuristiken innerhalb der ersten Sekunde bereits eine <em>bessere</em> Lösung finden als unsere Faustregel. Ein Startwert hilft nur, wenn er besser ist als das, was der Solver in derselben Zeit von allein findet.</p>
<p>Der Gegenfall — eine Heuristik mit echtem Domänenwissen:</p>
<div class="sourceCode" id="cb12"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb12-1"><a href="#cb12-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb12-2"><a href="#cb12-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-3"><a href="#cb12-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Warmstart_Effekt.py</span></span>
<span id="cb12-4"><a href="#cb12-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-5"><a href="#cb12-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: Wann ein Warm-Start wirklich etwas bringt.</span></span>
<span id="cb12-6"><a href="#cb12-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-7"><a href="#cb12-7" aria-hidden="true" tabindex="-1"></a><span class="co">Solverstatus_und_Gap.py zeigt einen Fall, in dem ein Startwert NICHTS</span></span>
<span id="cb12-8"><a href="#cb12-8" aria-hidden="true" tabindex="-1"></a><span class="co">bringt - HiGHS findet von allein schneller etwas Besseres. Hier der</span></span>
<span id="cb12-9"><a href="#cb12-9" aria-hidden="true" tabindex="-1"></a><span class="co">Gegenfall: eine Heuristik, die dem Solver echtes Domaenenwissen liefert.</span></span>
<span id="cb12-10"><a href="#cb12-10" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-11"><a href="#cb12-11" aria-hidden="true" tabindex="-1"></a><span class="co">Problem: Lastverteilung. n Auftraege mit bekannter Dauer sind auf m</span></span>
<span id="cb12-12"><a href="#cb12-12" aria-hidden="true" tabindex="-1"></a><span class="co">gleichartige Maschinen zu verteilen, sodass die zuletzt fertige Maschine</span></span>
<span id="cb12-13"><a href="#cb12-13" aria-hidden="true" tabindex="-1"></a><span class="co">so frueh wie moeglich fertig wird (Makespan-Minimierung).</span></span>
<span id="cb12-14"><a href="#cb12-14" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-15"><a href="#cb12-15" aria-hidden="true" tabindex="-1"></a><span class="co">Die Heuristik: LPT (Longest Processing Time first) - laengste Auftraege</span></span>
<span id="cb12-16"><a href="#cb12-16" aria-hidden="true" tabindex="-1"></a><span class="co">zuerst, jeder auf die momentan am wenigsten belastete Maschine. Sie ist</span></span>
<span id="cb12-17"><a href="#cb12-17" aria-hidden="true" tabindex="-1"></a><span class="co">Jahrzehnte alt, in zwei Zeilen geschrieben und beweisbar nie schlechter</span></span>
<span id="cb12-18"><a href="#cb12-18" aria-hidden="true" tabindex="-1"></a><span class="co">als 4/3 des Optimums.</span></span>
<span id="cb12-19"><a href="#cb12-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-20"><a href="#cb12-20" aria-hidden="true" tabindex="-1"></a><span class="co">Ueber model.AddHint() bekommt CP-SAT diese Loesung als Startpunkt.</span></span>
<span id="cb12-21"><a href="#cb12-21" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-22"><a href="#cb12-22" aria-hidden="true" tabindex="-1"></a><span class="co">WICHTIG: highspy wird hier bewusst NICHT importiert - es vertraegt sich</span></span>
<span id="cb12-23"><a href="#cb12-23" aria-hidden="true" tabindex="-1"></a><span class="co">nicht mit ortools im selben Prozess (siehe Kapitel Oekosystem).</span></span>
<span id="cb12-24"><a href="#cb12-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-25"><a href="#cb12-25" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, ortools</span></span>
<span id="cb12-26"><a href="#cb12-26" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb12-27"><a href="#cb12-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-28"><a href="#cb12-28" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb12-29"><a href="#cb12-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-30"><a href="#cb12-30" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb12-31"><a href="#cb12-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-32"><a href="#cb12-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb12-33"><a href="#cb12-33" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> ortools.sat.python <span class="im">import</span> cp_model</span>
<span id="cb12-34"><a href="#cb12-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-35"><a href="#cb12-35" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">4</span>)</span>
<span id="cb12-36"><a href="#cb12-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-37"><a href="#cb12-37" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-38"><a href="#cb12-38" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> lpt_heuristik(dauer: np.ndarray, n_maschinen: <span class="bu">int</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[np.ndarray, <span class="bu">int</span>]:</span>
<span id="cb12-39"><a href="#cb12-39" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Longest Processing Time first.</span></span>
<span id="cb12-40"><a href="#cb12-40" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-41"><a href="#cb12-41" aria-hidden="true" tabindex="-1"></a><span class="co"> Laengste Auftraege zuerst auf die jeweils freieste Maschine legen. Zwei</span></span>
<span id="cb12-42"><a href="#cb12-42" aria-hidden="true" tabindex="-1"></a><span class="co"> Zeilen, keine Bibliothek, Ergebnis in Mikrosekunden - und erstaunlich</span></span>
<span id="cb12-43"><a href="#cb12-43" aria-hidden="true" tabindex="-1"></a><span class="co"> nah am Optimum.</span></span>
<span id="cb12-44"><a href="#cb12-44" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb12-45"><a href="#cb12-45" aria-hidden="true" tabindex="-1"></a> zuordnung <span class="op">=</span> np.zeros(<span class="bu">len</span>(dauer), dtype<span class="op">=</span><span class="bu">int</span>)</span>
<span id="cb12-46"><a href="#cb12-46" aria-hidden="true" tabindex="-1"></a> belegung <span class="op">=</span> np.zeros(n_maschinen)</span>
<span id="cb12-47"><a href="#cb12-47" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> auftrag <span class="kw">in</span> np.argsort(<span class="op">-</span>dauer): <span class="co"># laengster zuerst</span></span>
<span id="cb12-48"><a href="#cb12-48" aria-hidden="true" tabindex="-1"></a> maschine <span class="op">=</span> <span class="bu">int</span>(np.argmin(belegung)) <span class="co"># freieste Maschine</span></span>
<span id="cb12-49"><a href="#cb12-49" aria-hidden="true" tabindex="-1"></a> zuordnung[auftrag] <span class="op">=</span> maschine</span>
<span id="cb12-50"><a href="#cb12-50" aria-hidden="true" tabindex="-1"></a> belegung[maschine] <span class="op">+=</span> dauer[auftrag]</span>
<span id="cb12-51"><a href="#cb12-51" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> zuordnung, <span class="bu">int</span>(belegung.<span class="bu">max</span>())</span>
<span id="cb12-52"><a href="#cb12-52" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-53"><a href="#cb12-53" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-54"><a href="#cb12-54" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(dauer: np.ndarray, n_maschinen: <span class="bu">int</span>, zeitlimit: <span class="bu">float</span>,</span>
<span id="cb12-55"><a href="#cb12-55" aria-hidden="true" tabindex="-1"></a> hinweis: np.ndarray <span class="op">|</span> <span class="va">None</span> <span class="op">=</span> <span class="va">None</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">str</span>, <span class="bu">int</span>, <span class="bu">float</span>]:</span>
<span id="cb12-56"><a href="#cb12-56" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Exaktes Modell mit CP-SAT, optional mit Startloesung als Hinweis.&quot;&quot;&quot;</span></span>
<span id="cb12-57"><a href="#cb12-57" aria-hidden="true" tabindex="-1"></a> n_auftraege <span class="op">=</span> <span class="bu">len</span>(dauer)</span>
<span id="cb12-58"><a href="#cb12-58" aria-hidden="true" tabindex="-1"></a> obergrenze <span class="op">=</span> <span class="bu">int</span>(dauer.<span class="bu">sum</span>())</span>
<span id="cb12-59"><a href="#cb12-59" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-60"><a href="#cb12-60" aria-hidden="true" tabindex="-1"></a> modell <span class="op">=</span> cp_model.CpModel()</span>
<span id="cb12-61"><a href="#cb12-61" aria-hidden="true" tabindex="-1"></a> <span class="co"># x[i][k] = 1 &lt;=&gt; Auftrag i laeuft auf Maschine k</span></span>
<span id="cb12-62"><a href="#cb12-62" 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>k<span class="sc">}</span><span class="ss">&quot;</span>) <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen)]</span>
<span id="cb12-63"><a href="#cb12-63" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege)]</span>
<span id="cb12-64"><a href="#cb12-64" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege):</span>
<span id="cb12-65"><a href="#cb12-65" aria-hidden="true" tabindex="-1"></a> modell.AddExactlyOne(x[i]) <span class="co"># jeder Auftrag genau einmal</span></span>
<span id="cb12-66"><a href="#cb12-66" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-67"><a href="#cb12-67" aria-hidden="true" tabindex="-1"></a> belegung <span class="op">=</span> [modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="ss">f&quot;last_</span><span class="sc">{</span>k<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-68"><a href="#cb12-68" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen)]</span>
<span id="cb12-69"><a href="#cb12-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen):</span>
<span id="cb12-70"><a href="#cb12-70" aria-hidden="true" tabindex="-1"></a> modell.Add(belegung[k] <span class="op">==</span> <span class="bu">sum</span>(<span class="bu">int</span>(dauer[i]) <span class="op">*</span> x[i][k]</span>
<span id="cb12-71"><a href="#cb12-71" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege)))</span>
<span id="cb12-72"><a href="#cb12-72" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-73"><a href="#cb12-73" aria-hidden="true" tabindex="-1"></a> makespan <span class="op">=</span> modell.NewIntVar(<span class="dv">0</span>, obergrenze, <span class="st">&quot;makespan&quot;</span>)</span>
<span id="cb12-74"><a href="#cb12-74" aria-hidden="true" tabindex="-1"></a> modell.AddMaxEquality(makespan, belegung) <span class="co"># das Maximum ueber alle Maschinen</span></span>
<span id="cb12-75"><a href="#cb12-75" aria-hidden="true" tabindex="-1"></a> modell.Minimize(makespan)</span>
<span id="cb12-76"><a href="#cb12-76" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-77"><a href="#cb12-77" aria-hidden="true" tabindex="-1"></a> <span class="co"># Der Warm-Start: ein Hinweis pro Variable. CP-SAT muss ihn nicht</span></span>
<span id="cb12-78"><a href="#cb12-78" aria-hidden="true" tabindex="-1"></a> <span class="co"># befolgen - er nutzt ihn als erste Loesung, wenn er zulaessig ist.</span></span>
<span id="cb12-79"><a href="#cb12-79" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> hinweis <span class="kw">is</span> <span class="kw">not</span> <span class="va">None</span>:</span>
<span id="cb12-80"><a href="#cb12-80" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(n_auftraege):</span>
<span id="cb12-81"><a href="#cb12-81" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> k <span class="kw">in</span> <span class="bu">range</span>(n_maschinen):</span>
<span id="cb12-82"><a href="#cb12-82" aria-hidden="true" tabindex="-1"></a> modell.AddHint(x[i][k], <span class="dv">1</span> <span class="cf">if</span> hinweis[i] <span class="op">==</span> k <span class="cf">else</span> <span class="dv">0</span>)</span>
<span id="cb12-83"><a href="#cb12-83" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-84"><a href="#cb12-84" aria-hidden="true" tabindex="-1"></a> loeser <span class="op">=</span> cp_model.CpSolver()</span>
<span id="cb12-85"><a href="#cb12-85" aria-hidden="true" tabindex="-1"></a> loeser.parameters.max_time_in_seconds <span class="op">=</span> zeitlimit</span>
<span id="cb12-86"><a href="#cb12-86" aria-hidden="true" tabindex="-1"></a> <span class="co"># Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -</span></span>
<span id="cb12-87"><a href="#cb12-87" aria-hidden="true" tabindex="-1"></a> <span class="co"># der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).</span></span>
<span id="cb12-88"><a href="#cb12-88" aria-hidden="true" tabindex="-1"></a> <span class="co"># Im Produktivbetrieb laesst man beides auf den Standardwerten.</span></span>
<span id="cb12-89"><a href="#cb12-89" aria-hidden="true" tabindex="-1"></a> loeser.parameters.num_workers <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-90"><a href="#cb12-90" aria-hidden="true" tabindex="-1"></a> loeser.parameters.random_seed <span class="op">=</span> <span class="dv">1</span></span>
<span id="cb12-91"><a href="#cb12-91" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-92"><a href="#cb12-92" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb12-93"><a href="#cb12-93" aria-hidden="true" tabindex="-1"></a> status <span class="op">=</span> loeser.Solve(modell)</span>
<span id="cb12-94"><a href="#cb12-94" aria-hidden="true" tabindex="-1"></a> dauer_s <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb12-95"><a href="#cb12-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-96"><a href="#cb12-96" 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="cb12-97"><a href="#cb12-97" aria-hidden="true" tabindex="-1"></a> <span class="cf">raise</span> <span class="pp">RuntimeError</span>(<span class="ss">f&quot;Kein Plan gefunden: </span><span class="sc">{</span>loeser<span class="sc">.</span>StatusName(status)<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-98"><a href="#cb12-98" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> loeser.StatusName(status), <span class="bu">int</span>(loeser.ObjectiveValue()), dauer_s</span>
<span id="cb12-99"><a href="#cb12-99" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-100"><a href="#cb12-100" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-101"><a href="#cb12-101" 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="cb12-102"><a href="#cb12-102" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb12-103"><a href="#cb12-103" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER&quot;</span>)</span>
<span id="cb12-104"><a href="#cb12-104" 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="cb12-105"><a href="#cb12-105" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,&quot;</span>)</span>
<span id="cb12-106"><a href="#cb12-106" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;sodass die letzte Maschine so frueh wie moeglich fertig wird.</span><span class="ch">\n</span><span class="st">&quot;</span>)</span>
<span id="cb12-107"><a href="#cb12-107" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-108"><a href="#cb12-108" 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;Instanz&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Variante&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Makespan&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;Zeit&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-109"><a href="#cb12-109" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;Faktor&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-110"><a href="#cb12-110" 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="cb12-111"><a href="#cb12-111" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-112"><a href="#cb12-112" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> n_auftraege, n_maschinen <span class="kw">in</span> [(<span class="dv">60</span>, <span class="dv">7</span>), (<span class="dv">80</span>, <span class="dv">9</span>)]:</span>
<span id="cb12-113"><a href="#cb12-113" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> RNG.integers(<span class="dv">10</span>, <span class="dv">90</span>, n_auftraege)</span>
<span id="cb12-114"><a href="#cb12-114" aria-hidden="true" tabindex="-1"></a> start, lpt_wert <span class="op">=</span> lpt_heuristik(dauer, n_maschinen)</span>
<span id="cb12-115"><a href="#cb12-115" aria-hidden="true" tabindex="-1"></a> untere_schranke <span class="op">=</span> dauer.<span class="bu">sum</span>() <span class="op">/</span> n_maschinen</span>
<span id="cb12-116"><a href="#cb12-116" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-117"><a href="#cb12-117" aria-hidden="true" tabindex="-1"></a> instanz <span class="op">=</span> <span class="ss">f&quot;</span><span class="sc">{</span>n_auftraege<span class="sc">}</span><span class="ss"> Auftr., </span><span class="sc">{</span>n_maschinen<span class="sc">}</span><span class="ss"> Masch.&quot;</span></span>
<span id="cb12-118"><a href="#cb12-118" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>instanz<span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;LPT-Heuristik&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>lpt_wert<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-119"><a href="#cb12-119" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&lt; 0.001s&#39;</span><span class="sc">:&gt;9}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-120"><a href="#cb12-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-121"><a href="#cb12-121" aria-hidden="true" tabindex="-1"></a> _, ziel_kalt, zeit_kalt <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>)</span>
<span id="cb12-122"><a href="#cb12-122" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;CP-SAT kalt&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>ziel_kalt<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-123"><a href="#cb12-123" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>zeit_kalt<span class="sc">:&gt;8.3f}</span><span class="ss">s </span><span class="sc">{</span><span class="st">&#39;1,0x&#39;</span><span class="sc">:&gt;8}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-124"><a href="#cb12-124" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-125"><a href="#cb12-125" aria-hidden="true" tabindex="-1"></a> _, ziel_warm, zeit_warm <span class="op">=</span> loese(dauer, n_maschinen, <span class="fl">60.0</span>, hinweis<span class="op">=</span>start)</span>
<span id="cb12-126"><a href="#cb12-126" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;CP-SAT + LPT-Hinweis&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>ziel_warm<span class="sc">:&gt;9}</span><span class="ss"> &quot;</span></span>
<span id="cb12-127"><a href="#cb12-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>zeit_warm<span class="sc">:&gt;8.3f}</span><span class="ss">s </span><span class="sc">{</span>zeit_kalt <span class="op">/</span> zeit_warm<span class="sc">:&gt;7.1f}</span><span class="ss">x&quot;</span>)</span>
<span id="cb12-128"><a href="#cb12-128" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-129"><a href="#cb12-129" aria-hidden="true" tabindex="-1"></a> <span class="cf">assert</span> ziel_kalt <span class="op">==</span> ziel_warm, <span class="op">\</span></span>
<span id="cb12-130"><a href="#cb12-130" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!&quot;</span></span>
<span id="cb12-131"><a href="#cb12-131" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span><span class="st">&#39;&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span><span class="st">&#39;untere Schranke&#39;</span><span class="sc">:&lt;22}</span><span class="ss"> </span><span class="sc">{</span>untere_schranke<span class="sc">:&gt;9.1f}</span><span class="ss">&quot;</span>)</span>
<span id="cb12-132"><a href="#cb12-132" 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="cb12-133"><a href="#cb12-133" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb12-134"><a href="#cb12-134" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">Drei Beobachtungen:&quot;</span>)</span>
<span id="cb12-135"><a href="#cb12-135" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden&quot;</span>)</span>
<span id="cb12-136"><a href="#cb12-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert.&quot;</span>)</span>
<span id="cb12-137"><a href="#cb12-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter&quot;</span>)</span>
<span id="cb12-138"><a href="#cb12-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Hinweis kostet Zeit, er verfaelscht aber nie die Loesung.&quot;</span>)</span>
<span id="cb12-139"><a href="#cb12-139" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer&quot;</span>)</span>
<span id="cb12-140"><a href="#cb12-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; den Solver liegt weniger in der Qualitaet als darin, dass sie&quot;</span>)</span>
<span id="cb12-141"><a href="#cb12-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; SOFORT da ist - der Solver kann von Beginn an alles verwerfen,&quot;</span>)</span>
<span id="cb12-142"><a href="#cb12-142" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; was schlechter ist.&quot;</span>)</span>
<span id="cb12-143"><a href="#cb12-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -&quot;</span>)</span>
<span id="cb12-144"><a href="#cb12-144" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt&quot;</span>)</span>
<span id="cb12-145"><a href="#cb12-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; davon ab, wie schnell der Solver von allein eine vergleichbar gute&quot;</span>)</span>
<span id="cb12-146"><a href="#cb12-146" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist&quot;</span>)</span>
<span id="cb12-147"><a href="#cb12-147" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; eine Messung wert, keine Glaubensfrage.&quot;</span>)</span>
<span id="cb12-148"><a href="#cb12-148" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe (Zeiten hardwareabhängig):</strong></p>
<pre><code>==============================================================================
WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER
==============================================================================
Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,
sodass die letzte Maschine so frueh wie moeglich fertig wird.
Instanz Variante Makespan Zeit Faktor
------------------------------------------------------------------------------
60 Auftr., 7 Masch. LPT-Heuristik 483 &lt; 0.001s
CP-SAT kalt 475 0.627s 1,0x
CP-SAT + LPT-Hinweis 475 0.292s 2.2x
untere Schranke 474.7
------------------------------------------------------------------------------
80 Auftr., 9 Masch. LPT-Heuristik 445 &lt; 0.001s
CP-SAT kalt 442 3.275s 1,0x
CP-SAT + LPT-Hinweis 442 2.420s 1.4x
untere Schranke 441.8
------------------------------------------------------------------------------</code></pre>
<p>Hier wirkt der Hinweis: Faktor 2,2 bzw. 1,4. Beachten Sie aber, dass der Faktor <strong>schwankt</strong> und sich nicht aus der Problemgröße ableiten lässt — bei der größeren Instanz ist er sogar kleiner. Es gibt keine Regel, es gibt nur die Messung.</p>
<blockquote>
<p><strong>⚠️ Typische Fehler</strong></p>
<ul>
<li><strong><code>if status == OPTIMAL: ... else: return None</code>.</strong> Wirft eine Lösung weg, die 9 % vom Optimum entfernt und damit vollkommen brauchbar ist. Werten Sie den Gap aus, nicht nur den Status.</li>
<li><strong>Zeitlimit ohne Gap-Auswertung.</strong> Ein Limit erzwingt ein Ende, sagt aber nichts über die Qualität. Ohne den Gap wissen Sie nicht, ob Sie 0,1 % oder 60 % danebenliegen.</li>
<li><strong>Warm-Start als Selbstverständlichkeit.</strong> Messen Sie ihn — auf Ihrer Instanzklasse, gegen den kalten Lauf. In der Hälfte der Fälle bringt er nichts.</li>
<li><strong>Einen Hinweis für eine unzulässige Lösung geben.</strong> CP-SAT und HiGHS verwerfen ihn dann stillschweigend. Prüfen Sie Ihre Startlösung vorher auf Zulässigkeit — sonst messen Sie einen Effekt, den es gar nicht gibt.</li>
</ul>
</blockquote>
<blockquote>
<p><strong>🎯 Merksatz</strong> Ein Warm-Start kann das Ergebnis nie verfälschen — im schlimmsten Fall kostet er Zeit. Genau deshalb darf man ihn ausprobieren. Aber man muss ihn auch <strong>messen</strong>, statt an ihn zu glauben.</p>
</blockquote>
<hr />
<h2 id="sec:milp-uebungsaufgaben">6.9 Übungsaufgaben</h2>
<blockquote>
<p>Lösungen: <a href="anhang-loesungen.html#sec:loesungen-milp">Abschnitt A.6</a>.</p>
</blockquote>
<p><strong>Aufgabe 6.1 ⭐ — Runden widerlegen.</strong> Konstruieren Sie selbst ein Beispiel mit zwei Variablen, bei dem Abrunden der LP-Lösung mindestens 50 % des optimalen Zielwerts verliert. Begründen Sie.</p>
<p><strong>Aufgabe 6.2 ⭐ — Big-M wählen.</strong> Ein Modell enthält <span class="math inline">x_j \le M y_j</span> mit <span class="math inline">x_j \le 250</span> als bekannter Kapazität. Welchen Wert sollte <span class="math inline">M</span> haben? Was passiert bei <span class="math inline">M = 10^6</span>?</p>
<p><strong>Aufgabe 6.3 ⭐⭐ — Regeln in Ungleichungen übersetzen.</strong> Formulieren Sie mit Binärvariablen: (a) „Wenn Produkt A produziert wird, darf Produkt B nicht produziert werden.“ (b) „Mindestens zwei der vier Standorte müssen eröffnet werden.“ (c) „Wenn Standort 1 <strong>und</strong> Standort 2 eröffnet werden, muss auch das Zentrallager gebaut werden.“ (d) „Die Produktionsmenge ist entweder 0 oder liegt zwischen 500 und 2000.“ (e) „Genau eine der drei Maschinen wird eingesetzt.“</p>
<p><strong>Aufgabe 6.4 ⭐⭐ — Branch-and-Bound von Hand.</strong> Lösen Sie mit Branch-and-Bound und zeichnen Sie den Suchbaum: <span class="math display">\max\ 8x_1+11x_2+6x_3+4x_4 \quad\text{u. d. N.}\quad 5x_1+7x_2+4x_3+3x_4\le14,\quad x_i\in\{0,1\}</span> Geben Sie für jeden Knoten die LP-Schranke an und markieren Sie, wo gekappt wird.</p>
<p><strong>Aufgabe 6.5 ⭐⭐⭐ — Kardinalität variieren.</strong> Erweitern Sie <code>MILP_Portfolio_Fixgebuehren.py</code> so, dass es <span class="math inline">K = 1, 2, \dots, 6</span> durchläuft und Netto-Ertrag sowie Rechenzeit tabelliert. (a) Ab welchem <span class="math inline">K</span> steigt der Ertrag nicht mehr? Warum? (b) Wie verhält sich die Rechenzeit? (c) Was wäre der „faire Preis“ für die Erlaubnis, eine vierte Position zu eröffnen?</p>
<p><strong>Aufgabe 6.6 ⭐⭐⭐ — Big-M-Effekt messen.</strong> Ersetzen Sie im Portfoliomodell <code>MAX_POSITION</code> in der Big-M-Zeile durch <span class="math inline">10^4</span>, <span class="math inline">10^6</span> und <span class="math inline">10^9</span> (die echte Obergrenze bleibt in den <code>bounds</code>). Messen Sie jeweils Laufzeit und Zahl der Branch-and-Bound-Knoten (<code>ergebnis.mip_node_count</code>). Stellen Sie die Ergebnisse dar und erklären Sie sie.</p>
<p><strong>Aufgabe 6.7 ⭐⭐⭐ — Standortplanung.</strong> Modellieren und lösen Sie: Fünf mögliche Lagerstandorte mit Fixkosten <span class="math inline">(80, 60, 90, 70, 50)</span> Tsd. € versorgen vier Regionen mit Bedarf <span class="math inline">(30, 45, 25, 40)</span> Einheiten. Transportkosten je Einheit stehen in einer <span class="math inline">5\times4</span>-Matrix Ihrer Wahl. Jedes eröffnete Lager hat Kapazität 80. Minimieren Sie Fix- plus Transportkosten.</p>
<hr />
<h2 id="sec:milp-denkfehler">6.10 Finde den Denkfehler</h2>
<p>Die Warnung „<span class="math inline">M</span> so klein wie möglich“ wird meist mit der Laufzeit begründet: schwache Relaxation, mehr Knoten. Das stimmt — und ist die <strong>harmlosere</strong> Hälfte der Wahrheit.</p>
<div class="card card-denkfehler">
<blockquote>
<p><strong>🐛 Finde den Denkfehler: Elf Lager, die keine Fixkosten kosten</strong></p>
<p>Ein Team plant Standorte: 12 mögliche Lager, 40 Kunden, Fixkosten je eröffnetem Lager. Für das Big-M in der Kopplung</p>
<p><span class="math display">\sum_j x_{ij} \le M \cdot y_i</span></p>
<p>setzt jemand „sicherheitshalber“ eine sehr große Zahl ein. Das Modell läuft durch und meldet Gesamtkosten von <strong>12 441,96 €</strong>. Mit einem knapp gewählten <span class="math inline">M</span> hatte dasselbe Modell zuvor 26 525,28 € gemeldet. Das Team freut sich über die Einsparung.</p>
<p><strong>Ihre Aufgabe:</strong> (a) Warum ist die zweite Zahl <em>kleiner</em>, obwohl sich am Problem nichts geändert hat? Was müsste für ein größeres <span class="math inline">M</span> mathematisch gelten? (b) Sehen Sie sich unten die Werte der Binärvariablen an — was fällt auf, und was hat das mit der Ganzzahltoleranz des Solvers zu tun? (c) Wie viele Lager liefern in dieser „Lösung“ tatsächlich Ware, und wie viele Fixkosten werden dafür verbucht? (d) Formulieren Sie die Prüfung, die diesen Fehler in jedem MILP-Skript auffliegen ließe.</p>
<p><em>Auflösung: <a href="anhang-loesungen.html#sec:loesungen-milp">Abschnitt A.6</a>.</em></p>
</blockquote>
</div>
<p>Das folgende Programm führt beide Läufe nebeneinander aus und wendet auf beide dieselbe Prüfung an.</p>
<div class="sourceCode" id="cb14"><pre class="sourceCode python"><code class="sourceCode python"><span id="cb14-1"><a href="#cb14-1" aria-hidden="true" tabindex="-1"></a><span class="co">#!/usr/bin/env python3</span></span>
<span id="cb14-2"><a href="#cb14-2" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-3"><a href="#cb14-3" aria-hidden="true" tabindex="-1"></a><span class="co"># Big_M_Falle.py</span></span>
<span id="cb14-4"><a href="#cb14-4" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-5"><a href="#cb14-5" aria-hidden="true" tabindex="-1"></a><span class="co">Kapitel MILP: Was ein zu grosses Big-M wirklich anrichtet.</span></span>
<span id="cb14-6"><a href="#cb14-6" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-7"><a href="#cb14-7" aria-hidden="true" tabindex="-1"></a><span class="co">Lehrbuecher warnen vor grossem M mit dem Hinweis &quot;die Relaxation wird</span></span>
<span id="cb14-8"><a href="#cb14-8" aria-hidden="true" tabindex="-1"></a><span class="co">schwach, der Solver braucht mehr Knoten&quot;. Das stimmt - ist aber die</span></span>
<span id="cb14-9"><a href="#cb14-9" aria-hidden="true" tabindex="-1"></a><span class="co">harmlosere Haelfte der Wahrheit. Die gefaehrlichere: Bei sehr grossem M</span></span>
<span id="cb14-10"><a href="#cb14-10" aria-hidden="true" tabindex="-1"></a><span class="co">kann der Solver eine Loesung als ganzzahlig ANNEHMEN, in der die</span></span>
<span id="cb14-11"><a href="#cb14-11" aria-hidden="true" tabindex="-1"></a><span class="co">Binaervariablen bei 1e-8 stehen. Dann liefern zugeschaltete Anlagen Ware</span></span>
<span id="cb14-12"><a href="#cb14-12" aria-hidden="true" tabindex="-1"></a><span class="co">aus, waehrend das Modell ihre Fixkosten mit 0 verbucht.</span></span>
<span id="cb14-13"><a href="#cb14-13" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-14"><a href="#cb14-14" aria-hidden="true" tabindex="-1"></a><span class="co">Beispiel: Standortplanung, 12 moegliche Lager, 40 Kunden.</span></span>
<span id="cb14-15"><a href="#cb14-15" aria-hidden="true" tabindex="-1"></a><span class="co"> min sum_i fix_i * y_i + sum_ij kosten_ij * x_ij</span></span>
<span id="cb14-16"><a href="#cb14-16" aria-hidden="true" tabindex="-1"></a><span class="co"> u.d.N. sum_i x_ij = bedarf_j (jeder Kunde beliefert)</span></span>
<span id="cb14-17"><a href="#cb14-17" aria-hidden="true" tabindex="-1"></a><span class="co"> sum_j x_ij &lt;= M_i * y_i (Lager offen, wenn es liefert)</span></span>
<span id="cb14-18"><a href="#cb14-18" aria-hidden="true" tabindex="-1"></a><span class="co"> y_i binaer, x_ij &gt;= 0</span></span>
<span id="cb14-19"><a href="#cb14-19" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-20"><a href="#cb14-20" aria-hidden="true" tabindex="-1"></a><span class="co">Zwei Laeufe:</span></span>
<span id="cb14-21"><a href="#cb14-21" aria-hidden="true" tabindex="-1"></a><span class="co"> 1. M knapp gewaehlt (= tatsaechliche Lagerkapazitaet) -&gt; richtig</span></span>
<span id="cb14-22"><a href="#cb14-22" aria-hidden="true" tabindex="-1"></a><span class="co"> 2. M = 1e7-fach zu gross, Presolve abgeschaltet -&gt; stilles Desaster</span></span>
<span id="cb14-23"><a href="#cb14-23" aria-hidden="true" tabindex="-1"></a><span class="co">Beide werden mit derselben Pruefung kontrolliert, die den Fall auffliegen laesst.</span></span>
<span id="cb14-24"><a href="#cb14-24" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-25"><a href="#cb14-25" aria-hidden="true" tabindex="-1"></a><span class="co">Benoetigt: numpy, highspy</span></span>
<span id="cb14-26"><a href="#cb14-26" aria-hidden="true" tabindex="-1"></a><span class="co">&quot;&quot;&quot;</span></span>
<span id="cb14-27"><a href="#cb14-27" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-28"><a href="#cb14-28" aria-hidden="true" tabindex="-1"></a><span class="im">from</span> __future__ <span class="im">import</span> annotations</span>
<span id="cb14-29"><a href="#cb14-29" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-30"><a href="#cb14-30" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> time</span>
<span id="cb14-31"><a href="#cb14-31" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-32"><a href="#cb14-32" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> numpy <span class="im">as</span> np</span>
<span id="cb14-33"><a href="#cb14-33" aria-hidden="true" tabindex="-1"></a><span class="im">import</span> highspy</span>
<span id="cb14-34"><a href="#cb14-34" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-35"><a href="#cb14-35" aria-hidden="true" tabindex="-1"></a>RNG <span class="op">=</span> np.random.default_rng(<span class="dv">11</span>)</span>
<span id="cb14-36"><a href="#cb14-36" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-37"><a href="#cb14-37" aria-hidden="true" tabindex="-1"></a>N_LAGER, N_KUNDE <span class="op">=</span> <span class="dv">12</span>, <span class="dv">40</span></span>
<span id="cb14-38"><a href="#cb14-38" aria-hidden="true" tabindex="-1"></a>FIXKOSTEN <span class="op">=</span> RNG.uniform(<span class="dv">2000</span>, <span class="dv">5000</span>, N_LAGER)</span>
<span id="cb14-39"><a href="#cb14-39" aria-hidden="true" tabindex="-1"></a>TRANSPORT <span class="op">=</span> RNG.uniform(<span class="dv">5</span>, <span class="dv">40</span>, (N_LAGER, N_KUNDE))</span>
<span id="cb14-40"><a href="#cb14-40" aria-hidden="true" tabindex="-1"></a>BEDARF <span class="op">=</span> RNG.uniform(<span class="dv">10</span>, <span class="dv">60</span>, N_KUNDE)</span>
<span id="cb14-41"><a href="#cb14-41" aria-hidden="true" tabindex="-1"></a>KAPAZITAET <span class="op">=</span> BEDARF.<span class="bu">sum</span>() <span class="op">*</span> <span class="fl">0.45</span> <span class="co"># jedes Lager schafft 45 % des Gesamtbedarfs</span></span>
<span id="cb14-42"><a href="#cb14-42" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-43"><a href="#cb14-43" aria-hidden="true" tabindex="-1"></a><span class="co"># Toleranz, ab der ein Solver eine Variable als ganzzahlig durchgehen laesst.</span></span>
<span id="cb14-44"><a href="#cb14-44" aria-hidden="true" tabindex="-1"></a><span class="co"># HiGHS und die meisten anderen verwenden 1e-6 als Standard.</span></span>
<span id="cb14-45"><a href="#cb14-45" aria-hidden="true" tabindex="-1"></a>GANZZAHL_TOLERANZ <span class="op">=</span> <span class="fl">1e-6</span></span>
<span id="cb14-46"><a href="#cb14-46" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-47"><a href="#cb14-47" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-48"><a href="#cb14-48" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> loese(big_m: <span class="bu">float</span>, presolve: <span class="bu">str</span> <span class="op">=</span> <span class="st">&quot;on&quot;</span>) <span class="op">-&gt;</span> <span class="bu">dict</span>:</span>
<span id="cb14-49"><a href="#cb14-49" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Baut und loest das Standortmodell. Liefert Loesung und Solverkennzahlen.&quot;&quot;&quot;</span></span>
<span id="cb14-50"><a href="#cb14-50" aria-hidden="true" tabindex="-1"></a> hochschule <span class="op">=</span> highspy.Highs()</span>
<span id="cb14-51"><a href="#cb14-51" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;output_flag&quot;</span>, <span class="va">False</span>)</span>
<span id="cb14-52"><a href="#cb14-52" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;presolve&quot;</span>, presolve)</span>
<span id="cb14-53"><a href="#cb14-53" aria-hidden="true" tabindex="-1"></a> hochschule.setOptionValue(<span class="st">&quot;time_limit&quot;</span>, <span class="fl">300.0</span>)</span>
<span id="cb14-54"><a href="#cb14-54" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-55"><a href="#cb14-55" aria-hidden="true" tabindex="-1"></a> anzahl_x <span class="op">=</span> N_LAGER <span class="op">*</span> N_KUNDE</span>
<span id="cb14-56"><a href="#cb14-56" aria-hidden="true" tabindex="-1"></a> unendlich <span class="op">=</span> highspy.kHighsInf</span>
<span id="cb14-57"><a href="#cb14-57" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-58"><a href="#cb14-58" aria-hidden="true" tabindex="-1"></a> <span class="co"># Spalten: erst alle x_ij, dann die y_i</span></span>
<span id="cb14-59"><a href="#cb14-59" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(anzahl_x, np.zeros(anzahl_x), np.full(anzahl_x, unendlich))</span>
<span id="cb14-60"><a href="#cb14-60" aria-hidden="true" tabindex="-1"></a> hochschule.addVars(N_LAGER, np.zeros(N_LAGER), np.ones(N_LAGER))</span>
<span id="cb14-61"><a href="#cb14-61" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb14-62"><a href="#cb14-62" aria-hidden="true" tabindex="-1"></a> hochschule.changeColIntegrality(anzahl_x <span class="op">+</span> i,</span>
<span id="cb14-63"><a href="#cb14-63" aria-hidden="true" tabindex="-1"></a> highspy.HighsVarType.kInteger)</span>
<span id="cb14-64"><a href="#cb14-64" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(anzahl_x <span class="op">+</span> i, FIXKOSTEN[i])</span>
<span id="cb14-65"><a href="#cb14-65" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb14-66"><a href="#cb14-66" aria-hidden="true" tabindex="-1"></a> hochschule.changeColCost(i <span class="op">*</span> N_KUNDE <span class="op">+</span> j, TRANSPORT[i, j])</span>
<span id="cb14-67"><a href="#cb14-67" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-68"><a href="#cb14-68" aria-hidden="true" tabindex="-1"></a> <span class="co"># Jeder Kunde wird genau beliefert</span></span>
<span id="cb14-69"><a href="#cb14-69" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE):</span>
<span id="cb14-70"><a href="#cb14-70" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER)], dtype<span class="op">=</span>np.int32)</span>
<span id="cb14-71"><a href="#cb14-71" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(BEDARF[j], BEDARF[j], <span class="bu">len</span>(index), index,</span>
<span id="cb14-72"><a href="#cb14-72" aria-hidden="true" tabindex="-1"></a> np.ones(<span class="bu">len</span>(index)))</span>
<span id="cb14-73"><a href="#cb14-73" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-74"><a href="#cb14-74" aria-hidden="true" tabindex="-1"></a> <span class="co"># Die Kopplung: sum_j x_ij - M * y_i &lt;= 0</span></span>
<span id="cb14-75"><a href="#cb14-75" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> i <span class="kw">in</span> <span class="bu">range</span>(N_LAGER):</span>
<span id="cb14-76"><a href="#cb14-76" aria-hidden="true" tabindex="-1"></a> index <span class="op">=</span> np.array([i <span class="op">*</span> N_KUNDE <span class="op">+</span> j <span class="cf">for</span> j <span class="kw">in</span> <span class="bu">range</span>(N_KUNDE)] <span class="op">+</span> [anzahl_x <span class="op">+</span> i],</span>
<span id="cb14-77"><a href="#cb14-77" aria-hidden="true" tabindex="-1"></a> dtype<span class="op">=</span>np.int32)</span>
<span id="cb14-78"><a href="#cb14-78" aria-hidden="true" tabindex="-1"></a> werte <span class="op">=</span> np.concatenate([np.ones(N_KUNDE), [<span class="op">-</span>big_m]])</span>
<span id="cb14-79"><a href="#cb14-79" aria-hidden="true" tabindex="-1"></a> hochschule.addRow(<span class="op">-</span>unendlich, <span class="fl">0.0</span>, <span class="bu">len</span>(index), index, werte)</span>
<span id="cb14-80"><a href="#cb14-80" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-81"><a href="#cb14-81" aria-hidden="true" tabindex="-1"></a> t0 <span class="op">=</span> time.perf_counter()</span>
<span id="cb14-82"><a href="#cb14-82" aria-hidden="true" tabindex="-1"></a> hochschule.run()</span>
<span id="cb14-83"><a href="#cb14-83" aria-hidden="true" tabindex="-1"></a> dauer <span class="op">=</span> time.perf_counter() <span class="op">-</span> t0</span>
<span id="cb14-84"><a href="#cb14-84" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-85"><a href="#cb14-85" aria-hidden="true" tabindex="-1"></a> loesung <span class="op">=</span> np.array(hochschule.getSolution().col_value)</span>
<span id="cb14-86"><a href="#cb14-86" aria-hidden="true" tabindex="-1"></a> info <span class="op">=</span> hochschule.getInfo()</span>
<span id="cb14-87"><a href="#cb14-87" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> {</span>
<span id="cb14-88"><a href="#cb14-88" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;x&quot;</span>: loesung[:anzahl_x].reshape(N_LAGER, N_KUNDE),</span>
<span id="cb14-89"><a href="#cb14-89" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;y&quot;</span>: loesung[anzahl_x:],</span>
<span id="cb14-90"><a href="#cb14-90" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;zielwert&quot;</span>: info.objective_function_value,</span>
<span id="cb14-91"><a href="#cb14-91" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;knoten&quot;</span>: info.mip_node_count,</span>
<span id="cb14-92"><a href="#cb14-92" aria-hidden="true" tabindex="-1"></a> <span class="st">&quot;dauer&quot;</span>: dauer,</span>
<span id="cb14-93"><a href="#cb14-93" aria-hidden="true" tabindex="-1"></a> }</span>
<span id="cb14-94"><a href="#cb14-94" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-95"><a href="#cb14-95" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-96"><a href="#cb14-96" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> pruefe(ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="bu">tuple</span>[<span class="bu">bool</span>, <span class="bu">list</span>[<span class="bu">str</span>]]:</span>
<span id="cb14-97"><a href="#cb14-97" aria-hidden="true" tabindex="-1"></a> <span class="co">&quot;&quot;&quot;Die Pruefung, die in jedes MILP-Auswertungsskript gehoert.</span></span>
<span id="cb14-98"><a href="#cb14-98" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-99"><a href="#cb14-99" aria-hidden="true" tabindex="-1"></a><span class="co"> Sie rechnet die Kosten AUS DER LOESUNG neu aus, statt dem Zielwert des</span></span>
<span id="cb14-100"><a href="#cb14-100" aria-hidden="true" tabindex="-1"></a><span class="co"> Solvers zu glauben - und vergleicht beide. Genau diese Gegenrechnung</span></span>
<span id="cb14-101"><a href="#cb14-101" aria-hidden="true" tabindex="-1"></a><span class="co"> entlarvt eine Loesung, in der Binaervariablen bei 1e-8 haengengeblieben</span></span>
<span id="cb14-102"><a href="#cb14-102" aria-hidden="true" tabindex="-1"></a><span class="co"> sind.</span></span>
<span id="cb14-103"><a href="#cb14-103" aria-hidden="true" tabindex="-1"></a><span class="co"> &quot;&quot;&quot;</span></span>
<span id="cb14-104"><a href="#cb14-104" aria-hidden="true" tabindex="-1"></a> beanstandungen <span class="op">=</span> []</span>
<span id="cb14-105"><a href="#cb14-105" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis[<span class="st">&quot;x&quot;</span>], ergebnis[<span class="st">&quot;y&quot;</span>]</span>
<span id="cb14-106"><a href="#cb14-106" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-107"><a href="#cb14-107" aria-hidden="true" tabindex="-1"></a> <span class="co"># 1. Sind die Binaervariablen wirklich binaer?</span></span>
<span id="cb14-108"><a href="#cb14-108" aria-hidden="true" tabindex="-1"></a> abstand <span class="op">=</span> np.<span class="bu">abs</span>(y <span class="op">-</span> np.<span class="bu">round</span>(y))</span>
<span id="cb14-109"><a href="#cb14-109" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> abstand.<span class="bu">max</span>() <span class="op">&gt;</span> GANZZAHL_TOLERANZ:</span>
<span id="cb14-110"><a href="#cb14-110" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-111"><a href="#cb14-111" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;y ist nicht ganzzahlig: groesster Abstand </span><span class="sc">{</span>abstand<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.2e}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-112"><a href="#cb14-112" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-113"><a href="#cb14-113" aria-hidden="true" tabindex="-1"></a> <span class="co"># 2. Liefert ein Lager, dessen Schalter aus ist?</span></span>
<span id="cb14-114"><a href="#cb14-114" aria-hidden="true" tabindex="-1"></a> liefert <span class="op">=</span> x.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">&gt;</span> <span class="fl">1e-6</span></span>
<span id="cb14-115"><a href="#cb14-115" aria-hidden="true" tabindex="-1"></a> geschlossen_aber_aktiv <span class="op">=</span> np.where(liefert <span class="op">&amp;</span> (y <span class="op">&lt;</span> <span class="fl">0.5</span>))[<span class="dv">0</span>]</span>
<span id="cb14-116"><a href="#cb14-116" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> geschlossen_aber_aktiv.size:</span>
<span id="cb14-117"><a href="#cb14-117" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-118"><a href="#cb14-118" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Lager </span><span class="sc">{</span>geschlossen_aber_aktiv<span class="sc">.</span>tolist()<span class="sc">}</span><span class="ss"> liefern Ware, &quot;</span></span>
<span id="cb14-119"><a href="#cb14-119" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;gelten im Modell aber als geschlossen&quot;</span>)</span>
<span id="cb14-120"><a href="#cb14-120" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-121"><a href="#cb14-121" aria-hidden="true" tabindex="-1"></a> <span class="co"># 3. Stimmt der Zielwert mit den echten Kosten ueberein?</span></span>
<span id="cb14-122"><a href="#cb14-122" aria-hidden="true" tabindex="-1"></a> echte_fixkosten <span class="op">=</span> FIXKOSTEN[liefert].<span class="bu">sum</span>()</span>
<span id="cb14-123"><a href="#cb14-123" aria-hidden="true" tabindex="-1"></a> echte_transportkosten <span class="op">=</span> <span class="bu">float</span>((TRANSPORT <span class="op">*</span> x).<span class="bu">sum</span>())</span>
<span id="cb14-124"><a href="#cb14-124" aria-hidden="true" tabindex="-1"></a> echte_kosten <span class="op">=</span> echte_fixkosten <span class="op">+</span> echte_transportkosten</span>
<span id="cb14-125"><a href="#cb14-125" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> <span class="bu">abs</span>(echte_kosten <span class="op">-</span> ergebnis[<span class="st">&quot;zielwert&quot;</span>]) <span class="op">&gt;</span> <span class="fl">1e-4</span> <span class="op">*</span> <span class="bu">max</span>(<span class="fl">1.0</span>, echte_kosten):</span>
<span id="cb14-126"><a href="#cb14-126" aria-hidden="true" tabindex="-1"></a> beanstandungen.append(</span>
<span id="cb14-127"><a href="#cb14-127" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;Zielwert </span><span class="sc">{</span>ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss"> weicht von den echten Kosten &quot;</span></span>
<span id="cb14-128"><a href="#cb14-128" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>echte_kosten<span class="sc">:,.2f}</span><span class="ss"> ab (Differenz </span><span class="sc">{</span>echte_kosten <span class="op">-</span> ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss">)&quot;</span>)</span>
<span id="cb14-129"><a href="#cb14-129" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-130"><a href="#cb14-130" aria-hidden="true" tabindex="-1"></a> <span class="cf">return</span> <span class="kw">not</span> beanstandungen, beanstandungen</span>
<span id="cb14-131"><a href="#cb14-131" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-132"><a href="#cb14-132" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-133"><a href="#cb14-133" aria-hidden="true" tabindex="-1"></a><span class="kw">def</span> zeige(titel: <span class="bu">str</span>, ergebnis: <span class="bu">dict</span>) <span class="op">-&gt;</span> <span class="va">None</span>:</span>
<span id="cb14-134"><a href="#cb14-134" aria-hidden="true" tabindex="-1"></a> x, y <span class="op">=</span> ergebnis[<span class="st">&quot;x&quot;</span>], ergebnis[<span class="st">&quot;y&quot;</span>]</span>
<span id="cb14-135"><a href="#cb14-135" aria-hidden="true" tabindex="-1"></a> liefert <span class="op">=</span> x.<span class="bu">sum</span>(axis<span class="op">=</span><span class="dv">1</span>) <span class="op">&gt;</span> <span class="fl">1e-6</span></span>
<span id="cb14-136"><a href="#cb14-136" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="ch">\n</span><span class="sc">{</span>titel<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-137"><a href="#cb14-137" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Zielwert laut Solver </span><span class="sc">{</span>ergebnis[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:&gt;14,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb14-138"><a href="#cb14-138" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Knoten / Zeit </span><span class="sc">{</span>ergebnis[<span class="st">&#39;knoten&#39;</span>]<span class="sc">:&gt;14,}</span><span class="ss"> &quot;</span></span>
<span id="cb14-139"><a href="#cb14-139" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;/ </span><span class="sc">{</span>ergebnis[<span class="st">&#39;dauer&#39;</span>]<span class="sc">:.3f}</span><span class="ss"> s&quot;</span>)</span>
<span id="cb14-140"><a href="#cb14-140" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Lager mit y = 1 </span><span class="sc">{</span><span class="bu">int</span>((y <span class="op">&gt;</span> <span class="fl">0.5</span>).<span class="bu">sum</span>())<span class="sc">:&gt;14}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-141"><a href="#cb14-141" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Lager, die tatsaechlich liefern </span><span class="sc">{</span><span class="bu">int</span>(liefert.<span class="bu">sum</span>())<span class="sc">:&gt;10}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-142"><a href="#cb14-142" aria-hidden="true" tabindex="-1"></a> unter <span class="op">=</span> y[y <span class="op">&lt;</span> <span class="fl">0.5</span>]</span>
<span id="cb14-143"><a href="#cb14-143" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; groesster y-Wert unter 0.5 &quot;</span></span>
<span id="cb14-144"><a href="#cb14-144" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>(unter.<span class="bu">max</span>() <span class="cf">if</span> unter.size <span class="cf">else</span> <span class="fl">0.0</span>)<span class="sc">:&gt;14.3e}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-145"><a href="#cb14-145" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; Fixkosten real / verbucht </span><span class="sc">{</span>FIXKOSTEN[liefert]<span class="sc">.</span><span class="bu">sum</span>()<span class="sc">:&gt;14,.2f}</span><span class="ss"> &quot;</span></span>
<span id="cb14-146"><a href="#cb14-146" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;/ </span><span class="sc">{</span><span class="bu">float</span>((FIXKOSTEN <span class="op">*</span> y).<span class="bu">sum</span>())<span class="sc">:,.2f}</span><span class="ss"> EUR&quot;</span>)</span>
<span id="cb14-147"><a href="#cb14-147" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-148"><a href="#cb14-148" aria-hidden="true" tabindex="-1"></a> in_ordnung, beanstandungen <span class="op">=</span> pruefe(ergebnis)</span>
<span id="cb14-149"><a href="#cb14-149" aria-hidden="true" tabindex="-1"></a> <span class="cf">if</span> in_ordnung:</span>
<span id="cb14-150"><a href="#cb14-150" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: bestanden&quot;</span>)</span>
<span id="cb14-151"><a href="#cb14-151" aria-hidden="true" tabindex="-1"></a> <span class="cf">else</span>:</span>
<span id="cb14-152"><a href="#cb14-152" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; PRUEFUNG: DURCHGEFALLEN&quot;</span>)</span>
<span id="cb14-153"><a href="#cb14-153" aria-hidden="true" tabindex="-1"></a> <span class="cf">for</span> text <span class="kw">in</span> beanstandungen:</span>
<span id="cb14-154"><a href="#cb14-154" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot; - </span><span class="sc">{</span>text<span class="sc">}</span><span class="ss">&quot;</span>)</span>
<span id="cb14-155"><a href="#cb14-155" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-156"><a href="#cb14-156" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-157"><a href="#cb14-157" 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="cb14-158"><a href="#cb14-158" 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="cb14-159"><a href="#cb14-159" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; DIE BIG-M-FALLE: STANDORTPLANUNG, 12 LAGER, 40 KUNDEN&quot;</span>)</span>
<span id="cb14-160"><a href="#cb14-160" 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="cb14-161"><a href="#cb14-161" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Tatsaechliche Lagerkapazitaet: </span><span class="sc">{</span>KAPAZITAET<span class="sc">:,.1f}</span><span class="ss"> Einheiten.&quot;</span>)</span>
<span id="cb14-162"><a href="#cb14-162" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Genau das ist das kleinstmoegliche gueltige M - mehr kann ein Lager&quot;</span>)</span>
<span id="cb14-163"><a href="#cb14-163" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ohnehin nicht ausliefern.&quot;</span>)</span>
<span id="cb14-164"><a href="#cb14-164" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-165"><a href="#cb14-165" aria-hidden="true" tabindex="-1"></a> knapp <span class="op">=</span> loese(KAPAZITAET)</span>
<span id="cb14-166"><a href="#cb14-166" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[1] M = Kapazitaet (richtig gewaehlt)&quot;</span>, knapp)</span>
<span id="cb14-167"><a href="#cb14-167" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-168"><a href="#cb14-168" aria-hidden="true" tabindex="-1"></a> gross <span class="op">=</span> loese(<span class="fl">1e7</span> <span class="op">*</span> KAPAZITAET, presolve<span class="op">=</span><span class="st">&quot;off&quot;</span>)</span>
<span id="cb14-169"><a href="#cb14-169" aria-hidden="true" tabindex="-1"></a> zeige(<span class="st">&quot;[2] M = 10 Millionen mal Kapazitaet, Presolve abgeschaltet&quot;</span>, gross)</span>
<span id="cb14-170"><a href="#cb14-170" aria-hidden="true" tabindex="-1"></a></span>
<span id="cb14-171"><a href="#cb14-171" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;</span><span class="ch">\n</span><span class="st">&quot;</span> <span class="op">+</span> <span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span>
<span id="cb14-172"><a href="#cb14-172" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; WAS DA PASSIERT IST&quot;</span>)</span>
<span id="cb14-173"><a href="#cb14-173" 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="cb14-174"><a href="#cb14-174" aria-hidden="true" tabindex="-1"></a> fehlbetrag <span class="op">=</span> knapp[<span class="st">&quot;zielwert&quot;</span>] <span class="op">-</span> gross[<span class="st">&quot;zielwert&quot;</span>]</span>
<span id="cb14-175"><a href="#cb14-175" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Lauf [2] meldet </span><span class="sc">{</span>gross[<span class="st">&#39;zielwert&#39;</span>]<span class="sc">:,.2f}</span><span class="ss"> EUR und sieht damit um&quot;</span>)</span>
<span id="cb14-176"><a href="#cb14-176" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;</span><span class="sc">{</span>fehlbetrag<span class="sc">:,.2f}</span><span class="ss"> EUR BESSER aus als die richtige Loesung - ein Ergebnis,&quot;</span>)</span>
<span id="cb14-177"><a href="#cb14-177" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ueber das sich jeder Auftraggeber freuen wuerde.&quot;</span>)</span>
<span id="cb14-178"><a href="#cb14-178" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-179"><a href="#cb14-179" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;Der Grund steht in der Zeile &#39;groesster y-Wert unter 0.5&#39;: Die&quot;</span>)</span>
<span id="cb14-180"><a href="#cb14-180" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Schaltervariablen stehen bei rund &quot;</span></span>
<span id="cb14-181"><a href="#cb14-181" aria-hidden="true" tabindex="-1"></a> <span class="ss">f&quot;</span><span class="sc">{</span>gross[<span class="st">&#39;y&#39;</span>][gross[<span class="st">&#39;y&#39;</span>] <span class="op">&lt;</span> <span class="fl">0.5</span>]<span class="sc">.</span><span class="bu">max</span>()<span class="sc">:.0e}</span><span class="ss">.&quot;</span>)</span>
<span id="cb14-182"><a href="#cb14-182" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="ss">f&quot;Das ist kleiner als die Ganzzahltoleranz </span><span class="sc">{</span>GANZZAHL_TOLERANZ<span class="sc">:.0e}</span><span class="ss">, also gilt&quot;</span>)</span>
<span id="cb14-183"><a href="#cb14-183" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;y = 0 - &#39;Lager geschlossen&#39;. Zugleich ist M so gross, dass&quot;</span>)</span>
<span id="cb14-184"><a href="#cb14-184" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot; sum_j x_ij &lt;= M * 1e-8&quot;</span>)</span>
<span id="cb14-185"><a href="#cb14-185" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;immer noch reichlich Liefermenge erlaubt. Die Lager liefern also,&quot;</span>)</span>
<span id="cb14-186"><a href="#cb14-186" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ohne dass ihre Fixkosten je bezahlt werden. Der Fachbegriff dafuer&quot;</span>)</span>
<span id="cb14-187"><a href="#cb14-187" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;ist &#39;trickle flow&#39;.&quot;</span>)</span>
<span id="cb14-188"><a href="#cb14-188" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>()</span>
<span id="cb14-189"><a href="#cb14-189" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;WICHTIG: Mit eingeschaltetem Presolve (Standard) faellt HiGHS hier&quot;</span>)</span>
<span id="cb14-190"><a href="#cb14-190" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht darauf herein - es zieht M selbst zurecht. Verlassen Sie sich&quot;</span>)</span>
<span id="cb14-191"><a href="#cb14-191" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;nicht darauf: Presolve kann das nur, wenn eine implizite Schranke&quot;</span>)</span>
<span id="cb14-192"><a href="#cb14-192" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;herleitbar ist. Die Pruefung aus pruefe() kostet Millisekunden und&quot;</span>)</span>
<span id="cb14-193"><a href="#cb14-193" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;funktioniert immer.&quot;</span>)</span>
<span id="cb14-194"><a href="#cb14-194" aria-hidden="true" tabindex="-1"></a> <span class="bu">print</span>(<span class="st">&quot;=&quot;</span> <span class="op">*</span> <span class="dv">78</span>)</span></code></pre></div>
<p><strong>Erwartete Ausgabe:</strong></p>
<pre><code>==============================================================================
DIE BIG-M-FALLE: STANDORTPLANUNG, 12 LAGER, 40 KUNDEN
==============================================================================
Tatsaechliche Lagerkapazitaet: 722.4 Einheiten.
Genau das ist das kleinstmoegliche gueltige M - mehr kann ein Lager
ohnehin nicht ausliefern.
[1] M = Kapazitaet (richtig gewaehlt)
Zielwert laut Solver 26,525.28 EUR
Knoten / Zeit 13 / 0.075 s
Lager mit y = 1 3
Lager, die tatsaechlich liefern 3
groesster y-Wert unter 0.5 0.000e+00
Fixkosten real / verbucht 10,671.79 / 10,671.79 EUR
PRUEFUNG: bestanden
[2] M = 10 Millionen mal Kapazitaet, Presolve abgeschaltet
Zielwert laut Solver 12,441.96 EUR
Knoten / Zeit 1 / 0.006 s
Lager mit y = 1 0
Lager, die tatsaechlich liefern 11
groesster y-Wert unter 0.5 4.074e-08
Fixkosten real / verbucht 35,847.91 / 0.00 EUR
PRUEFUNG: DURCHGEFALLEN
- Lager [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 11] liefern Ware, gelten im Modell aber als geschlossen
- Zielwert 12,441.96 weicht von den echten Kosten 48,289.86 ab (Differenz 35,847.91)
==============================================================================
WAS DA PASSIERT IST
==============================================================================
Lauf [2] meldet 12,441.96 EUR und sieht damit um
14,083.32 EUR BESSER aus als die richtige Loesung - ein Ergebnis,
ueber das sich jeder Auftraggeber freuen wuerde.
Der Grund steht in der Zeile &#39;groesster y-Wert unter 0.5&#39;: Die
Schaltervariablen stehen bei rund 4e-08.
Das ist kleiner als die Ganzzahltoleranz 1e-06, also gilt
y = 0 - &#39;Lager geschlossen&#39;. Zugleich ist M so gross, dass
sum_j x_ij &lt;= M * 1e-8
immer noch reichlich Liefermenge erlaubt. Die Lager liefern also,
ohne dass ihre Fixkosten je bezahlt werden. Der Fachbegriff dafuer
ist &#39;trickle flow&#39;.
WICHTIG: Mit eingeschaltetem Presolve (Standard) faellt HiGHS hier
nicht darauf herein - es zieht M selbst zurecht. Verlassen Sie sich
nicht darauf: Presolve kann das nur, wenn eine implizite Schranke
herleitbar ist. Die Pruefung aus pruefe() kostet Millisekunden und
funktioniert immer.
==============================================================================</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 zählt</th>
</tr>
</thead>
<tbody>
<tr class="odd">
<td><code>presolve="off"</code></td>
<td>schaltet HiGHS Vorverarbeitung ab</td>
<td>Mit Standardeinstellung fällt HiGHS <strong>nicht</strong> herein: Presolve zieht das übergroße <span class="math inline">M</span> selbst zurecht. Der Fehler ist trotzdem real — Presolve kann das nur, wenn eine implizite Schranke im Modell steckt. Verlassen Sie sich nicht auf eine Rettung, die Sie nicht kontrollieren.</td>
</tr>
<tr class="even">
<td><code>np.abs(y - np.round(y))</code></td>
<td>prüft, ob Binärvariablen wirklich binär sind</td>
<td>Ein Wert von <span class="math inline">4 \cdot 10^{-8}</span> liegt unter der Toleranz <span class="math inline">10^{-6}</span> und gilt dem Solver als 0. Für Ihr Geschäft ist er es nicht.</td>
</tr>
<tr class="odd">
<td><code>liefert &amp; (y &lt; 0.5)</code></td>
<td>sucht Lager, die liefern, obwohl der Schalter aus ist</td>
<td>Die inhaltliche Prüfung: Widerspricht die Lösung sich selbst?</td>
</tr>
<tr class="even">
<td>Kosten <strong>aus der Lösung</strong> neu berechnen</td>
<td>statt dem Zielwert zu glauben</td>
<td>Der wichtigste Test überhaupt. Er vergleicht nicht Modell mit Modell, sondern Modell mit Wirklichkeit — und deckt jeden Fehler dieser Art auf, egal wodurch er entstanden ist.</td>
</tr>
</tbody>
</table>
</blockquote>
<hr />
<h2 id="sec:milp-quiz">6.11 Micro-Quiz</h2>
<div class="card card-quiz">
<blockquote>
<p><strong>❓ Micro-Quiz 6: Drei Fragen zum Selbstcheck</strong></p>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <a href="anhang-loesungen.html#quiz-loesung-milp">Anhang A</a>.</p>
<p><strong>1. Ihr Solver meldet nach dem Zeitlimit: Incumbent 48 200 €, Schranke 47 100 €. Was berichten Sie dem Auftraggeber?</strong> (a) „Der Solver ist nicht fertig geworden, das Ergebnis ist unbrauchbar.“ (b) „Wir haben einen Plan für 48 200 €. Er ist garantiert höchstens 2,3 % teurer als das theoretisch bestmögliche.“ (c) „Der optimale Wert liegt bei 47 100 €, wir müssen den Plan nur noch dorthin verbessern.“</p>
<p><strong>2. In Ihrem Fixkostenmodell ist <span class="math inline">M = 10^9</span>, die tatsächliche Anlagenkapazität beträgt 500. Welche Gefahr ist die größere?</strong> (a) Der Solver braucht mehr Knoten und wird langsamer. (b) Binärvariablen können bei Werten um <span class="math inline">10^{-8}</span> hängenbleiben, gelten dem Solver als 0 — und Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden. (c) Die Zielfunktion wird numerisch instabil und liefert negative Kosten.</p>
<p><strong>3. Sie geben CP-SAT über <code>AddHint()</code> eine Startlösung mit, die eine Nebenbedingung verletzt. Was passiert?</strong> (a) Der Solver meldet <code>INFEASIBLE</code>, weil der Hinweis unzulässig ist. (b) Der Solver übernimmt den Hinweis und liefert eine unzulässige Lösung. (c) Der Hinweis wird verworfen; das Ergebnis bleibt korrekt, der erhoffte Zeitgewinn bleibt aber aus.</p>
</blockquote>
</div>
<hr />
<h2 id="sec:milp-selbsttest">6.12 Selbsttest</h2>
<blockquote>
<p>Antworten: <a href="anhang-loesungen.html#selbsttest-loesung-milp">Anhang A</a>.</p>
</blockquote>
<ol type="1">
<li>Warum liefert die LP-Relaxation bei Maximierung eine <strong>obere</strong> Schranke?</li>
<li>Nennen Sie die drei Gründe, aus denen ein Branch-and-Bound-Ast gekappt werden darf.</li>
<li>Wie modelliert man „<span class="math inline">x</span> ist entweder 0 oder mindestens 500“?</li>
<li>Warum verschlechtert ein zu großes <span class="math inline">M</span> die Laufzeit, obwohl das Modell korrekt bleibt?</li>
<li>Was bedeutet ein MIP-Gap von 3 %?</li>
</ol>
<hr />
<h2 id="sec:milp-zusammenfassung">6.13 Zusammenfassung</h2>
<ul>
<li><strong>Runden ist keine Lösung:</strong> Aufrunden ist fast immer unzulässig (98100 % der Fälle), Abrunden verliert im Mittel 1026 %, im Extremfall alles.</li>
<li><strong>Branch-and-Bound</strong> löst LP-Relaxationen, verzweigt an gebrochenen Variablen und kappt Äste anhand von Schranken. Der Preis: NP-Schwere.</li>
<li><strong>Fünf Modellierungsmuster</strong> decken die meisten Praxisfälle ab: Fixkosten, Entweder-Oder, Wenn-Dann, Kardinalität, semikontinuierlich.</li>
<li><strong><span class="math inline">M</span> so klein wie möglich</strong> — nicht nur wegen der Laufzeit. Ein übergroßes <span class="math inline">M</span> kann Binärvariablen bei <span class="math inline">10^{-8}</span> hängen lassen, wo sie dem Solver als 0 gelten: Anlagen produzieren, ohne dass ihre Fixkosten je verbucht werden (<em>Trickle Flow</em>).</li>
<li><strong><code>OPTIMAL</code> ist nicht der Normalfall.</strong> Werten Sie jeden Status aus und lesen Sie den <strong>MIP-Gap</strong> — er ist eine Garantie („höchstens 2,3 % vom Optimum“), keine Schätzung. Der Beweis der Optimalität kostet meist ein Vielfaches dessen, was das Finden der optimalen Lösung kostet.</li>
<li><strong>Warm-Starts können nichts verfälschen, aber auch nichts versprechen.</strong> Ob ein Startwert hilft, hängt davon ab, ob er besser ist als das, was der Solver in derselben Zeit von allein findet — im Buch gemessen: einmal Faktor 2,2, einmal gar nichts.</li>
<li><strong>Prüfen Sie jede modellierte Regel</strong> nach dem Lösen mit einer <code>assert</code>-Zeile — und rechnen Sie den Zielwert aus der Lösung nach, statt ihm zu glauben.</li>
</ul>
<p><strong>Ausblick.</strong> <a href="cpsat.html#kap-cpsat">Kapitel 7</a> wechselt das Paradigma: Bei Dienstplänen, Zuordnungen und Reihenfolgen ist die algebraische MILP-Sicht oft unhandlich. Constraint Programming denkt stattdessen in Wertebereichen und logischen Regeln — und löst solche Probleme um Größenordnungen schneller.</p>
</article>
<button type="button" class="fortschritt-knopf" data-kapitel="milp.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="lp.html"><svg class="icon" aria-hidden="true"><use href="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a><a class="prev-next-knopf prev-next-next" href="cpsat.html"><span><small>Weiter</small>Kapitel 7: Constraint Programming mit CP-SAT{idx:CP-SAT} — Logik, Scheduling und Zuweisung</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>