Compare commits
9
Commits
a553d7fd01
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cae3355bf1 | ||
|
|
1692129d89 | ||
|
|
586a52f9a5 | ||
|
|
c478317d76 | ||
|
|
646b9c6881 | ||
|
|
779fbab7c8 | ||
|
|
ed329d5d85 | ||
|
|
5aca875fa6 | ||
|
|
064d392d98 |
No files matched your search
@@ -19,6 +19,8 @@ build/
|
||||
CLAUDE.md
|
||||
.claude/
|
||||
specs/
|
||||
docs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
/.agents/
|
||||
@@ -0,0 +1,38 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq0.txt', ctx)
|
||||
tree = BinaryTree(ctx)
|
||||
for cell in values:
|
||||
tree.insert(cell.value)
|
||||
|
||||
# graph_traversal() erzeugt eine DOT-Datei und rendert sie als PDF.
|
||||
# Die Datei landet im Verzeichnis, aus dem das Skript gestartet wird.
|
||||
tree.graph_traversal()
|
||||
|
||||
# ── Antworten zu den Beobachtungsfragen ──────────────────────────────────────
|
||||
|
||||
# Welcher Knoten ist die Wurzel?
|
||||
# Das erste eingefügte Element wird immer die Wurzel, da der Baum anfangs
|
||||
# leer ist. Hier ist das -59 (erste Zeile von seq0.txt).
|
||||
print("Wurzel:", tree.root.value) # -59
|
||||
|
||||
# Wie viele Blattknoten hat der Baum?
|
||||
# Ein Blattknoten hat weder ein linkes noch ein rechtes Kind.
|
||||
blattknoten = []
|
||||
def merke_blatt(node):
|
||||
if node.left is None and node.right is None:
|
||||
blattknoten.append(node.value)
|
||||
tree.in_order_traversal(merke_blatt)
|
||||
print("Blattknoten:", blattknoten) # [-77, -50, 15, 48, 51, 58] → 6 Blätter
|
||||
|
||||
# Wie groß ist die Höhe des Baums?
|
||||
# height() zählt rekursiv die längste Kante von der Wurzel zu einem Blatt.
|
||||
# Mit 14 Elementen und zufälliger Reihenfolge ergibt sich hier Höhe 6.
|
||||
print("Höhe:", tree.root.height()) # 6
|
||||
@@ -0,0 +1,60 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq0.txt', ctx)
|
||||
tree = BinaryTree(ctx)
|
||||
for cell in values:
|
||||
tree.insert(cell.value)
|
||||
|
||||
# ── in_order_traversal ───────────────────────────────────────────────────────
|
||||
print("In-Order:")
|
||||
tree.in_order_traversal(lambda node: print(node.value, end=" "))
|
||||
print()
|
||||
# Ausgabe: -87 -77 -59 -50 14 15 34 46 47 48 50 51 52 58
|
||||
#
|
||||
# Warum sortiert?
|
||||
# Die BST-Eigenschaft garantiert: linker Teilbaum < Wurzel < rechter Teilbaum.
|
||||
# In-Order besucht genau in dieser Reihenfolge (links → Wurzel → rechts),
|
||||
# was rekursiv angewendet eine aufsteigende Sortierung ergibt.
|
||||
# In-Order-Traversal eines BST entspricht damit einer Sortierung der Schlüssel.
|
||||
|
||||
# ── level_order_traversal ────────────────────────────────────────────────────
|
||||
print("\nLevel-Order:")
|
||||
tree.level_order_traversal(lambda node, level: print(f"Ebene {level}: {node.value}"))
|
||||
# Ausgabe: Wurzel zuerst, dann alle Knoten der Ebene 1, dann Ebene 2, …
|
||||
#
|
||||
# Was zeigt level_order, was in_order nicht zeigt?
|
||||
# level_order_traversal entspricht einer Breitensuche (BFS). Sie zeigt,
|
||||
# auf welcher Tiefenebene sich jeder Knoten befindet, und damit die
|
||||
# tatsächliche Struktur des Baums – nicht nur die sortierte Schlüsselfolge.
|
||||
# Zwei BSTs mit denselben Schlüsseln liefern identische in_order-Ausgaben,
|
||||
# aber unterschiedliche level_order-Ausgaben, wenn ihre Struktur verschieden ist.
|
||||
|
||||
# ── graph_traversal ──────────────────────────────────────────────────────────
|
||||
# Relevante Stelle in bin_tree.py:
|
||||
#
|
||||
# def graph_traversal(self):
|
||||
# def define_node(node, level, line):
|
||||
# node.graphviz_rep(level, line, dot) ← Knoten positionieren
|
||||
# ...
|
||||
# self.tree_structure_traversal(define_node) ← Traversierung
|
||||
# _rec(self.root) ← Kanten eintragen
|
||||
#
|
||||
# graph_traversal nutzt intern tree_structure_traversal – eine In-Order-
|
||||
# Traversal, die zusätzlich zu jedem Knoten seine Tiefe (level) und seine
|
||||
# In-Order-Position (line) mitliefert.
|
||||
#
|
||||
# level → y-Koordinate im Graphviz-Layout: je tiefer im Baum, desto weiter
|
||||
# unten in der Abbildung (negiert: pos=f"{col},{-row}!")
|
||||
# line → x-Koordinate: die In-Order-Position ergibt automatisch eine
|
||||
# kollisionsfreie horizontale Anordnung ohne überlappende Knoten.
|
||||
#
|
||||
# tree_structure_traversal existiert also nicht als eigenständige Ausgabe-
|
||||
# Traversal, sondern als Hilfsmittel für die Positionsberechnung in graph_traversal.
|
||||
# Die Traversierungsreihenfolge ist identisch mit in_order_traversal.
|
||||
@@ -0,0 +1,57 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree
|
||||
|
||||
# ── Entarteter BST: sortierte Eingabe ────────────────────────────────────────
|
||||
|
||||
ctx = AlgoContext()
|
||||
tree_sorted = BinaryTree(ctx)
|
||||
for v in range(1, 9):
|
||||
tree_sorted.insert(v)
|
||||
|
||||
tree_sorted.graph_traversal() # → entarteter Baum sichtbar: eine gerade Kette
|
||||
|
||||
print("Höhe (sortierte Eingabe):", tree_sorted.root.height()) # 8
|
||||
|
||||
ctx.comparisons = 0
|
||||
tree_sorted.search(8)
|
||||
print("Vergleiche search(8) im entarteten Baum:", ctx.comparisons) # 16
|
||||
# Jeder Knoten (1 bis 8) erfordert zwei Vergleiche: value < node? (nein)
|
||||
# und value > node? (ja, bis der gesuchte Knoten gefunden ist).
|
||||
# Das entspricht einer linearen Suche: O(n).
|
||||
|
||||
# ── Referenz: ausgewogene Einfügereihenfolge ─────────────────────────────────
|
||||
|
||||
ctx2 = AlgoContext()
|
||||
tree_balanced = BinaryTree(ctx2)
|
||||
for v in [4, 2, 6, 1, 3, 5, 7, 8]: # Mitte zuerst → ausgewogener Baum
|
||||
tree_balanced.insert(v)
|
||||
|
||||
print("\nHöhe (ausgewogene Eingabe):", tree_balanced.root.height()) # 4
|
||||
|
||||
ctx2.comparisons = 0
|
||||
tree_balanced.search(8)
|
||||
print("Vergleiche search(8) im ausgewogenen Baum:", ctx2.comparisons) # 8
|
||||
|
||||
# ── Antworten zu den Analysefragen ───────────────────────────────────────────
|
||||
|
||||
# Wie viele Vergleiche benötigt search(8) im entarteten Baum?
|
||||
# 16 – der Baum entartet zur verketteten Liste, jeder Knoten wird besucht.
|
||||
|
||||
# Wie viele im ausgewogenen Baum mit 8 Knoten?
|
||||
# 8 – die Höhe beträgt 4, jede Ebene kostet 2 Vergleiche (< und >).
|
||||
|
||||
# Welche Eingabereihenfolge erzeugt immer einen ausgewogenen BST?
|
||||
# Median-First: stets den Median der aktuellen Teilfolge zuerst einfügen.
|
||||
# Das setzt voraus, dass alle Elemente bereits bekannt sind – bei dynamischen
|
||||
# Einfügungen (ein Element nach dem anderen, ohne Vorwissen) ist das nicht
|
||||
# möglich. Deshalb reicht es nicht, nur die Einfügereihenfolge zu steuern.
|
||||
|
||||
# Was muss eine Datenstruktur garantieren?
|
||||
# Sie muss die Baumhöhe nach jeder Einfüge- oder Löschoperation automatisch
|
||||
# auf O(log n) begrenzen – unabhängig von der Eingabereihenfolge.
|
||||
# AVL-Bäume erreichen das, indem sie nach jeder Operation den Balance-Faktor
|
||||
# prüfen und bei Bedarf Rotationen durchführen.
|
||||
@@ -0,0 +1,59 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from vorlesung.L05_binaere_baeume.avl_tree import AVLTree
|
||||
|
||||
ctx = AlgoContext()
|
||||
tree = AVLTree(ctx)
|
||||
for v in [10, 5, 15, 3, 7, 12, 20, 1, 4, 6, 8]:
|
||||
tree.insert(v)
|
||||
|
||||
print("Ausgangszustand:")
|
||||
tree.in_order_traversal(lambda n: print(n.value, end=" "))
|
||||
print()
|
||||
tree.graph_traversal()
|
||||
|
||||
# ── Fall 1: Blattknoten löschen (1 ist Blatt) ────────────────────────────────
|
||||
tree.delete(1)
|
||||
print("\nNach Löschen von 1 (Blatt):")
|
||||
tree.in_order_traversal(lambda n: print(n.value, end=" "))
|
||||
print()
|
||||
tree.graph_traversal()
|
||||
|
||||
# ── Fall 2: Knoten mit einem Kind löschen (3 hat jetzt nur noch Kind 4) ──────
|
||||
tree.delete(3)
|
||||
print("\nNach Löschen von 3 (ein Kind):")
|
||||
tree.in_order_traversal(lambda n: print(n.value, end=" "))
|
||||
print()
|
||||
tree.graph_traversal()
|
||||
|
||||
# ── Fall 3: Knoten mit zwei Kindern löschen (10 hat linkes Kind 5, rechtes 15)
|
||||
tree.delete(10)
|
||||
print("\nNach Löschen von 10 (zwei Kinder):")
|
||||
tree.in_order_traversal(lambda n: print(n.value, end=" "))
|
||||
print()
|
||||
tree.graph_traversal()
|
||||
# Inorder-Nachfolger von 10 ist 12 (kleinstes Element im rechten Teilbaum).
|
||||
# 12 übernimmt den Platz von 10, anschließend wird rebalanciert.
|
||||
|
||||
# ── Antworten zu den Verständnisfragen ───────────────────────────────────────
|
||||
|
||||
# Warum muss node.parent = parent nach dem Löschen neu gesetzt werden?
|
||||
#
|
||||
# BinaryTree kennt keine parent-Zeiger — delete() gibt lediglich
|
||||
# (nachfolgender_knoten, elternknoten) als plain-Python-Tupel zurück.
|
||||
# AVLTree pflegt parent-Zeiger, weil balance() sie benötigt, um den Pfad
|
||||
# von einem Knoten aufwärts zur Wurzel zu traversieren.
|
||||
# Ohne die Aktualisierung würde balance() an einem veralteten parent-Zeiger
|
||||
# hängen und die Rebalancierung unvollständig oder fehlerhaft ausführen.
|
||||
|
||||
# Warum beginnt die Rebalancierung mit balance(parent) statt balance(node)?
|
||||
#
|
||||
# Das Löschen verändert die Höhe des Teilbaums, in dem der Knoten stand.
|
||||
# Diese Höhenänderung kann den Balance-Faktor des Elternknotens (und aller
|
||||
# Vorfahren bis zur Wurzel) aus dem Gleichgewicht bringen.
|
||||
# Der gelöschte (oder ersetzte) Knoten selbst ist entweder weg oder ein Blatt
|
||||
# — er ist kein sinnvoller Startpunkt für die Aufwärts-Traversal.
|
||||
# balance(parent) startet genau dort, wo die Höhenänderung zuerst spürbar ist.
|
||||
@@ -0,0 +1,85 @@
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.join(os.path.dirname(os.path.abspath(__file__)), '..', '..'))
|
||||
|
||||
import random
|
||||
from utils.algo_context import AlgoContext
|
||||
from vorlesung.L05_binaere_baeume.bin_tree import BinaryTree
|
||||
from vorlesung.L05_binaere_baeume.avl_tree import AVLTree
|
||||
|
||||
# ── Theoretische Herleitung ───────────────────────────────────────────────────
|
||||
|
||||
# AVL-Baum / ausgeglichener BST
|
||||
# Bei jedem Schritt der Suche wird genau ein Teilbaum mit (ungefähr) halber
|
||||
# Größe weiterverfolgt:
|
||||
#
|
||||
# T(n) = T(n/2) + O(1)
|
||||
#
|
||||
# Master-Theorem: a=1, b=2, f(n)=O(1)
|
||||
# log_b(a) = log_2(1) = 0 → f(n) = O(n^0) = O(n^log_b(a))
|
||||
# → Fall 2: T(n) = O(log n)
|
||||
|
||||
# BST – Worst Case (sortierte Eingabe, entarteter Baum)
|
||||
# Der Baum ist eine verkettete Liste; jeder Schritt reduziert das Problem um 1:
|
||||
#
|
||||
# T(n) = T(n-1) + O(1)
|
||||
#
|
||||
# Kein Master-Theorem (b > 1 gefordert). Direkte Auflösung durch Entfalten:
|
||||
# T(n) = T(n-2) + 2·c = … = T(0) + n·c → O(n)
|
||||
|
||||
# BST – zufällige Eingabe (Expected Case)
|
||||
# Bei zufälliger Einfügereihenfolge teilt die Wurzel den Schlüsselraum im
|
||||
# Erwartungswert in zwei ungefähr gleich große Hälften – analog zum
|
||||
# durchschnittlichen Fall bei Quicksort. Daraus folgt eine erwartete Höhe
|
||||
# von O(log n), empirisch ca. 2,5 · log₂(n). Ein formaler Beweis erfordert
|
||||
# eine Wahrscheinlichkeitsanalyse über alle n! Permutationen.
|
||||
|
||||
# ── Empirische Messung ───────────────────────────────────────────────────────
|
||||
|
||||
SIZES = [10, 100, 500]
|
||||
|
||||
print(f"{'n':>5} {'BST zufällig':>14} {'BST sortiert':>14} {'AVL sortiert':>14}")
|
||||
print("-" * 55)
|
||||
|
||||
for n in SIZES:
|
||||
# BST, zufällige Eingabe
|
||||
ctx = AlgoContext()
|
||||
tree = BinaryTree(ctx)
|
||||
values = random.sample(range(n * 10), n)
|
||||
for v in values:
|
||||
tree.insert(v)
|
||||
ctx.comparisons = 0
|
||||
tree.search(values[-1])
|
||||
cmp_random = ctx.comparisons
|
||||
|
||||
# BST, sortierte Eingabe
|
||||
ctx = AlgoContext()
|
||||
tree = BinaryTree(ctx)
|
||||
for v in range(1, n + 1):
|
||||
tree.insert(v)
|
||||
ctx.comparisons = 0
|
||||
tree.search(n)
|
||||
cmp_sorted = ctx.comparisons
|
||||
|
||||
# AVL-Baum, sortierte Eingabe
|
||||
ctx = AlgoContext()
|
||||
tree = AVLTree(ctx)
|
||||
for v in range(1, n + 1):
|
||||
tree.insert(v)
|
||||
ctx.comparisons = 0
|
||||
tree.search(n)
|
||||
cmp_avl = ctx.comparisons
|
||||
|
||||
print(f"{n:>5} {cmp_random:>14} {cmp_sorted:>14} {cmp_avl:>14}")
|
||||
|
||||
# ── Abschlussfrage ────────────────────────────────────────────────────────────
|
||||
#
|
||||
# Stimmen die Messwerte mit den theoretischen Komplexitätsklassen überein?
|
||||
#
|
||||
# BST sortiert wächst linear mit n (2·n Vergleiche für das letzte Element),
|
||||
# was die hergeleitete O(n)-Komplexität bestätigt.
|
||||
#
|
||||
# BST zufällig und AVL sortiert wachsen logarithmisch: für n=500 liegen die
|
||||
# Vergleiche bei ca. 20 bzw. 18 – weit unter dem linearen Worst Case.
|
||||
# Das bestätigt O(log n) für beide, wobei der AVL-Baum die Schranke
|
||||
# garantiert und der zufällige BST sie nur im Erwartungswert einhält.
|
||||
@@ -0,0 +1,62 @@
|
||||
import sys
|
||||
import os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l06 = os.path.join(_root, 'vorlesung', 'L06_b_baeume')
|
||||
if _l06 not in sys.path: sys.path.insert(0, _l06)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
|
||||
import graphviz
|
||||
from datetime import datetime
|
||||
from b_tree import BTree
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
|
||||
|
||||
class BTreeVis(BTree):
|
||||
"""BTree-Erweiterung mit Graphviz-Ausgabe als Unterklasse."""
|
||||
|
||||
def graph_traversal(self):
|
||||
def _node_rep(node):
|
||||
label = " | ".join(str(node.value[i]) for i in range(node.n))
|
||||
dot.node(str(id(node)), label=label, shape="record", fontname="Arial")
|
||||
|
||||
def _rec(node):
|
||||
_node_rep(node)
|
||||
if not node.leaf:
|
||||
for i in range(node.n + 1):
|
||||
child = node.children[i]
|
||||
if child is not None:
|
||||
dot.edge(str(id(node)), str(id(child)))
|
||||
_rec(child)
|
||||
|
||||
dot = graphviz.Digraph(
|
||||
name=f"BTree_m{self.m}",
|
||||
engine="dot",
|
||||
node_attr={"fontname": "Arial"},
|
||||
format="pdf",
|
||||
)
|
||||
_rec(self.root)
|
||||
ts = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
dot.render(f"BTree_m{self.m}_{ts}.gv")
|
||||
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq1.txt', ctx)
|
||||
tree = BTreeVis(3, ctx)
|
||||
for cell in values:
|
||||
tree.insert(cell)
|
||||
|
||||
# graph_traversal() erzeugt eine DOT-Datei und rendert sie als PDF.
|
||||
tree.graph_traversal()
|
||||
|
||||
# ── Antworten zu den Beobachtungsfragen ──────────────────────────────────────
|
||||
|
||||
# Wie viele Schlüssel hat die Wurzel?
|
||||
# Ablesbar direkt aus tree.root.n.
|
||||
print("Schlüssel in der Wurzel:", tree.root.n)
|
||||
|
||||
# Liegen alle Blätter auf derselben Tiefe?
|
||||
# Ja – das ist eine B-Baum-Invariante: alle Blätter haben immer
|
||||
# dieselbe Tiefe, da Splits ausschließlich aufwärts propagieren.
|
||||
# In der PDF sind alle Blattknoten auf der untersten Ebene.
|
||||
print("Höhe des Baums:", tree.height())
|
||||
@@ -0,0 +1,51 @@
|
||||
import sys
|
||||
import os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l06 = os.path.join(_root, 'vorlesung', 'L06_b_baeume')
|
||||
if _l06 not in sys.path: sys.path.insert(0, _l06)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
|
||||
from b_tree import BTree
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq3.txt', ctx)
|
||||
|
||||
tree3 = BTree(3, ctx)
|
||||
tree5 = BTree(5, ctx)
|
||||
for cell in values:
|
||||
tree3.insert(cell)
|
||||
tree5.insert(cell)
|
||||
|
||||
# ── Welche Höhe ergibt sich für Ordnung 3 bzw. 5? ───────────────────────────
|
||||
print("Höhe Ordnung 3:", tree3.height())
|
||||
print("Höhe Ordnung 5:", tree5.height())
|
||||
|
||||
# Ordnung 3: Knoten fassen 2–5 Schlüssel → weniger Schlüssel pro Knoten,
|
||||
# mehr Ebenen nötig. seq3.txt hat 10 000 Werte → Höhe ca. 5–7.
|
||||
# Ordnung 5: Knoten fassen 4–9 Schlüssel → geringere Höhe, ca. 4–5.
|
||||
|
||||
# ── Traversierung und Sortierungsprüfung ─────────────────────────────────────
|
||||
results3 = []
|
||||
tree3.traversal(lambda v: results3.append(int(str(v))))
|
||||
print("Sortiert (Ordnung 3):", results3 == sorted(results3))
|
||||
|
||||
results5 = []
|
||||
tree5.traversal(lambda v: results5.append(int(str(v))))
|
||||
print("Sortiert (Ordnung 5):", results5 == sorted(results5))
|
||||
|
||||
# In-Order-Traversal eines B-Baums liefert immer eine sortierte Folge:
|
||||
# Die B-Baum-Eigenschaft (linker Teilbaum < Schlüssel < rechter Teilbaum)
|
||||
# gilt rekursiv für jeden Knoten.
|
||||
|
||||
# ── Knoten mit Wert 0 suchen ─────────────────────────────────────────────────
|
||||
node3 = tree3.search(0)
|
||||
node5 = tree5.search(0)
|
||||
print("Knoten mit 0 (Ordnung 3):", str(node3) if node3 else "nicht gefunden")
|
||||
print("Knoten mit 0 (Ordnung 5):", str(node5) if node5 else "nicht gefunden")
|
||||
|
||||
# search() gibt den Knoten zurück, der den gesuchten Schlüssel enthält.
|
||||
# __str__ gibt alle Schlüssel des Knotens aus, z.B.: ( -2 0 3 )
|
||||
# Größere Ordnung → mehr Schlüssel pro Knoten → der Knoten mit 0
|
||||
# enthält bei m=5 tendenziell mehr Nachbarn als bei m=3.
|
||||
@@ -0,0 +1,89 @@
|
||||
import sys
|
||||
import os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l06 = os.path.join(_root, 'vorlesung', 'L06_b_baeume')
|
||||
if _l06 not in sys.path: sys.path.insert(0, _l06)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from b_tree import BTree
|
||||
from b_tree_node import BTreeNode
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
|
||||
|
||||
def count_loads(node: BTreeNode) -> int:
|
||||
if node is None:
|
||||
return 0
|
||||
total = node.loaded_count
|
||||
for child in node.children:
|
||||
total += count_loads(child)
|
||||
return total
|
||||
|
||||
|
||||
def count_saves(node: BTreeNode) -> int:
|
||||
if node is None:
|
||||
return 0
|
||||
total = node.saved_count
|
||||
for child in node.children:
|
||||
total += count_saves(child)
|
||||
return total
|
||||
|
||||
|
||||
N = 500
|
||||
orders = [2, 3, 5, 10, 20]
|
||||
stats = {}
|
||||
|
||||
ctx = AlgoContext()
|
||||
base = Array.random(N, -1000, 1000, ctx)
|
||||
|
||||
for m in orders:
|
||||
ctx.reset()
|
||||
tree = BTree(m, ctx)
|
||||
for cell in base:
|
||||
tree.insert(cell)
|
||||
stats[m] = {
|
||||
"comparisons": ctx.comparisons,
|
||||
"loads": count_loads(tree.root),
|
||||
"saves": count_saves(tree.root),
|
||||
"height": tree.height(),
|
||||
}
|
||||
|
||||
print(f"{'m':>4} {'Vergleiche':>12} {'Loads':>8} {'Saves':>8} {'Höhe':>6}")
|
||||
for m, s in stats.items():
|
||||
print(f"{m:>4} {s['comparisons']:>12} {s['loads']:>8} {s['saves']:>8} {s['height']:>6}")
|
||||
|
||||
fig, axes = plt.subplots(2, 2, figsize=(10, 8))
|
||||
labels = ["comparisons", "loads", "saves", "height"]
|
||||
titles = ["Vergleiche", "Disk-Loads", "Disk-Saves", "Baumhöhe"]
|
||||
for ax, label, title in zip(axes.flat, labels, titles):
|
||||
ax.plot(orders, [stats[m][label] for m in orders], marker="o")
|
||||
ax.set_xlabel("Ordnung m")
|
||||
ax.set_ylabel(title)
|
||||
ax.set_title(title)
|
||||
plt.tight_layout()
|
||||
plt.show()
|
||||
|
||||
# ── Antworten zu den Analysefragen ───────────────────────────────────────────
|
||||
|
||||
# Warum sinkt die Zahl der Disk-Zugriffe mit wachsendem m?
|
||||
# Größeres m → mehr Schlüssel pro Knoten → weniger Knoten insgesamt →
|
||||
# geringere Baumhöhe. Jeder Einfügepfad durchläuft weniger Knoten,
|
||||
# also werden weniger load()/save()-Aufrufe ausgelöst.
|
||||
|
||||
# Warum steigen die internen Vergleiche mit wachsendem m?
|
||||
# Die Suche innerhalb eines Knotens ist immer linear: bis zu 2m-1 Schlüssel
|
||||
# werden verglichen. Die Gesamtkosten pro Einfügung betragen daher
|
||||
# O(m) · O(log_m n) = O(m / log(m) · log n).
|
||||
# Da m/log(m) monoton wächst, steigen die Vergleiche mit jedem größeren m —
|
||||
# nicht ab einem Kipppunkt, sondern kontinuierlich. Das ist der grundlegende
|
||||
# Trade-off: mehr m spart Disk-I/O, kostet aber mehr CPU-Vergleiche.
|
||||
|
||||
# Was bedeutet das für die Wahl von m bei Blockgröße 4 KB?
|
||||
# Ein Knoten enthält 2m-1 Integer-Schlüssel (je 8 Byte) und 2m Kindzeiger.
|
||||
# Die Kindzeiger sind Blocknummern auf dem Speichermedium (je 8 Byte) —
|
||||
# also keine Speicheradressen, sondern Verweise auf andere Disk-Blöcke.
|
||||
# m wird so gewählt, dass ein Knoten genau in einen 4-KB-Block passt:
|
||||
# (2m-1) · 8 + 2m · 8 = 4096
|
||||
# 8 · (4m - 1) = 4096 → m ≈ 128
|
||||
# Disk-I/O-Kosten dominieren gegenüber CPU-Vergleichen → großes m ist optimal.
|
||||
@@ -0,0 +1,58 @@
|
||||
import sys, os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
|
||||
if _l07 not in sys.path: sys.path.insert(0, _l07)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
|
||||
from collections.abc import Callable
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_int import Int
|
||||
|
||||
|
||||
class HashTableChaining:
|
||||
"""Hashtabelle mit Verkettung (Chaining) zur Kollisionsauflösung."""
|
||||
|
||||
def __init__(self, m: int, h_func: Callable, ctx: AlgoContext):
|
||||
self.ctx = ctx
|
||||
self.m = Int(m, ctx)
|
||||
self.h = h_func
|
||||
self.table = [[] for _ in range(m)]
|
||||
self._n = 0
|
||||
|
||||
def _slot(self, x: Int) -> int:
|
||||
return int(self.h(x, self.m))
|
||||
|
||||
def insert(self, x: Int) -> bool:
|
||||
j = self._slot(x)
|
||||
for elem in self.table[j]:
|
||||
if elem == x: # Duplikat – Vergleich wird gezählt
|
||||
return False
|
||||
self.table[j].append(x)
|
||||
self._n += 1
|
||||
return True
|
||||
|
||||
def search(self, x: Int) -> bool:
|
||||
j = self._slot(x)
|
||||
for elem in self.table[j]:
|
||||
if elem == x: # Vergleich wird gezählt
|
||||
return True
|
||||
return False
|
||||
|
||||
def delete(self, x: Int) -> bool:
|
||||
j = self._slot(x)
|
||||
for i, elem in enumerate(self.table[j]):
|
||||
if elem == x: # Vergleich wird gezählt
|
||||
self.table[j].pop(i)
|
||||
self._n -= 1
|
||||
return True
|
||||
return False
|
||||
|
||||
def __str__(self):
|
||||
rows = []
|
||||
for i, chain in enumerate(self.table):
|
||||
vals = ' -> '.join(str(e.value) for e in chain) if chain else '—'
|
||||
rows.append(f'[{i:2d}] {vals}')
|
||||
return '\n'.join(rows)
|
||||
|
||||
def alpha(self) -> float:
|
||||
return self._n / int(self.m)
|
||||
@@ -0,0 +1,49 @@
|
||||
import sys, os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
|
||||
_p06 = os.path.dirname(__file__)
|
||||
if _l07 not in sys.path: sys.path.insert(0, _l07)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
if _p06 not in sys.path: sys.path.insert(0, _p06)
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from utils.algo_int import Int
|
||||
from analyze_hashtable import h
|
||||
from aufgabe1_chaining import HashTableChaining
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq0.txt', ctx)
|
||||
|
||||
ht = HashTableChaining(20, h, ctx)
|
||||
for cell in values:
|
||||
ht.insert(cell)
|
||||
|
||||
# ── a) Belegungsfaktor ───────────────────────────────────────────────────────
|
||||
print("=== a) Nach Einfügen von seq0.txt (14 Werte, m=20) ===")
|
||||
print(ht)
|
||||
print(f"\nBelegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f}")
|
||||
|
||||
# ── b) Löschen von 52 ────────────────────────────────────────────────────────
|
||||
print("\n=== b) Nach delete(52) ===")
|
||||
ht.delete(Int(52, ctx))
|
||||
print(ht)
|
||||
print(f"Belegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f}")
|
||||
|
||||
# ── c) Erneut seq0.txt einfügen ──────────────────────────────────────────────
|
||||
print("\n=== c) Erneut seq0.txt einfügen ===")
|
||||
for cell in values:
|
||||
ht.insert(cell)
|
||||
print(f"Belegungsfaktor α = {ht._n}/{int(ht.m)} = {ht.alpha():.2f} (kann > 1 sein!)")
|
||||
print("insert kann bei Verkettung nie False zurückgeben –")
|
||||
print("die Kette wächst unbegrenzt, es gibt kein 'Tabelle voll'.")
|
||||
|
||||
# ── d) Erklärung kein DELETED_MARK ───────────────────────────────────────────
|
||||
print("\n=== d) Warum kein DELETED_MARK? ===")
|
||||
print(
|
||||
"Bei offener Adressierung wird DELETED_MARK benötigt, damit die\n"
|
||||
"Sondierungssequenz bei search nicht vorzeitig abbricht. Bei Verkettung\n"
|
||||
"wird die vollständige Liste eines Slots linear durchsucht – ein gelöschtes\n"
|
||||
"Element wird einfach aus der Liste entfernt. Es gibt keine Sequenz, die\n"
|
||||
"'unterbrochen' werden könnte, daher ist kein Tombstone notwendig."
|
||||
)
|
||||
@@ -0,0 +1,97 @@
|
||||
import sys, os, math
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
|
||||
if _l07 not in sys.path: sys.path.insert(0, _l07)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from utils.algo_int import Int
|
||||
from hashtable import HashTableOpenAddressing, DELETED_MARK, UNUSED_MARK
|
||||
from analyze_hashtable import h, f
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq0.txt', ctx)
|
||||
|
||||
ht = HashTableOpenAddressing(20, f, ctx)
|
||||
for cell in values:
|
||||
ht.insert(cell)
|
||||
|
||||
# ── a) delete(52) ────────────────────────────────────────────────────────────
|
||||
print("=== a) Nach delete(52) ===")
|
||||
ht.delete(Int(52, ctx))
|
||||
print(ht)
|
||||
|
||||
slot_52 = int(h(Int(52, ctx), ht.m))
|
||||
print(f"\nSlot h(52) = {slot_52}: Inhalt = {ht.table[Int(slot_52, ctx)].value!r} ← DELETED_MARK")
|
||||
|
||||
# ── b) Warum search bei DELETED_MARK weitersucht ─────────────────────────────
|
||||
print("\n=== b) Warum search bei DELETED_MARK fortgesetzt wird ===")
|
||||
print(
|
||||
"search prüft in jeder Iteration: Ist der aktuelle Slot UNUSED? → Abbruch\n"
|
||||
"(der gesuchte Wert wurde nie an eine spätere Position sondiert, also ist\n"
|
||||
"er nicht vorhanden). Ist der Slot DELETED_MARK? → weitersuchen, denn der\n"
|
||||
"Wert könnte durch einen früheren Eintrag über diesen Slot hinaus sondiert\n"
|
||||
"worden sein. DELETED_MARK erhält die Sondierungssequenz aufrecht."
|
||||
)
|
||||
|
||||
# ── c) Konkretes Gegenbeispiel: UNUSED_MARK statt DELETED_MARK ───────────────
|
||||
print("\n=== c) Konkretes Gegenbeispiel ===")
|
||||
# Wir bauen eine kleine Tabelle, in der das Problem klar sichtbar ist.
|
||||
ctx2 = AlgoContext()
|
||||
ht2 = HashTableOpenAddressing(20, f, ctx2)
|
||||
for cell in values:
|
||||
ht2.insert(cell)
|
||||
|
||||
# Finde einen Wert, der über denselben Slot wie 52 sondiert
|
||||
# h(52) = slot_52; suche einen anderen Wert v mit h(v)=slot_52 oder f(v,0)=slot_52
|
||||
slot_52_v = int(h(Int(52, ctx2), ht2.m))
|
||||
print(f"h(52) = {slot_52_v}")
|
||||
|
||||
# Prüfe: gibt es einen Wert im Baum, der bei i=0 auf slot_52 trifft?
|
||||
# Sondierungssequenz für jeden eingefügten Wert anzeigen
|
||||
from utils.algo_int import Int as I
|
||||
_A = (math.sqrt(5) - 1) / 2
|
||||
def h_raw(v, m=20):
|
||||
full = v * _A
|
||||
return int(abs(full - int(full)) * m)
|
||||
|
||||
def f_raw(v, i, m=20):
|
||||
return (h_raw(v, m) + i + 5*i*i) % m
|
||||
|
||||
print("\nSondierungssequenzen (erste 3 Schritte) für seq0.txt-Werte:")
|
||||
raw_vals = [int(str(c)) for c in values]
|
||||
for v in raw_vals:
|
||||
seq = [f_raw(v, i) for i in range(3)]
|
||||
print(f" h({v:4d}) = {h_raw(v):2d}, f(·,0)={seq[0]:2d}, f(·,1)={seq[1]:2d}, f(·,2)={seq[2]:2d}")
|
||||
|
||||
# 52 belegt slot_52; angenommen 58 kollidiert und liegt hinter 52
|
||||
# → delete(52) mit UNUSED würde search(58) frühzeitig abbrechen
|
||||
print(
|
||||
"\nAngenommen, zwei Werte v1 und v2 sondieren über denselben Slot:\n"
|
||||
" v1 landet in Slot s (erste Sondierung)\n"
|
||||
" v2 kollidiert bei Slot s und landet bei s' (zweite Sondierung)\n"
|
||||
"Wird v1 gelöscht und Slot s auf UNUSED gesetzt, bricht search(v2)\n"
|
||||
"bei Slot s ab – obwohl v2 noch in s' liegt. Das Ergebnis ist 'nicht\n"
|
||||
"gefunden', obwohl v2 vorhanden ist. DELETED_MARK verhindert das."
|
||||
)
|
||||
|
||||
# ── d) Wo landet 24 nach delete(52)? ─────────────────────────────────────────
|
||||
print("\n=== d) Wo landet 24 nach delete(52)? ===")
|
||||
h24 = h_raw(24)
|
||||
print(f"h(24) = int({{24·A}} mod 1 · 20) = {h24}")
|
||||
for i in range(5):
|
||||
slot = f_raw(24, i)
|
||||
val = ht.table[Int(slot, ctx)].value
|
||||
status = "frei (DELETED/UNUSED)" if val in (DELETED_MARK, UNUSED_MARK) else f"belegt ({val})"
|
||||
print(f"f(24, {i}, 20) = {slot:2d} → {status}")
|
||||
if val in (DELETED_MARK, UNUSED_MARK):
|
||||
print(f" ⇒ 24 landet in Slot {slot}")
|
||||
break
|
||||
|
||||
ht.insert(Int(24, ctx))
|
||||
print(f"\nVerifikation __str__ (relevante Slots):")
|
||||
for idx in [0, 1, 2, 3, 16, 17]:
|
||||
v = ht.table[Int(idx, ctx)].value
|
||||
marker = " ← 24 eingefügt" if str(v) == "24" else ""
|
||||
print(f" Slot {idx:2d}: {v}{marker}")
|
||||
@@ -0,0 +1,70 @@
|
||||
import sys, os
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
|
||||
_p06 = os.path.dirname(__file__)
|
||||
if _l07 not in sys.path: sys.path.insert(0, _l07)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
if _p06 not in sys.path: sys.path.insert(0, _p06)
|
||||
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from hashtable import HashTableOpenAddressing
|
||||
from analyze_hashtable import h, f
|
||||
from aufgabe1_chaining import HashTableChaining
|
||||
|
||||
ctx = AlgoContext()
|
||||
values = Array.from_file('data/seq1.txt', ctx)
|
||||
n = len(values)
|
||||
|
||||
# ── a) Größe 90 ───────────────────────────────────────────────────────────────
|
||||
ht90 = HashTableOpenAddressing(90, f, ctx)
|
||||
inserted90 = sum(1 for cell in values if ht90.insert(cell))
|
||||
rejected90 = n - inserted90
|
||||
print("=== a) HashTableOpenAddressing m=90, seq1.txt (100 Werte) ===")
|
||||
print(f"Eingefügt: {inserted90}, Abgewiesen: {rejected90}, "
|
||||
f"Freie Slots: {90 - inserted90}")
|
||||
print(
|
||||
"Ursache: Die quadratische Sondierungsfunktion f(x,i) = (h(x)+i+5i²) mod m\n"
|
||||
"erzeugt für m=90 (keine Primzahl) zyklische Sondierungssequenzen, die nicht\n"
|
||||
"alle 90 Slots abdecken. Sobald alle erreichbaren Slots belegt sind, schlägt\n"
|
||||
"insert fehl – auch wenn noch andere Slots frei sind."
|
||||
)
|
||||
|
||||
# ── b) Größe 89 ───────────────────────────────────────────────────────────────
|
||||
ctx2 = AlgoContext()
|
||||
values2 = Array.from_file('data/seq1.txt', ctx2)
|
||||
ht89 = HashTableOpenAddressing(89, f, ctx2)
|
||||
inserted89 = sum(1 for cell in values2 if ht89.insert(cell))
|
||||
rejected89 = n - inserted89
|
||||
print(f"\n=== b) HashTableOpenAddressing m=89 (Primzahl), seq1.txt ===")
|
||||
print(f"Eingefügt: {inserted89}, Abgewiesen: {rejected89}")
|
||||
print(
|
||||
"Bei Primzahlgröße garantiert die quadratische Sondierung, dass mindestens\n"
|
||||
"m/2 verschiedene Slots erreicht werden (bei geeigneten Konstanten c1, c2\n"
|
||||
"sogar alle m). Für m=89 ist die Sondierungssequenz deutlich länger, bevor\n"
|
||||
"sie sich wiederholt – dadurch können mehr Werte platziert werden."
|
||||
)
|
||||
|
||||
# ── c) HashTableChaining m=20, seq1.txt ───────────────────────────────────────
|
||||
ctx3 = AlgoContext()
|
||||
values3 = Array.from_file('data/seq1.txt', ctx3)
|
||||
htc = HashTableChaining(20, h, ctx3)
|
||||
for cell in values3:
|
||||
htc.insert(cell)
|
||||
print(f"\n=== c) HashTableChaining m=20, seq1.txt ===")
|
||||
unique = len(set(int(str(c)) for c in values3))
|
||||
print(f"Eingefügt: {htc._n} (seq1.txt hat {n} Werte, davon {unique} eindeutig; "
|
||||
f"Duplikate werden nicht doppelt gespeichert)")
|
||||
print(f"Belegungsfaktor α = {htc._n}/{int(htc.m)} = {htc.alpha():.2f}")
|
||||
print("Verkettung hat keine Kapazitätsgrenze – die Ketten wachsen unbegrenzt.")
|
||||
|
||||
# ── d) Theoretische Sondierungsanzahl ─────────────────────────────────────────
|
||||
print("\n=== d) Mittlere Sondierungsanzahl 1/(1-α) ===")
|
||||
for alpha in [0.5, 0.9]:
|
||||
probes = 1 / (1 - alpha)
|
||||
print(f" α = {alpha}: 1/(1−{alpha}) = {probes:.1f} Sondierungen")
|
||||
print(
|
||||
"Bis α ≈ 0,7 bleibt die offene Adressierung praktikabel (≈ 3,3 Sondierungen).\n"
|
||||
"Für α > 0,8 steigen die Kosten rapide; α = 0,9 bedeutet bereits 10 Sondierungen\n"
|
||||
"im Schnitt. Als Faustregel gilt: Tabelle vergrößern (Rehashing), wenn α > 0,75."
|
||||
)
|
||||
@@ -0,0 +1,106 @@
|
||||
import sys, os, math
|
||||
_root = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', '..'))
|
||||
_l07 = os.path.join(_root, 'vorlesung', 'L07_hashtable')
|
||||
_p06 = os.path.dirname(__file__)
|
||||
if _l07 not in sys.path: sys.path.insert(0, _l07)
|
||||
if _root not in sys.path: sys.path.insert(0, _root)
|
||||
if _p06 not in sys.path: sys.path.insert(0, _p06)
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from utils.algo_context import AlgoContext
|
||||
from utils.algo_array import Array
|
||||
from hashtable import HashTableOpenAddressing
|
||||
from analyze_hashtable import h, f
|
||||
from aufgabe1_chaining import HashTableChaining
|
||||
|
||||
|
||||
def measure(m: int, target_alpha: float, seed: int = 42):
|
||||
"""Befüllt beide Tabellentypen bis Ziel-α, misst Vergleiche beim Suchen."""
|
||||
n = int(m * target_alpha)
|
||||
|
||||
import random
|
||||
random.seed(seed)
|
||||
|
||||
ctx_oa = AlgoContext()
|
||||
vals_oa = Array.random(n, -10000, 10000, ctx_oa)
|
||||
ht_oa = HashTableOpenAddressing(m, f, ctx_oa)
|
||||
inserted = []
|
||||
for cell in vals_oa:
|
||||
if ht_oa.insert(cell):
|
||||
inserted.append(cell)
|
||||
|
||||
ctx_oa.reset()
|
||||
for cell in inserted:
|
||||
ht_oa.search(cell)
|
||||
cmp_oa = ctx_oa.comparisons
|
||||
|
||||
ctx_ch = AlgoContext()
|
||||
vals_ch = Array.random(n, -10000, 10000, ctx_ch)
|
||||
ht_ch = HashTableChaining(m, h, ctx_ch)
|
||||
for cell in vals_ch:
|
||||
ht_ch.insert(cell)
|
||||
|
||||
ctx_ch.reset()
|
||||
for i, chain in enumerate(ht_ch.table):
|
||||
for elem in chain:
|
||||
ht_ch.search(elem)
|
||||
cmp_ch = ctx_ch.comparisons
|
||||
|
||||
return cmp_oa, cmp_ch
|
||||
|
||||
|
||||
sizes = [50, 100, 200, 500, 1000]
|
||||
|
||||
for target_alpha, label in [(0.7, "α ≈ 0,7"), (0.9, "α ≈ 0,9")]:
|
||||
oa_vals, ch_vals = [], []
|
||||
for m in sizes:
|
||||
coa, cch = measure(m, target_alpha)
|
||||
oa_vals.append(coa)
|
||||
ch_vals.append(cch)
|
||||
print(f"\n{'m':>6} {'OA Vergl.':>12} {'Chaining Vergl.':>16} ({label})")
|
||||
for m, co, cc in zip(sizes, oa_vals, ch_vals):
|
||||
print(f"{m:>6} {co:>12} {cc:>16}")
|
||||
|
||||
# ── Plot ──────────────────────────────────────────────────────────────────────
|
||||
fig, axes = plt.subplots(1, 2, figsize=(11, 4))
|
||||
for ax, target_alpha, label in zip(axes, [0.7, 0.9], ["α ≈ 0,7", "α ≈ 0,9"]):
|
||||
oa_vals, ch_vals = [], []
|
||||
for m in sizes:
|
||||
coa, cch = measure(m, target_alpha)
|
||||
oa_vals.append(coa)
|
||||
ch_vals.append(cch)
|
||||
ax.plot(sizes, oa_vals, marker='o', label='Offene Adressierung')
|
||||
ax.plot(sizes, ch_vals, marker='s', label='Verkettung')
|
||||
ax.set_xlabel('Tabellengröße m')
|
||||
ax.set_ylabel('Vergleiche (Suchen aller Werte)')
|
||||
ax.set_title(label)
|
||||
ax.legend()
|
||||
plt.tight_layout()
|
||||
plt.savefig('vergleich_strategien.pdf')
|
||||
plt.show()
|
||||
|
||||
# ── Antworten ─────────────────────────────────────────────────────────────────
|
||||
print("\n=== a) Welche Strategie benötigt mehr Vergleiche? ===")
|
||||
print(
|
||||
"Bei gleichem α erzeugt die offene Adressierung mehr Vergleiche:\n"
|
||||
"Kollisionen führen zu langen Sondierungssequenzen, bei denen jeder\n"
|
||||
"besuchte Slot einen Vergleich kostet. Die Verkettung sucht nur in der\n"
|
||||
"jeweiligen Kette (im Schnitt α/2 Vergleiche pro Suche)."
|
||||
)
|
||||
|
||||
print("\n=== b) α = 0,9 ===")
|
||||
print(
|
||||
"Die offene Adressierung verschlechtert sich drastisch: 1/(1−0,9) = 10\n"
|
||||
"Sondierungen im Schnitt – das ist im Plot klar erkennbar. Bei der\n"
|
||||
"Verkettung wächst die mittlere Kettenlänge auf α = 0,9, also ca. 1 Extra-\n"
|
||||
"Vergleich. Der Nachteil der offenen Adressierung wird bei hohem α dominant."
|
||||
)
|
||||
|
||||
print("\n=== c) Systemvorteile ===")
|
||||
print(
|
||||
"Verkettung: Keine Kapazitätsgrenze; einfacheres Löschen (kein\n"
|
||||
" Tombstone); gut parallelisierbar (eine Kette pro Slot).\n"
|
||||
"Offene Adressierung: Besseres Cache-Verhalten (alle Einträge im\n"
|
||||
" zusammenhängenden Array, keine Pointer-Indirektion);\n"
|
||||
" geringerer Speicherverbrauch (keine Listenknoten)."
|
||||
)
|
||||
@@ -0,0 +1,89 @@
|
||||
import time
|
||||
import sys
|
||||
import os
|
||||
|
||||
from vorlesung.L08_graphen.graph import AdjacencyListGraph
|
||||
|
||||
GRID = [
|
||||
"Sabqponm",
|
||||
"abcryxxl",
|
||||
"accszExk",
|
||||
"acctuvwj",
|
||||
"abdefghi",
|
||||
]
|
||||
|
||||
def height(ch):
|
||||
if ch == 'S': return ord('a')
|
||||
if ch == 'E': return ord('z')
|
||||
return ord(ch)
|
||||
|
||||
def build_graph(grid):
|
||||
rows = len(grid)
|
||||
cols = len(grid[0])
|
||||
g = AdjacencyListGraph()
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
g.insert_vertex(f"{r}_{c}")
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
for dr, dc in [(-1, 0), (1, 0), (0, -1), (0, 1)]:
|
||||
nr, nc = r + dr, c + dc
|
||||
if 0 <= nr < rows and 0 <= nc < cols:
|
||||
if height(grid[nr][nc]) - height(grid[r][c]) <= 1:
|
||||
g.connect(f"{r}_{c}", f"{nr}_{nc}")
|
||||
return g, rows, cols
|
||||
|
||||
def find_start_end(grid):
|
||||
start = end = None
|
||||
for r, row in enumerate(grid):
|
||||
for c, ch in enumerate(row):
|
||||
if ch == 'S': start = f"{r}_{c}"
|
||||
if ch == 'E': end = f"{r}_{c}"
|
||||
return start, end
|
||||
|
||||
def solve(grid, label="Beispielgelände"):
|
||||
g, rows, cols = build_graph(grid)
|
||||
start, end = find_start_end(grid)
|
||||
|
||||
t0 = time.perf_counter()
|
||||
dist_map, pred_map = g.bfs(start)
|
||||
t1 = time.perf_counter()
|
||||
|
||||
pfad = g.path(end, pred_map)
|
||||
|
||||
def fmt(node):
|
||||
r, c = node.split("_")
|
||||
return f"({r},{c})"
|
||||
|
||||
print(f"=== {label} ===")
|
||||
print(f"|V| = {rows * cols}, |E| <= {rows * cols * 4} (real |E| gezählt separat)")
|
||||
print(f"Pfadlänge: {len(pfad) - 1} Schritte")
|
||||
print(f"Pfad: {' -> '.join(fmt(n) for n in pfad)}")
|
||||
print(f"BFS-Laufzeit: {(t1 - t0) * 1000:.4f} ms")
|
||||
print()
|
||||
return rows * cols, t1 - t0
|
||||
|
||||
# --- a) Modellierung ---------------------------------------------------------
|
||||
# Adjazenzliste ist besser geeignet: Das Grid ist licht (sparse).
|
||||
# Jeder Knoten hat maximal 4 Nachbarn => |E| <= 4|V|.
|
||||
# Eine Adjazenzmatrix hätte |V|^2 Einträge; für ein 41x122-Grid wären das
|
||||
# über 25 Millionen Einträge, obwohl nur ~4*|V| davon belegt wären.
|
||||
|
||||
# --- b) Pfadsuche -------------------------------------------------------------
|
||||
v_small, t_small = solve(GRID, "Beispielgelände (8x5)")
|
||||
|
||||
# --- c) AoC-Karte (optional) --------------------------------------------------
|
||||
if len(sys.argv) > 1 and os.path.exists(sys.argv[1]):
|
||||
with open(sys.argv[1]) as f:
|
||||
aoc_grid = [line.rstrip() for line in f if line.strip()]
|
||||
v_large, t_large = solve(aoc_grid, f"AoC-Karte ({len(aoc_grid)}x{len(aoc_grid[0])})")
|
||||
ratio_v = v_large / v_small
|
||||
ratio_t = t_large / t_small if t_small > 0 else float('inf')
|
||||
print(f"Verhältnis |V|: {ratio_v:.1f}x")
|
||||
print(f"Verhältnis Laufzeit: {ratio_t:.1f}x")
|
||||
print(f"Lineares Wachstum bestätigt: {'ja' if abs(ratio_t / ratio_v - 1) < 0.5 else 'nein'}")
|
||||
|
||||
# --- d) Optional: bester Startpunkt (AoC Teil 2) ------------------------------
|
||||
# Hinweis: Statt BFS von jedem 'a'-Feld zu starten, dreht man den Graphen um
|
||||
# und startet eine einzige BFS von E rückwärts. Der erste erreichte 'a'-Knoten
|
||||
# liefert den kürzesten Pfad.
|
||||
@@ -0,0 +1,55 @@
|
||||
from vorlesung.L08_graphen.graph import AdjacencyListGraph
|
||||
|
||||
DEPENDENCIES = {
|
||||
"main": ["parser", "codegen"],
|
||||
"parser": ["lexer", "ast"],
|
||||
"codegen": ["ast", "optimizer"],
|
||||
"optimizer": ["utils"],
|
||||
"ast": ["utils"],
|
||||
"lexer": [],
|
||||
"utils": [],
|
||||
}
|
||||
|
||||
# --- a) Graph aufbauen -------------------------------------------------------
|
||||
# Kantendefinition: A -> B bedeutet "A muss vor B gebaut werden" (B hängt von A ab).
|
||||
# Kanten zeigen also von der Voraussetzung zum Abhängigen – nur so liefert
|
||||
# die absteigende Sortierung nach leave_map die korrekte Baureihenfolge.
|
||||
g = AdjacencyListGraph()
|
||||
for module in DEPENDENCIES:
|
||||
g.insert_vertex(module)
|
||||
for module, deps in DEPENDENCIES.items():
|
||||
for dep in deps:
|
||||
g.connect(dep, module) # dep -> module: dep muss vor module gebaut werden
|
||||
|
||||
g.graph("BuildDependencies")
|
||||
|
||||
# --- b) DFS ausführen --------------------------------------------------------
|
||||
enter_map, leave_map, pred_map = g.dfs()
|
||||
|
||||
print("Zeitmarken (DFS):")
|
||||
for v in sorted(enter_map, key=lambda v: enter_map[v]):
|
||||
print(f" {v.value:12s} begin={enter_map[v]:2d} end={leave_map[v]:2d}")
|
||||
print()
|
||||
|
||||
# --- c) Topologische Sortierung ----------------------------------------------
|
||||
sorted_vertices = sorted(leave_map, key=lambda v: leave_map[v], reverse=True)
|
||||
reihenfolge = [v.value for v in sorted_vertices]
|
||||
print("Topologische Reihenfolge (Build-Reihenfolge):")
|
||||
print(" " + " -> ".join(reihenfolge))
|
||||
print()
|
||||
|
||||
# Überprüfung: Für jede Abhängigkeit A->B muss A nach B in der Liste kommen.
|
||||
pos = {name: i for i, name in enumerate(reihenfolge)}
|
||||
ok = all(pos[dep] < pos[module]
|
||||
for module, deps in DEPENDENCIES.items()
|
||||
for dep in deps)
|
||||
print(f"Alle Abhängigkeiten eingehalten: {ok}")
|
||||
print()
|
||||
|
||||
# --- d) Komplexität ----------------------------------------------------------
|
||||
v_count = len(list(g.all_vertices()))
|
||||
e_count = sum(len(g.get_adjacent_vertices(v.value)) for v in g.all_vertices())
|
||||
print(f"|V| = {v_count}, |E| = {e_count}")
|
||||
# Jeder Knoten wird genau einmal grau (begin) und einmal schwarz (end) => O(|V|)
|
||||
# Jede Kante wird beim ersten Besuch des Ausgangsknotens genau einmal betrachtet => O(|E|)
|
||||
# Zusammen: O(|V| + |E|)
|
||||
@@ -0,0 +1,84 @@
|
||||
import time
|
||||
from vorlesung.L08_graphen.graph import AdjacencyListGraph
|
||||
|
||||
# AoC 2024, Tag 16 – erstes Beispiel-Labyrinth (Antwort: 7036)
|
||||
GRID = """\
|
||||
###############
|
||||
#.......#....E#
|
||||
#.#.###.#.###.#
|
||||
#.....#.#...#.#
|
||||
#.###.#####.#.#
|
||||
#.#.#.......#.#
|
||||
#.#.#####.###.#
|
||||
#...........#.#
|
||||
###.#.#####.#.#
|
||||
#...#.....#.#.#
|
||||
#.#.#.###.#.#.#
|
||||
#.....#...#.#.#
|
||||
#.###.#.#.#.#.#
|
||||
#S..#.....#...#
|
||||
###############""".strip().split('\n')
|
||||
|
||||
# Richtungen: 0=N, 1=O, 2=S, 3=W (O = Ost = Startrichtung des Rentiers)
|
||||
DR = [-1, 0, 1, 0]
|
||||
DC = [0, 1, 0, -1]
|
||||
|
||||
|
||||
def build_graph(grid):
|
||||
g = AdjacencyListGraph()
|
||||
rows, cols = len(grid), len(grid[0])
|
||||
start = end = None
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if grid[r][c] == '#':
|
||||
continue
|
||||
if grid[r][c] == 'S':
|
||||
start = (r, c)
|
||||
elif grid[r][c] == 'E':
|
||||
end = (r, c)
|
||||
for d in range(4):
|
||||
g.insert_vertex(f"{r},{c},{d}")
|
||||
|
||||
for r in range(rows):
|
||||
for c in range(cols):
|
||||
if grid[r][c] == '#':
|
||||
continue
|
||||
for d in range(4):
|
||||
src = f"{r},{c},{d}"
|
||||
# Vorwärtsbewegen (Kosten 1)
|
||||
nr, nc = r + DR[d], c + DC[d]
|
||||
if 0 <= nr < rows and 0 <= nc < cols and grid[nr][nc] != '#':
|
||||
g.connect(src, f"{nr},{nc},{d}", 1)
|
||||
# Drehen links und rechts (Kosten 1000 je Drehung)
|
||||
g.connect(src, f"{r},{c},{(d - 1) % 4}", 1000)
|
||||
g.connect(src, f"{r},{c},{(d + 1) % 4}", 1000)
|
||||
|
||||
return g, start, end
|
||||
|
||||
|
||||
# --- a) + b) Graph aufbauen und Dijkstra ausführen ---
|
||||
|
||||
g, start, end = build_graph(GRID)
|
||||
v_count = len(list(g.all_vertices()))
|
||||
e_count = len(g.all_edges())
|
||||
print(f"Graph: |V| = {v_count}, |E| = {e_count}")
|
||||
|
||||
start_state = f"{start[0]},{start[1]},1" # Startposition, Blickrichtung Ost (1)
|
||||
dist_map, pred_map = g.dijkstra(start_state)
|
||||
|
||||
# Bestes Zielfeld: E in beliebiger Richtung (4 zulässige Zielzustände)
|
||||
end_vertices = [g.get_vertex(f"{end[0]},{end[1]},{d}") for d in range(4)]
|
||||
best_end = min(end_vertices, key=lambda v: dist_map[v])
|
||||
print(f"Gesamtkosten: {dist_map[best_end]}") # Erwartung: 11048
|
||||
|
||||
pfad = g.path(best_end.value, pred_map)
|
||||
print(f"Pfad: {len(pfad)} Zustände ({len(pfad) - 1} Schritte)")
|
||||
print("Erste Schritte:", " -> ".join(pfad[:5]), "...")
|
||||
print("Letzte Schritte: ...", " -> ".join(pfad[-3:]))
|
||||
|
||||
# --- c) Laufzeitmessung ---
|
||||
t0 = time.perf_counter()
|
||||
g.dijkstra(start_state)
|
||||
t1 = time.perf_counter()
|
||||
print(f"\nLaufzeit (Beispiel): {(t1 - t0) * 1000:.3f} ms")
|
||||
@@ -0,0 +1,82 @@
|
||||
from vorlesung.L08_graphen.graph import AdjacencyListGraph
|
||||
|
||||
# Korrigierte Kantenliste (Vorlesungsbeispiel).
|
||||
# Hinweis: Das Arbeitsblatt enthält c->b:-2, c->e:5, d->c:-5, e->c:4 –
|
||||
# diese Werte erzeugen einen negativen Zyklus (b->c->b: 1-2=-1) im
|
||||
# Basisgraphen und müssen in der Aufgabenstellung korrigiert werden.
|
||||
# Korrekte Werte (kein Vorzyklus, Ergebnis: b=7, c=2, d=4, e=6):
|
||||
EDGES = [
|
||||
('a', 'b', 9),
|
||||
('a', 'd', 4),
|
||||
('b', 'c', 1),
|
||||
('b', 'd', 2),
|
||||
('c', 'b', 5), # war -2 im Entwurf
|
||||
('c', 'e', 4), # war 5 im Entwurf
|
||||
('d', 'c', -2), # war -5 im Entwurf
|
||||
('d', 'e', 2),
|
||||
]
|
||||
|
||||
|
||||
def bellman_ford(graph, start_name):
|
||||
"""Bellman-Ford: kürzeste Wege mit Erkennung negativer Zyklen.
|
||||
|
||||
Gibt (distance_map, predecessor_map, has_negative_cycle) zurück.
|
||||
|V|-1 Relaxationsdurchläufe über alle Kanten, dann ein Prüfdurchlauf.
|
||||
"""
|
||||
distance_map = {}
|
||||
predecessor_map = {}
|
||||
for v in graph.all_vertices():
|
||||
distance_map[v] = float('inf')
|
||||
predecessor_map[v] = None
|
||||
|
||||
start = graph.get_vertex(start_name)
|
||||
distance_map[start] = 0
|
||||
|
||||
n = len(list(graph.all_vertices()))
|
||||
for _ in range(n - 1):
|
||||
for src_name, dst_name, weight in graph.all_edges():
|
||||
src = graph.get_vertex(src_name)
|
||||
dst = graph.get_vertex(dst_name)
|
||||
if distance_map[src] + weight < distance_map[dst]:
|
||||
distance_map[dst] = distance_map[src] + weight
|
||||
predecessor_map[dst] = src
|
||||
|
||||
# Prüfdurchlauf: jede weitere Verbesserung zeigt einen negativen Zyklus
|
||||
has_negative_cycle = False
|
||||
for src_name, dst_name, weight in graph.all_edges():
|
||||
src = graph.get_vertex(src_name)
|
||||
dst = graph.get_vertex(dst_name)
|
||||
if distance_map[src] + weight < distance_map[dst]:
|
||||
has_negative_cycle = True
|
||||
break
|
||||
|
||||
return distance_map, predecessor_map, has_negative_cycle
|
||||
|
||||
|
||||
# --- a) + b) Basisgraph ---
|
||||
|
||||
g = AdjacencyListGraph()
|
||||
for v in ['a', 'b', 'c', 'd', 'e']:
|
||||
g.insert_vertex(v)
|
||||
for src, dst, w in EDGES:
|
||||
g.connect(src, dst, w)
|
||||
|
||||
dist, pred, neg_cycle = bellman_ford(g, 'a')
|
||||
print(f"Negativer Zyklus: {neg_cycle}")
|
||||
print("Kürzeste Distanzen:")
|
||||
for name in ['a', 'b', 'c', 'd', 'e']:
|
||||
v = g.get_vertex(name)
|
||||
pfad = g.path(name, pred)
|
||||
print(f" {name}: d={dist[v]}, Pfad: {' -> '.join(pfad)}")
|
||||
|
||||
# --- d) Komplexität ---
|
||||
v_count = len(list(g.all_vertices()))
|
||||
e_count = len(g.all_edges())
|
||||
print(f"\n|V| = {v_count}, |E| = {e_count}")
|
||||
|
||||
# --- c) Negativer Zyklus ---
|
||||
print("\n--- Mit negativem Zyklus (e -> b, Gewicht -8) ---")
|
||||
g.connect('e', 'b', -8)
|
||||
dist2, pred2, neg_cycle2 = bellman_ford(g, 'a')
|
||||
print(f"Negativer Zyklus: {neg_cycle2}")
|
||||
# Betroffener Zyklus: b -> c -> e -> b (1 + 4 + (-8) = -3 < 0)
|
||||
@@ -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)
|
||||
@@ -127,7 +127,7 @@ class Graph:
|
||||
predecessor_map[vertex] = None
|
||||
|
||||
while white_vertices:
|
||||
v = white_vertices.pop()
|
||||
v = next(iter(white_vertices))
|
||||
dfs_visit(v)
|
||||
|
||||
return enter_map, leave_map, predecessor_map
|
||||
|
||||
Reference in new issue
Block a user