115 lines
4.6 KiB
Python
115 lines
4.6 KiB
Python
|
|
#!/usr/bin/env python3
|
||
|
|
|
||
|
|
# erzeuge_vrp_subtours.py
|
||
|
|
"""
|
||
|
|
Erzeugt das Kurzzyklus-Diagramm zum Graphen-Kapitel:
|
||
|
|
|
||
|
|
bilder_04/kap_graphen_subtouren.svg
|
||
|
|
|
||
|
|
Links, was ein Tourenmodell OHNE Kurzzyklus-Bedingungen zurueckgeben darf:
|
||
|
|
Jeder Kunde hat genau einen Vorgaenger und einen Nachfolger - alle
|
||
|
|
Gradbedingungen sind erfuellt -, und trotzdem ist die Loesung unbrauchbar, weil
|
||
|
|
die Fahrzeuge nie am Depot vorbeikommen. Rechts dieselbe Instanz mit gueltiger
|
||
|
|
Rundreise ab und bis Depot.
|
||
|
|
|
||
|
|
Der Punkt: Das linke Bild ist kein Rechenfehler. Es erfuellt jede Bedingung,
|
||
|
|
die naiv formuliert wurde. Was fehlt, ist eine Bedingung, an die man beim
|
||
|
|
Aufschreiben nicht denkt - und genau dafuer gibt es MTZ.
|
||
|
|
|
||
|
|
Die Instanz stammt aus erzeuge_vrp_touren.py, also aus demselben Datensatz wie
|
||
|
|
das Buchprogramm. Die Kurzzyklen links sind daraus konstruiert: Es sind die
|
||
|
|
Kundenfolgen der gueltigen Loesung, nur ohne das Depot geschlossen. So zeigt
|
||
|
|
das Bild denselben Fall und nicht ein zweites, erfundenes Beispiel.
|
||
|
|
|
||
|
|
Aufruf (aus dem Repository-Wurzelverzeichnis):
|
||
|
|
python3 bilder_04/erzeuge_vrp_subtours.py
|
||
|
|
|
||
|
|
Benoetigt: numpy, matplotlib, ortools
|
||
|
|
"""
|
||
|
|
|
||
|
|
from __future__ import annotations
|
||
|
|
|
||
|
|
import os
|
||
|
|
import sys
|
||
|
|
|
||
|
|
import numpy as np
|
||
|
|
|
||
|
|
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||
|
|
from stil_04 import FARBEN, speichere # noqa: E402
|
||
|
|
import matplotlib.pyplot as plt # noqa: E402
|
||
|
|
from erzeuge_vrp_touren import (TOURFARBEN, erzeuge_daten, # noqa: E402
|
||
|
|
loese)
|
||
|
|
|
||
|
|
|
||
|
|
def kurzzyklen(touren) -> list[list[int]]:
|
||
|
|
"""Aus jeder gueltigen Tour einen Kreis ohne Depot machen.
|
||
|
|
|
||
|
|
Genau das darf ein Modell liefern, das nur 'jeder Kunde einmal angefahren'
|
||
|
|
fordert: In sich geschlossene Kreise, die das Depot nie beruehren."""
|
||
|
|
aus = []
|
||
|
|
for tour in touren:
|
||
|
|
kunden = [k for k in tour["stationen"] if k != 0]
|
||
|
|
if len(kunden) >= 2:
|
||
|
|
aus.append(kunden + [kunden[0]])
|
||
|
|
return aus
|
||
|
|
|
||
|
|
|
||
|
|
def pruefe_grade(zyklen, anzahl_kunden: int) -> None:
|
||
|
|
"""Die unzulaessige Loesung muss die Gradbedingungen trotzdem erfuellen."""
|
||
|
|
besucht = [k for z in zyklen for k in z[:-1]]
|
||
|
|
if sorted(besucht) != list(range(1, anzahl_kunden + 1)):
|
||
|
|
raise SystemExit(
|
||
|
|
"Subtour-Bild: Die konstruierten Kurzzyklen besuchen nicht jeden "
|
||
|
|
"Kunden genau einmal. Dann waere das linke Bild kein gueltiges "
|
||
|
|
"Gegenbeispiel, sondern einfach falsch.")
|
||
|
|
|
||
|
|
|
||
|
|
def zeichne(koordinaten, touren, zyklen) -> None:
|
||
|
|
figur, achsen = plt.subplots(1, 2, figsize=(9.6, 4.6), sharex=True,
|
||
|
|
sharey=True)
|
||
|
|
|
||
|
|
for achse, titel in ((achsen[0], "ohne Kurzzyklus-Bedingungen"),
|
||
|
|
(achsen[1], "mit MTZ — jede Tour über das Depot")):
|
||
|
|
for ort in range(1, len(koordinaten)):
|
||
|
|
achse.scatter(*koordinaten[ort], s=34, color=FARBEN["text"],
|
||
|
|
zorder=3)
|
||
|
|
achse.scatter(*koordinaten[0], marker="s", s=150,
|
||
|
|
color=FARBEN["fehler"], zorder=4)
|
||
|
|
achse.annotate("Depot", koordinaten[0], textcoords="offset points",
|
||
|
|
xytext=(9, 7), fontsize=9, color=FARBEN["fehler"],
|
||
|
|
fontweight="bold")
|
||
|
|
achse.set_xlabel("km")
|
||
|
|
achse.set_title(titel, fontsize=10.5, color=FARBEN["text"])
|
||
|
|
|
||
|
|
# links: die Kurzzyklen
|
||
|
|
for nummer, (zyklus, farbe) in enumerate(zip(zyklen, TOURFARBEN), start=1):
|
||
|
|
weg = koordinaten[zyklus]
|
||
|
|
achsen[0].plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
|
||
|
|
alpha=0.9, zorder=2)
|
||
|
|
achsen[0].annotate(
|
||
|
|
"Jeder Kunde hat genau einen Vor- und\n"
|
||
|
|
"einen Nachfolger — und kein Fahrzeug\n"
|
||
|
|
"kommt je am Depot vorbei.",
|
||
|
|
xy=(0.03, 0.03), xycoords="axes fraction", fontsize=9,
|
||
|
|
color=FARBEN["fehler"], va="bottom")
|
||
|
|
|
||
|
|
# rechts: die gueltige Loesung
|
||
|
|
for tour, farbe in zip(touren, TOURFARBEN):
|
||
|
|
weg = koordinaten[tour["stationen"]]
|
||
|
|
achsen[1].plot(weg[:, 0], weg[:, 1], "-", color=farbe, linewidth=1.8,
|
||
|
|
alpha=0.9, zorder=2)
|
||
|
|
|
||
|
|
achsen[0].set_ylabel("km")
|
||
|
|
figur.suptitle("Warum Gradbedingungen allein nicht genügen",
|
||
|
|
fontsize=11, color=FARBEN["text"], y=1.01)
|
||
|
|
speichere(figur, "kap_graphen_subtouren")
|
||
|
|
|
||
|
|
|
||
|
|
if __name__ == "__main__":
|
||
|
|
koordinaten, distanz = erzeuge_daten()
|
||
|
|
touren = loese(distanz)
|
||
|
|
zyklen = kurzzyklen(touren)
|
||
|
|
pruefe_grade(zyklen, len(koordinaten) - 1)
|
||
|
|
print(f" {len(zyklen)} Kurzzyklen konstruiert, "
|
||
|
|
f"{sum(len(z) - 1 for z in zyklen)} Kunden — jeder genau einmal.")
|
||
|
|
zeichne(koordinaten, touren, zyklen)
|