<aclass="brand"href="index.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-book"></use></svg><span>Optimierte Entscheidungsfindung mit Python</span></a>
<navclass="sidebar"id="sidebar"aria-label="Kapitelnavigation"><divclass="sidebar-inhalt"><detailsclass="sidebar-gruppe"><summary>Einstieg</summary><ul><lidata-kapitel="vorwort.html"><ahref="vorwort.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Vorwort & Lesehilfe</span></a></li><lidata-kapitel="notation.html"><ahref="notation.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Notation & Abkürzungen</span></a></li></ul></details><detailsclass="sidebar-gruppe"><summary>Teil I: Grundlagen des Operations Research</summary><ul><lidata-kapitel="einfuehrung.html"><ahref="einfuehrung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 1: Einführung in Operations Research — Vom Ursprung zur mathematischen Entscheidungsfindung</span></a></li><lidata-kapitel="fundament.html"><ahref="fundament.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 2: Das mathematische Fundament — Vektoren, Matrizen, Konvexität</span></a></li><lidata-kapitel="oekosystem.html"><ahref="oekosystem.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 3: Das Python-Ökosystem für OR — Solver, Bindings und Modellierungsschichten</span></a></li><lidata-kapitel="modellierung.html"><ahref="modellierung.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 4: Vom Management-Wunsch zum Modell</span></a></li><lidata-kapitel="synthese-grundlagen.html"><ahref="synthese-grundlagen.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Synthese Teil I</span></a></li></ul></details><detailsclass="sidebar-gruppe"open><summary>Teil II: Die Kernverfahren der deterministischen Optimierung</summary><ul><lidata-kapitel="lp.html"><ahref="lp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 5: Lineare Programmierung — Simplex, Dualität und Schattenpreise</span></a></li><lidata-kapitel="milp.html"><ahref="milp.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 6: Gemischt-ganzzahlige Optimierung — Diskrete Entscheidungen und Branch-and-Bound</span></a></li><lidata-kapitel="cpsat.html"><ahref="cpsat.html"><spanclass="fortschritt-haken"><svgclass="icon"aria-hidden="true"><usehref="#icon-check"></use></svg></span><span>Kapitel 7: Constraint Programming mit CP-SAT — Logik, Scheduling und Zuweisung</span></a></li><lidata-kapitel="graphen.html"><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"class="aktiv"><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 9: Metaheuristiken — wenn der exakte Solver aussteigt</span></nav>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="graphen.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a><aclass="prev-next-knopf prev-next-next"href="dekomposition.html"><span><small>Weiter</small>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>
<article>
<h1id="kap-metaheuristiken">Kapitel 9: Metaheuristiken — wenn der exakte Solver aussteigt</h1>
<divclass="card card-blick">
<blockquote>
<p><strong>📌 Kapitel auf einen Blick</strong></p>
<p><strong>Worum geht es?</strong> Um den Fall, für den die bisherigen Kapitel keine Antwort haben: Das Problem ist zu groß, der Solver kommt im Zeitlimit nicht zu einem brauchbaren Ergebnis — und eine Entscheidung muss trotzdem heute fallen.</p>
<p><strong>Voraussetzungen:</strong><ahref="milp.html#kap-milp">Kapitel 6</a> und <ahref="cpsat.html#kap-cpsat">Kapitel 7</a>, insbesondere <ahref="milp.html#sec:milp-gap">Abschnitt 6.8</a> (MIP-Gap und Schranke).</p>
<p><strong>Danach können Sie:</strong> eine lokale Suche mit ihrer entscheidenden Zutat — der Kostenänderung in O(1) — selbst schreiben, die Temperatur eines Simulated Annealing ausmessen statt raten, den Umschlagpunkt zwischen exakt und heuristisch für Ihr eigenes Problem bestimmen, und mit Large Neighborhood Search beides kombinieren.</p>
<p><strong>Zeitbedarf:</strong> ca. 5 Stunden.</p>
<p><strong>Notebook:</strong><ahref="Notebooks_04/metaheuristiken.ipynb">metaheuristiken.ipynb</a> — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten</p>
<h2id="sec:metaheuristiken-schnellstart">9.1 In 5 Minuten gelöst</h2>
<divclass="card card-schnellstart">
<blockquote>
<p><strong>🚀 In 5 Minuten gelöst: Sechs Lackaufträge, eine Reihenfolge</strong></p>
<p>Eine Lackieranlage hat sechs Aufträge offen. Zwischen zwei Aufträgen muss gereinigt werden; wie lange das dauert, hängt von <strong>beiden</strong> ab — von Hell auf Dunkel geht schnell, umgekehrt dauert es. Die Anlage steht gerade auf <em>Weiß</em>.</p>
<p><strong>24 Minuten Unterschied, 40 % — bei sechs Aufträgen.</strong> Und die Faustregel ist nicht dumm: Sie nimmt immer den billigsten nächsten Schritt.</p>
</blockquote>
</div>
<p><strong>Und jetzt der Punkt.</strong> Sehen Sie sich an, wo die Faustregel danebengreift. Beide Pläne beginnen gleich: Weiß → Elfenbein → Beige, alles hell, alles billig. Dann muss die Anlage in die dunkle Gruppe wechseln, und das kostet in jedem Fall. Die Faustregel wählt den billigsten Übergang, den sie sieht: Beige → Dunkelrot für 49 Minuten. Der optimale Plan nimmt stattdessen Beige → <strong>Rot</strong> für 52 Minuten — drei Minuten teurer.</p>
<p>Diese drei Minuten mehr sparen am Ende 27. Denn wer über Dunkelrot einsteigt, lässt <em>Rot</em> übrig, und Rot ist von Schwarz aus nur für 29 Minuten erreichbar. Der optimale Plan betritt die dunkle Gruppe an der richtigen Stelle und arbeitet sie dann von hell nach dunkel ab: Rot → Dunkelrot → Schwarz, zweimal 2 Minuten.</p>
<blockquote>
<p><strong>🎯 Merksatz</strong> Der Fehler der Faustregel ist nicht Gier, sondern <strong>Kurzsichtigkeit</strong>. Sie bewertet einen Schritt danach, was er <em>kostet</em>, und nicht danach, was er <em>übrig lässt</em>. Jede Heuristik in diesem Kapitel ist eine Antwort auf genau diese Schwäche.</p>
</blockquote>
<p><strong>Warum funktioniert das?</strong> Bei sechs Aufträgen konnten wir alle <spanclass="math inline">5! = 120</span> Reihenfolgen durchprobieren und wissen deshalb <em>sicher</em>, dass 60 Minuten das Minimum sind. Bei 20 Aufträgen sind es schon <spanclass="math inline">19! \approx 1{,}2 \cdot 10^{17}</span> — und damit ist der Weg dieses Schnellstarts versperrt. Das ganze Kapitel handelt davon, was an seine Stelle tritt.</p>
<li>… erklären, warum eine lokale Suche ohne <strong>Kostenänderung in O(1)</strong> praktisch wertlos ist.</li>
<li>… Simulated Annealing implementieren und seine Starttemperatur <strong>an der Zuggröße</strong> kalibrieren statt am Zielfunktionswert.</li>
<li>… den Beitrag der Metaheuristik gegen die einfachere Alternative abgrenzen — reines Bergsteigen — statt ihn ihr gutzuschreiben.</li>
<li>… den <strong>Umschlagpunkt</strong> bestimmen, ab dem eine Metaheuristik den exakten Solver schlägt, und begründen, warum diese Zahl problemspezifisch ist.</li>
<li>… erklären, warum ein exakter Lauf auch dann dazugehört, wenn Sie am Ende die heuristische Lösung einsetzen.</li>
<li>… Large Neighborhood Search aufsetzen: zerstören, exakt reparieren, übernehmen.</li>
</ol>
<hr/>
<h2id="sec:metaheuristiken-aufgabe">9.3 Die Aufgabe: Rüstzeiten an der Lackieranlage</h2>
<p>Der Schnellstart war ein Spielzeug. Die Aufgabe dieses Kapitels ist dieselbe, nur in Betriebsgröße: <strong><spanclass="math inline">n</span> Aufträge in eine Reihenfolge bringen, so dass die Summe der Rüstzeiten minimal wird.</strong></p>
<td>„Über alle <strong>Übergänge</strong> summieren — bei <spanclass="math inline">n</span> Aufträgen gibt es <spanclass="math inline">n-1</span> davon, nicht <spanclass="math inline">n</span>.“</td>
</tr>
<trclass="even">
<td><spanclass="math inline">\min_\pi</span></td>
<td>„Unter allen <spanclass="math inline">n!</span> möglichen Reihenfolgen die billigste.“</td>
</tr>
</tbody>
</table>
<p><strong>Ohne Formel gesagt:</strong> „Sortiere die Aufträge so, dass möglichst wenig geputzt werden muss.“</p>
</blockquote>
</div>
<p>Wer die Aufgabe als Graph liest, erkennt sie wieder: Es ist ein <strong>asymmetrisches Rundreiseproblem</strong> (<ahref="graphen.html#kap-graphen">Kapitel 8</a>) mit offenem Ende. Das ist eine gute und eine schlechte Nachricht. Gut, weil damit alles bekannt ist, was man über das Problem wissen kann. Schlecht, weil dazu gehört, dass es NP-schwer ist.</p>
<p>Ein <strong>Verfahrensrahmen</strong>, der eine vorhandene Lösung schrittweise verändert und dabei steuert, welche Veränderungen übernommen werden. „Meta“, weil der Rahmen nichts über das Problem weiß: Er braucht nur eine Startlösung, einen Zug und eine Bewertung. Dieselbe Mechanik läuft über Tourenplanung, Personaleinsatz und Portfolioauswahl.</p>
<p>Der Preis dafür steht in derselben Zeile: Ein Verfahren, das nichts über das Problem weiß, kann auch nichts über die Güte seines Ergebnisses sagen. Es liefert eine Lösung, <strong>keine Schranke</strong>.</p>
</blockquote>
<h3id="die-instanz">Die Instanz</h3>
<p>Alle Programme dieses Kapitels benutzen dieselbe Aufgabe. Die Rüstzeit setzt sich aus zwei Teilen zusammen: einem unregelmäßigen Reinigungsaufwand zwischen den Produktfamilien und einem Zuschlag für den Wechsel von Dunkel nach Hell.</p>
<p>Eine Feinheit der Instanz ist wichtiger, als sie aussieht: Die <strong>Zahl der Produktfamilien wächst mit der Auftragszahl</strong> (eine Familie je zehn Aufträge). Bei fester Familienzahl würde das Problem mit wachsendem <spanclass="math inline">n</span> nämlich <em>leichter</em> — der Plan bestünde irgendwann aus ein paar großen Blöcken, und jede Faustregel fände ihn. Beim ersten Entwurf dieses Kapitels ist mir genau das passiert: Die Metaheuristik verbesserte bei 60 Aufträgen um 30 %, bei 200 Aufträgen um 0 %. Nicht weil sie versagte, sondern weil es nichts mehr zu verbessern gab.</p>
<hr/>
<h2id="sec:metaheuristiken-lokale-suche">9.4 Lokale Suche: der Zug und seine Kosten</h2>
<p>Eine lokale Suche braucht drei Dinge: eine Startlösung, einen <strong>Zug</strong> und eine Regel, welche Züge übernommen werden.</p>
<p>Der Zug in diesem Kapitel ist der einfachste, der für asymmetrische Rüstzeiten taugt: <strong>einen Auftrag herausnehmen und woanders einsetzen</strong>. (Das klassische 2-opt — ein Teilstück umdrehen — scheidet aus: Beim Umdrehen kehren sich alle Übergänge <em>innerhalb</em> des Stücks um, und weil <spanclass="math inline">s_{i,j} \neq s_{j,i}</span> ist, müsste man sie alle neu berechnen.)</p>
<blockquote>
<p><strong>⚠️ Die Stelle, an der Metaheuristiken scheitern</strong></p>
<p>Es ist verlockend, die Kosten eines Zugs so zu bewerten:</p>
<p>Das ist korrekt und trotzdem der Fehler, der ein Projekt scheitern lässt. Die volle Summe kostet <spanclass="math inline">O(n)</span> je Zug. Herausnehmen und Einsetzen verändert aber nur <strong>drei Kanten</strong> — die Kostenänderung steht in höchstens sechs Matrixeinträgen und ist in <spanclass="math inline">O(1)</span> zu haben.</p>
<p>Bei 500 Aufträgen ist das der Unterschied zwischen einigen tausend und einigen hunderttausend geprüften Zügen im selben Zeitbudget. Eine Metaheuristik lebt von der Zahl der Züge; wer sie durch eine bequeme Bewertungsfunktion um zwei Größenordnungen drückt, misst am Ende nicht das Verfahren, sondern seine eigene Implementierung.</p>
</blockquote>
<p>Herausgenommen wird der Auftrag aus zwei Kanten, seine Nachbarn rücken zusammen — das ist eine neue Kante. Eingesetzt wird er zwischen zwei andere Nachbarn, deren bisherige Kante dadurch verschwindet. Drei Kanten weniger, drei Kanten mehr:</p>
<p>wobei <spanclass="math inline">x</span> der verschobene Auftrag ist, <spanclass="math inline">u,v</span> seine alten und <spanclass="math inline">a,b</span> seine neuen Nachbarn.</p>
<p>Reines Bergsteigen — nur Verbesserungen annehmen — bleibt im ersten lokalen Optimum stehen. <strong>Simulated Annealing</strong> nimmt Verschlechterungen mit einer Wahrscheinlichkeit an, die von der Größe der Verschlechterung und von einer sinkenden <em>Temperatur</em> abhängt:</p>
<p><spanclass="math display">
P(\text{annehmen}) = \begin{cases}
1 & \Delta \le 0\\[2pt]
e^{-\Delta / T} & \Delta > 0
\end{cases}
</span></p>
<divclass="card card-formel">
<blockquote>
<p><strong>🔤 Formel-Übersetzer</strong></p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Mathematik</th>
<th>Alltagssprache</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><spanclass="math inline">\Delta \le 0 \Rightarrow P = 1</span></td>
<td>„Genau diese Verschlechterung geht in 37 % der Fälle durch (<spanclass="math inline">e^{-1}</span>).“</td>
</tr>
</tbody>
</table>
<p>Die letzte Zeile ist die praktisch wichtigste: <strong><spanclass="math inline">T</span> und <spanclass="math inline">\Delta</span> werden in derselben Einheit gemessen.</strong> Wer die Temperatur wählt, ohne die typische Zuggröße zu kennen, wählt blind.</p>
<spanid="cb4-5"><ahref="#cb4-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Metaheuristiken: Simulated Annealing an der Lackieranlage.</span></span>
<spanid="cb4-7"><ahref="#cb4-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Aufgabe: n Auftraege in eine Reihenfolge bringen. Zwischen zwei Auftraegen</span></span>
<spanid="cb4-8"><ahref="#cb4-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">faellt eine Ruestzeit an - die Anlage muss gereinigt werden. Wie lange das</span></span>
<spanid="cb4-9"><ahref="#cb4-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">dauert, haengt von BEIDEN Auftraegen ab: Ein Wechsel innerhalb derselben</span></span>
<spanid="cb4-10"><ahref="#cb4-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Produktfamilie kostet fast nichts, ein Wechsel von Dunkel nach Hell ist teuer,</span></span>
<spanid="cb4-11"><ahref="#cb4-11"aria-hidden="true"tabindex="-1"></a><spanclass="co">und zwischen manchen Familien ist der Reinigungsaufwand schlicht hoch. Gesucht</span></span>
<spanid="cb4-12"><ahref="#cb4-12"aria-hidden="true"tabindex="-1"></a><spanclass="co">ist die Reihenfolge mit der kleinsten Summe der Ruestzeiten.</span></span>
<spanid="cb4-16"><ahref="#cb4-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 1. Warum die Faustregel "immer der billigste naechste Auftrag" in eine</span></span>
<spanid="cb4-17"><ahref="#cb4-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Sackgasse laeuft - und wie weit man sie mit lokaler Suche verbessert.</span></span>
<spanid="cb4-18"><ahref="#cb4-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 2. Wie gross der Beitrag der Verschlechterungen WIRKLICH ist. Die Antwort</span></span>
<spanid="cb4-19"><ahref="#cb4-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> faellt bescheidener aus als das Lehrbuch verspricht, und genau das ist</span></span>
<spanid="cb4-20"><ahref="#cb4-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> der Grund, den Vergleich immer mitzurechnen.</span></span>
<spanid="cb4-21"><ahref="#cb4-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> 3. Dass die Starttemperatur kein Beiwerk ist: zu heiss macht die Suche</span></span>
<spanid="cb4-22"><ahref="#cb4-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> nutzlos, und man sieht es an einer einzigen Kennzahl kommen.</span></span>
<spanid="cb4-24"><ahref="#cb4-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zum Budget: Der Abkuehlplan laeuft ueber eine feste ZUGZAHL, nicht ueber eine</span></span>
<spanid="cb4-25"><ahref="#cb4-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">Sekundenzahl. Im Betrieb hat man zwar ein Zeitbudget - fuer einen VERGLEICH ist</span></span>
<spanid="cb4-26"><ahref="#cb4-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">die Zugzahl aber das ehrlichere Mass: Sie ist auf jeder Maschine dieselbe, und</span></span>
<spanid="cb4-27"><ahref="#cb4-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">die abgedruckten Zahlen unten lassen sich damit nachrechnen. Die gemessene</span></span>
<spanid="cb4-28"><ahref="#cb4-28"aria-hidden="true"tabindex="-1"></a><spanclass="co">Laufzeit steht trotzdem dabei.</span></span>
<spanid="cb4-47"><ahref="#cb4-47"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Ruestzeiten in Minuten zwischen je zwei Auftraegen.</span></span>
<spanid="cb4-49"><ahref="#cb4-49"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Zahl der Produktfamilien waechst mit der Auftragszahl (eine Familie je</span></span>
<spanid="cb4-50"><ahref="#cb4-50"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zehn Auftraege). Sonst wuerde das Problem mit wachsendem n LEICHTER: Bei</span></span>
<spanid="cb4-51"><ahref="#cb4-51"aria-hidden="true"tabindex="-1"></a><spanclass="co"> fester Familienzahl haette der Plan irgendwann nur noch ein paar grosse</span></span>
<spanid="cb4-52"><ahref="#cb4-52"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Bloecke, und jede Faustregel faende ihn.</span></span>
<spanid="cb4-55"><ahref="#cb4-55"aria-hidden="true"tabindex="-1"></a> familien <spanclass="op">=</span><spanclass="bu">max</span>(<spanclass="dv">6</span>, n <spanclass="op">//</span><spanclass="dv">10</span>)</span>
<spanid="cb4-56"><ahref="#cb4-56"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Reinigungsaufwand zwischen den Familien - unregelmaessig und asymmetrisch,</span></span>
<spanid="cb4-57"><ahref="#cb4-57"aria-hidden="true"tabindex="-1"></a><spanclass="co"># so wie im Betrieb: Von Klarlack auf Rot ist etwas anderes als umgekehrt.</span></span>
<spanid="cb4-58"><ahref="#cb4-58"aria-hidden="true"tabindex="-1"></a> zwischen <spanclass="op">=</span> rng.integers(<spanclass="dv">8</span>, <spanclass="dv">60</span>, (familien, familien))</span>
<spanid="cb4-65"><ahref="#cb4-65"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb4-67"><ahref="#cb4-67"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> i <spanclass="op">!=</span> j:</span>
<spanid="cb4-68"><ahref="#cb4-68"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Dunkel -> hell kostet zusaetzlich: die Anlage muss heller</span></span>
<spanid="cb4-69"><ahref="#cb4-69"aria-hidden="true"tabindex="-1"></a><spanclass="co"># werden, das braucht mehr Spuelgaenge.</span></span>
<spanid="cb4-76"><ahref="#cb4-76"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Summe der Ruestzeiten einer Reihenfolge (offene Kette, keine Rundreise)."""</span></span>
<spanid="cb4-77"><ahref="#cb4-77"aria-hidden="true"tabindex="-1"></a><spanclass="cf">return</span><spanclass="bu">int</span>(<spanclass="bu">sum</span>(matrix[reihe[k], reihe[k <spanclass="op">+</span><spanclass="dv">1</span>]] <spanclass="cf">for</span> k <spanclass="kw">in</span><spanclass="bu">range</span>(<spanclass="bu">len</span>(reihe) <spanclass="op">-</span><spanclass="dv">1</span>)))</span>
<spanid="cb4-81"><ahref="#cb4-81"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Immer der billigste noch offene Auftrag - 'naechster Nachbar'.</span></span>
<spanid="cb4-83"><ahref="#cb4-83"aria-hidden="true"tabindex="-1"></a><spanclass="co"> So plant ein Meister von Hand, und es ist keine schlechte Regel. Ihr Fehler</span></span>
<spanid="cb4-84"><ahref="#cb4-84"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ist die Kurzsichtigkeit: Sie spart am Anfang und laesst die teuren Wechsel</span></span>
<spanid="cb4-85"><ahref="#cb4-85"aria-hidden="true"tabindex="-1"></a><spanclass="co"> fuer das Ende uebrig, wo keine Wahl mehr bleibt.</span></span>
<spanid="cb4-97"><ahref="#cb4-97"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- 2. Der Zug und seine Kostenaenderung -----------------------------------</span></span>
<spanid="cb4-101"><ahref="#cb4-101"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Auftrag von Position 'von' an Position 'nach' versetzen.</span></span>
<spanid="cb4-103"><ahref="#cb4-103"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Liefert (Kostenaenderung, Restliste, Einfuegeposition) oder None, wenn der</span></span>
<spanid="cb4-106"><ahref="#cb4-106"aria-hidden="true"tabindex="-1"></a><spanclass="co"> DAS IST DIE WICHTIGSTE FUNKTION DES PROGRAMMS. Sie berechnet die</span></span>
<spanid="cb4-107"><ahref="#cb4-107"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Kostenaenderung aus HOECHSTENS SECHS Matrixeintraegen, statt die</span></span>
<spanid="cb4-108"><ahref="#cb4-108"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Reihenfolge neu durchzusummieren. Der Unterschied ist nicht kosmetisch:</span></span>
<spanid="cb4-109"><ahref="#cb4-109"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die volle Summe kostet O(n) je Zug, diese Rechnung O(1). Bei 500 Auftraegen</span></span>
<spanid="cb4-110"><ahref="#cb4-110"aria-hidden="true"tabindex="-1"></a><spanclass="co"> sind das rund 500-mal mehr geprueste Zuege im selben Zeitbudget - und eine</span></span>
<spanid="cb4-111"><ahref="#cb4-111"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Metaheuristik lebt von der Zahl der Zuege.</span></span>
<spanid="cb4-113"><ahref="#cb4-113"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Herausgerissen wird der Auftrag aus zwei Kanten, seine Nachbarn ruecken</span></span>
<spanid="cb4-114"><ahref="#cb4-114"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zusammen (eine neue Kante). Eingefuegt wird er zwischen zwei andere</span></span>
<spanid="cb4-115"><ahref="#cb4-115"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Nachbarn, deren bisherige Kante dadurch verschwindet.</span></span>
<spanid="cb4-117"><ahref="#cb4-117"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(reihe)</span>
<spanid="cb4-118"><ahref="#cb4-118"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">==</span> nach <spanclass="kw">or</span> nach <spanclass="op">==</span> von <spanclass="op">+</span><spanclass="dv">1</span>:</span>
<spanid="cb4-122"><ahref="#cb4-122"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb4-124"><ahref="#cb4-124"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>:</span>
<spanid="cb4-126"><ahref="#cb4-126"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>: <spanclass="co"># die Nachbarn ruecken zusammen</span></span>
<spanid="cb4-130"><ahref="#cb4-130"aria-hidden="true"tabindex="-1"></a> stelle <spanclass="op">=</span> nach <spanclass="cf">if</span> nach <spanclass="op"><</span> von <spanclass="cf">else</span> nach <spanclass="op">-</span><spanclass="dv">1</span></span>
<spanid="cb4-133"><ahref="#cb4-133"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb4-135"><ahref="#cb4-135"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest):</span>
<spanid="cb4-137"><ahref="#cb4-137"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest): <spanclass="co"># die aufgetrennte Kante faellt weg</span></span>
<spanid="cb4-146"><ahref="#cb4-146"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Was ein Lauf ausser dem Ergebnis noch verraet."""</span></span>
<spanid="cb4-152"><ahref="#cb4-152"aria-hidden="true"tabindex="-1"></a><spanclass="va">self</span>.schlechtester_zustand <spanclass="op">=</span><spanclass="dv">0</span><spanclass="co"># wie weit die Suche abgedriftet ist</span></span>
<spanid="cb4-153"><ahref="#cb4-153"aria-hidden="true"tabindex="-1"></a><spanclass="va">self</span>.sekunden <spanclass="op">=</span><spanclass="fl">0.0</span><spanclass="co"># nur zur Information, nicht zum Vergleich</span></span>
<spanid="cb4-167"><ahref="#cb4-167"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Der einzige Unterschied zwischen beiden steht in der Annahmezeile: Das</span></span>
<spanid="cb4-168"><ahref="#cb4-168"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Bergsteigen nimmt nur Verbesserungen, SA nimmt Verschlechterungen mit einer</span></span>
<spanid="cb4-169"><ahref="#cb4-169"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Wahrscheinlichkeit an, die mit der Zeit gegen null geht.</span></span>
<spanid="cb4-186"><ahref="#cb4-186"aria-hidden="true"tabindex="-1"></a> von <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n))</span>
<spanid="cb4-187"><ahref="#cb4-187"aria-hidden="true"tabindex="-1"></a> nach <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n <spanclass="op">+</span><spanclass="dv">1</span>))</span>
<spanid="cb4-197"><ahref="#cb4-197"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Metropolis-Kriterium: je groesser die Verschlechterung und je</span></span>
<spanid="cb4-198"><ahref="#cb4-198"aria-hidden="true"tabindex="-1"></a><spanclass="co"># kaelter es ist, desto unwahrscheinlicher.</span></span>
<spanid="cb4-222"><ahref="#cb4-222"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" SIMULATED ANNEALING AN DER LACKIERANLAGE"</span>)</span>
<spanid="cb4-224"><ahref="#cb4-224"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>N<spanclass="sc">}</span><spanclass="ss"> Auftraege, </span><spanclass="sc">{</span><spanclass="bu">max</span>(<spanclass="dv">6</span>, N <spanclass="op">//</span><spanclass="dv">10</span>)<spanclass="sc">}</span><spanclass="ss"> Produktfamilien, "</span></span>
<spanid="cb4-225"><ahref="#cb4-225"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Zugbudget </span><spanclass="sc">{</span>ZUEGE<spanclass="sc">:,}</span><spanclass="ss"> je Lauf.</span><spanclass="ch">\n</span><spanclass="ss">"</span>)</span>
<spanid="cb4-229"><ahref="#cb4-229"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Wie gross sind die Zuege ueberhaupt? ---------------------------</span></span>
<spanid="cb4-230"><ahref="#cb4-230"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Ohne diese Zahl kann man die Temperatur nicht waehlen: Das</span></span>
<spanid="cb4-231"><ahref="#cb4-231"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Metropolis-Kriterium vergleicht die Verschlechterung MIT der Temperatur.</span></span>
<spanid="cb4-235"><ahref="#cb4-235"aria-hidden="true"tabindex="-1"></a> e <spanclass="op">=</span> delta_verschieben(start_reihe, matrix,</span>
<spanid="cb4-236"><ahref="#cb4-236"aria-hidden="true"tabindex="-1"></a><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, N)), <spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, N <spanclass="op">+</span><spanclass="dv">1</span>)))</span>
<spanid="cb4-252"><ahref="#cb4-252"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> Beide pruefen genau </span><spanclass="sc">{</span>berg_verlauf<spanclass="sc">.</span>schritte<spanclass="sc">:,}</span><spanclass="ss"> Zuege "</span></span>
<spanid="cb4-253"><ahref="#cb4-253"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"(</span><spanclass="sc">{</span>berg_verlauf<spanclass="sc">.</span>sekunden<spanclass="sc">:.1f}</span><spanclass="ss"> bzw. </span><spanclass="sc">{</span>gluehen_verlauf<spanclass="sc">.</span>sekunden<spanclass="sc">:.1f}</span><spanclass="ss"> Sekunden)."</span>)</span>
<spanid="cb4-254"><ahref="#cb4-254"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Annealing nimmt davon </span><spanclass="sc">{</span>gluehen_verlauf<spanclass="sc">.</span>annahmequote <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.2f}</span><spanclass="ss"> % der "</span></span>
<spanid="cb4-255"><ahref="#cb4-255"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"VERSCHLECHTERUNGEN an, Bergsteigen keine."</span>)</span>
<spanid="cb4-256"><ahref="#cb4-256"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Bemerkenswert ist, wie klein der Abstand ist: Das simple"</span>)</span>
<spanid="cb4-257"><ahref="#cb4-257"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Bergsteigen holt hier bereits </span><spanclass="sc">{</span>(start_kosten <spanclass="op">-</span> berg) <spanclass="op">/</span> (start_kosten <spanclass="op">-</span> gluehen) <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.0f}</span><spanclass="ss"> % dessen, was Annealing"</span>)</span>
<spanid="cb4-258"><ahref="#cb4-258"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" schafft. Wer eine Metaheuristik einsetzt, sollte diesen Vergleich"</span>)</span>
<spanid="cb4-259"><ahref="#cb4-259"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" IMMER mitrechnen - sonst schreibt man einer aufwendigen Methode"</span>)</span>
<spanid="cb4-260"><ahref="#cb4-260"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" gut, was schon die einfache geliefert haette."</span>)</span>
<spanid="cb4-262"><ahref="#cb4-262"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Die Temperatur ist kein Beiwerk ---------------------------------</span></span>
<spanid="cb4-281"><ahref="#cb4-281"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> Bester Wert bei T0 = </span><spanclass="sc">{</span>beste_temperatur<spanclass="sc">}</span><spanclass="ss">."</span>)</span>
<spanid="cb4-282"><ahref="#cb4-282"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Die letzten beiden Spalten erklaeren, warum es nach oben kippt."</span>)</span>
<spanid="cb4-283"><ahref="#cb4-283"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Bei T0 = 8 gehen nur </span><spanclass="sc">{</span>heiss<spanclass="sc">.</span>annahmequote <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.2f}</span><spanclass="ss"> % der Verschlechterungen"</span>)</span>
<spanid="cb4-284"><ahref="#cb4-284"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" durch - wenig genug, dass man es fuer harmlos halten koennte. Es"</span>)</span>
<spanid="cb4-285"><ahref="#cb4-285"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" genuegt aber: Die Suche driftet bis auf </span><spanclass="sc">{</span>heiss<spanclass="sc">.</span>schlechtester_zustand<spanclass="sc">:,}</span><spanclass="ss"> Minuten ab, das"</span>)</span>
<spanid="cb4-286"><ahref="#cb4-286"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>heiss<spanclass="sc">.</span>schlechtester_zustand <spanclass="op">/</span> start_kosten<spanclass="sc">:.1f}</span><spanclass="ss">-fache der Startloesung, und findet im Zugbudget nicht"</span>)</span>
<spanid="cb4-287"><ahref="#cb4-287"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" mehr zurueck. Vom Ergebnis bleibt fast nichts uebrig: Die Suche hat"</span>)</span>
<spanid="cb4-288"><ahref="#cb4-288"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" in </span><spanclass="sc">{</span>ZUEGE<spanclass="sc">:,}</span><spanclass="ss"> Zuegen nie etwas GESEHEN, das mehr als"</span>)</span>
<spanid="cb4-289"><ahref="#cb4-289"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>(start_kosten <spanclass="op">-</span> ergebnisse[<spanclass="fl">8.0</span>]) <spanclass="op">/</span> start_kosten <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.1f}</span><spanclass="ss"> % unter der Startloesung lag."</span>)</span>
<spanid="cb4-290"><ahref="#cb4-290"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Bemerkenswert ist die andere Richtung: Die besten Werte entstehen"</span>)</span>
<spanid="cb4-291"><ahref="#cb4-291"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" bei Annahmequoten nahe null. Die Lehrbuchregel 'anfangs sollen"</span>)</span>
<spanid="cb4-292"><ahref="#cb4-292"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" 20 bis 50 Prozent der Verschlechterungen durchgehen' fuehrt hier in"</span>)</span>
<spanid="cb4-293"><ahref="#cb4-293"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" die Irre - sie stammt aus Problemen mit sehr kleinen Zuggroessen."</span>)</span>
<spanid="cb4-294"><ahref="#cb4-294"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Bei Ruestzeiten in Minuten ist ein einziger schlechter Zug teuer."</span>)</span>
<spanid="cb4-295"><ahref="#cb4-295"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Was stattdessen traegt: Den SCHLECHTESTEN Zwischenwert protokollieren"</span>)</span>
<spanid="cb4-296"><ahref="#cb4-296"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" und T0 so waehlen, dass er die Startloesung nicht wesentlich"</span>)</span>
<spanid="cb4-297"><ahref="#cb4-297"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ueberschreitet. Diese eine Zahl haette hier auf Anhieb gezeigt, dass"</span>)</span>
<spanid="cb4-298"><ahref="#cb4-298"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" T0 = 8 unbrauchbar ist - ohne dass man das Endergebnis abwarten muss."</span>)</span>
<imgsrc="bilder_04/kap_metaheuristiken_landschaft.svg"alt="Abb. 9.1: Dieselbe Rechnung als Bild. Links der Zielwert während der Suche: Bergsteigen fällt monoton, Annealing bei T_0=1 fast genauso — bei T_0=8 irrt die Suche bis auf das 2,1-fache der Startlösung ab und findet im Zugbudget nicht zurück. Rechts das Ergebnis über die Starttemperatur: Das Optimum ist schmal, und jenseits davon ist der ganze Vorsprung aufgezehrt. Erzeugt von bilder_04/erzeuge_metaheuristik_landschaft.py."/>
<figcaptionaria-hidden="true">Abb. 9.1: Dieselbe Rechnung als Bild. Links der Zielwert während der Suche: Bergsteigen fällt monoton, Annealing bei <spanclass="math inline">T_0=1</span> fast genauso — bei <spanclass="math inline">T_0=8</span> irrt die Suche bis auf das 2,1-fache der Startlösung ab und findet im Zugbudget nicht zurück. Rechts das Ergebnis über die Starttemperatur: Das Optimum ist schmal, und jenseits davon ist der ganze Vorsprung aufgezehrt. Erzeugt von <code>bilder_04/erzeuge_metaheuristik_landschaft.py</code>.</figcaption>
<h3id="was-die-messung-zeigt-und-was-sie-nicht-zeigt">Was die Messung zeigt — und was sie nicht zeigt</h3>
<p><strong>Erstens: Der Beitrag des Annealings ist bescheiden.</strong> Bergsteigen kommt auf 917 Minuten, Annealing auf 907 — das simple Verfahren holt 93 % des Ertrags. Das ist kein Argument gegen Annealing, aber ein starkes Argument dafür, <strong>immer beide zu rechnen</strong>. Wer nur die aufwendige Variante misst, schreibt ihr gut, was schon die einfache geliefert hätte.</p>
<p><strong>Zweitens: Die Temperatur ist keine Stellschraube unter vielen.</strong> Bei <spanclass="math inline">T_0 = 8</span> liefert das Verfahren praktisch nichts mehr. Die letzten beiden Spalten der Tabelle erklären, warum — und die Erklärung ist nicht die, die man erwartet:</p>
<p>Bei <spanclass="math inline">T_0 = 8</span> gehen nur <strong>0,29 %</strong> der Verschlechterungen durch. Das klingt harmlos. Es genügt aber, um die Suche bis auf das 2,1-fache der Startlösung abdriften zu lassen — und aus diesem Zustand findet sie im verbleibenden Budget nicht zurück.</p>
<blockquote>
<p><strong>⚠️ Eine Lehrbuchregel, die hier in die Irre führt</strong></p>
<p>In vielen Darstellungen steht: <em>„Wählen Sie <spanclass="math inline">T_0</span> so, dass anfangs 20 bis 50 % der Verschlechterungen angenommen werden.“</em> Auf dieser Aufgabe wäre das katastrophal — die besten Werte entstehen bei Annahmequoten <strong>nahe null</strong>.</p>
<p>Der Grund ist die Zuggröße. Die Regel stammt aus Problemen, in denen ein einzelner Zug die Lösung kaum verändert. Hier kostet ein schlechter Zug im Median 70 Minuten bei einer Gesamtlösung von rund 1 000 — jeder einzelne Zug ist eine spürbare Verschlechterung.</p>
<p><strong>Was stattdessen trägt:</strong> den <em>schlechtesten Zwischenwert</em> protokollieren und <spanclass="math inline">T_0</span> so wählen, dass er die Startlösung nicht wesentlich überschreitet. Diese eine Zahl zeigt sofort, dass <spanclass="math inline">T_0 = 8</span> unbrauchbar ist — ohne dass man das Endergebnis abwarten muss.</p>
</blockquote>
<hr/>
<h2id="sec:metaheuristiken-umschlagpunkt">9.6 Ab wann lohnt es sich?</h2>
<p>„Kleine Probleme exakt, große mit Metaheuristiken“ ist richtig und nutzlos, solange niemand sagt, wo <em>groß</em> anfängt. Diese Zahl lässt sich messen.</p>
<spanid="cb6-5"><ahref="#cb6-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Metaheuristiken: Ab wann lohnt sich eine Metaheuristik? Die Antwort ist eine Zahl.</span></span>
<spanid="cb6-7"><ahref="#cb6-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Die verbreitete Regel lautet: "Kleine Probleme exakt, grosse mit</span></span>
<spanid="cb6-8"><ahref="#cb6-8"aria-hidden="true"tabindex="-1"></a><spanclass="co">Metaheuristiken." Sie ist richtig - aber voellig nutzlos, solange niemand sagt,</span></span>
<spanid="cb6-9"><ahref="#cb6-9"aria-hidden="true"tabindex="-1"></a><spanclass="co">wo 'gross' anfaengt. Dieses Programm misst es fuer die Ruestzeitenaufgabe aus</span></span>
<spanid="cb6-10"><ahref="#cb6-10"aria-hidden="true"tabindex="-1"></a><spanclass="co">Simulated_Annealing.py nach, und das Ergebnis ueberrascht in beide Richtungen:</span></span>
<spanid="cb6-12"><ahref="#cb6-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Bei 60 Auftraegen loest CP-SAT BEWEISBAR optimal, in wenigen Sekunden.</span></span>
<spanid="cb6-13"><ahref="#cb6-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Metaheuristik ist hier schlicht die schlechtere Wahl - sie liefert ein</span></span>
<spanid="cb6-14"><ahref="#cb6-14"aria-hidden="true"tabindex="-1"></a><spanclass="co"> schwaecheres Ergebnis und weiss nicht einmal, wie schwach.</span></span>
<spanid="cb6-15"><ahref="#cb6-15"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Bei 200 Auftraegen liegt CP-SAT ohne Optimalitaetsbeweis vorne.</span></span>
<spanid="cb6-16"><ahref="#cb6-16"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Erst bei 500 Auftraegen kippt es: Dort gibt CP-SAT im Zeitlimit eine</span></span>
<spanid="cb6-17"><ahref="#cb6-17"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Loesung aus, die SCHLECHTER ist als die Faustregel eines Meisters.</span></span>
<spanid="cb6-19"><ahref="#cb6-19"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der Punkt des Kapitels steht in der letzten Spalte: Die Schranke. Nur der</span></span>
<spanid="cb6-20"><ahref="#cb6-20"aria-hidden="true"tabindex="-1"></a><spanclass="co">exakte Solver sagt, wie gut die Loesung sein KANN. Eine Metaheuristik allein</span></span>
<spanid="cb6-21"><ahref="#cb6-21"aria-hidden="true"tabindex="-1"></a><spanclass="co">liefert eine Zahl ohne Massstab - man weiss nie, ob man 2 % oder 40 % neben dem</span></span>
<spanid="cb6-24"><ahref="#cb6-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">ZUR REPRODUZIERBARKEIT: Die Spalte 'Annealing' ist auf jeder Maschine gleich -</span></span>
<spanid="cb6-25"><ahref="#cb6-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">sie laeuft ueber ein festes ZUGbudget. Die CP-SAT-Spalten sind es nicht: Ein</span></span>
<spanid="cb6-26"><ahref="#cb6-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeitlimit ist ein Wanduhr-Limit, und wie weit der Solver in 30 Sekunden kommt,</span></span>
<spanid="cb6-27"><ahref="#cb6-27"aria-hidden="true"tabindex="-1"></a><spanclass="co">haengt von der Maschine ab. CP-SAT kennt zwar ein deterministisches Zeitmass</span></span>
<spanid="cb6-28"><ahref="#cb6-28"aria-hidden="true"tabindex="-1"></a><spanclass="co">(max_deterministic_time), das aber in einer Groessenordnung rechnet, die fuer</span></span>
<spanid="cb6-29"><ahref="#cb6-29"aria-hidden="true"tabindex="-1"></a><spanclass="co">ein Buchbeispiel unbrauchbar langsam ist. Die Zahlen unten sind also</span></span>
<spanid="cb6-30"><ahref="#cb6-30"aria-hidden="true"tabindex="-1"></a><spanclass="co">hardwareabhaengig - die AUSSAGE der Tabelle ist es nicht, sie ist auf</span></span>
<spanid="cb6-31"><ahref="#cb6-31"aria-hidden="true"tabindex="-1"></a><spanclass="co">verschiedenen Groessen und in mehreren Laeufen stabil.</span></span>
<spanid="cb6-33"><ahref="#cb6-33"aria-hidden="true"tabindex="-1"></a><spanclass="co">WICHTIG: Hier wird nur ortools importiert, nicht highspy (Kapitel Oekosystem).</span></span>
<spanid="cb6-50"><ahref="#cb6-50"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Instanz, Faustregel und Suche: identisch zu Simulated_Annealing.py -----</span></span>
<spanid="cb6-54"><ahref="#cb6-54"aria-hidden="true"tabindex="-1"></a> familien <spanclass="op">=</span><spanclass="bu">max</span>(<spanclass="dv">6</span>, n <spanclass="op">//</span><spanclass="dv">10</span>)</span>
<spanid="cb6-55"><ahref="#cb6-55"aria-hidden="true"tabindex="-1"></a> zwischen <spanclass="op">=</span> rng.integers(<spanclass="dv">8</span>, <spanclass="dv">60</span>, (familien, familien))</span>
<spanid="cb6-60"><ahref="#cb6-60"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb6-84"><ahref="#cb6-84"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(reihe)</span>
<spanid="cb6-85"><ahref="#cb6-85"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">==</span> nach <spanclass="kw">or</span> nach <spanclass="op">==</span> von <spanclass="op">+</span><spanclass="dv">1</span>:</span>
<spanid="cb6-88"><ahref="#cb6-88"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb6-90"><ahref="#cb6-90"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>:</span>
<spanid="cb6-92"><ahref="#cb6-92"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>:</span>
<spanid="cb6-95"><ahref="#cb6-95"aria-hidden="true"tabindex="-1"></a> stelle <spanclass="op">=</span> nach <spanclass="cf">if</span> nach <spanclass="op"><</span> von <spanclass="cf">else</span> nach <spanclass="op">-</span><spanclass="dv">1</span></span>
<spanid="cb6-97"><ahref="#cb6-97"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb6-99"><ahref="#cb6-99"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest):</span>
<spanid="cb6-101"><ahref="#cb6-101"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest):</span>
<spanid="cb6-108"><ahref="#cb6-108"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Dieselbe Suche wie in Simulated_Annealing.py, hier nur als Vergleichswert."""</span></span>
<spanid="cb6-120"><ahref="#cb6-120"aria-hidden="true"tabindex="-1"></a> von <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n))</span>
<spanid="cb6-121"><ahref="#cb6-121"aria-hidden="true"tabindex="-1"></a> nach <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n <spanclass="op">+</span><spanclass="dv">1</span>))</span>
<spanid="cb6-133"><ahref="#cb6-133"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Der exakte Weg: CP-SAT mit AddCircuit ---------------------------------</span></span>
<spanid="cb6-136"><ahref="#cb6-136"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Reihenfolge als Rundreise mit einem kostenlosen Hilfsknoten.</span></span>
<spanid="cb6-138"><ahref="#cb6-138"aria-hidden="true"tabindex="-1"></a><spanclass="co"> AddCircuit verlangt einen geschlossenen Kreis. Unsere Aufgabe ist aber eine</span></span>
<spanid="cb6-139"><ahref="#cb6-139"aria-hidden="true"tabindex="-1"></a><spanclass="co"> offene Kette: Der erste Auftrag hat keinen Vorgaenger, der letzte keinen</span></span>
<spanid="cb6-140"><ahref="#cb6-140"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Nachfolger. Der uebliche Kniff ist ein zusaetzlicher Hilfsknoten n, dessen</span></span>
<spanid="cb6-141"><ahref="#cb6-141"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Kanten nichts kosten. Der Kreis laeuft dann Hilfsknoten -> Kette -></span></span>
<spanid="cb6-142"><ahref="#cb6-142"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Hilfsknoten, und die Kosten des Kreises sind genau die der Kette.</span></span>
<spanid="cb6-144"><ahref="#cb6-144"aria-hidden="true"tabindex="-1"></a><spanclass="co"> AddCircuit ist der Grund, warum CP-SAT hier so lange mithaelt: Die</span></span>
<spanid="cb6-145"><ahref="#cb6-145"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Nebenbedingung sorgt selbst dafuer, dass keine Teilkreise entstehen. Wer</span></span>
<spanid="cb6-146"><ahref="#cb6-146"aria-hidden="true"tabindex="-1"></a><spanclass="co"> dasselbe als MILP formuliert, braucht dafuer entweder exponentiell viele</span></span>
<spanid="cb6-147"><ahref="#cb6-147"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Ungleichungen oder die schwache MTZ-Formulierung (Kapitel Graphen).</span></span>
<spanid="cb6-149"><ahref="#cb6-149"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(matrix)</span>
<spanid="cb6-150"><ahref="#cb6-150"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb6-151"><ahref="#cb6-151"aria-hidden="true"tabindex="-1"></a> kanten, ziel <spanclass="op">=</span> [], []</span>
<spanid="cb6-152"><ahref="#cb6-152"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n <spanclass="op">+</span><spanclass="dv">1</span>):</span>
<spanid="cb6-156"><ahref="#cb6-156"aria-hidden="true"tabindex="-1"></a> aktiv <spanclass="op">=</span> modell.NewBoolVar(<spanclass="ss">f"kante_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb6-176"><ahref="#cb6-176"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" AB WANN LOHNT SICH DIE METAHEURISTIK?"</span>)</span>
<spanid="cb6-178"><ahref="#cb6-178"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Dieselbe Aufgabe in drei Groessen. Metaheuristik: </span><spanclass="sc">{</span>ZUEGE<spanclass="sc">:,}</span><spanclass="ss"> Zuege. "</span></span>
<spanid="cb6-179"><ahref="#cb6-179"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"Exakt: </span><spanclass="sc">{</span>ZEITLIMIT<spanclass="sc">:.0f}</span><spanclass="ss"> s Zeitlimit.</span><spanclass="ch">\n</span><spanclass="ss">"</span>)</span>
<spanid="cb6-196"><ahref="#cb6-196"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st">Was in jeder Zeile steht:</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb6-197"><ahref="#cb6-197"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> n, start, heur, wert, status, schranke, _ <spanclass="kw">in</span> zeilen:</span>
<spanid="cb6-198"><ahref="#cb6-198"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> status <spanclass="op">==</span><spanclass="st">"OPTIMAL"</span>:</span>
<spanid="cb6-199"><ahref="#cb6-199"aria-hidden="true"tabindex="-1"></a> urteil <spanclass="op">=</span> (<spanclass="ss">f"CP-SAT beweist das Optimum. Annealing liegt "</span></span>
<spanid="cb6-201"><ahref="#cb6-201"aria-hidden="true"tabindex="-1"></a> zweite <spanclass="op">=</span><spanclass="st">" das ohne den exakten Lauf nicht erfahren."</span></span>
<spanid="cb6-205"><ahref="#cb6-205"aria-hidden="true"tabindex="-1"></a> zweite <spanclass="op">=</span><spanclass="st">" Auch ohne Optimalitaetsbeweis ist es die bessere Wahl."</span></span>
<spanid="cb6-208"><ahref="#cb6-208"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"SCHLECHTER als Annealing"</span>)</span>
<spanid="cb6-209"><ahref="#cb6-209"aria-hidden="true"tabindex="-1"></a> zweite <spanclass="op">=</span> (<spanclass="ss">f" und sogar </span><spanclass="sc">{</span>(wert <spanclass="op">-</span> start) <spanclass="op">/</span> start <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.1f}</span><spanclass="ss"> % schlechter "</span></span>
<spanid="cb6-210"><ahref="#cb6-210"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"als die Faustregel des Meisters."</span>)</span>
<spanid="cb6-216"><ahref="#cb6-216"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" DIE SPALTE, DIE MAN NICHT WEGLASSEN DARF"</span>)</span>
<spanid="cb6-218"><ahref="#cb6-218"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Schranke ist der eigentliche Ertrag des exakten Solvers - auch dann,"</span>)</span>
<spanid="cb6-219"><ahref="#cb6-219"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"wenn seine LOESUNG unbrauchbar ist."</span>)</span>
<spanid="cb6-223"><ahref="#cb6-223"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"besser als die Faustregel. Die Schranke aus demselben CP-SAT-Lauf, dessen"</span>)</span>
<spanid="cb6-224"><ahref="#cb6-224"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Loesung wir gerade verworfen haben, sagt aber: Unter </span><spanclass="sc">{</span>schranke<spanclass="sc">:,}</span><spanclass="ss"> Minuten geht"</span>)</span>
<spanid="cb6-225"><ahref="#cb6-225"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"es nicht. Zwischen </span><spanclass="sc">{</span>schranke<spanclass="sc">:,}</span><spanclass="ss"> und </span><spanclass="sc">{</span>heur<spanclass="sc">:,}</span><spanclass="ss"> liegen </span><spanclass="sc">{</span>(heur <spanclass="op">-</span> schranke) <spanclass="op">/</span> heur <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.0f}</span><spanclass="ss"> % - so viel Luft"</span>)</span>
<spanid="cb6-226"><ahref="#cb6-226"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"kann noch nach oben sein."</span>)</span>
<spanid="cb6-228"><ahref="#cb6-228"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Das ist der Unterschied zwischen 'besser als vorher' und 'gut'. Eine"</span>)</span>
<spanid="cb6-229"><ahref="#cb6-229"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Metaheuristik allein kann nur das Erste. Deshalb gehoert auch dann ein"</span>)</span>
<spanid="cb6-230"><ahref="#cb6-230"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"exakter Lauf dazu, wenn man am Ende die heuristische Loesung einsetzt:"</span>)</span>
<spanid="cb6-231"><ahref="#cb6-231"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nicht wegen seiner Loesung, sondern wegen seiner Schranke."</span>)</span>
<p>Drei Größen, drei völlig verschiedene Empfehlungen:</p>
<table>
<colgroup>
<colstyle="width: 57%"/>
<colstyle="width: 42%"/>
</colgroup>
<thead>
<trclass="header">
<thstyle="text-align: right;">Aufträge</th>
<th>Empfehlung</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<tdstyle="text-align: right;">60</td>
<td><strong>Exakt.</strong> CP-SAT beweist das Optimum in 2 Sekunden. Die Metaheuristik liegt 3,8 % daneben — und wüsste es nicht.</td>
</tr>
<trclass="even">
<tdstyle="text-align: right;">200</td>
<td><strong>Exakt.</strong> Kein Beweis mehr, aber der Gap liegt unter 1 %, und der Wert ist rund 11 % besser als der heuristische.</td>
</tr>
<trclass="odd">
<tdstyle="text-align: right;">500</td>
<td><strong>Heuristisch.</strong> CP-SAT liefert eine Lösung, die 5,2 % <strong>schlechter</strong> ist als die Faustregel eines Meisters.</td>
</tr>
</tbody>
</table>
<blockquote>
<p><strong>🎯 Merksatz</strong> Der Umschlagpunkt ist keine Eigenschaft der Verfahren, sondern des <strong>Problems</strong>. Für die Standortplanung aus <ahref="milp.html#sec:milp-gap">Abschnitt 6.8</a> liegt er woanders als hier. Wer ihn nicht für seine eigene Aufgabe gemessen hat, rät.</p>
</blockquote>
<h3id="die-spalte-die-man-nicht-weglassen-darf">Die Spalte, die man nicht weglassen darf</h3>
<p>Bei 500 Aufträgen haben wir die CP-SAT-<em>Lösung</em> verworfen. Seine <strong>Schranke</strong> ist trotzdem das Wertvollste in der ganzen Tabelle.</p>
<p>Annealing liefert 2 343 Minuten — 6,2 % besser als die Faustregel. Klingt nach einem Erfolg. Die Schranke sagt: unter 1 768 Minuten geht es nicht. Zwischen 1 768 und 2 343 liegen <strong>25 %</strong>.</p>
<p>Das ist der Unterschied zwischen <em>besser als vorher</em> und <em>gut</em>. Eine Metaheuristik allein kann nur das Erste behaupten. Deshalb gehört ein exakter Lauf auch dann dazu, wenn man am Ende die heuristische Lösung einsetzt — nicht wegen seiner Lösung, sondern wegen seiner Schranke.</p>
<hr/>
<h2id="sec:metaheuristiken-lns">9.7 Large Neighborhood Search</h2>
<p>Die Tabelle oben endet mit einem Widerspruch. Bei 500 Aufträgen scheitert der exakte Solver an der <strong>Größe</strong>; die Metaheuristik scheitert an der <strong>Kleinheit ihrer Züge</strong> — sie verschiebt jeweils einen einzigen Auftrag und kann eine ganze Passage nicht auf einmal umbauen.</p>
<p><strong>Large Neighborhood Search</strong> (LNS) setzt beide ein, jeden für das, was er kann:</p>
<table>
<colgroup>
<colstyle="width: 50%"/>
<colstyle="width: 50%"/>
</colgroup>
<thead>
<trclass="header">
<th>Schritt</th>
<th>Was passiert</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><strong>Zerstören</strong></td>
<td>Ein zusammenhängendes Stück des Plans herausbrechen — hier 20 Aufträge.</td>
</tr>
<trclass="even">
<td><strong>Reparieren</strong></td>
<td>Genau dieses Stück <strong>exakt</strong> neu optimieren. 20 Aufträge sind für CP-SAT eine Kleinigkeit, 500 sind es nicht.</td>
</tr>
<trclass="odd">
<td><strong>Übernehmen</strong></td>
<td>Nur behalten, wenn der Gesamtplan besser wurde.</td>
</tr>
</tbody>
</table>
<p>Der Solver bekommt also nicht mehr das ganze Problem, sondern immer wieder ein kleines.</p>
<spanid="cb8-5"><ahref="#cb8-5"aria-hidden="true"tabindex="-1"></a><spanclass="co">Kapitel Metaheuristiken: Zerstoeren und exakt reparieren - der Solver im Dienst der Heuristik.</span></span>
<spanid="cb8-7"><ahref="#cb8-7"aria-hidden="true"tabindex="-1"></a><spanclass="co">Metaheuristik_vs_Exakt.py endet mit einem Widerspruch. Bei 500 Auftraegen gilt:</span></span>
<spanid="cb8-9"><ahref="#cb8-9"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Der exakte Solver scheitert an der GROESSE - seine Loesung nach 30 Sekunden</span></span>
<spanid="cb8-10"><ahref="#cb8-10"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ist schlechter als die Faustregel eines Meisters.</span></span>
<spanid="cb8-11"><ahref="#cb8-11"aria-hidden="true"tabindex="-1"></a><spanclass="co"> * Die Metaheuristik kommt weiter, laesst aber laut Schranke noch rund ein</span></span>
<spanid="cb8-12"><ahref="#cb8-12"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Viertel liegen. Ihre Zuege sind zu kleinteilig: Sie verschiebt jeweils</span></span>
<spanid="cb8-13"><ahref="#cb8-13"aria-hidden="true"tabindex="-1"></a><spanclass="co"> EINEN Auftrag und kann eine ganze Passage nicht auf einmal umbauen.</span></span>
<spanid="cb8-15"><ahref="#cb8-15"aria-hidden="true"tabindex="-1"></a><spanclass="co">Large Neighborhood Search loest den Widerspruch, indem sie beide einsetzt -</span></span>
<spanid="cb8-16"><ahref="#cb8-16"aria-hidden="true"tabindex="-1"></a><spanclass="co">jeden fuer das, was er kann:</span></span>
<spanid="cb8-18"><ahref="#cb8-18"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ZERSTOEREN Ein Stueck des Plans herausbrechen (hier: ein zusammen-</span></span>
<spanid="cb8-19"><ahref="#cb8-19"aria-hidden="true"tabindex="-1"></a><spanclass="co"> haengendes Fenster von 20 Auftraegen).</span></span>
<spanid="cb8-20"><ahref="#cb8-20"aria-hidden="true"tabindex="-1"></a><spanclass="co"> REPARIEREN Genau dieses Stueck EXAKT neu optimieren. 20 Auftraege sind</span></span>
<spanid="cb8-21"><ahref="#cb8-21"aria-hidden="true"tabindex="-1"></a><spanclass="co"> fuer CP-SAT eine Kleinigkeit, 500 sind es nicht.</span></span>
<spanid="cb8-22"><ahref="#cb8-22"aria-hidden="true"tabindex="-1"></a><spanclass="co"> UEBERNEHMEN Nur behalten, wenn der Gesamtplan besser wurde.</span></span>
<spanid="cb8-24"><ahref="#cb8-24"aria-hidden="true"tabindex="-1"></a><spanclass="co">Der Solver bekommt also nicht mehr das ganze Problem, sondern immer wieder ein</span></span>
<spanid="cb8-25"><ahref="#cb8-25"aria-hidden="true"tabindex="-1"></a><spanclass="co">kleines. Das ist der ganze Trick, und er ist in der Praxis der wichtigste</span></span>
<spanid="cb8-26"><ahref="#cb8-26"aria-hidden="true"tabindex="-1"></a><spanclass="co">Baustein dieses Kapitels.</span></span>
<spanid="cb8-28"><ahref="#cb8-28"aria-hidden="true"tabindex="-1"></a><spanclass="co">ZUR REPRODUZIERBARKEIT: Alle Laeufe haben eine feste RUNDENzahl, nicht ein</span></span>
<spanid="cb8-29"><ahref="#cb8-29"aria-hidden="true"tabindex="-1"></a><spanclass="co">Zeitbudget - die Ergebniswerte sind damit auf jeder Maschine gleich. Die</span></span>
<spanid="cb8-30"><ahref="#cb8-30"aria-hidden="true"tabindex="-1"></a><spanclass="co">Laufzeiten daneben sind hardwareabhaengig.</span></span>
<spanid="cb8-43"><ahref="#cb8-43"aria-hidden="true"tabindex="-1"></a>FENSTER <spanclass="op">=</span><spanclass="dv">20</span><spanclass="co"># so viele Auftraege werden je Runde herausgebrochen</span></span>
<spanid="cb8-49"><ahref="#cb8-49"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Instanz und Faustregel (wie in Simulated_Annealing.py) ----------------</span></span>
<spanid="cb8-53"><ahref="#cb8-53"aria-hidden="true"tabindex="-1"></a> familien <spanclass="op">=</span><spanclass="bu">max</span>(<spanclass="dv">6</span>, n <spanclass="op">//</span><spanclass="dv">10</span>)</span>
<spanid="cb8-54"><ahref="#cb8-54"aria-hidden="true"tabindex="-1"></a> zwischen <spanclass="op">=</span> rng.integers(<spanclass="dv">8</span>, <spanclass="dv">60</span>, (familien, familien))</span>
<spanid="cb8-59"><ahref="#cb8-59"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(n):</span>
<spanid="cb8-83"><ahref="#cb8-83"aria-hidden="true"tabindex="-1"></a> n <spanclass="op">=</span><spanclass="bu">len</span>(reihe)</span>
<spanid="cb8-84"><ahref="#cb8-84"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">==</span> nach <spanclass="kw">or</span> nach <spanclass="op">==</span> von <spanclass="op">+</span><spanclass="dv">1</span>:</span>
<spanid="cb8-87"><ahref="#cb8-87"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb8-89"><ahref="#cb8-89"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>:</span>
<spanid="cb8-91"><ahref="#cb8-91"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> von <spanclass="op"><</span> n <spanclass="op">-</span><spanclass="dv">1</span>:</span>
<spanid="cb8-94"><ahref="#cb8-94"aria-hidden="true"tabindex="-1"></a> stelle <spanclass="op">=</span> nach <spanclass="cf">if</span> nach <spanclass="op"><</span> von <spanclass="cf">else</span> nach <spanclass="op">-</span><spanclass="dv">1</span></span>
<spanid="cb8-96"><ahref="#cb8-96"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op">></span><spanclass="dv">0</span>:</span>
<spanid="cb8-98"><ahref="#cb8-98"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest):</span>
<spanid="cb8-100"><ahref="#cb8-100"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span><spanclass="dv">0</span><spanclass="op"><</span> stelle <spanclass="op"><</span><spanclass="bu">len</span>(rest):</span>
<spanid="cb8-114"><ahref="#cb8-114"aria-hidden="true"tabindex="-1"></a> von, nach <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n)), <spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, n <spanclass="op">+</span><spanclass="dv">1</span>))</span>
<spanid="cb8-127"><ahref="#cb8-127"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Der Reparaturschritt: ein kleines Problem, exakt geloest --------------</span></span>
<spanid="cb8-131"><ahref="#cb8-131"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Ordnet reihe[a:b] optimal neu; die beiden Raender bleiben, wo sie sind.</span></span>
<spanid="cb8-133"><ahref="#cb8-133"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Die Raender festzuhalten ist entscheidend. Ohne sie waere das Teilproblem</span></span>
<spanid="cb8-134"><ahref="#cb8-134"aria-hidden="true"tabindex="-1"></a><spanclass="co"> ein anderes als der Ausschnitt aus dem Gesamtplan: Der Uebergang vom</span></span>
<spanid="cb8-135"><ahref="#cb8-135"aria-hidden="true"tabindex="-1"></a><spanclass="co"> Vorgaenger in das Fenster und aus dem Fenster in den Nachfolger gehoert</span></span>
<spanid="cb8-136"><ahref="#cb8-136"aria-hidden="true"tabindex="-1"></a><spanclass="co"> zu den Kosten dazu. Genau dafuer steht der Hilfsknoten k - er vertritt</span></span>
<spanid="cb8-137"><ahref="#cb8-137"aria-hidden="true"tabindex="-1"></a><spanclass="co"> beide Raender in einem.</span></span>
<spanid="cb8-144"><ahref="#cb8-144"aria-hidden="true"tabindex="-1"></a> modell <spanclass="op">=</span> cp_model.CpModel()</span>
<spanid="cb8-145"><ahref="#cb8-145"aria-hidden="true"tabindex="-1"></a> kanten, ziel <spanclass="op">=</span> [], []</span>
<spanid="cb8-146"><ahref="#cb8-146"aria-hidden="true"tabindex="-1"></a><spanclass="cf">for</span> i <spanclass="kw">in</span><spanclass="bu">range</span>(k <spanclass="op">+</span><spanclass="dv">1</span>):</span>
<spanid="cb8-150"><ahref="#cb8-150"aria-hidden="true"tabindex="-1"></a> aktiv <spanclass="op">=</span> modell.NewBoolVar(<spanclass="ss">f"kante_</span><spanclass="sc">{</span>i<spanclass="sc">}</span><spanclass="ss">_</span><spanclass="sc">{</span>j<spanclass="sc">}</span><spanclass="ss">"</span>)</span>
<spanid="cb8-152"><ahref="#cb8-152"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> i <spanclass="op"><</span> k <spanclass="kw">and</span> j <spanclass="op"><</span> k:</span>
<spanid="cb8-154"><ahref="#cb8-154"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> i <spanclass="op">==</span> k <spanclass="kw">and</span> j <spanclass="op"><</span> k <spanclass="kw">and</span> vorgaenger <spanclass="kw">is</span><spanclass="kw">not</span><spanclass="va">None</span>:</span>
<spanid="cb8-156"><ahref="#cb8-156"aria-hidden="true"tabindex="-1"></a><spanclass="cf">elif</span> j <spanclass="op">==</span> k <spanclass="kw">and</span> i <spanclass="op"><</span> k <spanclass="kw">and</span> nachfolger_rand <spanclass="kw">is</span><spanclass="kw">not</span><spanclass="va">None</span>:</span>
<spanid="cb8-165"><ahref="#cb8-165"aria-hidden="true"tabindex="-1"></a> status <spanclass="op">=</span> loeser.Solve(modell)</span>
<spanid="cb8-166"><ahref="#cb8-166"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="cb8-167"><ahref="#cb8-167"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Auch das gehoert dazu: Wenn das Teilproblem nicht loest, bleibt der</span></span>
<spanid="cb8-168"><ahref="#cb8-168"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Plan, wie er war. Eine LNS-Runde darf scheitern.</span></span>
<spanid="cb8-171"><ahref="#cb8-171"aria-hidden="true"tabindex="-1"></a> nachfolger <spanclass="op">=</span> {i: j <spanclass="cf">for</span> i, j, aktiv <spanclass="kw">in</span> kanten <spanclass="cf">if</span> loeser.Value(aktiv)}</span>
<spanid="cb8-181"><ahref="#cb8-181"aria-hidden="true"tabindex="-1"></a><spanclass="co">"""Zerstoeren, exakt reparieren, uebernehmen - so lange das Budget reicht."""</span></span>
<spanid="cb8-187"><ahref="#cb8-187"aria-hidden="true"tabindex="-1"></a> a <spanclass="op">=</span><spanclass="bu">int</span>(rng.integers(<spanclass="dv">0</span>, <spanclass="bu">len</span>(reihe) <spanclass="op">-</span> fenster))</span>
<spanid="cb8-188"><ahref="#cb8-188"aria-hidden="true"tabindex="-1"></a> neu <spanclass="op">=</span> repariere_fenster(reihe, matrix, a, a <spanclass="op">+</span> fenster)</span>
<spanid="cb8-189"><ahref="#cb8-189"aria-hidden="true"tabindex="-1"></a><spanclass="cf">if</span> neu <spanclass="kw">is</span><spanclass="va">None</span>:</span>
<spanid="cb8-202"><ahref="#cb8-202"aria-hidden="true"tabindex="-1"></a> SCHRANKE <spanclass="op">=</span><spanclass="dv">1768</span><spanclass="co"># aus dem CP-SAT-Lauf in Metaheuristik_vs_Exakt.py</span></span>
<spanid="cb8-205"><ahref="#cb8-205"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ZERSTOEREN UND EXAKT REPARIEREN"</span>)</span>
<spanid="cb8-207"><ahref="#cb8-207"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>N<spanclass="sc">}</span><spanclass="ss"> Auftraege. Je Runde werden </span><spanclass="sc">{</span>FENSTER<spanclass="sc">}</span><spanclass="ss"> aufeinanderfolgende Auftraege"</span>)</span>
<spanid="cb8-208"><ahref="#cb8-208"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"herausgebrochen und exakt neu geordnet, </span><spanclass="sc">{</span>RUNDEN<spanclass="sc">}</span><spanclass="ss"> Runden lang.</span><spanclass="ch">\n</span><spanclass="ss">"</span>)</span>
<spanid="cb8-210"><ahref="#cb8-210"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Erst die Metaheuristik allein, dann LNS auf ihrem Ergebnis:</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb8-223"><ahref="#cb8-223"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"LNS ab Faustregel"</span>, lns_ab_faustregel[<spanclass="dv">1</span>]),</span>
<spanid="cb8-224"><ahref="#cb8-224"aria-hidden="true"tabindex="-1"></a> (<spanclass="st">"Annealing, dann LNS"</span>, lns_ab_annealing[<spanclass="dv">1</span>])]:</span>
<spanid="cb8-228"><ahref="#cb8-228"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="ch">\n</span><spanclass="ss"> LNS ab Faustregel: </span><spanclass="sc">{</span>lns_ab_faustregel[<spanclass="dv">2</span>]<spanclass="sc">}</span><spanclass="ss"> von </span><spanclass="sc">{</span>RUNDEN<spanclass="sc">}</span><spanclass="ss"> Runden brachten "</span></span>
<spanid="cb8-230"><ahref="#cb8-230"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" LNS ab Annealing: </span><spanclass="sc">{</span>lns_ab_annealing[<spanclass="dv">2</span>]<spanclass="sc">}</span><spanclass="ss"> von </span><spanclass="sc">{</span>RUNDEN<spanclass="sc">}</span><spanclass="ss"> Runden "</span></span>
<spanid="cb8-233"><ahref="#cb8-233"aria-hidden="true"tabindex="-1"></a><spanclass="co"># --- Die Fenstergroesse ----------------------------------------------</span></span>
<spanid="cb8-234"><ahref="#cb8-234"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Verglichen wird bei ANNAEHERND GLEICHER ZEIT, nicht bei gleicher</span></span>
<spanid="cb8-235"><ahref="#cb8-235"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Rundenzahl: Eine Runde mit Fenster 30 kostet ein Vielfaches einer Runde</span></span>
<spanid="cb8-236"><ahref="#cb8-236"aria-hidden="true"tabindex="-1"></a><spanclass="co"># mit Fenster 10. Wer nach Runden vergleicht, misst nur, dass groessere</span></span>
<spanid="cb8-237"><ahref="#cb8-237"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Teilprobleme mehr finden - und uebersieht, was sie kosten. Die</span></span>
<spanid="cb8-238"><ahref="#cb8-238"aria-hidden="true"tabindex="-1"></a><spanclass="co"># Rundenzahlen unten sind so gewaehlt, dass alle drei Laeufe in derselben</span></span>
<spanid="cb8-241"><ahref="#cb8-241"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Die Fenstergroesse ist die eine Stellschraube - und sie hat ein"</span>)</span>
<spanid="cb8-242"><ahref="#cb8-242"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Optimum in der Mitte. Gleiche Zeit, verschiedene Fenster:</span><spanclass="ch">\n</span><spanclass="st">"</span>)</span>
<spanid="cb8-254"><ahref="#cb8-254"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Nach unten ist die Grenze nicht die Zeit, sondern die SAETTIGUNG:"</span>)</span>
<spanid="cb8-255"><ahref="#cb8-255"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" Mit Fenster 10 bleiben von </span><spanclass="sc">{</span><spanclass="dv">4000</span><spanclass="sc">:,}</span><spanclass="ss"> Runden nur eine Handvoll"</span>)</span>
<spanid="cb8-256"><ahref="#cb8-256"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" Verbesserungen uebrig. Zwanzig Auftraege lassen sich in einem Zug"</span>)</span>
<spanid="cb8-257"><ahref="#cb8-257"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" umbauen, zehn nicht - und was ein Fenster nicht umbauen kann, findet"</span>)</span>
<spanid="cb8-258"><ahref="#cb8-258"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" es auch in beliebig vielen Runden nicht."</span>)</span>
<spanid="cb8-259"><ahref="#cb8-259"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Nach oben ist die Grenze die Rechenzeit: Fenster 30 findet pro Runde"</span>)</span>
<spanid="cb8-260"><ahref="#cb8-260"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" mehr, kommt in derselben Zeit aber nur auf einen Bruchteil der Runden"</span>)</span>
<spanid="cb8-261"><ahref="#cb8-261"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f" und landet bei </span><spanclass="sc">{</span>fenster_ergebnis[<spanclass="dv">30</span>]<spanclass="sc">:,}</span><spanclass="ss"> statt </span><spanclass="sc">{</span>fenster_ergebnis[<spanclass="dv">20</span>]<spanclass="sc">:,}</span><spanclass="ss">. Eine Reparatur mit 30 Auftraegen"</span>)</span>
<spanid="cb8-262"><ahref="#cb8-262"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" ist eben genau das Problem, an dem der exakte Solver im Grossen"</span>)</span>
<spanid="cb8-263"><ahref="#cb8-263"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" scheitert - nur eine Nummer kleiner."</span>)</span>
<spanid="cb8-264"><ahref="#cb8-264"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"</span><spanclass="ch">\n</span><spanclass="st"> Diese Tabelle gehoert an den Anfang jedes LNS-Projekts. Sie zu raten"</span>)</span>
<spanid="cb8-265"><ahref="#cb8-265"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" statt zu messen ist der haeufigste Fehler beim Einsatz des Verfahrens."</span>)</span>
<spanid="cb8-268"><ahref="#cb8-268"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">" WAS DAS HEISST"</span>)</span>
<spanid="cb8-271"><ahref="#cb8-271"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Der beste Plan kommt aus der Kombination: </span><spanclass="sc">{</span>bester<spanclass="sc">:,}</span><spanclass="ss"> Minuten,"</span>)</span>
<spanid="cb8-272"><ahref="#cb8-272"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"</span><spanclass="sc">{</span>(start_kosten <spanclass="op">-</span> bester) <spanclass="op">/</span> start_kosten <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.1f}</span><spanclass="ss"> % unter der Faustregel und "</span></span>
<spanid="cb8-273"><ahref="#cb8-273"aria-hidden="true"tabindex="-1"></a><spanclass="ss">f"</span><spanclass="sc">{</span>(sa_kosten <spanclass="op">-</span> bester) <spanclass="op">/</span> sa_kosten <spanclass="op">*</span><spanclass="dv">100</span><spanclass="sc">:.1f}</span><spanclass="ss"> % unter dem reinen Annealing."</span>)</span>
<spanid="cb8-275"><ahref="#cb8-275"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Weder der exakte Solver noch die Metaheuristik allein kommen dorthin."</span>)</span>
<spanid="cb8-276"><ahref="#cb8-276"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Der Solver scheitert an der Groesse, die Metaheuristik an der Kleinheit"</span>)</span>
<spanid="cb8-277"><ahref="#cb8-277"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"ihrer Zuege. LNS gibt dem Solver Teilprobleme in einer Groesse, die er"</span>)</span>
<spanid="cb8-278"><ahref="#cb8-278"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"beherrscht, und der Metaheuristik die Umbauten, die sie nicht kann."</span>)</span>
<spanid="cb8-280"><ahref="#cb8-280"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="ss">f"Und die Ehrlichkeit zum Schluss: Auch </span><spanclass="sc">{</span>bester<spanclass="sc">:,}</span><spanclass="ss"> liegt noch "</span></span>
<spanid="cb8-282"><ahref="#cb8-282"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Schranke. Ob dort wirklich noch so viel Luft ist oder ob die Schranke"</span>)</span>
<spanid="cb8-283"><ahref="#cb8-283"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"nur schwach ist, weiss man nicht - das ist die Lage, in der man mit"</span>)</span>
<spanid="cb8-284"><ahref="#cb8-284"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"einer Metaheuristik arbeitet. Man kennt seine Loesung, nicht ihren"</span>)</span>
<spanid="cb8-285"><ahref="#cb8-285"aria-hidden="true"tabindex="-1"></a><spanclass="bu">print</span>(<spanclass="st">"Abstand zum Optimum."</span>)</span>
<p><strong>💻 Code-Durchgang: warum die Ränder festgehalten werden</strong></p>
<p><code>repariere_fenster()</code> hält den Auftrag <strong>vor</strong> und <strong>nach</strong> dem Fenster fest. Das ist keine Vereinfachung, sondern notwendig: Der Übergang vom Vorgänger in das Fenster und aus dem Fenster in den Nachfolger gehört zu den Kosten. Ein Teilproblem, das diese beiden Kanten ignoriert, optimiert etwas anderes als den Ausschnitt aus dem Gesamtplan — und liefert Umbauten, die im Ganzen teurer sind.</p>
<p>Im Modell erledigt das der Hilfsknoten <code>k</code>: Er vertritt beide Ränder in einem. Seine eingehende Kante trägt die Kosten <em>letzter Auftrag → Nachfolger</em>, seine ausgehende die Kosten <em>Vorgänger → erster Auftrag</em>.</p>
</blockquote>
<h3id="das-fenster-ist-die-eine-stellschraube">Das Fenster ist die eine Stellschraube</h3>
<p>Das Optimum liegt <strong>in der Mitte</strong>, und zwar aus zwei verschiedenen Gründen:</p>
<ul>
<li><strong>Nach unten</strong> ist die Grenze nicht die Zeit, sondern die <strong>Sättigung.</strong> Mit Fenster 10 bleiben von 4 000 Runden ganze 6 Verbesserungen übrig. Was ein Fenster nicht umbauen kann, findet es auch in beliebig vielen Runden nicht.</li>
<li><strong>Nach oben</strong> ist die Grenze die Rechenzeit. Eine Reparatur mit 30 Aufträgen ist genau das Problem, an dem der exakte Solver im Großen scheitert — nur eine Nummer kleiner.</li>
</ul>
<blockquote>
<p><strong>🎯 Merksatz</strong> Diese Tabelle gehört an den Anfang jedes LNS-Projekts, nicht ans Ende. Die Fenstergröße zu raten statt zu messen ist der häufigste Fehler beim Einsatz des Verfahrens.</p>
</blockquote>
<h3id="das-gesamtbild">Das Gesamtbild</h3>
<table>
<thead>
<trclass="header">
<th>Verfahren</th>
<thstyle="text-align: right;">Rüstzeit</th>
<thstyle="text-align: right;">über der Schranke</th>
<p>Der beste Plan kommt aus der Kombination. Und die letzte Spalte bleibt ehrlich: Auch 2 289 liegt noch 29,5 % über der Schranke. Ob dort wirklich so viel Luft ist oder ob die Schranke nur schwach ist, weiß man nicht — das ist die Lage, in der man mit einer Metaheuristik arbeitet.</p>
<hr/>
<h2id="sec:metaheuristiken-abgrenzung">9.8 Was dieses Kapitel nicht behandelt</h2>
<p>Damit die Landkarte vollständig ist — drei Verfahren, die in dieselbe Familie gehören und hier bewusst nur benannt werden:</p>
<table>
<colgroup>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
<colstyle="width: 33%"/>
</colgroup>
<thead>
<trclass="header">
<th>Verfahren</th>
<th>Idee in einem Satz</th>
<th>Wann anschauen</th>
</tr>
</thead>
<tbody>
<trclass="odd">
<td><strong>Genetische Algorithmen</strong></td>
<td>Eine Population von Lösungen wird gekreuzt und mutiert.</td>
<td>Wenn es eine natürliche „Kreuzung“ zweier Lösungen gibt. Für Reihenfolgen ist genau das der schwierige Teil. Bibliothek: <code>pymoo</code>.</td>
</tr>
<trclass="even">
<td><strong>Tabu-Suche</strong></td>
<td>Bergsteigen, das zuletzt gemachte Züge eine Zeit lang verbietet.</td>
<td>Wenn die Suche zwischen zwei Zuständen pendelt. Oft mit weniger Kalibrierung als Annealing.</td>
</tr>
<trclass="odd">
<td><strong>Spaltengenerierung</strong></td>
<td>Nicht die Lösung heuristisch machen, sondern das <strong>Modell</strong> zerlegen: Ein Master-Problem fragt bei einem Teilproblem nach neuen, lohnenden Spalten.</td>
<td>Bei Zuschnitt- und Dienstplanproblemen mit sehr vielen gleichartigen Mustern. <strong>Das folgende Kapitel</strong> führt es aus: <ahref="dekomposition.html#kap-dekomposition">Kapitel 10</a>.</td>
</tr>
</tbody>
</table>
<p>Die Spaltengenerierung ist die interessanteste davon, weil sie eine <strong>andere Antwort auf dieselbe Frage</strong> gibt: Statt die Lösung zu approximieren, formuliert sie das Modell so um, dass exaktes Lösen wieder möglich wird. Sie hat eine eigene Voraussetzung — das Problem muss sich in ein Master- und ein Teilproblem zerlegen lassen — und ist deshalb kein Ersatz für LNS, sondern eine Alternative für eine engere Problemklasse.</p>
<p>Genau deshalb bekommt sie ein eigenes Kapitel: <ahref="dekomposition.html#kap-dekomposition">Kapitel 10</a>. Es zeigt nebenbei, warum First-Fit bei manchen Zuschnittproblemen bereits optimal ist — eine Frage, die dieses Kapitel offenlässt.</p>
<p><strong>Aufgabe 9.1 ⭐ — Die Kurzsichtigkeit von Hand.</strong> Rechnen Sie im Schnellstart nach, was die Faustregel kostet, wenn sie nicht bei <em>Weiß</em>, sondern bei <em>Schwarz</em> startet. Ist sie dort besser oder schlechter — und warum?</p>
<p><strong>Aufgabe 9.2 ⭐ — Zuggröße und Temperatur.</strong><code>Simulated_Annealing.py</code> misst den Median der Zuggröße mit 70 Minuten. Bei welcher Starttemperatur würde eine Verschlechterung von genau 70 Minuten in 37 % der Fälle angenommen? Vergleichen Sie das mit den Temperaturen in der Tabelle.</p>
<p><strong>Aufgabe 9.3 ⭐⭐ — Die teure Bewertung.</strong> Ersetzen Sie in <code>Simulated_Annealing.py</code> die Funktion <code>delta_verschieben</code> durch eine Variante, die den Zug ausführt und <code>gesamtruestzeit()</code> neu berechnet. Wie viele Züge schafft das Programm dann in derselben Zeit, und wie verändert sich das Ergebnis bei gleichem Zeitbudget?</p>
<p><strong>Aufgabe 9.4 ⭐⭐ — Der Umschlagpunkt genauer.</strong><code>Metaheuristik_vs_Exakt.py</code> misst drei Größen. Zwischen 200 und 500 liegt der Umschlagpunkt. Grenzen Sie ihn mit weiteren Größen auf ±50 Aufträge ein. Ist er scharf oder verwaschen?</p>
<p><strong>Aufgabe 9.5 ⭐⭐⭐ — Zerstören nach Maß.</strong><code>Large_Neighborhood_Search.py</code> bricht ein <em>zusammenhängendes</em> Fenster heraus. Bauen Sie eine zweite Zerstörungsstrategie: Entfernen Sie die 20 Aufträge mit den <strong>teuersten</strong> Übergängen, egal wo sie stehen, und setzen Sie sie exakt neu ein. Welche Strategie ist besser — und liegt es an der Strategie oder an der Zahl der Runden, die sie schafft?</p>
<hr/>
<h2id="sec:metaheuristiken-denkfehler">9.10 Finde den Denkfehler</h2>
<divclass="card card-denkfehler">
<blockquote>
<p><strong>🐛 „Unsere Heuristik ist 6 % besser“</strong></p>
<p>Ein Kollege stellt das Ergebnis seiner Optimierung vor:</p>
<blockquote>
<p><em>„Die Disposition hat bisher nach Faustregel geplant: 2 497 Minuten Rüstzeit. Unser > Simulated Annealing kommt auf 2 343 — <strong>6,2 % Ersparnis</strong>, jede Nacht, vollautomatisch. > Bei 250 Produktionstagen sind das über 640 Stunden Maschinenzeit im Jahr.“</em></p>
</blockquote>
<p>Die Rechnung stimmt. Das Programm ist korrekt. Die 2 343 Minuten sind echt, die Ersparnis ist echt, und das Projekt wird genehmigt.</p>
<p><strong>Trotzdem ist die Darstellung irreführend. Woran liegt es?</strong></p>
<p>Ein Hinweis: Die Zahl, die fehlt, steht in der Ausgabe von <code>Metaheuristik_vs_Exakt.py</code> — in einer Spalte, die mit der Lösung des Problems nichts zu tun hat.</p>
<p><strong>1. Warum ist die Kostenänderung in <spanclass="math inline">O(1)</span> so wichtig?</strong> a) Weil die volle Summe numerisch ungenau wird. b) Weil eine lokale Suche von der Zahl geprüfter Züge lebt und die volle Summe sie um Größenordnungen senkt. c) Weil Simulated Annealing sonst mathematisch nicht konvergiert.</p>
<p><strong>2. Bei <spanclass="math inline">T_0 = 8</span> werden 0,29 % der Verschlechterungen angenommen, und das Ergebnis ist unbrauchbar. Warum genügen 0,29 %?</strong> a) Weil die Annahmewahrscheinlichkeit im Lauf noch steigt. b) Weil die angenommenen Verschlechterungen sich aufsummieren und die Suche weit von der guten Startlösung wegtragen, bevor die Temperatur fällt. c) Weil das Metropolis-Kriterium bei kleinen Quoten instabil wird.</p>
<p><strong>3. Bei 500 Aufträgen liefert CP-SAT eine schlechtere Lösung als die Faustregel. Was ist der Lauf trotzdem wert?</strong> a) Nichts — abgebrochene Läufe sind wertlos. b) Er liefert die untere Schranke und damit die einzige Aussage darüber, wie viel Luft noch nach oben ist. c) Er liefert eine gute Startlösung für das Annealing.</p>
<li>Erklären Sie in eigenen Worten, warum eine Faustregel „kurzsichtig“ ist — und warum das nicht dasselbe ist wie „schlecht“.</li>
<li>Warum lässt sich 2-opt bei <strong>asymmetrischen</strong> Rüstzeiten nicht in <spanclass="math inline">O(1)</span> bewerten?</li>
<li>Sie setzen eine Metaheuristik ein und berichten „12 % Verbesserung“. Welche zweite Zahl müssen Sie mitliefern, damit die Aussage belastbar ist, und woher bekommen Sie sie?</li>
<li>Ihr LNS verbessert kaum noch. Nennen Sie zwei Ursachen, die man an der Ausgabe unterscheiden kann.</li>
<li>Warum ist der Umschlagpunkt zwischen exakt und heuristisch keine allgemeine Zahl?</li>
<li>Eine <strong>Faustregel</strong> ist selten dumm, aber immer kurzsichtig: Sie bewertet einen Schritt danach, was er kostet, nicht danach, was er übrig lässt.</li>
<li><strong>Lokale Suche</strong> steht und fällt mit der Kostenänderung in <spanclass="math inline">O(1)</span>. Wer den Zug über die volle Zielfunktion bewertet, misst am Ende seine Implementierung statt des Verfahrens.</li>
<li><strong>Simulated Annealing</strong> nimmt Verschlechterungen mit <spanclass="math inline">e^{-\Delta/T}</span> an. <spanclass="math inline">T</span> und <spanclass="math inline">\Delta</span> haben dieselbe Einheit — die Temperatur gehört an der <strong>Zuggröße</strong> kalibriert, nicht am Zielfunktionswert. Die verbreitete Regel „20 bis 50 % Annahmequote“ war auf dieser Aufgabe falsch.</li>
<li>Der <strong>schlechteste Zwischenwert</strong> ist die beste Diagnosezahl eines Annealing-Laufs: Er zeigt eine zu heiße Starttemperatur sofort, ohne dass man das Ergebnis abwarten muss.</li>
<li>Rechnen Sie <strong>immer</strong> das simple Bergsteigen mit. Hier lieferte es 93 % des Ertrags.</li>
<li>Der <strong>Umschlagpunkt</strong> zwischen exakt und heuristisch ist problemspezifisch und messbar. Hier liegt er zwischen 200 und 500 Aufträgen — bei 60 wäre die Metaheuristik die schlechtere Wahl gewesen.</li>
<li>Ein exakter Lauf lohnt sich auch dann, wenn Sie seine <strong>Lösung</strong> verwerfen: Seine <strong>Schranke</strong> ist die einzige Aussage darüber, wie gut Ihre heuristische Lösung sein könnte.</li>
<li><strong>LNS</strong> kombiniert beide: zerstören, exakt reparieren, übernehmen. Die Fenstergröße hat ein Optimum in der Mitte und gehört gemessen.</li>
<navclass="prev-next"><aclass="prev-next-knopf prev-next-prev"href="graphen.html"><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-left"></use></svg><span><small>Zurück</small>Kapitel 8: Graphen, Flüsse und Touren — Min-Cost-Flow, Matching und VRP</span></a><aclass="prev-next-knopf prev-next-next"href="dekomposition.html"><span><small>Weiter</small>Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten</span><svgclass="icon"aria-hidden="true"><usehref="#icon-chevron-right"></use></svg></a></nav>