Version 04 als eigenes Repository
Erster Commit des Strangs "Optimierte Entscheidungsfindung mit Python" (Version 04). Die Historie der 71 Commits bis zur Trennung bleibt im uebergeordneten Repository OR_mit_Python liegen, das ab jetzt nur noch Version_03 (eingefroren) verwaltet und Version_04/ ignoriert. Bewusst kein "git subtree split": Der Pfad Version_04/ existiert erst seit der Verzeichnistrennung, ein Split braechte daher nur 7 der 41 einschlaegigen Commits - eine Teilhistorie, die vollstaendig aussieht und es nicht ist. Stand: 5 Teile, 23 Kapitel, 5 Anhaenge, 292 Abschnitte, 703 Querverweise, 325 Indexmarken, 73 Beispielprogramme, 32 SVGs, 4 Plotly-Figuren, 25 Notebooks, PDF mit 715 Seiten. Zusaetzlich in diesem Commit: * pyproject.toml mit Abhaengigkeitsgruppen finance, large-scale, api, figures, dev, empfehlungen. Die abgedruckte requirements.txt bleibt unveraendert daneben bestehen. ortools steht in der Grundausstattung, highspy erst in [large-scale] - so kann der HiGHS-Symbolkonflikt bei der schlanken Installation gar nicht erst auftreten. * Dabei zwei Funde: graphviz wird von erzeuge_architektur_diagramme.py importiert, fehlt aber in requirements.txt (jetzt in [figures]); pymoo steht in requirements.txt, wird aber von keinem Programm importiert, sondern nur im Kapitel Metaheuristiken empfohlen (jetzt in [empfehlungen]). * NEUER_TITEL.md nach Kritik_und_Verbesserungsvorschlaege/ verschoben - es ist die Vorlage des Titelblatts, kein Bestandteil des Werks. Die beiden Fundstellen in PROGRESS.md und erzeuge_titelseite.py nachgezogen. * PROGRESS.md nannte noch den Untertitel der ersten Fassung; auf den tatsaechlichen aus erzeuge_titelseite.py korrigiert. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
commit
b7af2f1d9a
468 changed files with 262141 additions and 0 deletions
347
OR_HTML_04/Notebooks_04/dekomposition.ipynb
Normal file
347
OR_HTML_04/Notebooks_04/dekomposition.ipynb
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
{
|
||||
"cells": [
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"# Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten\n",
|
||||
"\n",
|
||||
"Begleitnotebook zu *Optimierte Entscheidungsfindung mit Python*. Die Codezellen sind identisch mit den im Buch abgedruckten Programmen.\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"# Einmalig ausfuehren: installiert alle im Buch verwendeten Pakete.\n",
|
||||
"# Lokal in einer virtuellen Umgebung genauso gueltig wie in Google Colab.\n",
|
||||
"%pip install --quiet ortools highspy cvxpy scipy numpy pandas polars \\\n",
|
||||
" scikit-learn matplotlib plotly pyomo linopy pymoo pydantic openpyxl"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "markdown",
|
||||
"metadata": {},
|
||||
"source": [
|
||||
"## Das Programm\n",
|
||||
"\n",
|
||||
"`Spaltengenerierung.py`\n"
|
||||
]
|
||||
},
|
||||
{
|
||||
"cell_type": "code",
|
||||
"execution_count": null,
|
||||
"metadata": {},
|
||||
"outputs": [],
|
||||
"source": [
|
||||
"#!/usr/bin/env python3\n",
|
||||
"\n",
|
||||
"# Spaltengenerierung.py\n",
|
||||
"\"\"\"\n",
|
||||
"Kapitel Dekomposition: Das Modell umbauen, statt die Loesung zu raten.\n",
|
||||
"\n",
|
||||
"Eine Papierfabrik liefert Rollen von 5.600 mm Breite. Die Kunden bestellen\n",
|
||||
"schmalere Breiten, und aus jeder Mutterrolle werden mehrere davon geschnitten.\n",
|
||||
"Gesucht ist der Schnittplan, der mit den wenigsten Mutterrollen auskommt.\n",
|
||||
"\n",
|
||||
"Der naheliegende Modellansatz - \"welches Stueck kommt auf welche Rolle?\" -\n",
|
||||
"fuehrt in eine Sackgasse: Er braucht eine Binaervariable je Stueck-und-Rolle,\n",
|
||||
"und weil alle Rollen gleich sind, ist er hochgradig symmetrisch. Der Solver\n",
|
||||
"probiert dieselbe Loesung in tausend Umbenennungen durch.\n",
|
||||
"\n",
|
||||
"Die Spaltengenerierung dreht die Frage um:\n",
|
||||
"\n",
|
||||
" Nicht \"welches Stueck auf welche Rolle\",\n",
|
||||
" sondern \"wie oft schneide ich welches MUSTER\".\n",
|
||||
"\n",
|
||||
"Damit verschwindet die Symmetrie - aber es entsteht ein neues Problem: Die Zahl\n",
|
||||
"der moeglichen Muster waechst kombinatorisch. Man kann sie nicht aufschreiben.\n",
|
||||
"Der Trick besteht darin, sie auch nicht aufzuschreiben, sondern nur die wenigen\n",
|
||||
"zu erzeugen, die tatsaechlich gebraucht werden - und zwar mit Hilfe der\n",
|
||||
"Schattenpreise aus dem Kapitel LP.\n",
|
||||
"\n",
|
||||
"Das Programm zeigt vier Dinge:\n",
|
||||
"\n",
|
||||
" 1. Die beiden Formulierungen desselben Problems und ihre Groesse.\n",
|
||||
" 2. Wie schnell die Musterzahl waechst - und wie wenige davon genuegen.\n",
|
||||
" 3. Die Schleife selbst: Master-LP, Dualwerte, Rucksack, neue Spalte.\n",
|
||||
" 4. Wann sich der Aufwand lohnt und wann nicht. Die Antwort haengt an einer\n",
|
||||
" einzigen Kennzahl der Instanz, und sie erklaert nebenbei, warum eine\n",
|
||||
" Faustregel bei manchen Zuschnittproblemen schon optimal ist.\n",
|
||||
"\n",
|
||||
"Benoetigt: numpy, scipy\n",
|
||||
"\"\"\"\n",
|
||||
"\n",
|
||||
"from __future__ import annotations\n",
|
||||
"\n",
|
||||
"import numpy as np\n",
|
||||
"from scipy.optimize import linprog\n",
|
||||
"\n",
|
||||
"ROLLENBREITE = 5600\n",
|
||||
"\n",
|
||||
"# Zwei Instanzen mit demselben Bedarf, aber verschiedenen Breiten. Der\n",
|
||||
"# Unterschied ist die eine Kennzahl, um die es in Teil 4 geht.\n",
|
||||
"BEDARF = [22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20]\n",
|
||||
"BREIT = [1380, 1520, 1560, 1710, 1820, 1880, 1930, 2000, 2050, 2100, 2140,\n",
|
||||
" 2150, 2200]\n",
|
||||
"SCHMAL = [380, 520, 560, 710, 820, 880, 930, 1000, 1050, 1100, 1140,\n",
|
||||
" 1150, 1200]\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# --- Die Musterzahl ---------------------------------------------------------\n",
|
||||
"\n",
|
||||
"def zaehle_muster(breiten, rollenbreite: int = ROLLENBREITE) -> int:\n",
|
||||
" \"\"\"Wie viele zulaessige Schnittmuster gibt es ueberhaupt?\n",
|
||||
"\n",
|
||||
" Nur zum Zeigen - in einem produktiven Programm wuerde man das nie\n",
|
||||
" ausrechnen, weil die Zahl bei realistischen Instanzen jede Vorstellung\n",
|
||||
" sprengt. Genau das ist der Punkt.\n",
|
||||
" \"\"\"\n",
|
||||
" speicher: dict[tuple[int, int], int] = {}\n",
|
||||
"\n",
|
||||
" def ab(i: int, rest: int) -> int:\n",
|
||||
" if i == len(breiten):\n",
|
||||
" return 1\n",
|
||||
" if (i, rest) in speicher:\n",
|
||||
" return speicher[(i, rest)]\n",
|
||||
" summe = sum(ab(i + 1, rest - anzahl * breiten[i])\n",
|
||||
" for anzahl in range(rest // breiten[i] + 1))\n",
|
||||
" speicher[(i, rest)] = summe\n",
|
||||
" return summe\n",
|
||||
"\n",
|
||||
" return ab(0, rollenbreite)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"# --- Die beiden Bausteine der Schleife --------------------------------------\n",
|
||||
"\n",
|
||||
"def loese_master(muster, bedarf, ganzzahlig: bool = False):\n",
|
||||
" \"\"\"Restringiertes Master: Wie oft schneide ich jedes bekannte Muster?\n",
|
||||
"\n",
|
||||
" min sum_p x_p unter sum_p a_ip * x_p >= bedarf_i\n",
|
||||
"\n",
|
||||
" Als LP gerechnet liefert es zusaetzlich die DUALWERTE - und die sind der\n",
|
||||
" eigentliche Ertrag: Der Dualwert zu Breite i sagt, was ein zusaetzliches\n",
|
||||
" Stueck dieser Breite an Rollen kostet. Es ist derselbe Schattenpreis wie\n",
|
||||
" im Kapitel LP, nur dass er hier nicht berichtet, sondern weiterverarbeitet\n",
|
||||
" wird.\n",
|
||||
" \"\"\"\n",
|
||||
" matrix = -np.array(muster, dtype=float).T # >= wird zu <= mit Minus\n",
|
||||
" ergebnis = linprog(np.ones(len(muster)),\n",
|
||||
" A_ub=matrix, b_ub=-np.array(bedarf, dtype=float),\n",
|
||||
" bounds=(0, None),\n",
|
||||
" integrality=(1 if ganzzahlig else 0), method=\"highs\")\n",
|
||||
" if not ergebnis.success:\n",
|
||||
" raise SystemExit(f\"Master nicht loesbar: {ergebnis.message}\")\n",
|
||||
" dual = None if ganzzahlig else -ergebnis.ineqlin.marginals\n",
|
||||
" return ergebnis.fun, ergebnis.x, dual\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def bestes_neues_muster(dual, breiten, rollenbreite: int = ROLLENBREITE):\n",
|
||||
" \"\"\"Pricing: Gibt es ein Muster, das sich noch lohnt?\n",
|
||||
"\n",
|
||||
" Gesucht ist das Muster mit dem groessten Gesamtwert zu den aktuellen\n",
|
||||
" Schattenpreisen - unter der Bedingung, dass es in die Rolle passt. Das ist\n",
|
||||
" ein RUCKSACKPROBLEM (Kapitel MILP, Rucksack.py): Preise sind der Nutzen,\n",
|
||||
" Breiten das Gewicht, die Rollenbreite die Kapazitaet. Hier mit dynamischer\n",
|
||||
" Programmierung geloest, weil die Breiten ganzzahlig sind.\n",
|
||||
"\n",
|
||||
" Bei Gleichstand wird bewusst der KLEINSTE Index bevorzugt (>-Vergleich mit\n",
|
||||
" Toleranz statt >=). Sonst haengt das erzeugte Muster von der Reihenfolge\n",
|
||||
" der Gleitkommaoperationen ab, und das Programm liefert von Lauf zu Lauf\n",
|
||||
" verschiedene Ausgaben.\n",
|
||||
" \"\"\"\n",
|
||||
" wert = np.zeros(rollenbreite + 1)\n",
|
||||
" herkunft = [-1] * (rollenbreite + 1)\n",
|
||||
" for platz in range(rollenbreite + 1):\n",
|
||||
" for i, breite in enumerate(breiten):\n",
|
||||
" if breite <= platz and wert[platz - breite] + dual[i] > wert[platz] + 1e-9:\n",
|
||||
" wert[platz] = wert[platz - breite] + dual[i]\n",
|
||||
" herkunft[platz] = i\n",
|
||||
"\n",
|
||||
" muster = [0] * len(breiten)\n",
|
||||
" platz = rollenbreite\n",
|
||||
" while herkunft[platz] >= 0:\n",
|
||||
" i = herkunft[platz]\n",
|
||||
" muster[i] += 1\n",
|
||||
" platz -= breiten[i]\n",
|
||||
" return float(wert[rollenbreite]), muster\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def spaltengenerierung(breiten, bedarf, rollenbreite: int = ROLLENBREITE):\n",
|
||||
" \"\"\"Die Schleife. Startbasis: je ein Muster mit nur einer Breite.\n",
|
||||
"\n",
|
||||
" Abbruch, wenn kein Muster mehr einen Wert ueber 1 hat: Eine zusaetzliche\n",
|
||||
" Rolle kostet 1, also lohnt sich ein neues Muster nur, wenn es zu den\n",
|
||||
" aktuellen Preisen mehr als 1 wert ist. Das ist das Kriterium der\n",
|
||||
" reduzierten Kosten.\n",
|
||||
" \"\"\"\n",
|
||||
" anzahl = len(breiten)\n",
|
||||
" muster = [[0] * anzahl for _ in range(anzahl)]\n",
|
||||
" for i in range(anzahl):\n",
|
||||
" muster[i][i] = rollenbreite // breiten[i]\n",
|
||||
"\n",
|
||||
" runden = 0\n",
|
||||
" while True:\n",
|
||||
" zielwert, _, dual = loese_master(muster, bedarf)\n",
|
||||
" wert, neu = bestes_neues_muster(dual, breiten, rollenbreite)\n",
|
||||
" runden += 1\n",
|
||||
" if wert <= 1 + 1e-6:\n",
|
||||
" return muster, zielwert, runden\n",
|
||||
" muster.append(neu)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def first_fit(breiten, bedarf, rollenbreite: int = ROLLENBREITE) -> int:\n",
|
||||
" \"\"\"Die Faustregel: groesstes Stueck zuerst, auf die erste passende Rolle.\"\"\"\n",
|
||||
" stuecke = sorted([b for b, menge in zip(breiten, bedarf)\n",
|
||||
" for _ in range(menge)], reverse=True)\n",
|
||||
" rollen: list[list[int]] = []\n",
|
||||
" for stueck in stuecke:\n",
|
||||
" for rolle in rollen:\n",
|
||||
" if sum(rolle) + stueck <= rollenbreite:\n",
|
||||
" rolle.append(stueck)\n",
|
||||
" break\n",
|
||||
" else:\n",
|
||||
" rollen.append([stueck])\n",
|
||||
" return len(rollen)\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"def auswerten(name: str, breiten, bedarf) -> dict:\n",
|
||||
" muster, schranke, runden = spaltengenerierung(breiten, bedarf)\n",
|
||||
" ganz, _, _ = loese_master(muster, bedarf, ganzzahlig=True)\n",
|
||||
" return {\"name\": name, \"muster_gesamt\": zaehle_muster(breiten),\n",
|
||||
" \"muster_erzeugt\": len(muster), \"runden\": runden,\n",
|
||||
" \"schranke\": schranke, \"ganzzahlig\": int(round(ganz)),\n",
|
||||
" \"first_fit\": first_fit(breiten, bedarf),\n",
|
||||
" \"stuecke_je_rolle\": ROLLENBREITE / np.mean(breiten)}\n",
|
||||
"\n",
|
||||
"\n",
|
||||
"if __name__ == \"__main__\":\n",
|
||||
" print(\"=\" * 84)\n",
|
||||
" print(\" SPALTENGENERIERUNG: NUR DIE MUSTER ERZEUGEN, DIE MAN BRAUCHT\")\n",
|
||||
" print(\"=\" * 84)\n",
|
||||
" print(f\"Mutterrolle {ROLLENBREITE:,} mm, {len(BREIT)} bestellte Breiten, \"\n",
|
||||
" f\"{sum(BEDARF)} Zuschnitte.\\n\")\n",
|
||||
"\n",
|
||||
" # --- 1. Zwei Formulierungen ------------------------------------------\n",
|
||||
" stuecke = sum(BEDARF)\n",
|
||||
" # Wie viele Rollen braucht man hoechstens? Die Faustregel liefert eine\n",
|
||||
" # brauchbare Obergrenze - mehr Rollen als das wird niemand benoetigen.\n",
|
||||
" rollen_obergrenze = first_fit(BREIT, BEDARF)\n",
|
||||
" print(\"1. Zwei Modelle fuer dieselbe Aufgabe\\n\")\n",
|
||||
" print(f\" {'Formulierung':<34} {'Binaervariablen':>16} {'symmetrisch?':>16}\")\n",
|
||||
" print(\" \" + \"-\" * 70)\n",
|
||||
" print(f\" {'Stueck -> Rolle (naheliegend)':<34} \"\n",
|
||||
" f\"{stuecke * rollen_obergrenze:>16,} {'ja':>16}\")\n",
|
||||
" print(f\" {'wie oft welches Muster':<34} \"\n",
|
||||
" f\"{'eine je Muster':>16} {'nein':>16}\")\n",
|
||||
" print(f\"\\n Der naheliegende Ansatz braucht eine Variable je Stueck und Rolle:\")\n",
|
||||
" print(f\" {stuecke} Zuschnitte x {rollen_obergrenze} Rollen (Obergrenze aus der Faustregel)\")\n",
|
||||
" print(f\" = {stuecke * rollen_obergrenze:,} Binaervariablen.\")\n",
|
||||
" print(\"\\n Schlimmer als die Zahl ist die Symmetrie: Alle Mutterrollen sind\")\n",
|
||||
" print(\" gleich, also beschreibt jede Loesung dieselbe Schnittvorschrift in\")\n",
|
||||
" print(\" unzaehligen Umbenennungen. Branch-and-Bound probiert sie einzeln\")\n",
|
||||
" print(\" durch und kommt nicht voran - der Suchbaum waechst, ohne dass sich\")\n",
|
||||
" print(\" die Schranke bewegt.\")\n",
|
||||
" print(\"\\n Das Mustermodell hat je Muster genau eine Variable und kennt keine\")\n",
|
||||
" print(\" einzelnen Rollen mehr. Damit ist die Symmetrie weg. Sein Problem ist\")\n",
|
||||
" print(\" ein anderes - und zwar das folgende.\")\n",
|
||||
"\n",
|
||||
" # --- 2. Die Musterzahl ------------------------------------------------\n",
|
||||
" print(\"\\n\" + \"-\" * 84)\n",
|
||||
" print(\"2. Warum man die Muster nicht aufschreiben kann\\n\")\n",
|
||||
" gross = auswerten(\"breite Zuschnitte (1.380-2.200 mm)\", BREIT, BEDARF)\n",
|
||||
" schmal = auswerten(\"schmale Zuschnitte (380-1.200 mm)\", SCHMAL, BEDARF)\n",
|
||||
"\n",
|
||||
" print(f\" {'Instanz':<36} {'Muster':>12} {'davon erzeugt':>14} {'Anteil':>9}\")\n",
|
||||
" print(\" \" + \"-\" * 76)\n",
|
||||
" for fall in (gross, schmal):\n",
|
||||
" anteil = fall[\"muster_erzeugt\"] / fall[\"muster_gesamt\"]\n",
|
||||
" print(f\" {fall['name']:<36} {fall['muster_gesamt']:>12,} \"\n",
|
||||
" f\"{fall['muster_erzeugt']:>14} {anteil:>8.1%}\")\n",
|
||||
"\n",
|
||||
" print(f\"\\n Schon das Halbieren der Breiten laesst die Musterzahl von \"\n",
|
||||
" f\"{gross['muster_gesamt']:,} auf\")\n",
|
||||
" print(f\" {schmal['muster_gesamt']:,} springen - Faktor \"\n",
|
||||
" f\"{schmal['muster_gesamt'] / gross['muster_gesamt']:.0f}. Bei einer echten \"\n",
|
||||
" f\"Papierfabrik mit\")\n",
|
||||
" print(\" vierzig Breiten und Millimeterschritten sind es mehr, als sich\")\n",
|
||||
" print(\" speichern liesse.\")\n",
|
||||
" print(f\"\\n Gebraucht werden davon {schmal['muster_erzeugt']} - \"\n",
|
||||
" f\"{schmal['muster_erzeugt'] / schmal['muster_gesamt']:.2%} der Gesamtzahl.\")\n",
|
||||
"\n",
|
||||
" # --- 3. Die Schleife --------------------------------------------------\n",
|
||||
" print(\"\\n\" + \"-\" * 84)\n",
|
||||
" print(\"3. Die Schleife an der breiten Instanz\\n\")\n",
|
||||
" print(f\" Startbasis: {len(BREIT)} triviale Muster (je Rolle nur eine Breite)\")\n",
|
||||
" print(f\" Runden bis kein Muster mehr lohnt: {gross['runden']}\")\n",
|
||||
" print(f\" Muster am Ende: {gross['muster_erzeugt']}\")\n",
|
||||
" print(f\" LP-Schranke: {gross['schranke']:.4f} Rollen\")\n",
|
||||
" print(f\"\\n Die LP-Schranke ist eine ZUSAGE: Weniger als \"\n",
|
||||
" f\"{np.ceil(gross['schranke'] - 1e-9):.0f} Rollen sind\")\n",
|
||||
" print(\" nicht moeglich - unabhaengig davon, wie clever man weiterschneidet.\")\n",
|
||||
" print(\" Genau diese Aussage fehlt einer Heuristik (Kapitel Metaheuristiken).\")\n",
|
||||
"\n",
|
||||
" # --- 4. Wann es sich lohnt --------------------------------------------\n",
|
||||
" print(\"\\n\" + \"-\" * 84)\n",
|
||||
" print(\"4. Wann sich der Aufwand lohnt - und wann nicht\\n\")\n",
|
||||
" print(f\" {'Instanz':<36} {'Faustregel':>11} {'exakt':>8} {'Schranke':>10} \"\n",
|
||||
" f\"{'Ersparnis':>11}\")\n",
|
||||
" print(\" \" + \"-\" * 80)\n",
|
||||
" for fall in (gross, schmal):\n",
|
||||
" ersparnis = (fall[\"first_fit\"] - fall[\"ganzzahlig\"]) / fall[\"first_fit\"]\n",
|
||||
" print(f\" {fall['name']:<36} {fall['first_fit']:>11} \"\n",
|
||||
" f\"{fall['ganzzahlig']:>8} {fall['schranke']:>10.2f} \"\n",
|
||||
" f\"{ersparnis:>10.0%}\")\n",
|
||||
"\n",
|
||||
" print(f\"\\n Bei den breiten Zuschnitten spart die Spaltengenerierung \"\n",
|
||||
" f\"{gross['first_fit'] - gross['ganzzahlig']} von\")\n",
|
||||
" print(f\" {gross['first_fit']} Rollen. Bei den schmalen spart sie NICHTS - dort ist die\")\n",
|
||||
" print(\" Faustregel bereits optimal.\")\n",
|
||||
" print(f\"\\n Der Unterschied haengt an einer einzigen Kennzahl:\\n\")\n",
|
||||
" print(f\" {'Instanz':<36} {'Stuecke je Rolle (etwa)':>24}\")\n",
|
||||
" print(\" \" + \"-\" * 62)\n",
|
||||
" for fall in (gross, schmal):\n",
|
||||
" print(f\" {fall['name']:<36} {fall['stuecke_je_rolle']:>24.1f}\")\n",
|
||||
"\n",
|
||||
" print(\"\\n Passen nur zwei bis drei Stuecke auf eine Rolle, entscheidet jede\")\n",
|
||||
" print(\" einzelne Zuordnung viel, und eine kurzsichtige Regel verschenkt\")\n",
|
||||
" print(\" ganze Rollen. Passen sechs oder mehr darauf, gleichen sich die\")\n",
|
||||
" print(\" Fehler aus - die Reste sind klein gegen die Rollenbreite, und die\")\n",
|
||||
" print(\" Faustregel trifft es fast immer.\")\n",
|
||||
"\n",
|
||||
" print(\"\\n\" + \"=\" * 84)\n",
|
||||
" print(\" WAS MAN DARAUS MITNIMMT\")\n",
|
||||
" print(\"=\" * 84)\n",
|
||||
" print(\"1. Der Perspektivwechsel ist die eigentliche Arbeit: nicht 'welches\")\n",
|
||||
" print(\" Stueck wohin', sondern 'wie oft welches Muster'. Damit verschwindet\")\n",
|
||||
" print(\" die Symmetrie, die das naheliegende Modell unloesbar macht.\")\n",
|
||||
" print(\"2. Der Preis dafuer ist eine unaufschreibbare Zahl von Variablen. Die\")\n",
|
||||
" print(\" Spaltengenerierung zahlt ihn nicht, sondern erzeugt nur die wenigen\")\n",
|
||||
" print(\" Spalten, die die Dualwerte als lohnend ausweisen.\")\n",
|
||||
" print(\"3. Das Teilproblem ist ein Rucksack - ein Verfahren, das die Leser\")\n",
|
||||
" print(\" dieses Buchs schon kennen. Neu ist allein die Schleife.\")\n",
|
||||
" print(\"4. Und die unbequeme Erkenntnis: Ob sich das alles lohnt, entscheidet\")\n",
|
||||
" print(\" die Instanz, nicht die Methode. Bei sechs Stuecken je Rolle ist die\")\n",
|
||||
" print(\" Faustregel so gut wie das Optimum - und der Ertrag der\")\n",
|
||||
" print(\" Spaltengenerierung liegt dann allein in der SCHRANKE, die beweist,\")\n",
|
||||
" print(\" dass man aufhoeren kann zu suchen.\")\n",
|
||||
" print(\"=\" * 84)"
|
||||
]
|
||||
}
|
||||
],
|
||||
"metadata": {
|
||||
"kernelspec": {
|
||||
"display_name": "Python 3",
|
||||
"language": "python",
|
||||
"name": "python3"
|
||||
},
|
||||
"language_info": {
|
||||
"name": "python",
|
||||
"version": "3.11"
|
||||
}
|
||||
},
|
||||
"nbformat": 4,
|
||||
"nbformat_minor": 5
|
||||
}
|
||||
Loading…
Reference in a new issue