From cae3355bf144a1399e4fd7b8a165f9fff5e5c6a5 Mon Sep 17 00:00:00 2001 From: Oliver Hofmann Date: Mon, 22 Jun 2026 17:39:06 +0200 Subject: [PATCH] =?UTF-8?q?Musterl=C3=B6sung=20Praktikum=209?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- praktika/09_mst/aufgabe_kruskal.py | 186 +++++++++++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 praktika/09_mst/aufgabe_kruskal.py diff --git a/praktika/09_mst/aufgabe_kruskal.py b/praktika/09_mst/aufgabe_kruskal.py new file mode 100644 index 0000000..1948cbb --- /dev/null +++ b/praktika/09_mst/aufgabe_kruskal.py @@ -0,0 +1,186 @@ +import math +import sys +from itertools import combinations + +from vorlesung.L08_graphen.graph import AdjacencyListGraph + + +# --------------------------------------------------------------------------- +# b) Union-Find +# --------------------------------------------------------------------------- + +class DisjointSet: + """Union-Find mit Pfadkompression und Union-by-Size.""" + + def __init__(self): + self._parent = {} + self._size = {} + + def make_set(self, v): + self._parent[v] = v + self._size[v] = 1 + + def find_set(self, v): + if self._parent[v] != v: + self._parent[v] = self.find_set(self._parent[v]) # Pfadkompression + return self._parent[v] + + def union(self, u, v): + """Vereinigt die Schaltkreise von u und v. + Gibt True zurück, wenn sie vorher getrennt waren.""" + ru, rv = self.find_set(u), self.find_set(v) + if ru == rv: + return False + if self._size[ru] < self._size[rv]: + ru, rv = rv, ru + self._parent[rv] = ru + self._size[ru] += self._size[rv] + return True + + def size(self, v): + return self._size[self.find_set(v)] + + def circuit_sizes(self): + """Alle Schaltkreisgrößen, absteigend sortiert.""" + return sorted( + [self._size[v] for v in self._parent if self._parent[v] == v], + reverse=True + ) + + +# --------------------------------------------------------------------------- +# Hilfsfunktionen +# --------------------------------------------------------------------------- + +def euclidean(a, b): + return math.sqrt(sum((ai - bi) ** 2 for ai, bi in zip(a, b))) + + +def parse_boxes(text): + boxes = [] + for line in text.strip().splitlines(): + line = line.strip() + if line: + boxes.append(tuple(int(x) for x in line.split(','))) + return boxes + + +# --------------------------------------------------------------------------- +# c–e) Kruskal +# --------------------------------------------------------------------------- + +def solve(text, k=1000): + boxes = parse_boxes(text) + n = len(boxes) + print(f"Anzahl Boxen: {n}") + print(f"Potenzielle Verbindungen: {n * (n - 1) // 2}") + + # a) Alle Paare nach Distanz sortiert + edges = sorted( + ((euclidean(a, b), a, b) for a, b in combinations(boxes, 2)), + key=lambda e: e[0] + ) + + ds = DisjointSet() + for b in boxes: + ds.make_set(b) + + # --- Teil 1: die k kürzesten Paare verbinden (inkl. No-Ops) --- + for _, u, v in edges[:k]: + ds.union(u, v) + idx = k + + sizes = ds.circuit_sizes() + top3 = sizes[:3] + result1 = top3[0] * top3[1] * top3[2] + print(f"\n--- Teil 1 (k={k}) ---") + print(f"Schaltkreisgrößen: {sizes}") + print(f"Produkt der 3 größten: {top3[0]} × {top3[1]} × {top3[2]} = {result1}") + + # --- Teil 2: weiter bis ein einziger Schaltkreis --- + last_u = last_v = None + while idx < len(edges): + _, u, v = edges[idx] + idx += 1 + if ds.union(u, v): + last_u, last_v = u, v + if ds.size(last_u) == n: + break + + result2 = last_u[0] * last_v[0] + print(f"\n--- Teil 2 ---") + print(f"Letzte Verbindung: {last_u} -- {last_v}") + print(f"Produkt der X-Koordinaten: {last_u[0]} × {last_v[0]} = {result2}") + + # --- Verifikation mit graph.mst_kruskal() --- + # mst_kruskal() liefert alle MST-Kanten aufsteigend nach Gewicht sortiert. + # Die letzte Kante ist die schwerste im MST = die zuletzt hinzugefügte + # Kante bei Kruskal = identisch mit dem Ergebnis aus Teil 2. + # connect() ist gerichtet; beide Richtungen werden eingetragen, damit + # all_edges() alle Paare sieht. Der Same-Set-Check in mst_kruskal() + # filtert die Rückrichtung als No-Op heraus. + g = AdjacencyListGraph() + for b in boxes: + g.insert_vertex(f"{b[0]},{b[1]},{b[2]}") + for a, b in combinations(boxes, 2): + name_a = f"{a[0]},{a[1]},{a[2]}" + name_b = f"{b[0]},{b[1]},{b[2]}" + d = euclidean(a, b) + g.connect(name_a, name_b, d) + g.connect(name_b, name_a, d) + mst_edges, _ = g.mst_kruskal() + last = mst_edges[-1] + gu = tuple(int(x) for x in last[0].split(',')) + gv = tuple(int(x) for x in last[1].split(',')) + result2_graph = gu[0] * gv[0] + assert result2_graph == result2, ( + f"Verifikation fehlgeschlagen: graph.mst_kruskal() → {result2_graph}, " + f"eigene Lösung → {result2}" + ) + print(f"Verifikation mit graph.mst_kruskal() bestanden.") + + return result1, result2 + + +# --------------------------------------------------------------------------- +# Beispieldaten (AoC 2025, Tag 8 – 20 Junction Boxes) +# Erwartetes Ergebnis: Teil 1 (k=10) → 40, Teil 2 → 25272 +# --------------------------------------------------------------------------- + +BEISPIEL = """ +162,817,812 +57,618,57 +906,360,560 +592,479,940 +352,342,300 +466,668,158 +542,29,236 +431,825,988 +739,650,466 +52,470,668 +216,146,977 +819,987,18 +117,168,530 +805,96,715 +346,949,466 +970,615,88 +941,993,340 +862,61,35 +984,92,344 +425,690,689 +""" + + +if __name__ == '__main__': + if len(sys.argv) == 1: + print("=== Selbsttest mit Beispieldaten ===") + r1, r2 = solve(BEISPIEL, k=10) + assert r1 == 40, f"Teil 1 erwartet 40, erhalten {r1}" + assert r2 == 25272, f"Teil 2 erwartet 25272, erhalten {r2}" + print("Selbsttest bestanden.") + else: + filename = sys.argv[1] + k = int(sys.argv[2]) if len(sys.argv) > 2 else 1000 + with open(filename) as f: + text = f.read() + solve(text, k)