Answer to Question 1
a)
1. log^2(n)
2. 3 * log(n)
3. n * log(n)
4. 3 * sqrt(n)
5. n!, da dies die größte Wachstumsrate hat
6. n^2 / log(n)
7. n^n

b) 
Die Funktion foo(n) hat eine Laufzeit von O(n^2), da die while-Schleife n-mal durchlaufen wird und in jeder Iteration die for-Schleife mit n Durchläufen ausgeführt wird.

c)
1. O(n + m)
2. O(log(n))
3. O(n)

d)
1. Eine Hashtabelle ist geeignet, da sie schnelles Einfügen und Zugriff auf den besten bisherigen Versuch ermöglicht. Relevant sind dabei die Operationen insert und lookup.
2. Ein (2, 3)-Baum ist geeignet, da er Ordnung und Suche in logarithmischer Zeit ermöglicht. Relevant sind die Operationen insert und search.
3. Eine dynamische Liste ist geeignet, da sie das Einfügen und Entfernen am Anfang oder Ende des Turms ermöglicht. Relevant sind insert und delete Operationen.





****************************************************************************************
****************************************************************************************




Answer to Question 2
a) Um die Zustände 2, 3 und 4 des Heaps zu zeichnen, müsste ich die gegebenen Operationen Schritt für Schritt durchführen und dabei den Heap aktualisieren. Da leider die Figuren nicht angezeigt werden können, kann ich den genauen Ablauf der Veränderungen nicht beschreiben.

b) Das Array A, das den Min-Heap in Zustand 1 repräsentiert, hätte folgende Form:
A = [4, 7, 8, 10, 9, 15, 13]

c) Pseudocode für reduceToLargerHalf(H):
```
reduceToLargerHalf(H):
  n = H.size()
  for i from 1 to (n/2):
    print(H.removeMin())
```

d) Zu zeigen oder zu widerlegen, dass für jedes n ∈ ℕ eine Folge von n insert-Operationen existiert, welche jeweils eine Laufzeit von Θ(1) haben: 
Dieses Statement ist falsch. 
Da ein Heap eine spezielle Sortierung der Elemente aufrechterhalten muss, ist es im Allgemeinen nicht möglich, eine O(1)-Einfügeoperation zu gewährleisten. Die Einfügeoperation in einem Heap hat normalerweise eine Laufzeit von O(log n), da die Heap-Eigenschaften aufrechterhalten werden müssen.





****************************************************************************************
****************************************************************************************




Answer to Question 3
### Antwort

#### a) Reihenfolge der Entnahme aus der Priority-Queue und markierte Kanten:

Die Reihenfolge, in der die Knoten aus der Priority-Queue entnommen werden, ist: A, D, B, E, C, F.

Die markierten Kanten des kürzeste-Wege-Baums sind wie folgt:
- \( A \rightarrow D \)
- \( A \rightarrow B \)
- \( B \rightarrow E \)
- \( D \rightarrow C \)
- \( E \rightarrow F \)

#### b) Fehlerhafte Einträge im Distanzarray:

1. Fehlerhafter Eintrag für Knoten A:
   - **Begründung**: Der Eintrag ist falsch, weil die Distanz des Startknotens oft mit 0 initialisiert wird und nicht mit 2.
   
2. Fehlerhafter Eintrag für Knoten C:
   - **Begründung**: Der Eintrag ist falsch, da die Distanz, die durch den markierten Eintrag angegeben ist, nicht die kürzeste Entfernung zum Startknoten ist.

#### c) Graphen für \(\boldsymbol{n > 3}\) mit \(\boldsymbol{s}\) und \(\boldsymbol{t}\):

Für jedes \(n > 3\) können wir einen Graphen mit \(n\) Knoten und den Knoten \(s\) und \(t\) angeben, sodass sich bei der Ausführung von Dijkstras Algorithmus mit Startknoten \(s\) die Länge des aktuell bekannten kürzesten Weges von \(s\) zu \(t\) insgesamt \(\Theta(n)\) Mal ändert. Ein solcher Graph wäre beispielsweise ein gerichteter Kreis mit \(n\) Knoten, wobei \(s\) Startknoten und \(t\) Endknoten des Kreises sind. Jeder Knoten hätte eine Kante zu seinem direkten Nachfolger im Uhrzeigersinn. Bei der Entfernung von jedem zweiten Knoten würde der kürzeste Weg sich ändern, was insgesamt \(\Theta(n)\) Änderungen ergibt.





****************************************************************************************
****************************************************************************************




Answer to Question 4
a) Um die Via-Knoten im gegebenen Graphen zu markieren, müssen wir alle Knoten finden, die eine Distanz von 1 zu s und eine Distanz von 3 zu t haben. Nach Überprüfung des Grafen in der Datei "passender-knoten.pdf" sind die Via-Knoten: b und d.

b) Um Knoten f zu einem Via-Knoten zu machen, müssen wir ds und dt so wählen, dass dist(s, f) = ds = 1 und dist(f, t) = dt = 3. Das bedeutet, dass f eine Distanz von 1 zu s und eine Distanz von 3 zu t haben muss.

c) 
1. Diese Aussage ist wahr. Wenn die Summe von ds und dt kleiner als die direkte Distanz von s zu t ist, können keine Via-Knoten existieren, da es unmöglich ist, in kürzerer Zeit von s nach t zu gelangen, als die direkte Distanz erfordert.
2. Diese Aussage ist falsch. Es könnten mehrere Via-Knoten existieren, wenn ds + dt gleich der direkten Distanz von s zu t ist.
3. Diese Aussage ist falsch. Nur weil dt kleiner oder gleich der Distanz von s zu t ist, bedeutet nicht, dass alle Via-Knoten auf dem kürzesten s-t-Pfad liegen.

d) Um in einem gerichteten Graphen mit n Knoten und m Kanten die Distanz von allen Knoten zu einem gegebenen Knoten x in O(n + m) Zeit zu berechnen, kann man den Breitensuche-Algorithmus (BFS) verwenden. Man startet die BFS von Knoten x aus und speichert die Distanz zu jedem erreichten Knoten.

e) Ein Algorithmus, um alle Via-Knoten auszugeben, könnte folgendermaßen funktionieren:
1. Führe eine Breitensuche von Knoten s aus, um die Distanzen von s zu allen anderen Knoten im Graphen zu berechnen.
2. Führe eine Umgekehrte Breitensuche von Knoten t aus, um die Distanzen von t zu allen anderen Knoten im Graphen zu berechnen.
3. Überprüfe für jeden Knoten v im Graphen, ob dist(s, v) = ds und dist(v, t) = dt gilt. Wenn ja, dann handelt es sich um einen Via-Knoten.
Dieser Algorithmus hat eine Laufzeit von O(n + m), da sowohl die Breitensuche von s als auch die Umgekehrte Breitensuche von t jede Kante und jeden Knoten höchstens einmal besuchen, was insgesamt O(n + m) Operationen benötigt.





****************************************************************************************
****************************************************************************************




Answer to Question 5
a) Alice hat eine Gewinnstrategie, wenn das Spiel mit den Steinen \(\langle 1, 2, 3, 4, 5, 6, 7, 8, 9, 2 \rangle\) beginnt. 

b) Wenn Alice für jedes \(i < n\) weiß, ob es bei \(i\) verbleibenden Steinen eine Gewinnstrategie gibt, kann sie entscheiden, ob es bei \(n\) verbleibenden Steinen eine Gewinnstrategie gibt, indem sie prüft, ob es eine Möglichkeit gibt, die den Zug auf eine Position \(i < n\) führt, bei der der Gegner keine Gewinnstrategie hat, bzw. sicher verliert.

c) Die Rekurrenz zur Berechnung des Arrays X lautet:
\[X[i] = \neg (X[i-a_1] \land X[i-a_2] \land \ldots \land X[i-a_k])\]
mit den Basisfällen \(X[0] = \text{false}\) und \(X[j] = \text{true}\) für alle \(j \in \{a_1, a_2, \ldots, a_n\}\).

d) Pseudocode für den Algorithmus:

```
aliceCanWin(A):
    n = length(A)
    X = new Array of size n+1
    X[0] = false
    for j from 1 to n:
        X[j] = true if j is in A, false otherwise
    for i from 1 to n:
        X[i] = not (X[i-A[1]] and X[i-A[2]] and ... and X[i-A[k]])
    return X[n]
```

In diesem Algorithmus wird über die Elemente der Eingabeliste iteriert, um das Array X korrekt zu füllen und am Ende wird der Wert von \(X[n]\) zurückgegeben, um festzustellen, ob Alice eine Gewinnstrategie hat.






****************************************************************************************
****************************************************************************************




Answer to Question 6
### Subquestion a:
Um den Zustand der Datenstruktur nach den beschriebenen Operationen zu zeichnen, würde ich folgendermaßen vorgehen:
1. Zuerst zeichne ich den Zustand 2 gemäß der gegebenen Datenstruktur und markiere die betroffenen Personen (zum Beispiel C6 als Person p).
2. Dann führe ich die Operationen help(), appease() und skip(C6, 2) entsprechend aus und aktualisiere den Zustand der Datenstruktur.
3. Anschließend zeichne ich den Zustand 3 mit den veränderten Positionen der Personen und markiere die verärgerten Personen.
4. Danach wiederhole ich den Vorgang für die Operationen queue(C8), skip(C8, 2) und skip(C8, 1) vom Zustand 3 aus und zeichne entsprechend den Zustand 4.

### Subquestion b:
Um eine Sequenz von Operationen anzugeben, die die genannten Kriterien erfüllen, könnte eine mögliche Sequenz wie folgt aussehen:
1. queue(C1) - füge eine Person hinzu
2. skip(C1, 1) - überspringe eine Person
3. queue(C2) - füge eine Person hinzu
4. skip(C2, 1) - überspringe eine Person
5. ...
Dies könnte so fortgesetzt werden, bis die Bedingungen erfüllt sind. Dies zeigt, dass es möglich ist, eine Sequenz von Operationen mit den angegebenen Eigenschaften zu erstellen.

### Subquestion c:
Um zu zeigen, dass jede Operation amortisierte konstante Kosten hat, würde ich für jede Operation eine Kostenanalyse durchführen und zeigen, dass die Gesamtkosten für eine Serie von Operationen eine obere Schranke besitzen, die konstant ist. Dies könnte durch eine potenzielle Methode wie das Banker's Queueing Model erreicht werden.





****************************************************************************************
****************************************************************************************




