Lerninhalte finden
Features
Entdecke
© StudySmarter 2024, all rights reserved.
Innerhalb dieser Aufgabe betrachten wir Greedy-Algorithmen, die in jedem Schritt die lokal beste Wahl treffen, um ein global optimales Ergebnis zu erreichen. Obwohl solche Algorithmen effizient sind, ist die globale Optimalität oft nicht gesichert. Anwendungen umfassen Prim's Algorithmus, Dijkstra und Kruskal, welche zur Lösung von verschiedenen Problemen verwendet werden können.
(a) Erläutere das Prinzip der Greedy-Algorithmen und beschreibe eine Situation in der Praxis, in der ein Greedy-Algorithmus eine optimale Lösung findet. Begründe, warum die lokale Entscheidung in dieser Situation zu einer global optimalen Lösung führt.
Lösung:
Prinzip der Greedy-Algorithmen:
(b) Gegeben sei ein ungerichteter Graph G = (V, E) mit positiven Kantengewichten. Beschreibe detailliert, wie Prim's Algorithmus funktioniert, um den minimalen Spannbaum zu finden. Implementiere den Algorithmus in Python.
def prim(graph): # Deine Implementierung hier pass
Lösung:
Prim's Algorithmus zum Finden des minimalen Spannbaums:
Hier ist eine detaillierte Implementierung von Prim's Algorithmus in Python:
Erklärung des Codes:import heapq # Für die Priority Queue (Heap-Datenstruktur) verwendet
def prim(graph):
min_heap = []
start_node = list(graph.keys())[0]
for dest, weight in graph[start_node].items():
heapq.heappush(min_heap, (weight, start_node, dest))
visited = set([start_node])
mst = []
total_weight = 0
while min_heap:
weight, frm, to = heapq.heappop(min_heap)
if to not in visited:
visited.add(to)
mst.append((frm, to, weight))
total_weight += weight
for next_dest, next_weight in graph[to].items():
if next_dest not in visited:
heapq.heappush(min_heap, (next_weight, to, next_dest))
return mst, total_weight
graph
: Ein Dictionary, das den Graphen darstellt. Die Schlüssel sind Knoten, und die Werte sind Dictionaries, die die Nachbarn und die entsprechenden Kantengewichte repräsentieren.min_heap
: Eine Priority Queue (Heap), die verwendet wird, um die Kanten nach ihrem Gewicht zu sortieren.visited
: Eine Menge, die alle besuchten Knoten verfolgt, um Zyklen zu vermeiden.mst
: Eine Liste, die die Kanten des minimalen Spannbaums speichert.total_weight
: Die Gesamtsumme der Kantengewichte im minimalen Spannbaums.
(c) Analysiere die Laufzeit von Dijkstra's Algorithmus. Welchen Einfluss hat die Wahl der Datenstrukturen auf die Komplexität des Algorithmus? Vergleiche die Komplexität bei Verwendung von binären Heaps und Fibonacci Heaps.
Lösung:
Laufzeitanalyse von Dijkstra's Algorithmus:
Verwendung von Binären Heaps:
Verwendung von Fibonacci Heaps:
Zusammenfassung:
(d) Für den Graphen G = (V, E) mit den Knoten V = {1, 2, 3, 4, 5} und den Kanten E = {(1,2,2), (1,3,3), (2,3,1), (2,4,4), (3,4,2), (4,5,3)} in der Form (u, v, w) wobei u und v die Knoten und w das Kantengewicht ist, wende Kruskal's Algorithmus an, um den minimalen Spannbaum zu finden. Berechne die Gesamtkosten des resultierenden Spannbaums und zeichne den minimalen Spannbaum grafisch.
Lösung:
Anwendung von Kruskal's Algorithmus:
Für den gegebenen Graphen:
1. Sortiere die Kanten nach ihrem Gewicht:
2. Füge die Kanten in den Spannbaum ein, ohne Zyklen zu erzeugen:
Endgültige Kosten und Kanten des minimalen Spannbaums:
Grafische Darstellung:
Zu einer Zeichnung wie unten dargestellt:
1 -- 2 -- 3 \ \ | 3 | 4 -- 5
Betrachte das klassische Rucksackproblem (Knapsack-Problem), bei dem Du eine Menge von Gegenständen mit jeweils einem Gewicht und einem Wert gegeben hast und einen Rucksack mit einer maximalen Gewichtskapazität hast. Ziel ist es, den maximal möglichen Wert zu ermitteln, den Du in den Rucksack packen kannst, ohne die Kapazität zu überschreiten.
a) Formuliere das Rucksackproblem als ein dynamisches Programmierungsproblem. Definiere die rekursive Formel unter Berücksichtigung von Memoisierung. Schreibe die Pseudocode-Implementierung der rekursiven Top-Down-Lösung mit Memoisierung.
Lösung:
Das Rucksackproblem kann als dynamisches Programmierungsproblem formuliert werden. Hierbei wird das Problem mit Hilfe einer rekursiven Formel gelöst, die die Entscheidungen hinsichtlich der Aufnahme oder Nichtaufnahme eines Gegenstandes berücksichtigt. Memoisierung hilft dabei, bereits berechnete Werte zu speichern und so die Berechnungszeit zu verringern.
Rekursive Formel:
Sei dp[i][w] der maximale Wert, den wir mit den ersten i Gegenständen und einer maximalen Gewichtskapazität w erreichen können. Die rekursive Formel lautet dann:
Pseudocode-Implementierung der rekursiven Top-Down-Lösung mit Memoisierung:
def knapsack(values, weights, W): # Anzahl der Gegenstände n = len(values) # Memoisierungs-Tabelle memo = [[-1 for _ in range(W + 1)] for _ in range(n + 1)] def dp(i, w): # Basisfall if i == 0 or w == 0: return 0 # Wert wurde schon berechnet if memo[i][w] != -1: return memo[i][w] # Gewicht des aktuellen Gegenstandes weight = weights[i-1] value = values[i-1] # Wenn der Gegenstand nicht mit einbezogen wird if weight > w: memo[i][w] = dp(i-1, w) else: # Maximales Wert auswählen memo[i][w] = max(dp(i-1, w), value + dp(i-1, w - weight)) return memo[i][w] return dp(n, W)# Beispielaufruf:values = [60, 100, 120]weights = [10, 20, 30]W = 50print(knapsack(values, weights, W)) # Ausgabe: 220
In diesem Pseudocode umfasst die Rekursivfunktion dp(i, w) die oben beschriebene rekursive Formel und verwendet eine Memoisierungstabelle memo zur Speicherung der Zwischenergebnisse.
b) Erstelle eine Tabelle zur Lösung des Rucksackproblems mit der Bottom-Up-Methode und erkläre den Tabellenausfüllprozess. Zeige Schritt für Schritt, wie die Tabelle für eine gegebene Menge von Gegenständen und einer bestimmten Rucksackkapazität ausgefüllt wird.
Lösung:
Die Bottom-Up-Methode zur Lösung des Rucksackproblems ist ein dynamisches Programmierungsansatz, bei dem wir eine Tabelle verwenden, um die Zwischenlösungen zu speichern und schrittweise das Endergebnis zu ermitteln.
Erklärung des Tabellenausfüllprozesses:
Wir erstellen eine Tabelle dp mit den Dimensionen (n + 1) x (W + 1), wobei n die Anzahl der Gegenstände und W die maximale Gewichtskapazität des Rucksacks ist. Das Zellenelement dp[i][w] repräsentiert den maximalen Wert, der mit den ersten i Gegenständen und einer maximalen Gewichtskapazität w erreicht werden kann.
Die Schritte zum Ausfüllen der Tabelle sind:
Hier ist ein Beispiel, um den Tabellenausfüllprozess Schritt für Schritt zu veranschaulichen:
Angenommen, wir haben drei Gegenstände mit folgenden Werten und Gewichten:
Schritt-für-Schritt-Ausfüllen der Tabelle:
values = [60, 100, 120]weights = [10, 20, 30]W = 50n = len(values)dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)]for i in range(1, n + 1): for w in range(1, W + 1): if weights[i-1] > w: dp[i][w] = dp[i-1][w] else: dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w-weights[i-1]])
Schauen wir uns an, wie die Tabelle iterativ gefüllt wird:
Initiale Tabelle:dp = [ [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 0]]Schritt 1 - Andocken des ersten Gegenstandes:Für i=1, w=1 bis 50:dp = [ [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 60, 60, ..., 60], [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 0]]Schritt 2 - Andocken des zweiten Gegenstandes:Für i=2, w=1 bis 50:dp = [ [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 60, 60, ..., 60], [0, 0, 0, 0, 0, ..., 100, 100, ..., 160, 160, ..., 160], [0, 0, 0, 0, 0, ..., 0]]Schritt 3 - Andocken des dritten Gegenstandes:Für i=3, w=1 bis 50:dp = [ [0, 0, 0, 0, 0, ..., 0], [0, 0, 0, 0, 0, ..., 60, 60, ..., 60], [0, 0, 0, 0, 0, ..., 100, 100, ..., 160, 160, ..., 160], [0, 0, 0, 0, 0, ..., 100, 120, ..., 180, 220, ..., 220, 220]]
Das Endergebnis befindet sich in dp[3][50], was 220 ist.
Dieser Prozess zeigt, wie die Tabelle systematisch gefüllt wird, um den maximalen erreichbaren Wert zu bestimmen.
c) Analysiere die Zeit- und Platzkomplexität der Top-Down- und Bottom-Up-Ansätze für das Rucksackproblem. Diskutiere, warum dynamische Programmierung effizienter als naive Ansätze ist.
Lösung:
Die Analyse der Zeit- und Platzkomplexität der Top-Down- und Bottom-Up-Ansätze für das Rucksackproblem zeigt, warum dynamische Programmierung (DP) effizienter als naive Ansätze ist.
Top-Down-Ansatz (mit Memoisierung):
Bottom-Up-Ansatz:
Vergleich mit naiven Ansätzen:
Warum dynamische Programmierung effizienter ist:
Fazit: Die Top-Down- und Bottom-Up-Ansätze der dynamischen Programmierung sind beide signifikant effizienter als naive Ansätze in Bezug auf Zeit- und Platzkomplexität, was sie zu bevorzugten Methoden für die Lösung des Rucksackproblems macht.
d) Implementiere einen Algorithmus (in Python oder einer anderen Programmiersprache Deiner Wahl) zur Lösung des Rucksackproblems mit der Bottom-Up-Methode. Verwende die definierte Formel aus Teil b) und zeige den vollständigen Code.
Lösung:
Um das Rucksackproblem mit der Bottom-Up-Methode zu lösen, können wir einen dynamischen Programmierungsansatz verwenden, wie in Teil b) besprochen. Hier ist eine Implementierung des Algorithmus in Python:
def knapsack_bottom_up(values, weights, W): # Anzahl der Gegenstände n = len(values) # Erstellen der dp-Tabelle dp = [[0 for _ in range(W + 1)] for _ in range(n + 1)] # Füllen der dp-Tabelle for i in range(1, n + 1): for w in range(1, W + 1): if weights[i-1] <= w: dp[i][w] = max(dp[i-1][w], values[i-1] + dp[i-1][w - weights[i-1]]) else: dp[i][w] = dp[i-1][w] # Der gesuchte Wert befindet sich in dp[n][W] return dp[n][W]# Beispielaufruf:values = [60, 100, 120]weights = [10, 20, 30]W = 50print(knapsack_bottom_up(values, weights, W)) # Ausgabe: 220
Erklärung des Codes:
Dieser Algorithmus implementiert die Bottom-Up-Methode effizient und löst das klassische Rucksackproblem in polynomialer Zeit.
Gegeben sei ein ungerichteter Graph G mit den Knoten V und Kanten E. Der Graph wird verwendet, um eine Netzwerkstruktur zu modellieren. Der Graph ist wie folgt dargestellt: Der Knoten V besteht aus den Punkten (A, B, C, D, E) und die Kanten E bestehen aus den Verbindungen [(A, B), (A, C), (B, D), (C, D), (D, E)]. Verwenden Sie diesen Graphen, um die folgenden Aufgaben zu lösen.
Erstelle eine Adjazenzmatrix für den gegebenen Graphen G. Welches sind die Eintragswerte in der Adjazenzmatrix und welche Bedeutung haben sie?
Lösung:
Um eine Adjazenzmatrix für den gegebenen ungerichteten Graphen G zu erstellen, befolge die folgenden Schritte:
Der Graph G hat die folgenden Kanten: [(A, B), (A, C), (B, D), (C, D), (D, E)].
Die Adjazenzmatrix sieht wie folgt aus:
A | B | C | D | E | |
---|---|---|---|---|---|
A | 0 | 1 | 1 | 0 | 0 |
B | 1 | 0 | 0 | 1 | 0 |
C | 1 | 0 | 0 | 1 | 0 |
D | 0 | 1 | 1 | 0 | 1 |
E | 0 | 0 | 0 | 1 | 0 |
Eintragswerte in der Adjazenzmatrix:
Bedeutung der Eintragswerte:
Implementiere den DFS-Algorithmus in Python, um alle Knoten des Graphen zu durchlaufen, beginnend bei Knoten A. Gib die Reihenfolge der besuchten Knoten an.
Lösung:
Um den DFS-Algorithmus (Depth-First Search) in Python zu implementieren und alle Knoten des gegebenen Graphen zu durchlaufen, beginne beim Knoten A und befolge die folgenden Schritte:
Hier ist der Python-Code zur Lösung der Aufgabe:
def dfs(graph, start, visited=None): if visited is None: visited = set() visited.add(start) print(start, end=' ') for neighbor in graph[start]: if neighbor not in visited: dfs(graph, neighbor, visited) return visited# Graph in Adjazenzlisten-Darstellunggraph = { 'A': ['B', 'C'], 'B': ['A', 'D'], 'C': ['A', 'D'], 'D': ['B', 'C', 'E'], 'E': ['D']}# DFS-Aufruf und Ausgabe der besuchten Reihenfolgeprint('DFS-Reihenfolge der besuchten Knoten:')dfs(graph, 'A')
Reihenfolge der besuchten Knoten:
Wenn Du diesen Code ausführst, erhältst Du die folgende Ausgabe (die Reihenfolge der besuchten Knoten):
DFS-Reihenfolge der besuchten Knoten:A B D C E
Hash-TabellenDatenstruktur zur schnellen Suche, Einfügen und Löschen von Elementen mithilfe einer Hash-Funktion.
Beschreibe die Funktionsweise einer Hash-Tabelle und erkläre, wie die Hash-Funktion zur Speicherung und zum Abruf von Daten beiträgt. Was ist die Bedeutung der Kollisionsbehandlung in diesem Kontext?
Lösung:
Funktionsweise einer Hash-Tabelle:
Eine Hash-Tabelle ist eine Datenstruktur, die entwickelt wurde, um schnelle Einfügungen, Löschungen und Suchvorgänge zu ermöglichen. Das Herzstück einer Hash-Tabelle ist die Hash-Funktion, die einen gegebenen Schlüssel in einen spezifischen Index umwandelt.
Bedeutung der Kollisionsbehandlung:
Kollisionsbehandlung ist von wesentlicher Bedeutung, um sicherzustellen, dass die Hash-Tabelle effektiv arbeiten kann und keine Daten verloren gehen oder überschrieben werden. Zwei gebräuchliche Methoden zur Behandlung von Kollisionen sind:
Die Wahl der richtigen Kollisionsbehandlungsmethode ist entscheidend für die Leistung der Hash-Tabelle. Obwohl die durchschnittliche Zeitkomplexität für Suchoperationen, Einfügungen und Löschungen \textit{O(1)} ist, kann die Leistung im schlimmsten Fall auf \textit{O(n)} anwachsen, wenn viele Kollisionen auftreten und nicht effektiv behandelt werden.
Angenommen, Du verwendest Verkettung zur Kollisionsbehandlung. Skizziere die Schritte, die zum Suchen eines Elements in einer Hash-Tabelle unter Verwendung dieser Methode erforderlich sind. Vergleiche die durchschnittliche und die schlechteste Laufzeitkomplexität dieses Verfahrens.
Lösung:
Schritte zum Suchen eines Elements in einer Hash-Tabelle unter Verwendung von Verkettung:
Ein konkretes Beispiel in Pseudocode:
function search(HashTable, key): index = hash_function(key) current_list = HashTable[index] for element in current_list: if element.key == key: return element return null
Vergleich der durchschnittlichen und der schlechtesten Laufzeitkomplexität:
Erläutere die Methode der offenen Adressierung zur Kollisionsbehandlung in Hash-Tabellen. Beschreibe dabei insbesondere die Vorgehensweise bei \textit{linearer Sondierung} (linear probing). Führe die Vor- und Nachteile gegenüber der Verkettung auf.
Lösung:
Offene Adressierung zur Kollisionsbehandlung in Hash-Tabellen:
Bei der offenen Adressierung wird ein kollidierendes Element nicht in einer Liste an einem bestimmten Array-Index gespeichert, sondern es wird nach einem alternativen Index im Array gesucht. Dies wird durch eine Probe- oder Suchstrategie bestimmt, und es gibt verschiedene Techniken, um diese alternativen Positionen zu bestimmen. Eine dieser Techniken ist die lineare Sondierung (linear probing).
Lineare Sondierung (linear probing):
Die lineare Sondierung ist eine Methode, bei der im Falle einer Kollision sequentiell nach dem nächsten freien Platz im Array gesucht wird. Das bedeutet, dass wenn der ursprüngliche Index durch die Hash-Funktion besetzt ist, der nächste zu überprüfende Index der nächste in Folge ist (d.h. der Index wird um 1 erhöht).
Ein einfaches Beispiel in Pseudocode für das Einfügen mit linearer Sondierung:
function insert(HashTable, key, value): index = hash_function(key) while HashTable[index] is not empty: index = (index + 1) % len(HashTable) HashTable[index] = (key, value)
Vor- und Nachteile der linearen Sondierung gegenüber der Verkettung:
Eine Hash-Tabelle hat eine Größe von 7, und die Hash-Funktion lautet h(x) = x % 7. Füge die Schlüssel 10, 20, 15, 7 und 14 ein. Zeichne die resultierende Hash-Tabelle, wenn Du lineare Sondierung zur Kollisionsbehandlung verwendest. Berechne die Belastung der Hash-Tabelle nach den Einfügungen.
Lösung:
Einfügen der Schlüssel 10, 20, 15, 7 und 14 in eine Hash-Tabelle der Größe 7 unter Verwendung von linearer Sondierung:
Gegebene Hash-Funktion: h(x) = x % 7
Starten wir mit einer leeren Hash-Tabelle:
| Index | Value ||--------|-------|| 0 | || 1 | || 2 | || 3 | || 4 | || 5 | || 6 | |
Einfügeoperationen:
| Index | Value ||--------|-------|| 0 | || 1 | || 2 | || 3 | 10 || 4 | || 5 | || 6 | |
| Index | Value ||--------|-------|| 0 | || 1 | || 2 | || 3 | 10 || 4 | || 5 | || 6 | 20 |
| Index | Value ||--------|-------|| 0 | || 1 | 15 || 2 | || 3 | 10 || 4 | || 5 | || 6 | 20 |
| Index | Value ||--------|-------|| 0 | 7 || 1 | 15 || 2 | || 3 | 10 || 4 | || 5 | || 6 | 20 |
| Index | Value ||--------|-------|| 0 | 7 || 1 | 15 || 2 | 14 || 3 | 10 || 4 | || 5 | || 6 | 20 |
Berechnung der Belastung der Hash-Tabelle:
Die Belastung wird als Verhältnis der Anzahl der gespeicherten Elemente zur Gesamtgröße der Hash-Tabelle berechnet.
Die Belastung ist daher:
\[\text{Belastung} = \frac{\text{Anzahl der gespeicherten Elemente}}{\text{Größe der Hash-Tabelle}} = \frac{5}{7} \approx 0.714\]
Die Hash-Tabelle ist also zu etwa 71.4% ausgelastet.
Mit unserer kostenlosen Lernplattform erhältst du Zugang zu Millionen von Dokumenten, Karteikarten und Unterlagen.
Kostenloses Konto erstellenDu hast bereits ein Konto? Anmelden