Wirtschaft

Wie berechne ich
das Minimalprinzip?

Am Beispiel des Ernährungsrechners: feste Nährstoff-Grenzen, kleinste Tageskosten. So ist das Minimalprinzip im Code gebaut. Die Idee dahinter: Was ist das Minimalprinzip?

Simon Stark | Veröffentlicht 08. August 2026 | ca. 14 Min. Lesezeit

01

Minimalprinzip im Ernährungsrechner

Im Tool ist das Ergebnis der Satz an Nährstoff-Schranken: mindestens so viel Protein, Kalorien, Eisen, …, höchstens so viel von dem, was du begrenzen willst. Der Einsatz ist das Geld: Summe der Lebensmittelpreise pro Tag.

Die Frage des Rechners lautet deshalb: Welche Mengen welcher Lebensmittel halten alle gesetzten Min- und Max-Grenzen und kosten am wenigsten? Das ist Minimalprinzip, nicht „möglichst gesund um jeden Preis“ und nicht „möglichst billig ohne Grenzen“.

Technisch ist das ein lineares Programm. Bibliothek: javascript-lp-solver (solver.js). Aufruf: solver.Solve(model), normalerweise im Web Worker plan-worker.js. Unter file:// oft ohne Worker, dann derselbe Call im Hauptthread.


02

Welche Lebensmittel mitspielen

Bevor der Solver läuft, schneidet foodsForPlan() die Datenbank zurecht. Nur dieser Pool wird zum Modell.

  • Ernährungsstil (matchesDiet): vegan, vegetarisch, pescetarisch, carnivor oder alle. Bestimmt, welche Tags ein Lebensmittel haben muss.
  • Eigenschaften (matchesTagFilters): z. B. Whole Food, Supplement, Verarbeitet. Bei Laktose und Gluten gilt: Lebensmittel, die den Tag haben, werden ausgeschlossen (Vermeidung).

Jedes Lebensmittel im Pool bekommt einen Variablennamen f0, f1, f2, … in der Reihenfolge des Arrays. Der Index ist wichtig, weil das Solver-Ergebnis dieselben Keys zurückgibt.


03

Bezugsgröße in der Datenbank

Preis und Nährwerte liegen pro Referenz (foodRef / UNIT_META):

  • g und ml: Referenz 100 (pro 100 g bzw. 100 ml)
  • stk und tr: Referenz 1 (pro Stück bzw. Tropfen)

foodPrice(f) ist der gespeicherte Preis pro Referenz. Nährwerte in f.nutrients[p] sind ebenfalls pro Referenz. Wenn du das falsch skaliert, rechnet der Solver mit Müll.

Die Funktion buildModel(list, asInteger, reqMap) baut daraus die Solver-Variablen. Was die Variable u bedeutet, hängt vom Modus ab.

Kontinuierlich (asInteger = false, Default im UI):

  • u = Anzahl Referenz-Pakete (z. B. 1,5 → 150 g bei ref = 100)
  • Kostenkoeffizient: foodPrice(f)
  • Nährstoffkoeffizient: Rohwert pro Referenz (nur wenn > 0)
  • physische Menge später: u × foodRef(f)

Ganze Mengen (asInteger = true, Schalter „Nur ganze Mengen“):

  • u = ganze physische Einheiten (1 g, 1 ml, 1 Stück, 1 Tropfen)
  • scale = 1 / foodRef(f)
  • Kosten: foodPrice(f) × scale (Preis pro 1 g / 1 ml / …)
  • Nährstoff: raw × scale
  • physische Menge: u
  • Variable ist integer, Optionen: Timeout 8000 ms, Tolerance 0,01

04

Was an den Solver geht

Exakt die Struktur aus buildModel:

{ "optimize": "cost", "opType": "min", "constraints": { "Protein (g)": { "min": 160 }, "Energy (cal)": { "min": 2200, "max": 2800 } }, "variables": { "f0": { "cost": 0.44, "Energy (cal)": 371, "Protein (g)": 13.6 }, "f1": { "cost": 0.22, … } } } Nur Parameter mit gesetztem min und/oder max. Rohwert 0 entfällt. cost und Nährstoffe sind schon skaliert.

Die Schranken kommen aus dem Anforderungen-Tab (req), optional als reqMap übergeben. Jeder Eintrag in PARAMS wird geprüft: hat er min und/oder max? Dann landet er in constraints.

In Worten, 1:1 die Zielfunktion des Tools:

min Σ cost_i × u_i s.t. für jeden Nährstoff n mit Min: Σ (nährstoff_i,n × u_i) ≥ Min_n für jeden Nährstoff n mit Max: Σ (nährstoff_i,n × u_i) ≤ Max_n u_i ≥ 0 (bei ganzen Mengen: u_i ganzzahlig) Kosten min. Grenzen halten. Das ist das Minimalprinzip im Code.

05

Ablauf ohne „Nur ganze Mengen“

Wenn der Integer-Schalter aus ist, bleibt der Weg kurz:

  1. Pool = foodsForPlan()
  2. model = buildModel(pool, false) mit dem aktuellen req
  3. result = await solveModelAsync(model) → Worker oder Fallback solver.Solve
  4. Wenn !result.feasible: Abbruch und diagnoseInfeasible
  5. Sonst: alle f_i mit u > 0 in Mengen und Kosten umrechnen und anzeigen

Umrechnung im kontinuierlichen Fall:

  • Kosten: planCost(u, f, false) = u × foodPrice(f)
  • Nährstoff: planNutrient(u, f, p, false) = u × raw
  • Anzeige-Menge: planAmount(u, false, f) = u × foodRef(f)

Das Ergebnis ist der billigste zulässige Plan im kontinuierlichen Raum. Mengen dürfen Bruchteile von 100 g-Paketen sein (z. B. 37,2 g).


06

Zwei Phasen im Code

Integer-LP über die ganze Datenbank wäre zu langsam und oft unnötig. Deshalb solveIntegerPlan(pool) in zwei Phasen. Konstante: INTEGER_KCAL_BUFFER = 25 auf dem Max von Energy (cal).

Phase 1 – schneller Plan mit Kalorien-Puffer

  1. phase1Req = reqWithKcalBuffer(realReq, 25): Energy-Max um 25 gesenkt, aber nie unter das Energy-Min.
  2. Kontinuierliches LP mit phase1Req. Scheitert es, nochmal mit den echten Grenzen realReq.
  3. Support: alle Lebensmittel mit kontinuierlichem u > 1e-6, höchstens 50 (die mit dem größten u).
  4. Integer-LP nur auf dem Support, mit denselben Phase-1-Schranken. Timeout 8 s, Tolerance 0,01.
  5. Wenn unmöglich: runden round(u_cont × ref), sonst ceil.
  6. Wenn die Schranken noch reißen: repairIntegerPlan (siehe unten).

Phase 2 – nur wenn die echten Grenzen noch verletzt sind

  1. collectViolations(chosen, realReq): welche Nährstoffe unter Min, welche über Max?
  2. Support erweitern: aktuelle Plan-Lebensmittel ∪ Top-Kandidaten pro unterschrittenem Nährstoff (topFoodsForNutrient: Score = Nährstoff pro € und Einheit, Supplements × 1,5) ∪ Supplements, die den Defizit-Nährstoff liefern.
  3. Integer-LP auf diesem Pool mit den vollen realReq.
  4. Sonst wieder repairIntegerPlan gegen realReq.
  5. Immer noch unzulässig: Plan wird mit approx: true zurückgegeben (Näherung).

Der Puffer in Phase 1 schafft etwas Luft bei den Kalorien, damit Phase 2 noch Einheiten nachlegen kann, ohne sofort das Energy-Max zu sprengen.


07

Greedy, wenn der MILP nicht reicht

repairIntegerPlan(chosen, pool, reqMap) läuft maximal 250 Schritte:

  • Min zu niedrig: Für den betroffenen Nährstoff nimm die Einheit im Pool mit dem besten Score (Nährstoff/€, Supplement × 1,5), die kein Max sprengt (canAddOneUnit). +1 Einheit, dann nächster Schritt.
  • Max zu hoch: −1 Einheit bei dem Lebensmittel, das pro Einheit am meisten zu diesem Nährstoff beiträgt.

Das ist kein exaktes Kostenminimum mehr. Es ist ein Notnagel, damit der Plan möglichst alle Grenzen hält. Im Ergebnis kann method z. B. milp-support, round+repair oder milp-deficit+repair heißen.


08

Welche Grenze blockiert?

Wenn das kontinuierliche Modell unzulässig ist, läuft diagnoseInfeasible:

  1. Volles Modell bauen (kontinuierlich).
  2. Je eine Constraint-Zeile streichen und neu lösen.
  3. Wird das Modell dadurch zulässig, war diese Schranke (mit) der Blocker („Culprit“).

Die UI listet diese Nährstoffe und schlägt vor: Schranke lockern oder Lebensmittel hinzufügen. Das ist die wirtschaftliche Übersetzung von „das Soll und der erlaubte Pool passen nicht zusammen“.


09

Von u zu Gramm und Euro

Der Solver liefert für jede Variable f_i einen Wert u (oder 0 / fehlend). Daraus:

  • Kontinuierlich: Menge = u × ref, Kosten = u × price, Nährstoff = u × raw
  • Integer: Menge = u, Kosten = u × price / ref, Nährstoff = u × raw / ref

Die Tageskosten des Plans sind die Summe der Einzelkosten. Die Nährstoff-Summen sind die Summe der Beiträge. Der „limiting factor“ im UI markiert den Nährstoff, der am engsten an einer Grenze liegt: dort sitzt oft der nächste Hebel (Datenbank erweitern oder Schranke anpassen).


10

Wie im Smoke-Test des Codes

Zwei „Lebensmittel“, eine Untergrenze (vereinfacht wie der Demo-Assert im Tool):

min 1×A + 3×B s.t. 10×A + 20×B ≥ 20 A, B ≥ 0 Lösung: A = 2, B = 0, Kosten = 2. Genug „Leistung“, billiger als B.

Mit echten Nährstoffen und vielen Maxima wird der Raum enger. Die Aufgabe bleibt dieselbe: billigster Punkt, der noch alle Grenzen des Profils und der Anforderungen hält.


11

Minimalprinzip im Tool, in einem Satz

Nährstoff-Min und -Max sind das Soll. Euro sind der Einsatz. Der Solver (und bei ganzen Mengen die zwei Phasen plus Repair) sucht den günstigsten zulässigen Tagesplan im gefilterten Lebensmittel-Pool. Das ist das Minimalprinzip, ausprogrammiert.


Live im Browser

Datenbank, Profil, Schranken, optimaler Plan. Dieselbe Rechnung wie oben.

Zum Ernährungsrechner

Weiterlesen

Verwandte Artikel