<aclass="brand"href="index.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-book"></use></svg><span>Optimierte Entscheidungsfindung mit Python</span></a>
<navclass="sidebar"id="sidebar"aria-label="Kapitelnavigation"><divclass="sidebar-inhalt"><detailsclass="sidebar-gruppe"><summary>Einstieg</summary><ul><lidata-kapitel="vorwort.html"><ahref="vorwort.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><lidata-kapitel="notation.html"><ahref="notation.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><lidata-kapitel="einfuehrung.html"><ahref="einfuehrung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><lidata-kapitel="fundament.html"><ahref="fundament.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><lidata-kapitel="oekosystem.html"><ahref="oekosystem.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><lidata-kapitel="modellierung.html"><ahref="modellierung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><lidata-kapitel="synthese-grundlagen.html"><ahref="synthese-grundlagen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><detailsclass="sidebar-gruppe"open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><lidata-kapitel="lp.html"><ahref="lp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><lidata-kapitel="milp.html"class="aktiv"><ahref="milp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><lidata-kapitel="cpsat.html"><ahref="cpsat.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><lidata-kapitel="graphen.html"><ahref="graphen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a></li><lidata-kapitel="metaheuristiken.html"><ahref="metaheuristiken.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></a></li><lidata-kapitel="dekomposition.html"><ahref="dekomposition.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span></a></li><lidata-kapitel="synthese-kernverfahren.html"><ahref="synthese-kernverfahren.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil II</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil III: Nichtlinearität, Unsicherheit
<navclass="breadcrumb"aria-label="Breadcrumb"><ahref="index.html">Start</a>›<span>Teil II</span>›<span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="lp.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a><aclass="prev-next-knopf prev-next-next"href="cpsat.html"><span><small>Weiter</small>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-milp">Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</h1>
<divclass="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>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>Notebook:</strong><ahref="Notebooks_04/milp.ipynb">milp.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<h2id="sec:milp-schnellstart">6.1 In 5 Minuten gelöst</h2>
<divclass="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>
<spanid="cb1-9"><ahref="#cb1-9"aria-hidden="true"tabindex="-1"></a>A[<spanclass="dv">0</span>, :<spanclass="dv">3</span>] <spanclass="op">=</span><spanclass="fl">1.0</span><spanclass="co"># Summe der Mengen = 100</span></span>
<spanid="cb1-10"><ahref="#cb1-10"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="dv">3</span>):</span>
<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>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Vorgehen</th>
<th>Ergebnis</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td>Abrunden auf <spanclass="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>
<trclass="even">
<td>Aufrunden auf <spanclass="math inline">y = (0, 1, 1)</span></td>
<td>Zulässig, aber <strong>947 €</strong> — 65 € teurer als nötig</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 <ahref="#sec:milp-branch-and-bound">Abschnitt 6.4</a>.</p>
<hr/>
<h2id="sec:milp-lernziele">6.2 Lernziele</h2>
<p>Nach diesem Kapitel können Sie …</p>
<oltype="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 <spanclass="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/>
<h2id="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 (<spanclass="math inline">\mathbb{Z}</span>) und binäre (<spanclass="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 20–50 % des Gewinns“. Rechnen wir es nach, statt es zu behaupten.</p>
<figure>
<imgsrc="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."/>
<figcaptionaria-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 <spanclass="math inline">(1;1)</span> führt aus dem zulässigen Bereich heraus, Abrunden auf <spanclass="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><spanclass="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 <spanclass="math inline">x_1 + x_2 = 1{,}5</span> ist optimal, z. B. <spanclass="math inline">x_1 = x_2 = 0{,}75</span> mit <spanclass="math inline">Z_{LP} = 1{,}5</span>.</p>
<p><strong>Aufrunden</strong> auf <spanclass="math inline">(1, 1)</span>: <spanclass="math inline">2\cdot1 + 2\cdot1 = 4 > 3</span> → <strong>unzulässig</strong>. <strong>Abrunden</strong> auf <spanclass="math inline">(0, 0)</span>: zulässig, aber <spanclass="math inline">Z = 0</span>. <strong>Wahres ganzzahliges Optimum:</strong><spanclass="math inline">(1, 0)</span> oder <spanclass="math inline">(0, 1)</span> mit <spanclass="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>
<oltype="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>
<spanid="cb3-7"><ahref="#cb3-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ersetzt die unbelegte Behauptung "20-50 % Verlust" durch eine Messung ueber</span></span>
<spanid="cb3-15"><ahref="#cb3-15"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> erzeuge_instanz(n, m, rng):</span>
<spanid="cb3-16"><ahref="#cb3-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Zufaelliges Rucksack-aehnliches MILP mit kleinen Zahlen."""</span></span>
<spanid="cb3-17"><ahref="#cb3-17"aria-hidden="true"tabindex="-1"></a> c <spanclass="op">=</span> rng.integers(<spanclass="dv">3</span>, <spanclass="dv">20</span>, size<spanclass="op">=</span>n).astype(<spanclass="bu">float</span>) <spanclass="co"># Ertraege</span></span>
<spanid="cb3-18"><ahref="#cb3-18"aria-hidden="true"tabindex="-1"></a> A <spanclass="op">=</span> rng.integers(<spanclass="dv">1</span>, <spanclass="dv">9</span>, size<spanclass="op">=</span>(m, n)).astype(<spanclass="bu">float</span>) <spanclass="co"># Verbraeuche</span></span>
<spanid="cb3-19"><ahref="#cb3-19"aria-hidden="true"tabindex="-1"></a> b <spanclass="op">=</span> (A.<spanclass="bu">sum</span>(axis<spanclass="op">=</span><spanclass="dv">1</span>) <spanclass="op">*</span> rng.uniform(<spanclass="fl">0.25</span>, <spanclass="fl">0.45</span>)).<spanclass="bu">round</span>() <spanclass="co"># knappe Kapazitaeten</span></span>
<spanid="cb3-20"><ahref="#cb3-20"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span> c, A, b</span>
<spanid="cb3-23"><ahref="#cb3-23"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> loese_lp(c, A, b, ganzzahlig<spanclass="op">=</span><spanclass="va">False</span>):</span>
<spanid="cb3-24"><ahref="#cb3-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""LP-Relaxation oder exaktes MILP ueber HiGHS."""</span></span>
<spanid="cb3-25"><ahref="#cb3-25"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(c)</span>
<spanid="cb3-33"><ahref="#cb3-33"aria-hidden="true"tabindex="-1"></a><spanclass="kw">def</span> abrunden_und_reparieren(x_lp, c, A, b):</span>
<spanid="cb3-34"><ahref="#cb3-34"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Naive Strategie: abrunden, dann gierig auffuellen, solange zulaessig."""</span></span>
<spanid="cb3-35"><ahref="#cb3-35"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> np.floor(x_lp <spanclass="op">+</span><spanclass="fl">1e-9</span>)</span>
<spanid="cb3-52"><ahref="#cb3-52"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)"</span>)</span>
<spanid="cb3-89"><ahref="#cb3-89"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Lesart: 'Aufrunden unzul.' = Anteil der Faelle, in denen die aufgerundete"</span>)</span>
<spanid="cb3-90"><ahref="#cb3-90"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Loesung eine Nebenbedingung verletzt. 'Verlust' = Abstand zum exakten Optimum."</span>)</span>
<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 16–18 %</strong>, in den schlimmsten gemessenen Fällen <strong>100 %</strong> (dann bleibt das Ergebnis bei null). Die eingangs zitierte Behauptung „20–50 %“ 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>
<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>
<imgsrc="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."/>
<figcaptionaria-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>
<h3id="die-drei-phasen">Die drei Phasen</h3>
<oltype="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 <spanclass="math inline">x_1 = 2{,}7</span>, wird das Problem in zwei disjunkte Teilprobleme zerlegt: <spanclass="math display">\text{Ast 1: } x_1 \le 2 \qquad\text{und}\qquad \text{Ast 2: } x_1 \ge 3</span> Der Wert <spanclass="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>
<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><strong>Ast B (<spanclass="math inline">x_2 \ge 2</span>):</strong> LP liefert <spanclass="math inline">x = (2{,}67;\ 2)</span>, <spanclass="math inline">Z = 21{,}33</span>… halt: Prüfen wir <spanclass="math inline">6x_1+4x_2\le24</span> mit <spanclass="math inline">x_2=2</span>: <spanclass="math inline">6x_1 \le 16</span>, also <spanclass="math inline">x_1 \le 2{,}67</span>; und <spanclass="math inline">x_1+2x_2\le6</span> gibt <spanclass="math inline">x_1 \le 2</span>. Also <spanclass="math inline">x_1 = 2</span>, <spanclass="math inline">Z = 5\cdot2+4\cdot2 = 18</span> → ganzzahlig, aber <strong>schlechter</strong> als der Incumbent 20 → verworfen.</p>
<p><strong>Ergebnis:</strong><spanclass="math inline">x^* = (4;\ 0)</span>, <spanclass="math inline">Z^* = 20</span>. Wir haben <strong>fünf</strong> LPs gelöst — <spanclass="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 <spanclass="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 <ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>).</p>
</blockquote>
<hr/>
<h2id="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 <spanclass="math inline">y \in \{0,1\}</span> und einer hinreichend großen Konstante <spanclass="math inline">M</span>.</p>
<p><em>Regel:</em> Wenn <spanclass="math inline">y_1 = 1</span>, dann muss auch <spanclass="math inline">y_2 = 1</span> sein.</p>
<p><spanclass="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: <spanclass="math inline">(0,0)</span> ✓, <spanclass="math inline">(0,1)</span> ✓, <spanclass="math inline">(1,1)</span> ✓, <spanclass="math inline">(1,0)</span> ✗ — genau die verbotene Kombination wird ausgeschlossen.</p>
<h3id="muster-4-kardinalität-höchstens-k-aus-n">Muster 4 — Kardinalität („höchstens <spanclass="math inline">K</span> aus <spanclass="math inline">N</span>“)</h3>
<p><spanclass="math display">
\sum_{j=1}^N y_j \le K
</span></p>
<h3id="muster-5-semikontinuierlich-entweder-0-oder-mindestens-l">Muster 5 — Semikontinuierlich („entweder 0 oder mindestens <spanclass="math inline">L</span>“)</h3>
<p><em>Regel:</em> Eine Position ist entweder gar nicht besetzt oder mit mindestens <spanclass="math inline">L</span> Euro.</p>
<p><spanclass="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: <spanclass="math inline">M</span> so klein wie möglich!</strong></p>
<p><spanclass="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 <spanclass="math inline">M = 10^9</span> und <spanclass="math inline">x_j \le 10^9 y_j</span> genügt schon <spanclass="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 <spanclass="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>
<trclass="header">
<th><spanclass="math inline">M</span></th>
<thstyle="text-align: right;">Knoten im Suchbaum</th>
<td><spanclass="math inline">10\times</span> zu groß</td>
<tdstyle="text-align: right;">51</td>
<td>richtig</td>
</tr>
<trclass="odd">
<td><spanclass="math inline">10^4\times</span> zu groß</td>
<tdstyle="text-align: right;">51</td>
<td>richtig</td>
</tr>
<trclass="even">
<td><spanclass="math inline">10^7\times</span> zu groß</td>
<tdstyle="text-align: right;">51</td>
<td>richtig</td>
</tr>
</tbody>
</table>
<p>Zwei Dinge sind daran bemerkenswert. Erstens: Das knappe <spanclass="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 <spanclass="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 <ahref="#sec:milp-denkfehler">Abschnitt 6.10</a>.</p>
</blockquote>
<hr/>
<h2id="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>
<spanid="cb5-5"><ahref="#cb5-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel MILP: Das Rucksackproblem (Knapsack).</span></span>
<spanid="cb5-6"><ahref="#cb5-6"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeigt LP-Relaxation, Branch-and-Bound-Ergebnis und den Preis der Ganzzahligkeit</span></span>
<spanid="cb5-7"><ahref="#cb5-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">an einem Beispiel, das man vollstaendig im Kopf nachvollziehen kann.</span></span>
<spanid="cb5-23"><ahref="#cb5-23"aria-hidden="true"tabindex="-1"></a> bounds<spanclass="op">=</span>[(<spanclass="dv">0</span>, <spanclass="dv">1</span>)] <spanclass="op">*</span> n, <spanclass="co"># jedes Teil hoechstens einmal</span></span>
<spanid="cb5-39"><ahref="#cb5-39"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, name <spanclass="kw">in</span><spanclass="bu">enumerate</span>(GEGENSTAENDE):</span>
<spanid="cb5-40"><ahref="#cb5-40"aria-hidden="true"tabindex="-1"></a><spanclass="co"># int(round(...)) statt Format "%.0f": vermeidet die Ausgabe "-0"</span></span>
<spanid="cb5-48"><ahref="#cb5-48"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Obere Schranke aus der LP-Relaxation: </span><spanclass="sc">{</span>z_lp<spanclass="sc">:.2f}</span><spanclass="ss">"</span>)</span>
<spanid="cb5-53"><ahref="#cb5-53"aria-hidden="true"tabindex="-1"></a> gebrochen <spanclass="op">=</span> [GEGENSTAENDE[i] <spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(NUTZEN)) <spanclass="cf">if</span><spanclass="fl">1e-6</span><spanclass="op"><</span> x_lp[i] <spanclass="op"><</span><spanclass="dv">1</span><spanclass="op">-</span><spanclass="fl">1e-6</span>]</span>
<spanid="cb5-54"><ahref="#cb5-54"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss">In der LP-Loesung nur teilweise eingepackt: </span><spanclass="sc">{</span>gebrochen<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb5-55"><ahref="#cb5-55"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Genau hier wuerde Branch-and-Bound verzweigen:"</span>)</span>
<spanid="cb5-56"><ahref="#cb5-56"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Ast 1: </span><spanclass="sc">{</span>gebrochen[<spanclass="dv">0</span>]<spanclass="sc">}</span><spanclass="ss"> bleibt ganz zuhause (x=0)"</span>)</span>
<spanid="cb5-57"><ahref="#cb5-57"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Ast 2: </span><spanclass="sc">{</span>gebrochen[<spanclass="dv">0</span>]<spanclass="sc">}</span><spanclass="ss"> kommt ganz mit (x=1)"</span>)</span>
<p>Drei Beobachtungen, die den Kern des Kapitels illustrieren:</p>
<oltype="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 <spanclass="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 <ahref="praxisfallen.html#kap-praxisfallen">Kapitel 22</a>.</li>
</ol>
<hr/>
<h2id="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 <spanclass="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 <spanclass="math inline">i</span>: <spanclass="math inline">x_i \ge 0</span> (Euro-Betrag) und <spanclass="math inline">y_i \in \{0,1\}</span> (Position aktiv).</p>
<p><spanclass="math display">
\max \sum_{i=1}^n \mu_i x_i - F\sum_{i=1}^n y_i
</span></p>
<p><spanclass="math display">
\begin{aligned}
\sum_i x_i &= B && \text{(Budget vollständig investiert)}\\
\sum_i y_i &\le K && \text{(höchstens $K$ Positionen)}\\
<p><strong>📐 Formel-Lesehilfe</strong> * <spanclass="math inline">\mu_i</span> — erwartete Jahresrendite der Anlage <spanclass="math inline">i</span> (z. B. 0,11 = 11 %). * <spanclass="math inline">\mu_i x_i</span> — erwarteter Ertrag in Euro. * <spanclass="math inline">F\sum y_i</span> — Summe der Ordergebühren: 50 € je aktivierter Position. * <spanclass="math inline">L y_i \le x_i \le U y_i</span> — die Doppelungleichung erledigt <strong>beides</strong> auf einmal: Ist <spanclass="math inline">y_i=0</span>, folgt <spanclass="math inline">0 \le x_i \le 0</span>, also <spanclass="math inline">x_i=0</span>. Ist <spanclass="math inline">y_i=1</span>, folgt <spanclass="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 (<spanclass="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 (<ahref="praxisfallen.html#sec:praxisfallen-or-kern">Abschnitt 22.6</a>). Der Unterschied ist kein Schönheitsfehler:</p>
<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>
<spanid="cb7-5"><ahref="#cb7-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel MILP: MILP-Portfolio-Selektion mit Fixkosten und Kardinalitaet.</span></span>
<spanid="cb7-7"><ahref="#cb7-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">CP-SAT-freie, gut lesbare Formulierung ueber scipy/HiGHS (kein manueller</span></span>
<spanid="cb7-8"><ahref="#cb7-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">CSR-Matrixaufbau, deutlich leichter nachvollziehbar), mit Vergleich gegen die</span></span>
<spanid="cb7-9"><ahref="#cb7-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Loesung OHNE Restriktionen.</span></span>
<spanid="cb7-11"><ahref="#cb7-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die Auswertung laeuft ueber SolverStatus und das Loesung-Objekt aus</span></span>
<spanid="cb7-12"><ahref="#cb7-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">or_kern.py: Alle Statusfaelle werden behandelt, und der MIP-Gap steht im</span></span>
<spanid="cb7-13"><ahref="#cb7-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">Bericht - statt eines blossen 'success', das nicht verraet, ob der Solver</span></span>
<spanid="cb7-14"><ahref="#cb7-14"aria-hidden="true"tabindex="-1"></a><spanclass="co">fertig geworden ist oder nur aufgegeben hat.</span></span>
<spanid="cb7-35"><ahref="#cb7-35"aria-hidden="true"tabindex="-1"></a>MAX_POSITION <spanclass="op">=</span><spanclass="fl">40_000.0</span><spanclass="co"># U (dient zugleich als Big-M!)</span></span>
<spanid="cb7-36"><ahref="#cb7-36"aria-hidden="true"tabindex="-1"></a>GEBUEHR <spanclass="op">=</span><spanclass="fl">50.0</span><spanclass="co"># F, je aktivierter Position</span></span>
<spanid="cb7-64"><ahref="#cb7-64"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N):</span>
<spanid="cb7-72"><ahref="#cb7-72"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N):</span>
<spanid="cb7-96"><ahref="#cb7-96"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Zurueck in die Maximierungswelt: Zielwert UND Schranke negieren.</span></span>
<spanid="cb7-97"><ahref="#cb7-97"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Aus beiden zusammen rechnet das Loesung-Objekt den MIP-Gap aus.</span></span>
<spanid="cb7-98"><ahref="#cb7-98"aria-hidden="true"tabindex="-1"></a> x, y <spanclass="op">=</span> ergebnis.x[:N], np.<spanclass="bu">round</span>(ergebnis.x[N:])</span>
<spanid="cb7-109"><ahref="#cb7-109"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Vergleichsfall: nur Budget, keine Gebuehren/Kardinalitaet/Mindestgroesse."""</span></span>
<spanid="cb7-114"><ahref="#cb7-114"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="ss">f"Vergleichsfall nicht loesbar: </span><spanclass="sc">{</span>status<spanclass="sc">.</span>value<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb7-119"><ahref="#cb7-119"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Prueft die Loesung gegen die Anforderungen - ohne den Solver zu fragen.</span></span>
<spanid="cb7-121"><ahref="#cb7-121"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Bewusst kein assert: Eine Beanstandungsliste laesst sich protokollieren,</span></span>
<spanid="cb7-122"><ahref="#cb7-122"aria-hidden="true"tabindex="-1"></a><spanclass="co"> weiterreichen und testen. Ein assert verschwindet ausserdem, sobald</span></span>
<spanid="cb7-123"><ahref="#cb7-123"aria-hidden="true"tabindex="-1"></a><spanclass="co"> jemand Python mit -O startet.</span></span>
<spanid="cb7-125"><ahref="#cb7-125"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> np.array([loesung.werte[name] <spanclass="cf">for</span> name <spanclass="kw">in</span> ANLAGEN])</span>
<spanid="cb7-126"><ahref="#cb7-126"aria-hidden="true"tabindex="-1"></a> y <spanclass="op">=</span> np.array([loesung.werte[<spanclass="ss">f"aktiv:</span><spanclass="sc">{</span>name<spanclass="sc">}</span><spanclass="ss">"</span>] <spanclass="cf">for</span> name <spanclass="kw">in</span> ANLAGEN])</span>
<spanid="cb7-134"><ahref="#cb7-134"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i, name <spanclass="kw">in</span><spanclass="bu">enumerate</span>(ANLAGEN):</span>
<spanid="cb7-136"><ahref="#cb7-136"aria-hidden="true"tabindex="-1"></a> beanstandungen.append(<spanclass="ss">f"</span><spanclass="sc">{</span>name<spanclass="sc">}</span><spanclass="ss">: y = </span><spanclass="sc">{</span>y[i]<spanclass="sc">!r}</span><spanclass="ss"> ist nicht ganzzahlig"</span>)</span>
<spanid="cb7-151"><ahref="#cb7-151"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" OPTIMALE MILP-PORTFOLIO-ALLOKATION MIT FIXGEBUEHREN"</span>)</span>
<spanid="cb7-156"><ahref="#cb7-156"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Zuerst der Status - erst danach interessieren die Zahlen.</span></span>
<spanid="cb7-158"><ahref="#cb7-158"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">SystemExit</span>(<spanclass="ss">f"Das Modell ist nicht loesbar (</span><spanclass="sc">{</span>loesung<spanclass="sc">.</span>status<spanclass="sc">.</span>value<spanclass="sc">}</span><spanclass="ss">). "</span></span>
<spanid="cb7-162"><ahref="#cb7-162"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zeitlimit erhoehen oder Modell vereinfachen."</span>)</span>
<spanid="cb7-164"><ahref="#cb7-164"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"ACHTUNG: nicht beweisbar optimal - Gap </span><spanclass="sc">{</span>loesung<spanclass="sc">.</span>gap<spanclass="sc">:.2%}</span><spanclass="ch">\n</span><spanclass="ss">"</span>)</span>
<spanid="cb7-166"><ahref="#cb7-166"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> np.array([loesung.werte[name] <spanclass="cf">for</span> name <spanclass="kw">in</span> ANLAGEN])</span>
<spanid="cb7-167"><ahref="#cb7-167"aria-hidden="true"tabindex="-1"></a> y <spanclass="op">=</span> np.array([loesung.werte[<spanclass="ss">f"aktiv:</span><spanclass="sc">{</span>name<spanclass="sc">}</span><spanclass="ss">"</span>] <spanclass="cf">for</span> name <spanclass="kw">in</span> ANLAGEN], dtype<spanclass="op">=</span><spanclass="bu">int</span>)</span>
<spanid="cb7-171"><ahref="#cb7-171"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Aktiv"</span>: [<spanclass="st">"JA"</span><spanclass="cf">if</span> y[i] <spanclass="cf">else</span><spanclass="st">"-"</span><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N)],</span>
<spanid="cb7-172"><ahref="#cb7-172"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Investition (EUR)"</span>: [<spanclass="ss">f"</span><spanclass="sc">{</span>x[i]<spanclass="sc">:,.0f}</span><spanclass="ss">"</span><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N)],</span>
<spanid="cb7-173"><ahref="#cb7-173"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Anteil"</span>: [<spanclass="ss">f"</span><spanclass="sc">{</span>x[i]<spanclass="op">/</span>BUDGET<spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:5.1f}</span><spanclass="ss"> %"</span><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N)],</span>
<spanid="cb7-174"><ahref="#cb7-174"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Erw. Rendite"</span>: [<spanclass="ss">f"</span><spanclass="sc">{</span>RENDITE[i]<spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:4.1f}</span><spanclass="ss"> %"</span><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N)],</span>
<spanid="cb7-175"><ahref="#cb7-175"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Erw. Ertrag (EUR)"</span>: [<spanclass="ss">f"</span><spanclass="sc">{</span>x[i]<spanclass="op">*</span>RENDITE[i]<spanclass="sc">:,.0f}</span><spanclass="ss">"</span><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N)],</span>
<spanid="cb7-187"><ahref="#cb7-187"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Vergleich mit dem unbeschraenkten Fall ---------------------------</span></span>
<spanid="cb7-190"><ahref="#cb7-190"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Zum Vergleich ohne jede Restriktion (alles in den Bestwert): "</span></span>
<spanid="cb7-202"><ahref="#cb7-202"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Abnahmepruefung: alle Nebenbedingungen geprueft und eingehalten."</span>)</span>
<td>übersetzt den SciPy-Rückgabewert in die gemeinsame Sprache aus <ahref="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>
<td>die zweite Zahl aus <ahref="#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>
<trclass="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/>
<h2id="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>
<h3id="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>
<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>
<p>Der zweite Teil der Professionalität ist unspektakulär, aber entscheidend: <strong>jeden</strong> Rückgabewert auswerten, nicht nur <code>OPTIMAL</code>.</p>
<spanid="cb9-5"><ahref="#cb9-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel MILP: Was tun, wenn der Solver nicht fertig wird?</span></span>
<spanid="cb9-7"><ahref="#cb9-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Bei einem LP kommt entweder eine optimale Loesung oder eine klare Absage.</span></span>
<spanid="cb9-8"><ahref="#cb9-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Bei einem MILP ist der haeufigste Ausgang im Betrieb ein dritter: "Ich habe</span></span>
<spanid="cb9-9"><ahref="#cb9-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">eine Loesung, ich weiss aber nicht, ob sie die beste ist - und die Zeit ist</span></span>
<spanid="cb9-10"><ahref="#cb9-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">um." Dieses Programm zeigt, wie man mit diesem Fall umgeht.</span></span>
<spanid="cb9-12"><ahref="#cb9-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. Alle Statusfaelle explizit behandeln, statt OPTIMAL vorauszusetzen.</span></span>
<spanid="cb9-13"><ahref="#cb9-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. Den MIP-Gap lesen: Wie weit kann ich hoechstens danebenliegen?</span></span>
<spanid="cb9-14"><ahref="#cb9-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. Messen, was zusaetzliche Rechenzeit ueberhaupt noch bringt.</span></span>
<spanid="cb9-15"><ahref="#cb9-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 4. Warm-Start ausprobieren - und ehrlich messen, ob er etwas bringt.</span></span>
<spanid="cb9-42"><ahref="#cb9-42"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Alles, was nach einem Solverlauf ausgewertet werden muss - nicht nur</span></span>
<spanid="cb9-43"><ahref="#cb9-43"aria-hidden="true"tabindex="-1"></a><spanclass="co"> der Zielwert."""</span></span>
<spanid="cb9-45"><ahref="#cb9-45"aria-hidden="true"tabindex="-1"></a> brauchbar: <spanclass="bu">bool</span><spanclass="co"># Gibt es ueberhaupt eine zulaessige Loesung?</span></span>
<spanid="cb9-56"><ahref="#cb9-56"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Loest das Standortmodell mit Zeitlimit und wertet ALLE Statusfaelle aus."""</span></span>
<spanid="cb9-66"><ahref="#cb9-66"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N_LAGER):</span>
<spanid="cb9-67"><ahref="#cb9-67"aria-hidden="true"tabindex="-1"></a> hochschule.changeColIntegrality(anzahl_x <spanclass="op">+</span> i, highspy.HighsVarType.kInteger)</span>
<spanid="cb9-68"><ahref="#cb9-68"aria-hidden="true"tabindex="-1"></a> hochschule.changeColCost(anzahl_x <spanclass="op">+</span> i, FIXKOSTEN[i])</span>
<spanid="cb9-76"><ahref="#cb9-76"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N_LAGER):</span>
<spanid="cb9-91"><ahref="#cb9-91"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> hochschule.modelStatusToString(hochschule.getModelStatus())</span>
<spanid="cb9-92"><ahref="#cb9-92"aria-hidden="true"tabindex="-1"></a> info <spanclass="op">=</span> hochschule.getInfo()</span>
<spanid="cb9-94"><ahref="#cb9-94"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Der Kern der Sache: Aus dem Status folgt, WAS man mit dem Ergebnis</span></span>
<spanid="cb9-97"><ahref="#cb9-97"aria-hidden="true"tabindex="-1"></a> beweisbar_optimal <spanclass="op">=</span> status <spanclass="op">==</span><spanclass="st">"Optimal"</span></span>
<spanid="cb9-98"><ahref="#cb9-98"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">in</span> (<spanclass="st">"Infeasible"</span>, <spanclass="st">"Unbounded"</span>, <spanclass="st">"Primal infeasible or unbounded"</span>):</span>
<spanid="cb9-115"><ahref="#cb9-115"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Eine Faustregel-Loesung, wie sie ein Disponent von Hand erstellen wuerde:</span></span>
<spanid="cb9-116"><ahref="#cb9-116"aria-hidden="true"tabindex="-1"></a><spanclass="co"> die guenstigsten Lager oeffnen (Fixkosten je Kapazitaetseinheit), dann</span></span>
<spanid="cb9-117"><ahref="#cb9-117"aria-hidden="true"tabindex="-1"></a><spanclass="co"> jeden Kunden dem naechstgelegenen offenen Lager mit Restkapazitaet</span></span>
<spanid="cb9-118"><ahref="#cb9-118"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zuordnen. Kein Solver noetig - und in Sekunden fertig.</span></span>
<spanid="cb9-146"><ahref="#cb9-146"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Status </span><spanclass="sc">{</span>e<spanclass="sc">.</span>status<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb9-148"><ahref="#cb9-148"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" -> KEINE verwertbare Loesung. Nicht weiterrechnen!"</span>)</span>
<spanid="cb9-157"><ahref="#cb9-157"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" -> zulaessig, aber nicht bewiesen optimal. Der wahre Bestwert"</span>)</span>
<spanid="cb9-158"><ahref="#cb9-158"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" liegt zwischen </span><spanclass="sc">{</span>e<spanclass="sc">.</span>schranke<spanclass="sc">:,.2f}</span><spanclass="ss"> und </span><spanclass="sc">{</span>e<spanclass="sc">.</span>zielwert<spanclass="sc">:,.2f}</span><spanclass="ss"> EUR."</span>)</span>
<spanid="cb9-174"><ahref="#cb9-174"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS BRINGT MEHR RECHENZEIT?"</span>)</span>
<spanid="cb9-179"><ahref="#cb9-179"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Gewinn durch </span><spanclass="sc">{</span>lang<spanclass="sc">.</span>dauer <spanclass="op">-</span> kurz<spanclass="sc">.</span>dauer<spanclass="sc">:.1f}</span><spanclass="ss"> Sekunden mehr Rechenzeit: "</span></span>
<spanid="cb9-183"><ahref="#cb9-183"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Das ist die Frage, die im Betrieb wirklich zaehlt: Der Gap von"</span>)</span>
<spanid="cb9-184"><ahref="#cb9-184"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>kurz<spanclass="sc">.</span>gap <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.1f}</span><spanclass="ss"> % nach 2 Sekunden ist eine GARANTIE - schlechter als"</span>)</span>
<spanid="cb9-185"><ahref="#cb9-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"dieser Wert kann die Loesung nicht sein. Ob sich die restliche"</span>)</span>
<spanid="cb9-186"><ahref="#cb9-186"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Rechenzeit lohnt, entscheidet nicht der Solver, sondern die Anwendung:"</span>)</span>
<spanid="cb9-187"><ahref="#cb9-187"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Ein naechtlicher Tourenplan darf eine Stunde rechnen, eine Umplanung"</span>)</span>
<spanid="cb9-188"><ahref="#cb9-188"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"bei Maschinenausfall hat 30 Sekunden."</span>)</span>
<spanid="cb9-191"><ahref="#cb9-191"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" BRINGT EIN WARM-START ETWAS?"</span>)</span>
<spanid="cb9-205"><ahref="#cb9-205"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Ergebnis: praktisch kein Unterschied. Der Grund ist nicht, dass"</span>)</span>
<spanid="cb9-206"><ahref="#cb9-206"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Warm-Starts nichts taugen - sondern dass HiGHS' eigene Heuristiken"</span>)</span>
<spanid="cb9-207"><ahref="#cb9-207"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"innerhalb der ersten Sekunde bereits eine BESSERE Loesung finden als"</span>)</span>
<spanid="cb9-208"><ahref="#cb9-208"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"unsere Faustregel. Ein Startwert hilft nur, wenn er besser ist als"</span>)</span>
<spanid="cb9-209"><ahref="#cb9-209"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"das, was der Solver von allein in derselben Zeit findet."</span>)</span>
<spanid="cb9-211"><ahref="#cb9-211"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Warm-Starts lohnen sich damit vor allem in zwei Faellen:"</span>)</span>
<spanid="cb9-212"><ahref="#cb9-212"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Sie haben Domaenenwissen, das der Solver nicht hat (siehe"</span>)</span>
<spanid="cb9-213"><ahref="#cb9-213"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Warmstart_Effekt.py - dort halbiert ein Heuristik-Hinweis die Zeit)."</span>)</span>
<spanid="cb9-214"><ahref="#cb9-214"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" * Sie planen laufend neu und der gestrige Plan ist fast noch gueltig."</span>)</span>
<spanid="cb9-215"><ahref="#cb9-215"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"In beiden Faellen gilt: MESSEN, nicht glauben."</span>)</span>
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>
<h3id="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>
<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>
<spanid="cb12-5"><ahref="#cb12-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel MILP: Wann ein Warm-Start wirklich etwas bringt.</span></span>
<spanid="cb12-7"><ahref="#cb12-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Solverstatus_und_Gap.py zeigt einen Fall, in dem ein Startwert NICHTS</span></span>
<spanid="cb12-8"><ahref="#cb12-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">bringt - HiGHS findet von allein schneller etwas Besseres. Hier der</span></span>
<spanid="cb12-9"><ahref="#cb12-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">Gegenfall: eine Heuristik, die dem Solver echtes Domaenenwissen liefert.</span></span>
<spanid="cb12-11"><ahref="#cb12-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Problem: Lastverteilung. n Auftraege mit bekannter Dauer sind auf m</span></span>
<spanid="cb12-12"><ahref="#cb12-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">gleichartige Maschinen zu verteilen, sodass die zuletzt fertige Maschine</span></span>
<spanid="cb12-13"><ahref="#cb12-13"aria-hidden="true"tabindex="-1"></a><spanclass="co">so frueh wie moeglich fertig wird (Makespan-Minimierung).</span></span>
<spanid="cb12-15"><ahref="#cb12-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die Heuristik: LPT (Longest Processing Time first) - laengste Auftraege</span></span>
<spanid="cb12-16"><ahref="#cb12-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">zuerst, jeder auf die momentan am wenigsten belastete Maschine. Sie ist</span></span>
<spanid="cb12-17"><ahref="#cb12-17"aria-hidden="true"tabindex="-1"></a><spanclass="co">Jahrzehnte alt, in zwei Zeilen geschrieben und beweisbar nie schlechter</span></span>
<spanid="cb12-18"><ahref="#cb12-18"aria-hidden="true"tabindex="-1"></a><spanclass="co">als 4/3 des Optimums.</span></span>
<spanid="cb12-20"><ahref="#cb12-20"aria-hidden="true"tabindex="-1"></a><spanclass="co">Ueber model.AddHint() bekommt CP-SAT diese Loesung als Startpunkt.</span></span>
<spanid="cb12-22"><ahref="#cb12-22"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: highspy wird hier bewusst NICHT importiert - es vertraegt sich</span></span>
<spanid="cb12-23"><ahref="#cb12-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">nicht mit ortools im selben Prozess (siehe Kapitel Oekosystem).</span></span>
<spanid="cb12-41"><ahref="#cb12-41"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Laengste Auftraege zuerst auf die jeweils freieste Maschine legen. Zwei</span></span>
<spanid="cb12-42"><ahref="#cb12-42"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Zeilen, keine Bibliothek, Ergebnis in Mikrosekunden - und erstaunlich</span></span>
<spanid="cb12-43"><ahref="#cb12-43"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nah am Optimum.</span></span>
<spanid="cb12-56"><ahref="#cb12-56"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Exaktes Modell mit CP-SAT, optional mit Startloesung als Hinweis."""</span></span>
<spanid="cb12-60"><ahref="#cb12-60"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb12-61"><ahref="#cb12-61"aria-hidden="true"tabindex="-1"></a><spanclass="co"># x[i][k] = 1 <=> Auftrag i laeuft auf Maschine k</span></span>
<spanid="cb12-62"><ahref="#cb12-62"aria-hidden="true"tabindex="-1"></a> x <spanclass="op">=</span> [[modell.NewBoolVar(<spanclass="ss">f"x_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>k<spanclass="sc">}</span><spanclass="ss">"</span>) <spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(n_maschinen)]</span>
<spanid="cb12-63"><ahref="#cb12-63"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_auftraege)]</span>
<spanid="cb12-64"><ahref="#cb12-64"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_auftraege):</span>
<spanid="cb12-68"><ahref="#cb12-68"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(n_maschinen)]</span>
<spanid="cb12-69"><ahref="#cb12-69"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(n_maschinen):</span>
<spanid="cb12-71"><ahref="#cb12-71"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_auftraege)))</span>
<spanid="cb12-74"><ahref="#cb12-74"aria-hidden="true"tabindex="-1"></a> modell.AddMaxEquality(makespan, belegung) <spanclass="co"># das Maximum ueber alle Maschinen</span></span>
<spanid="cb12-77"><ahref="#cb12-77"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Der Warm-Start: ein Hinweis pro Variable. CP-SAT muss ihn nicht</span></span>
<spanid="cb12-78"><ahref="#cb12-78"aria-hidden="true"tabindex="-1"></a><spanclass="co"># befolgen - er nutzt ihn als erste Loesung, wenn er zulaessig ist.</span></span>
<spanid="cb12-80"><ahref="#cb12-80"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n_auftraege):</span>
<spanid="cb12-81"><ahref="#cb12-81"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(n_maschinen):</span>
<spanid="cb12-82"><ahref="#cb12-82"aria-hidden="true"tabindex="-1"></a> modell.AddHint(x[i][k], <spanclass="dv">1</span><spanclass="cf">if</span> hinweis[i] <spanclass="op">==</span> k <spanclass="cf">else</span><spanclass="dv">0</span>)</span>
<spanid="cb12-86"><ahref="#cb12-86"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ein Arbeiter und fester Startwert, damit die Messung reproduzierbar ist -</span></span>
<spanid="cb12-87"><ahref="#cb12-87"aria-hidden="true"tabindex="-1"></a><spanclass="co"># der Seed allein genuegt dafuer NICHT (Kapitel Constraint Programming).</span></span>
<spanid="cb12-88"><ahref="#cb12-88"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Im Produktivbetrieb laesst man beides auf den Standardwerten.</span></span>
<spanid="cb12-96"><ahref="#cb12-96"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="kw">not</span><spanclass="kw">in</span> (cp_model.OPTIMAL, cp_model.FEASIBLE):</span>
<spanid="cb12-97"><ahref="#cb12-97"aria-hidden="true"tabindex="-1"></a><spanclass="cf">raise</span><spanclass="pp">RuntimeError</span>(<spanclass="ss">f"Kein Plan gefunden: </span><spanclass="sc">{</span>loeser<spanclass="sc">.</span>StatusName(status)<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb12-103"><ahref="#cb12-103"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WARM-START: WENN DIE HEURISTIK MEHR WEISS ALS DER SOLVER"</span>)</span>
<spanid="cb12-105"><ahref="#cb12-105"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Lastverteilung: Auftraege auf gleichartige Maschinen verteilen,"</span>)</span>
<spanid="cb12-106"><ahref="#cb12-106"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"sodass die letzte Maschine so frueh wie moeglich fertig wird.</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb12-130"><ahref="#cb12-130"aria-hidden="true"tabindex="-1"></a><spanclass="st">"Der Hinweis darf das Optimum nicht veraendern - nur den Weg dorthin!"</span></span>
<spanid="cb12-135"><ahref="#cb12-135"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"1. Der Hinweis aendert das ERGEBNIS nicht - beide Laeufe finden"</span>)</span>
<spanid="cb12-136"><ahref="#cb12-136"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" dasselbe Optimum. Er aendert nur, wie lange der Beweis dauert."</span>)</span>
<spanid="cb12-137"><ahref="#cb12-137"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Genau deshalb ist ein Warm-Start ungefaehrlich: Ein schlechter"</span>)</span>
<spanid="cb12-138"><ahref="#cb12-138"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Hinweis kostet Zeit, er verfaelscht aber nie die Loesung."</span>)</span>
<spanid="cb12-139"><ahref="#cb12-139"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"2. Die LPT-Heuristik liegt schon sehr nah am Optimum. Ihr Wert fuer"</span>)</span>
<spanid="cb12-140"><ahref="#cb12-140"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" den Solver liegt weniger in der Qualitaet als darin, dass sie"</span>)</span>
<spanid="cb12-141"><ahref="#cb12-141"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" SOFORT da ist - der Solver kann von Beginn an alles verwerfen,"</span>)</span>
<spanid="cb12-142"><ahref="#cb12-142"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" was schlechter ist."</span>)</span>
<spanid="cb12-143"><ahref="#cb12-143"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"3. Der Faktor schwankt von Instanz zu Instanz - oben 2,2x und 1,4x -"</span>)</span>
<spanid="cb12-144"><ahref="#cb12-144"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" und laesst sich NICHT aus der Problemgroesse ableiten. Er haengt"</span>)</span>
<spanid="cb12-145"><ahref="#cb12-145"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" davon ab, wie schnell der Solver von allein eine vergleichbar gute"</span>)</span>
<spanid="cb12-146"><ahref="#cb12-146"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Loesung findet. Das ist die eigentliche Lehre: Ein Warm-Start ist"</span>)</span>
<spanid="cb12-147"><ahref="#cb12-147"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" eine Messung wert, keine Glaubensfrage."</span>)</span>
<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>
<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 <spanclass="math inline">x_j \le M y_j</span> mit <spanclass="math inline">x_j \le 250</span> als bekannter Kapazität. Welchen Wert sollte <spanclass="math inline">M</span> haben? Was passiert bei <spanclass="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: <spanclass="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 <spanclass="math inline">K = 1, 2, \dots, 6</span> durchläuft und Netto-Ertrag sowie Rechenzeit tabelliert. (a) Ab welchem <spanclass="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 <spanclass="math inline">10^4</span>, <spanclass="math inline">10^6</span> und <spanclass="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 <spanclass="math inline">(80, 60, 90, 70, 50)</span> Tsd. € versorgen vier Regionen mit Bedarf <spanclass="math inline">(30, 45, 25, 40)</span> Einheiten. Transportkosten je Einheit stehen in einer <spanclass="math inline">5\times4</span>-Matrix Ihrer Wahl. Jedes eröffnete Lager hat Kapazität 80. Minimieren Sie Fix- plus Transportkosten.</p>
<hr/>
<h2id="sec:milp-denkfehler">6.10 Finde den Denkfehler</h2>
<p>Die Warnung „<spanclass="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>
<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><spanclass="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 <spanclass="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 <spanclass="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>
<spanid="cb14-5"><ahref="#cb14-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel MILP: Was ein zu grosses Big-M wirklich anrichtet.</span></span>
<spanid="cb14-7"><ahref="#cb14-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Lehrbuecher warnen vor grossem M mit dem Hinweis "die Relaxation wird</span></span>
<spanid="cb14-8"><ahref="#cb14-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">schwach, der Solver braucht mehr Knoten". Das stimmt - ist aber die</span></span>
<spanid="cb14-9"><ahref="#cb14-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">harmlosere Haelfte der Wahrheit. Die gefaehrlichere: Bei sehr grossem M</span></span>
<spanid="cb14-10"><ahref="#cb14-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">kann der Solver eine Loesung als ganzzahlig ANNEHMEN, in der die</span></span>
<spanid="cb14-11"><ahref="#cb14-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">Binaervariablen bei 1e-8 stehen. Dann liefern zugeschaltete Anlagen Ware</span></span>
<spanid="cb14-12"><ahref="#cb14-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">aus, waehrend das Modell ihre Fixkosten mit 0 verbucht.</span></span>
<spanid="cb14-21"><ahref="#cb14-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. M knapp gewaehlt (= tatsaechliche Lagerkapazitaet) -> richtig</span></span>
<spanid="cb14-22"><ahref="#cb14-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. M = 1e7-fach zu gross, Presolve abgeschaltet -> stilles Desaster</span></span>
<spanid="cb14-23"><ahref="#cb14-23"aria-hidden="true"tabindex="-1"></a><spanclass="co">Beide werden mit derselben Pruefung kontrolliert, die den Fall auffliegen laesst.</span></span>
<spanid="cb14-43"><ahref="#cb14-43"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Toleranz, ab der ein Solver eine Variable als ganzzahlig durchgehen laesst.</span></span>
<spanid="cb14-44"><ahref="#cb14-44"aria-hidden="true"tabindex="-1"></a><spanclass="co"># HiGHS und die meisten anderen verwenden 1e-6 als Standard.</span></span>
<spanid="cb14-49"><ahref="#cb14-49"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Baut und loest das Standortmodell. Liefert Loesung und Solverkennzahlen."""</span></span>
<spanid="cb14-61"><ahref="#cb14-61"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N_LAGER):</span>
<spanid="cb14-64"><ahref="#cb14-64"aria-hidden="true"tabindex="-1"></a> hochschule.changeColCost(anzahl_x <spanclass="op">+</span> i, FIXKOSTEN[i])</span>
<spanid="cb14-74"><ahref="#cb14-74"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Die Kopplung: sum_j x_ij - M * y_i <= 0</span></span>
<spanid="cb14-75"><ahref="#cb14-75"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(N_LAGER):</span>
<spanid="cb14-97"><ahref="#cb14-97"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Die Pruefung, die in jedes MILP-Auswertungsskript gehoert.</span></span>
<spanid="cb14-99"><ahref="#cb14-99"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Sie rechnet die Kosten AUS DER LOESUNG neu aus, statt dem Zielwert des</span></span>
<spanid="cb14-100"><ahref="#cb14-100"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Solvers zu glauben - und vergleicht beide. Genau diese Gegenrechnung</span></span>
<spanid="cb14-101"><ahref="#cb14-101"aria-hidden="true"tabindex="-1"></a><spanclass="co"> entlarvt eine Loesung, in der Binaervariablen bei 1e-8 haengengeblieben</span></span>
<spanid="cb14-111"><ahref="#cb14-111"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"y ist nicht ganzzahlig: groesster Abstand </span><spanclass="sc">{</span>abstand<spanclass="sc">.</span><spanclass="bu">max</span>()<spanclass="sc">:.2e}</span><spanclass="ss">"</span>)</span>
<spanid="cb14-113"><ahref="#cb14-113"aria-hidden="true"tabindex="-1"></a><spanclass="co"># 2. Liefert ein Lager, dessen Schalter aus ist?</span></span>
<spanid="cb14-119"><ahref="#cb14-119"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"gelten im Modell aber als geschlossen"</span>)</span>
<spanid="cb14-121"><ahref="#cb14-121"aria-hidden="true"tabindex="-1"></a><spanclass="co"># 3. Stimmt der Zielwert mit den echten Kosten ueberein?</span></span>
<spanid="cb14-127"><ahref="#cb14-127"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zielwert </span><spanclass="sc">{</span>ergebnis[<spanclass="st">'zielwert'</span>]<spanclass="sc">:,.2f}</span><spanclass="ss"> weicht von den echten Kosten "</span></span>
<spanid="cb14-128"><ahref="#cb14-128"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>echte_kosten<spanclass="sc">:,.2f}</span><spanclass="ss"> ab (Differenz </span><spanclass="sc">{</span>echte_kosten <spanclass="op">-</span> ergebnis[<spanclass="st">'zielwert'</span>]<spanclass="sc">:,.2f}</span><spanclass="ss">)"</span>)</span>
<spanid="cb14-137"><ahref="#cb14-137"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Zielwert laut Solver </span><spanclass="sc">{</span>ergebnis[<spanclass="st">'zielwert'</span>]<spanclass="sc">:>14,.2f}</span><spanclass="ss"> EUR"</span>)</span>
<spanid="cb14-138"><ahref="#cb14-138"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Knoten / Zeit </span><spanclass="sc">{</span>ergebnis[<spanclass="st">'knoten'</span>]<spanclass="sc">:>14,}</span><spanclass="ss">"</span></span>
<spanid="cb14-140"><ahref="#cb14-140"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Lager mit y = 1 </span><spanclass="sc">{</span><spanclass="bu">int</span>((y <spanclass="op">></span><spanclass="fl">0.5</span>).<spanclass="bu">sum</span>())<spanclass="sc">:>14}</span><spanclass="ss">"</span>)</span>
<spanid="cb14-141"><ahref="#cb14-141"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Lager, die tatsaechlich liefern </span><spanclass="sc">{</span><spanclass="bu">int</span>(liefert.<spanclass="bu">sum</span>())<spanclass="sc">:>10}</span><spanclass="ss">"</span>)</span>
<spanid="cb14-142"><ahref="#cb14-142"aria-hidden="true"tabindex="-1"></a> unter <spanclass="op">=</span> y[y <spanclass="op"><</span><spanclass="fl">0.5</span>]</span>
<spanid="cb14-143"><ahref="#cb14-143"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" groesster y-Wert unter 0.5 "</span></span>
<spanid="cb14-153"><ahref="#cb14-153"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> text <spanclass="kw">in</span> beanstandungen:</span>
<spanid="cb14-162"><ahref="#cb14-162"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Genau das ist das kleinstmoegliche gueltige M - mehr kann ein Lager"</span>)</span>
<spanid="cb14-163"><ahref="#cb14-163"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ohnehin nicht ausliefern."</span>)</span>
<spanid="cb14-169"><ahref="#cb14-169"aria-hidden="true"tabindex="-1"></a> zeige(<spanclass="st">"[2] M = 10 Millionen mal Kapazitaet, Presolve abgeschaltet"</span>, gross)</span>
<spanid="cb14-172"><ahref="#cb14-172"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DA PASSIERT IST"</span>)</span>
<spanid="cb14-175"><ahref="#cb14-175"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Lauf [2] meldet </span><spanclass="sc">{</span>gross[<spanclass="st">'zielwert'</span>]<spanclass="sc">:,.2f}</span><spanclass="ss"> EUR und sieht damit um"</span>)</span>
<spanid="cb14-176"><ahref="#cb14-176"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>fehlbetrag<spanclass="sc">:,.2f}</span><spanclass="ss"> EUR BESSER aus als die richtige Loesung - ein Ergebnis,"</span>)</span>
<spanid="cb14-177"><ahref="#cb14-177"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ueber das sich jeder Auftraggeber freuen wuerde."</span>)</span>
<spanid="cb14-179"><ahref="#cb14-179"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Der Grund steht in der Zeile 'groesster y-Wert unter 0.5': Die"</span>)</span>
<spanid="cb14-180"><ahref="#cb14-180"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Schaltervariablen stehen bei rund "</span></span>
<spanid="cb14-182"><ahref="#cb14-182"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Das ist kleiner als die Ganzzahltoleranz </span><spanclass="sc">{</span>GANZZAHL_TOLERANZ<spanclass="sc">:.0e}</span><spanclass="ss">, also gilt"</span>)</span>
<spanid="cb14-183"><ahref="#cb14-183"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"y = 0 - 'Lager geschlossen'. Zugleich ist M so gross, dass"</span>)</span>
<spanid="cb14-184"><ahref="#cb14-184"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" sum_j x_ij <= M * 1e-8"</span>)</span>
<spanid="cb14-185"><ahref="#cb14-185"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"immer noch reichlich Liefermenge erlaubt. Die Lager liefern also,"</span>)</span>
<spanid="cb14-186"><ahref="#cb14-186"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ohne dass ihre Fixkosten je bezahlt werden. Der Fachbegriff dafuer"</span>)</span>
<spanid="cb14-189"><ahref="#cb14-189"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"WICHTIG: Mit eingeschaltetem Presolve (Standard) faellt HiGHS hier"</span>)</span>
<spanid="cb14-190"><ahref="#cb14-190"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht darauf herein - es zieht M selbst zurecht. Verlassen Sie sich"</span>)</span>
<spanid="cb14-191"><ahref="#cb14-191"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht darauf: Presolve kann das nur, wenn eine implizite Schranke"</span>)</span>
<spanid="cb14-192"><ahref="#cb14-192"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"herleitbar ist. Die Pruefung aus pruefe() kostet Millisekunden und"</span>)</span>
<td>Mit Standardeinstellung fällt HiGHS <strong>nicht</strong> herein: Presolve zieht das übergroße <spanclass="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>
<trclass="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 <spanclass="math inline">4 \cdot 10^{-8}</span> liegt unter der Toleranz <spanclass="math inline">10^{-6}</span> und gilt dem Solver als 0. Für Ihr Geschäft ist er es nicht.</td>
</tr>
<trclass="odd">
<td><code>liefert & (y < 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>
<trclass="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>
<p>Genau eine Antwort ist jeweils richtig. Auflösung in <ahref="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 <spanclass="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 <spanclass="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>
<li><strong>Runden ist keine Lösung:</strong> Aufrunden ist fast immer unzulässig (98–100 % der Fälle), Abrunden verliert im Mittel 10–26 %, 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><spanclass="math inline">M</span> so klein wie möglich</strong> — nicht nur wegen der Laufzeit. Ein übergroßes <spanclass="math inline">M</span> kann Binärvariablen bei <spanclass="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><ahref="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>