#!/usr/bin/env python3 # Runden_Gegenbeispiel.py """ Kapitel MILP: Wie schlecht ist Runden wirklich? Ersetzt die unbelegte Behauptung "20-50 % Verlust" durch eine Messung ueber viele Zufallsinstanzen. """ import numpy as np from scipy.optimize import linprog def erzeuge_instanz(n, m, rng): """Zufaelliges Rucksack-aehnliches MILP mit kleinen Zahlen.""" c = rng.integers(3, 20, size=n).astype(float) # Ertraege A = rng.integers(1, 9, size=(m, n)).astype(float) # Verbraeuche b = (A.sum(axis=1) * rng.uniform(0.25, 0.45)).round() # knappe Kapazitaeten return c, A, b def loese_lp(c, A, b, ganzzahlig=False): """LP-Relaxation oder exaktes MILP ueber HiGHS.""" n = len(c) ergebnis = linprog( c=-c, A_ub=A, b_ub=b, bounds=[(0, None)] * n, integrality=np.ones(n) if ganzzahlig else None, method="highs") return (-ergebnis.fun, ergebnis.x) if ergebnis.success else (None, None) def abrunden_und_reparieren(x_lp, c, A, b): """Naive Strategie: abrunden, dann gierig auffuellen, solange zulaessig.""" x = np.floor(x_lp + 1e-9) verbessert = True while verbessert: # gierig auffuellen verbessert = False for j in np.argsort(-c): # bester Ertrag zuerst kandidat = x.copy() kandidat[j] += 1 if np.all(A @ kandidat <= b + 1e-9): x = kandidat verbessert = True break return c @ x, x if __name__ == "__main__": rng = np.random.default_rng(2026) print("=" * 82) print(" WIE TEUER IST RUNDEN? (200 Zufallsinstanzen je Groesse)") print("=" * 82) print(f"{'n x m':>8} | {'Aufrunden unzul.':>17} | {'Abrunden: mittl.':>17} | " f"{'schlimmster':>12} | {'gierig':>8}") print(f"{'':>8} | {'':>17} | {'Verlust':>17} | {'Fall':>12} | {'Verlust':>8}") print("-" * 82) for n, m in [(5, 2), (10, 3), (20, 5), (40, 8)]: unzulaessig = 0 verluste_ab, verluste_gierig = [], [] for _ in range(200): c, A, b = erzeuge_instanz(n, m, rng) z_lp, x_lp = loese_lp(c, A, b, ganzzahlig=False) z_ip, _ = loese_lp(c, A, b, ganzzahlig=True) if z_lp is None or z_ip is None or z_ip <= 0: continue # Variante 1: aufrunden x_auf = np.ceil(x_lp - 1e-9) if np.any(A @ x_auf > b + 1e-9): unzulaessig += 1 # Variante 2: abrunden x_ab = np.floor(x_lp + 1e-9) verluste_ab.append(1.0 - (c @ x_ab) / z_ip) # Variante 3: abrunden + gierig auffuellen z_gierig, _ = abrunden_und_reparieren(x_lp, c, A, b) verluste_gierig.append(1.0 - z_gierig / z_ip) print(f"{n:>3} x {m:<3} | {unzulaessig/2:>15.1f} % | " f"{np.mean(verluste_ab)*100:>15.1f} % | " f"{np.max(verluste_ab)*100:>10.1f} % | " f"{np.mean(verluste_gierig)*100:>6.1f} %") print("-" * 82) print("Lesart: 'Aufrunden unzul.' = Anteil der Faelle, in denen die aufgerundete") print("Loesung eine Nebenbedingung verletzt. 'Verlust' = Abstand zum exakten Optimum.") print("=" * 82)