Kapitel 10: Spaltengenerierung — das Modell umbauen statt die Lösung raten
📌 Kapitel auf einen Blick
Worum geht es? Um die andere Antwort auf die Frage aus Kapitel 9: Was tun, wenn der Solver an der Größe scheitert? Statt die Lösung zu approximieren, formuliert man das Modell so um, dass exaktes Lösen wieder möglich wird.
Voraussetzungen: Abschnitt 5.6 (Dualwerte) und Kapitel 6 (
Rucksack.py). Beides wird gebraucht, aber nicht neu erklärt.Danach können Sie: ein Problem in Master und Teilproblem zerlegen; die Spaltengenerierungsschleife selbst schreiben; und begründen, wann sich der Aufwand lohnt — die Antwort hängt an einer einzigen Kennzahl der Instanz.
Zeitbedarf: ca. 3 Stunden.
Programme:
Spaltengenerierung.pyNotebook: dekomposition.ipynb — herunterladen und in Jupyter öffnen, in Colab hochladen oder mit dem Kurs-Image starten
10.1 In 5 Minuten gelöst
🚀 In 5 Minuten gelöst: Denken Sie in Mustern, nicht in Stücken
Eine Rolle ist 1 000 mm breit. Bestellt sind 9 Stück à 420 mm, 12 à 310 mm und 14 à 250 mm. Wie viele Rollen braucht man?
Die naheliegende Frage — „welches Stück kommt auf welche Rolle?“ — führt zu 35 Einzelentscheidungen. Die bessere Frage lautet: „Wie oft schneide ich welches Muster?“
import itertools import numpy as np from scipy.optimize import linprog ROLLE = 1000 # mm Mutterrolle breiten, bedarf = [420, 310, 250], [9, 12, 14] # Alle Schnittmuster aufzaehlen, die in die Rolle passen muster = [m for m in itertools.product(*[range(ROLLE // b + 1) for b in breiten]) if sum(a * b for a, b in zip(m, breiten)) <= ROLLE and sum(m) > 0] # Wie oft schneide ich welches Muster? min sum(x) u.d.N. A x >= bedarf loesung = linprog(np.ones(len(muster)), A_ub=-np.array(muster, float).T, b_ub=-np.array(bedarf, float), bounds=(0, None), integrality=1, method="highs") print(f"{len(muster)} zulaessige Muster, {int(round(loesung.fun))} Rollen noetig:") for anzahl, m in zip(np.round(loesung.x).astype(int), muster): if anzahl: rest = ROLLE - sum(a * b for a, b in zip(m, breiten)) print(f" {anzahl:2d} x " + " ".join(f"{a}x{b}mm" for a, b in zip(m, breiten)) + f" Verschnitt {rest} mm")Ausgabe:
16 zulaessige Muster, 12 Rollen noetig: 2 x 0x420mm 0x310mm 4x250mm Verschnitt 0 mm 1 x 0x420mm 3x310mm 0x250mm Verschnitt 70 mm 9 x 1x420mm 1x310mm 1x250mm Verschnitt 20 mm
Und jetzt der Punkt. Sehen Sie sich an, was da herauskommt: drei Schnittmuster. Kein Zuordnungsplan für 35 Stücke, sondern eine Anweisung, die an der Maschine hängen kann — neunmal dieses Muster, zweimal jenes, einmal das dritte.
Das ist kein kosmetischer Unterschied. Das Modell hat eine Variable je Muster statt einer je Stück-und-Rolle, und es kennt gar keine einzelnen Rollen mehr. Damit verschwindet ein Problem, das das naheliegende Modell praktisch unlösbar macht — dazu gleich mehr.
🎯 Merksatz Die schwierigste Arbeit an einem Optimierungsmodell ist selten die Zielfunktion. Es ist die Frage, worüber die Variablen laufen.
Warum funktioniert das? Weil hier alle 16 möglichen Muster aufzählbar waren. Bei drei Breiten und einer 1 000-mm-Rolle sind es 16; bei dreizehn Breiten und 5 600 mm sind es zehntausende, und bei einer echten Papierfabrik mehr, als sich speichern lässt. Das ganze Kapitel handelt davon, wie man mit Mustern rechnet, ohne sie aufzuschreiben.
10.2 Lernziele
Nach diesem Kapitel können Sie …
- … erklären, warum das naheliegende Zuordnungsmodell an Symmetrie scheitert.
- … ein Problem in Master und Pricing-Teilproblem zerlegen.
- … die Schleife schreiben: Master-LP lösen, Dualwerte auslesen, Teilproblem lösen, Spalte hinzufügen.
- … das Abbruchkriterium über die reduzierten Kosten begründen.
- … an einer Kennzahl der Instanz abschätzen, ob sich das Verfahren lohnt.
10.3 Warum das naheliegende Modell scheitert
Der erste Entwurf eines Zuschnittmodells sieht fast immer so aus: eine Binärvariable z_{jr} für „Stück j liegt auf Rolle r“, dazu y_r für „Rolle r wird benutzt”.
Zwei Dinge gehen dabei schief.
Die Größe. Bei 219 Zuschnitten und höchstens 82 Rollen sind das knapp 18 000 Binärvariablen — unangenehm, aber allein noch kein Hindernis.
Die Symmetrie. Das ist das eigentliche Problem. Alle Mutterrollen sind gleich. Jede Lösung existiert deshalb in unzähligen Umbenennungen: Vertauscht man Rolle 3 und Rolle 47, entsteht eine formal andere, inhaltlich identische Lösung. Branch-and-Bound weiß das nicht und arbeitet sie einzeln ab.
⚠️ Woran man ein Symmetrieproblem erkennt
Der Suchbaum wächst, aber die Schranke bewegt sich nicht. Im Protokoll aus Abschnitt 6.8 sieht das so aus: Die Zahl der Knoten steigt in die Hunderttausende, der Incumbent verbessert sich hin und wieder, und der Dual Bound steht praktisch still.
Das ist ein anderes Bild als „das Problem ist einfach zu groß”. Bei einem großen, aber unsymmetrischen Problem nähern sich beide Werte einander an, nur langsam.
Das Mustermodell hat dieses Problem nicht, weil es keine einzelnen Rollen kennt. Es zählt nur, wie oft welches Schnittmuster geschnitten wird:
\min \sum_{p \in P} x_p \qquad \text{unter} \qquad \sum_{p \in P} a_{ip}\, x_p \ge d_i \quad \forall i, \qquad x_p \ge 0 \ \text{ganzzahlig}
🔤 Formel-Übersetzer
Mathematik Alltagssprache P die Menge aller zulässigen Schnittmuster a_{ip} „Wie viele Stücke der Breite i liefert Muster p?” x_p „Wie oft schneide ich Muster p?” — die einzige Entscheidung d_i die bestellte Stückzahl der Breite i \sum_p x_p die Zahl der verbrauchten Mutterrollen Ohne Formel gesagt: „Stelle eine Schnittliste zusammen, die alle Bestellungen deckt, und benutze dabei so wenige Rollen wie möglich.”
Das Modell ist verblüffend klein — eine Zeile je Breite. Sein ganzes Gewicht steckt in P, und das ist der Haken.
10.4 Rechnen mit Mustern, ohne sie aufzuschreiben
Die Idee ist alt (Gilmore und Gomory, 1961) und in einem Satz gesagt:
Fange mit wenigen Mustern an. Frage nach jeder Lösung, ob es ein noch nicht bekanntes Muster gibt, das sich lohnen würde — und wenn ja, nimm nur dieses eine dazu.
Die Frage „lohnt sich noch ein Muster?” beantworten die Dualwerte des Master-LPs. Der Dualwert \pi_i zur Bedarfszeile i ist genau der Schattenpreis aus Abschnitt 5.6: Er sagt, wie viele Rollen ein zusätzliches Stück der Breite i kostet.
Ein neues Muster a lohnt sich, wenn seine reduzierten Kosten negativ sind:
1 - \sum_i \pi_i\, a_i < 0 \qquad\Longleftrightarrow\qquad \sum_i \pi_i\, a_i > 1
🔤 Formel-Übersetzer
Mathematik Alltagssprache die 1 „Ein zusätzliches Muster zu schneiden kostet eine Rolle.” \sum_i \pi_i a_i „So viel ist das, was dabei herauskommt, zu den aktuellen Schattenpreisen wert.” > 1 „Es bringt mehr, als es kostet — dieses Muster nehmen wir dazu.” \le 1 „Kein Muster lohnt sich mehr. Fertig — und zwar beweisbar.” Die letzte Zeile ist der Grund, warum das Verfahren exakt ist und nicht heuristisch: Wenn kein Muster mehr lohnende reduzierte Kosten hat, ist die LP-Lösung über allen Mustern optimal — auch über den nie erzeugten.
Und wie findet man das beste neue Muster? Man sucht die Zusammenstellung von Stücken mit dem größten Gesamtwert, die noch in die Rolle passt. Das ist ein Rucksackproblem:
| Rucksack (Kapitel 6) | hier |
|---|---|
| Nutzen eines Gegenstands | Schattenpreis \pi_i der Breite |
| Gewicht | Breite in mm |
| Kapazität des Rucksacks | Rollenbreite |
Das Teilproblem ist also ein Verfahren, das die Leser dieses Buchs schon kennen. Neu ist allein die Schleife.
10.5 Das Programm
#!/usr/bin/env python3
# Spaltengenerierung.py
"""
Kapitel Dekomposition: Das Modell umbauen, statt die Loesung zu raten.
Eine Papierfabrik liefert Rollen von 5.600 mm Breite. Die Kunden bestellen
schmalere Breiten, und aus jeder Mutterrolle werden mehrere davon geschnitten.
Gesucht ist der Schnittplan, der mit den wenigsten Mutterrollen auskommt.
Der naheliegende Modellansatz - "welches Stueck kommt auf welche Rolle?" -
fuehrt in eine Sackgasse: Er braucht eine Binaervariable je Stueck-und-Rolle,
und weil alle Rollen gleich sind, ist er hochgradig symmetrisch. Der Solver
probiert dieselbe Loesung in tausend Umbenennungen durch.
Die Spaltengenerierung dreht die Frage um:
Nicht "welches Stueck auf welche Rolle",
sondern "wie oft schneide ich welches MUSTER".
Damit verschwindet die Symmetrie - aber es entsteht ein neues Problem: Die Zahl
der moeglichen Muster waechst kombinatorisch. Man kann sie nicht aufschreiben.
Der Trick besteht darin, sie auch nicht aufzuschreiben, sondern nur die wenigen
zu erzeugen, die tatsaechlich gebraucht werden - und zwar mit Hilfe der
Schattenpreise aus dem Kapitel LP.
Das Programm zeigt vier Dinge:
1. Die beiden Formulierungen desselben Problems und ihre Groesse.
2. Wie schnell die Musterzahl waechst - und wie wenige davon genuegen.
3. Die Schleife selbst: Master-LP, Dualwerte, Rucksack, neue Spalte.
4. Wann sich der Aufwand lohnt und wann nicht. Die Antwort haengt an einer
einzigen Kennzahl der Instanz, und sie erklaert nebenbei, warum eine
Faustregel bei manchen Zuschnittproblemen schon optimal ist.
Benoetigt: numpy, scipy
"""
from __future__ import annotations
import numpy as np
from scipy.optimize import linprog
ROLLENBREITE = 5600
# Zwei Instanzen mit demselben Bedarf, aber verschiedenen Breiten. Der
# Unterschied ist die eine Kennzahl, um die es in Teil 4 geht.
BEDARF = [22, 25, 12, 14, 18, 18, 20, 10, 12, 14, 16, 18, 20]
BREIT = [1380, 1520, 1560, 1710, 1820, 1880, 1930, 2000, 2050, 2100, 2140,
2150, 2200]
SCHMAL = [380, 520, 560, 710, 820, 880, 930, 1000, 1050, 1100, 1140,
1150, 1200]
# --- Die Musterzahl ---------------------------------------------------------
def zaehle_muster(breiten, rollenbreite: int = ROLLENBREITE) -> int:
"""Wie viele zulaessige Schnittmuster gibt es ueberhaupt?
Nur zum Zeigen - in einem produktiven Programm wuerde man das nie
ausrechnen, weil die Zahl bei realistischen Instanzen jede Vorstellung
sprengt. Genau das ist der Punkt.
"""
speicher: dict[tuple[int, int], int] = {}
def ab(i: int, rest: int) -> int:
if i == len(breiten):
return 1
if (i, rest) in speicher:
return speicher[(i, rest)]
summe = sum(ab(i + 1, rest - anzahl * breiten[i])
for anzahl in range(rest // breiten[i] + 1))
speicher[(i, rest)] = summe
return summe
return ab(0, rollenbreite)
# --- Die beiden Bausteine der Schleife --------------------------------------
def loese_master(muster, bedarf, ganzzahlig: bool = False):
"""Restringiertes Master: Wie oft schneide ich jedes bekannte Muster?
min sum_p x_p unter sum_p a_ip * x_p >= bedarf_i
Als LP gerechnet liefert es zusaetzlich die DUALWERTE - und die sind der
eigentliche Ertrag: Der Dualwert zu Breite i sagt, was ein zusaetzliches
Stueck dieser Breite an Rollen kostet. Es ist derselbe Schattenpreis wie
im Kapitel LP, nur dass er hier nicht berichtet, sondern weiterverarbeitet
wird.
"""
matrix = -np.array(muster, dtype=float).T # >= wird zu <= mit Minus
ergebnis = linprog(np.ones(len(muster)),
A_ub=matrix, b_ub=-np.array(bedarf, dtype=float),
bounds=(0, None),
integrality=(1 if ganzzahlig else 0), method="highs")
if not ergebnis.success:
raise SystemExit(f"Master nicht loesbar: {ergebnis.message}")
dual = None if ganzzahlig else -ergebnis.ineqlin.marginals
return ergebnis.fun, ergebnis.x, dual
def bestes_neues_muster(dual, breiten, rollenbreite: int = ROLLENBREITE):
"""Pricing: Gibt es ein Muster, das sich noch lohnt?
Gesucht ist das Muster mit dem groessten Gesamtwert zu den aktuellen
Schattenpreisen - unter der Bedingung, dass es in die Rolle passt. Das ist
ein RUCKSACKPROBLEM (Kapitel MILP, Rucksack.py): Preise sind der Nutzen,
Breiten das Gewicht, die Rollenbreite die Kapazitaet. Hier mit dynamischer
Programmierung geloest, weil die Breiten ganzzahlig sind.
Bei Gleichstand wird bewusst der KLEINSTE Index bevorzugt (>-Vergleich mit
Toleranz statt >=). Sonst haengt das erzeugte Muster von der Reihenfolge
der Gleitkommaoperationen ab, und das Programm liefert von Lauf zu Lauf
verschiedene Ausgaben.
"""
wert = np.zeros(rollenbreite + 1)
herkunft = [-1] * (rollenbreite + 1)
for platz in range(rollenbreite + 1):
for i, breite in enumerate(breiten):
if breite <= platz and wert[platz - breite] + dual[i] > wert[platz] + 1e-9:
wert[platz] = wert[platz - breite] + dual[i]
herkunft[platz] = i
muster = [0] * len(breiten)
platz = rollenbreite
while herkunft[platz] >= 0:
i = herkunft[platz]
muster[i] += 1
platz -= breiten[i]
return float(wert[rollenbreite]), muster
def spaltengenerierung(breiten, bedarf, rollenbreite: int = ROLLENBREITE):
"""Die Schleife. Startbasis: je ein Muster mit nur einer Breite.
Abbruch, wenn kein Muster mehr einen Wert ueber 1 hat: Eine zusaetzliche
Rolle kostet 1, also lohnt sich ein neues Muster nur, wenn es zu den
aktuellen Preisen mehr als 1 wert ist. Das ist das Kriterium der
reduzierten Kosten.
"""
anzahl = len(breiten)
muster = [[0] * anzahl for _ in range(anzahl)]
for i in range(anzahl):
muster[i][i] = rollenbreite // breiten[i]
runden = 0
while True:
zielwert, _, dual = loese_master(muster, bedarf)
wert, neu = bestes_neues_muster(dual, breiten, rollenbreite)
runden += 1
if wert <= 1 + 1e-6:
return muster, zielwert, runden
muster.append(neu)
def first_fit(breiten, bedarf, rollenbreite: int = ROLLENBREITE) -> int:
"""Die Faustregel: groesstes Stueck zuerst, auf die erste passende Rolle."""
stuecke = sorted([b for b, menge in zip(breiten, bedarf)
for _ in range(menge)], reverse=True)
rollen: list[list[int]] = []
for stueck in stuecke:
for rolle in rollen:
if sum(rolle) + stueck <= rollenbreite:
rolle.append(stueck)
break
else:
rollen.append([stueck])
return len(rollen)
def auswerten(name: str, breiten, bedarf) -> dict:
muster, schranke, runden = spaltengenerierung(breiten, bedarf)
ganz, _, _ = loese_master(muster, bedarf, ganzzahlig=True)
return {"name": name, "muster_gesamt": zaehle_muster(breiten),
"muster_erzeugt": len(muster), "runden": runden,
"schranke": schranke, "ganzzahlig": int(round(ganz)),
"first_fit": first_fit(breiten, bedarf),
"stuecke_je_rolle": ROLLENBREITE / np.mean(breiten)}
if __name__ == "__main__":
print("=" * 84)
print(" SPALTENGENERIERUNG: NUR DIE MUSTER ERZEUGEN, DIE MAN BRAUCHT")
print("=" * 84)
print(f"Mutterrolle {ROLLENBREITE:,} mm, {len(BREIT)} bestellte Breiten, "
f"{sum(BEDARF)} Zuschnitte.\n")
# --- 1. Zwei Formulierungen ------------------------------------------
stuecke = sum(BEDARF)
# Wie viele Rollen braucht man hoechstens? Die Faustregel liefert eine
# brauchbare Obergrenze - mehr Rollen als das wird niemand benoetigen.
rollen_obergrenze = first_fit(BREIT, BEDARF)
print("1. Zwei Modelle fuer dieselbe Aufgabe\n")
print(f" {'Formulierung':<34} {'Binaervariablen':>16} {'symmetrisch?':>16}")
print(" " + "-" * 70)
print(f" {'Stueck -> Rolle (naheliegend)':<34} "
f"{stuecke * rollen_obergrenze:>16,} {'ja':>16}")
print(f" {'wie oft welches Muster':<34} "
f"{'eine je Muster':>16} {'nein':>16}")
print(f"\n Der naheliegende Ansatz braucht eine Variable je Stueck und Rolle:")
print(f" {stuecke} Zuschnitte x {rollen_obergrenze} Rollen (Obergrenze aus der Faustregel)")
print(f" = {stuecke * rollen_obergrenze:,} Binaervariablen.")
print("\n Schlimmer als die Zahl ist die Symmetrie: Alle Mutterrollen sind")
print(" gleich, also beschreibt jede Loesung dieselbe Schnittvorschrift in")
print(" unzaehligen Umbenennungen. Branch-and-Bound probiert sie einzeln")
print(" durch und kommt nicht voran - der Suchbaum waechst, ohne dass sich")
print(" die Schranke bewegt.")
print("\n Das Mustermodell hat je Muster genau eine Variable und kennt keine")
print(" einzelnen Rollen mehr. Damit ist die Symmetrie weg. Sein Problem ist")
print(" ein anderes - und zwar das folgende.")
# --- 2. Die Musterzahl ------------------------------------------------
print("\n" + "-" * 84)
print("2. Warum man die Muster nicht aufschreiben kann\n")
gross = auswerten("breite Zuschnitte (1.380-2.200 mm)", BREIT, BEDARF)
schmal = auswerten("schmale Zuschnitte (380-1.200 mm)", SCHMAL, BEDARF)
print(f" {'Instanz':<36} {'Muster':>12} {'davon erzeugt':>14} {'Anteil':>9}")
print(" " + "-" * 76)
for fall in (gross, schmal):
anteil = fall["muster_erzeugt"] / fall["muster_gesamt"]
print(f" {fall['name']:<36} {fall['muster_gesamt']:>12,} "
f"{fall['muster_erzeugt']:>14} {anteil:>8.1%}")
print(f"\n Schon das Halbieren der Breiten laesst die Musterzahl von "
f"{gross['muster_gesamt']:,} auf")
print(f" {schmal['muster_gesamt']:,} springen - Faktor "
f"{schmal['muster_gesamt'] / gross['muster_gesamt']:.0f}. Bei einer echten "
f"Papierfabrik mit")
print(" vierzig Breiten und Millimeterschritten sind es mehr, als sich")
print(" speichern liesse.")
print(f"\n Gebraucht werden davon {schmal['muster_erzeugt']} - "
f"{schmal['muster_erzeugt'] / schmal['muster_gesamt']:.2%} der Gesamtzahl.")
# --- 3. Die Schleife --------------------------------------------------
print("\n" + "-" * 84)
print("3. Die Schleife an der breiten Instanz\n")
print(f" Startbasis: {len(BREIT)} triviale Muster (je Rolle nur eine Breite)")
print(f" Runden bis kein Muster mehr lohnt: {gross['runden']}")
print(f" Muster am Ende: {gross['muster_erzeugt']}")
print(f" LP-Schranke: {gross['schranke']:.4f} Rollen")
print(f"\n Die LP-Schranke ist eine ZUSAGE: Weniger als "
f"{np.ceil(gross['schranke'] - 1e-9):.0f} Rollen sind")
print(" nicht moeglich - unabhaengig davon, wie clever man weiterschneidet.")
print(" Genau diese Aussage fehlt einer Heuristik (Kapitel Metaheuristiken).")
# --- 4. Wann es sich lohnt --------------------------------------------
print("\n" + "-" * 84)
print("4. Wann sich der Aufwand lohnt - und wann nicht\n")
print(f" {'Instanz':<36} {'Faustregel':>11} {'exakt':>8} {'Schranke':>10} "
f"{'Ersparnis':>11}")
print(" " + "-" * 80)
for fall in (gross, schmal):
ersparnis = (fall["first_fit"] - fall["ganzzahlig"]) / fall["first_fit"]
print(f" {fall['name']:<36} {fall['first_fit']:>11} "
f"{fall['ganzzahlig']:>8} {fall['schranke']:>10.2f} "
f"{ersparnis:>10.0%}")
print(f"\n Bei den breiten Zuschnitten spart die Spaltengenerierung "
f"{gross['first_fit'] - gross['ganzzahlig']} von")
print(f" {gross['first_fit']} Rollen. Bei den schmalen spart sie NICHTS - dort ist die")
print(" Faustregel bereits optimal.")
print(f"\n Der Unterschied haengt an einer einzigen Kennzahl:\n")
print(f" {'Instanz':<36} {'Stuecke je Rolle (etwa)':>24}")
print(" " + "-" * 62)
for fall in (gross, schmal):
print(f" {fall['name']:<36} {fall['stuecke_je_rolle']:>24.1f}")
print("\n Passen nur zwei bis drei Stuecke auf eine Rolle, entscheidet jede")
print(" einzelne Zuordnung viel, und eine kurzsichtige Regel verschenkt")
print(" ganze Rollen. Passen sechs oder mehr darauf, gleichen sich die")
print(" Fehler aus - die Reste sind klein gegen die Rollenbreite, und die")
print(" Faustregel trifft es fast immer.")
print("\n" + "=" * 84)
print(" WAS MAN DARAUS MITNIMMT")
print("=" * 84)
print("1. Der Perspektivwechsel ist die eigentliche Arbeit: nicht 'welches")
print(" Stueck wohin', sondern 'wie oft welches Muster'. Damit verschwindet")
print(" die Symmetrie, die das naheliegende Modell unloesbar macht.")
print("2. Der Preis dafuer ist eine unaufschreibbare Zahl von Variablen. Die")
print(" Spaltengenerierung zahlt ihn nicht, sondern erzeugt nur die wenigen")
print(" Spalten, die die Dualwerte als lohnend ausweisen.")
print("3. Das Teilproblem ist ein Rucksack - ein Verfahren, das die Leser")
print(" dieses Buchs schon kennen. Neu ist allein die Schleife.")
print("4. Und die unbequeme Erkenntnis: Ob sich das alles lohnt, entscheidet")
print(" die Instanz, nicht die Methode. Bei sechs Stuecken je Rolle ist die")
print(" Faustregel so gut wie das Optimum - und der Ertrag der")
print(" Spaltengenerierung liegt dann allein in der SCHRANKE, die beweist,")
print(" dass man aufhoeren kann zu suchen.")
print("=" * 84)Erwartete Ausgabe:
====================================================================================
SPALTENGENERIERUNG: NUR DIE MUSTER ERZEUGEN, DIE MAN BRAUCHT
====================================================================================
Mutterrolle 5,600 mm, 13 bestellte Breiten, 219 Zuschnitte.
1. Zwei Modelle fuer dieselbe Aufgabe
Formulierung Binaervariablen symmetrisch?
----------------------------------------------------------------------
Stueck -> Rolle (naheliegend) 17,958 ja
wie oft welches Muster eine je Muster nein
Der naheliegende Ansatz braucht eine Variable je Stueck und Rolle:
219 Zuschnitte x 82 Rollen (Obergrenze aus der Faustregel)
= 17,958 Binaervariablen.
Schlimmer als die Zahl ist die Symmetrie: Alle Mutterrollen sind
gleich, also beschreibt jede Loesung dieselbe Schnittvorschrift in
unzaehligen Umbenennungen. Branch-and-Bound probiert sie einzeln
durch und kommt nicht voran - der Suchbaum waechst, ohne dass sich
die Schranke bewegt.
Das Mustermodell hat je Muster genau eine Variable und kennt keine
einzelnen Rollen mehr. Damit ist die Symmetrie weg. Sein Problem ist
ein anderes - und zwar das folgende.
------------------------------------------------------------------------------------
2. Warum man die Muster nicht aufschreiben kann
Instanz Muster davon erzeugt Anteil
----------------------------------------------------------------------------
breite Zuschnitte (1.380-2.200 mm) 309 38 12.3%
schmale Zuschnitte (380-1.200 mm) 46,408 28 0.1%
Schon das Halbieren der Breiten laesst die Musterzahl von 309 auf
46,408 springen - Faktor 150. Bei einer echten Papierfabrik mit
vierzig Breiten und Millimeterschritten sind es mehr, als sich
speichern liesse.
Gebraucht werden davon 28 - 0.06% der Gesamtzahl.
------------------------------------------------------------------------------------
3. Die Schleife an der breiten Instanz
Startbasis: 13 triviale Muster (je Rolle nur eine Breite)
Runden bis kein Muster mehr lohnt: 26
Muster am Ende: 38
LP-Schranke: 72.9167 Rollen
Die LP-Schranke ist eine ZUSAGE: Weniger als 73 Rollen sind
nicht moeglich - unabhaengig davon, wie clever man weiterschneidet.
Genau diese Aussage fehlt einer Heuristik (Kapitel Metaheuristiken).
------------------------------------------------------------------------------------
4. Wann sich der Aufwand lohnt - und wann nicht
Instanz Faustregel exakt Schranke Ersparnis
--------------------------------------------------------------------------------
breite Zuschnitte (1.380-2.200 mm) 82 73 72.92 11%
schmale Zuschnitte (380-1.200 mm) 35 35 33.60 0%
Bei den breiten Zuschnitten spart die Spaltengenerierung 9 von
82 Rollen. Bei den schmalen spart sie NICHTS - dort ist die
Faustregel bereits optimal.
Der Unterschied haengt an einer einzigen Kennzahl:
Instanz Stuecke je Rolle (etwa)
--------------------------------------------------------------
breite Zuschnitte (1.380-2.200 mm) 3.0
schmale Zuschnitte (380-1.200 mm) 6.4
Passen nur zwei bis drei Stuecke auf eine Rolle, entscheidet jede
einzelne Zuordnung viel, und eine kurzsichtige Regel verschenkt
ganze Rollen. Passen sechs oder mehr darauf, gleichen sich die
Fehler aus - die Reste sind klein gegen die Rollenbreite, und die
Faustregel trifft es fast immer.
====================================================================================
WAS MAN DARAUS MITNIMMT
====================================================================================
1. Der Perspektivwechsel ist die eigentliche Arbeit: nicht 'welches
Stueck wohin', sondern 'wie oft welches Muster'. Damit verschwindet
die Symmetrie, die das naheliegende Modell unloesbar macht.
2. Der Preis dafuer ist eine unaufschreibbare Zahl von Variablen. Die
Spaltengenerierung zahlt ihn nicht, sondern erzeugt nur die wenigen
Spalten, die die Dualwerte als lohnend ausweisen.
3. Das Teilproblem ist ein Rucksack - ein Verfahren, das die Leser
dieses Buchs schon kennen. Neu ist allein die Schleife.
4. Und die unbequeme Erkenntnis: Ob sich das alles lohnt, entscheidet
die Instanz, nicht die Methode. Bei sechs Stuecken je Rolle ist die
Faustregel so gut wie das Optimum - und der Ertrag der
Spaltengenerierung liegt dann allein in der SCHRANKE, die beweist,
dass man aufhoeren kann zu suchen.
====================================================================================
10.6 Was die Zahlen zeigen
Die Musterzahl explodiert, der Bedarf nicht
| Instanz | mögliche Muster | erzeugt | Anteil |
|---|---|---|---|
| breite Zuschnitte (1 380–2 200 mm) | 309 | 38 | 12,3 % |
| schmale Zuschnitte (380–1 200 mm) | 46 408 | 28 | 0,06 % |
Schon das Halbieren der Breiten lässt die Musterzahl um den Faktor 150 springen. Die Zahl der gebrauchten Muster bleibt dagegen konstant — sie steigt sogar nicht, sie sinkt leicht. Das ist der ganze Ertrag des Verfahrens: Der Aufwand hängt an der Zahl der Bedarfszeilen, nicht an der Zahl der Muster.
Der unbequeme Teil: Es lohnt sich nicht immer
| Instanz | Faustregel | exakt | Schranke | Ersparnis |
|---|---|---|---|---|
| breite Zuschnitte | 82 Rollen | 73 | 72,92 | 11 % |
| schmale Zuschnitte | 35 Rollen | 35 | 33,60 | 0 % |
Bei den breiten Zuschnitten spart die Spaltengenerierung neun von 82 Rollen. Bei den schmalen spart sie nichts — dort ist First-Fit-Decreasing bereits optimal.
Der Unterschied hängt an einer einzigen Kennzahl:
| Instanz | Stücke je Rolle |
|---|---|
| breite Zuschnitte | 3,0 |
| schmale Zuschnitte | 6,4 |
Passen nur zwei bis drei Stücke auf eine Rolle, entscheidet jede einzelne Zuordnung viel, und eine kurzsichtige Regel verschenkt ganze Rollen. Passen sechs oder mehr darauf, gleichen sich die Fehler aus: Die Reste sind klein gegen die Rollenbreite, und die Faustregel trifft es fast immer.
💡 Das erklärt einen Befund aus Kapitel 9
Dort steht, dass Bin Packing als Aufhänger für Metaheuristiken nicht taugt, weil First-Fit in den geprüften Größen bereits optimal war. Der Grund ist jetzt benennbar: Die dortige Instanz hatte Stücke von 700 bis 2 600 mm bei 5 600 mm Rollenbreite — im Mittel gut drei je Rolle, aber mit viel kleineren Stücken durchsetzt, so dass die Reste sich auffüllen ließen.
Die brauchbare Faustregel lautet also nicht „Bin Packing ist einfach”, sondern: Je weniger Stücke auf einen Behälter passen, desto mehr ist mit exakter Optimierung zu holen. Das ist eine Zahl, die man vor dem Projekt ausrechnen kann.
Und selbst dort, wo die Ersparnis null ist, liefert das Verfahren etwas, das die Faustregel nicht kann: die Schranke 33,60. Sie beweist, dass 34 Rollen das Minimum wären und 35 höchstens eine daneben liegen — also dass man aufhören kann zu suchen.
10.7 Übungsaufgaben
Lösungen: Abschnitt A.10.
Aufgabe 10.1 ⭐ — Das Abbruchkriterium. Warum lautet die Schwelle beim Pricing genau 1 und nicht 0? Woher kommt die Eins?
Aufgabe 10.2 ⭐ — Die Startbasis. Das Programm startet mit Mustern, die je nur eine Breite enthalten. Warum ist das immer zulässig, und warum wäre eine leere Startmenge ein Problem?
Aufgabe 10.3 ⭐⭐ — Die Kennzahl prüfen. Erzeugen Sie Instanzen mit 2, 4, 8 und 16 Stücken je Rolle und tragen Sie die Ersparnis gegenüber First-Fit auf. Bestätigt sich der Zusammenhang aus Abschnitt 10.6?
Aufgabe 10.4 ⭐⭐ — Dienstplanung. Formulieren Sie die Wochendienstplanung als Spaltengenerierung: Ein „Muster” ist ein zulässiger Wochenplan einer Person. Was ist das Teilproblem, und welche Nebenbedingungen stehen im Master, welche im Teilproblem?
Aufgabe 10.5 ⭐⭐⭐ — Branch-and-Price. Das ganzzahlige Master über die erzeugten Spalten ist nicht garantiert optimal — es könnte Muster geben, die erst nach einer Verzweigung lohnend werden. Recherchieren Sie, was Branch-and-Price daran ändert, und erklären Sie, warum man nicht einfach auf den erzeugten Spalten verzweigen kann.
10.8 Finde den Denkfehler
🐛 „Die LP-Lösung sagt 72,92 — also runden wir auf”
Ein Kollege hat die Spaltengenerierung implementiert und ist zufrieden:
„Das Master-LP liefert 72,92 Rollen, verteilt auf 13 Muster. Halbe Rollen kann man > nicht schneiden, also runde ich jedes Muster auf die nächste ganze Zahl auf. Damit sind > alle Bestellungen sicher gedeckt — aufrunden kann ja nur zu viel liefern, nie zu wenig. > Ergebnis: ein zulässiger Schnittplan, und die Schranke sagt mir, dass ich höchstens eine > Rolle daneben liege.”
Der erste Teil stimmt: Aufrunden liefert tatsächlich einen zulässigen Plan. Der zweite Satz über die Schranke ist der Fehler — und er ist teuer.
Wie viele Rollen kostet das Aufrunden hier wirklich, und was hätte der Kollege stattdessen tun müssen?
Ein Hinweis: Er hat alles, was er dafür braucht, bereits vorliegen.
10.9 Micro-Quiz
❓ Drei Fragen
1. Warum ist das Mustermodell dem Zuordnungsmodell überlegen? a) Weil es weniger Nebenbedingungen hat. b) Weil es keine einzelnen Rollen kennt und damit die Symmetrie verschwindet. c) Weil Musterprobleme immer ganzzahlige LP-Lösungen haben.
2. Was ist das Pricing-Teilproblem beim Zuschnitt? a) Ein zweites LP über dieselben Variablen. b) Ein Rucksackproblem: Welche Stücke passen in eine Rolle und sind zu den aktuellen Schattenpreisen am meisten wert? c) Eine Heuristik, die neue Muster zufällig erzeugt.
3. Bei der schmalen Instanz spart die Spaltengenerierung null Rollen gegenüber First-Fit. War der Aufwand umsonst? a) Ja — wo nichts gespart wird, hat sich das Verfahren nicht gelohnt. b) Nein — sie liefert die untere Schranke und damit den Beweis, dass die Faustregel höchstens eine Rolle danebenliegt. c) Nein — sie wird bei der nächsten Instanz mehr sparen.
10.10 Selbsttest
- Erklären Sie Symmetrie in einem MILP an einem Beispiel aus Ihrem Arbeitsumfeld.
- Was ist beim Zuschnitt eine „Spalte”, und was steht darin?
- Woher kommen die Preise, mit denen das Teilproblem rechnet?
- Warum ist das Verfahren exakt, obwohl es fast alle Muster nie ansieht?
- Sie sollen vor dem Projekt abschätzen, ob sich Spaltengenerierung lohnt. Welche eine Zahl rechnen Sie aus?
10.11 Zusammenfassung
- Die folgenreichste Entscheidung an einem Modell ist, worüber die Variablen laufen. Beim Zuschnitt schlägt „wie oft welches Muster” das naheliegende „welches Stück auf welche Rolle” — weil damit die Symmetrie verschwindet, an der Branch-and-Bound scheitert.
- Der Preis dafür ist eine Variablenmenge, die man nicht aufschreiben kann. Die Spaltengenerierung zahlt ihn nicht: Sie erzeugt nur die Muster, die die Dualwerte als lohnend ausweisen — im Beispiel 28 von 46 408.
- Das Abbruchkriterium sind die reduzierten Kosten: Solange ein Muster zu den aktuellen Schattenpreisen mehr als eine Rolle wert ist, lohnt es sich. Danach ist die Lösung beweisbar optimal — auch über die nie erzeugten Muster.
- Das Teilproblem ist ein Rucksack. Neu ist allein die Schleife.
- Ob sich das lohnt, entscheidet die Instanz. Bei drei Stücken je Rolle bringt das Verfahren 11 %, bei sechs nichts. Diese Kennzahl lässt sich vor dem Projekt ausrechnen.
- Auch wo es nichts spart, liefert es die Schranke — und damit die Erlaubnis, aufzuhören.