Prof. Dr. Peter Sanders G.V. Batz, C. Schulz, J. Speck Karlsruher Institut für Technologie Institut für Theoretische Informatik 2. Wiederholungsblatt zu Algorithmen I im SS 2010 http://algo2.iti.kit.edu/AlgorithmenI.php {sanders,batz,christian.schulz,speck}@kit.edu Musterlösungen Aufgabe 1 (Bellman-Ford, 4 Zusatzpunkte) Bestimmen Sie für folgenden Graphen den Baum der kürzesten Wege mit Startknoten s, und bestimmen Sie die Distanzen aller Knoten von s. Tragen Sie das Endergebnis in die Zeichnung ein. −1 4 −2 −2 3 −2 3 6 5 −1 6 −1 4 1 7 s Musterlösung: −1 4 5 −2 −2 −2 3 3 3 6 6 5 −1 3 −1 1 7 6 4 1 4 0 s Aufgabe 2 (Schnelles Potenzieren, 4 Zusatzpunkte) Gegeben ist der Algorithmus power(a : R; n0 : N) aus der Vorlesung (siehe Folie 51 der Vorlesungsfolien). Führen Sie den Algorithmus für a = 2, n0 = 15 aus und geben Sie die Belegung der Variablen p, r, n zu Beginn des Programms und jeweils am Ende der Whileschleife an. Nutzen Sie dafür die folgende Tabelle! Durchlauf Start 1 2 3 4 5 6 7 n r p Musterlösung: Durchlauf Start 1 2 3 4 5 6 7 Aufgabe 3 n 15 14 7 6 3 2 1 0 r 1 2 2 8 8 128 128 32768 p 2 2 4 4 16 16 256 256 (Anwendungsproblem) Ein Staat möchte seine Internet-Infrastruktur erneuern. Es gibt n Städte die alle angebunden werden n×n insollen, wobei indirekte Verbindungen ausreichen. Eine symmetrische Matrix C = ((cij )) ∈ R>0 formiert darüber, wieviel Einheiten der einheimischen Währung es jeweils kosten würde, eine Leitung zwischen je zwei Städten zu bauen. a) Entwickeln Sie einen Algorithmus, der eine kostenminimale Netzinfrastruktur berechnet, die alle n Städte direkt oder indirekt anbindet. Ihr Algorithmus darf höchsten O n2 log n Zeit brauchen. b) Begründen Sie kurz, warum Ihr Algorithmus das gewünschte Laufzeitverhalten aufweist. Musterlösung: a) Eine optimal kostengünstige Netzinfrastruktur entspricht einem MST im gewichteten vollständigen ungerichten Graphen mit Knotenmenge {1, . . . , n}, wobei eine Kante {i, j} jeweils das Gewicht cij hat. Dazu interpretiere man die Matrix C als Adjazenzmatrix mit zusätzlicher Gewichtsinformation. Aus dieser erzeuge man einen Folge von gewichteten Kanten h. . . , ({i, j}, cij ), . . . i mit 1 ≤ i < j ≤ n und wende darauf Kruskals Algorithmus an (was sortieren der Kantenfolge beinhaltet). b) Das Erzeugen der Folge von Kanten braucht O n2 Zeit, die erzeugte Folge hat n(n − 1)/2 ≤ n2 Elemente. Für sortieren O n2 log n2 = diese 2Folge benötigt Kruskals Algorithmus inklusive 2 2 O n · 2 log n = O n log n Zeit. Insgesamt braucht man also O n log n Zeit. 2 Aufgabe 4 (MST ) Gegeben sei ein ungerichteter, zusammenhängender Graph ohne Mehrfachkanten und Schleifen. Die Kanten seien gewichtet und o. B. d. A. nach Gewicht sortiert: w(e1 ) ≤ w(e2 ) ≤ . . . ≤ w(em ). Zeigen Sie, dass e2 in diesem Fall immer Teil eines minimalen Spannbaums sein kann. Musterlösung: Wir geben drei mögliche Lösungen an: a) Da der Graph eine zweitleichteste Kante besitzt, hat er auch mindestens zwei Kanten. Also gibt es auch mindestens drei Knoten im Graphen, sonst hätten wir schon mindestens eine Mehrfachkante. Ein minimaler Spannbaum enthält damit auch mindestens zwei Kanten. Führt man den Algorithmus von Kruskal aus, so kann e2 an zweiter Stelle gewählt werden. Die ersten beiden Kanten werden auf jeden Fall zum minimalen Spannbaum hinzugenommen, da man mit einer bzw. zwei Kanten noch keinen Zyklus bilden kann (Argument wiederum: keine Mehrfachkanten). Außerdem arbeitet Kruskals Algorithmus gierig, nimmt also niemals eine Kante wieder aus dem Spannbaum heraus. b) Sei T ein minimaler Spannbaum des Graphen, und e eine beliebige Kante mit Gewicht w(e2 ). Falls e ∈ T , sind wir fertig. Andernfalls hat T ∪ {e} genau einen Zyklus C mit mindestens drei Kanten, also außer e mindestens zwei andere. Eine dieser beiden anderen, nämlich ex , hat mindestens Gewicht w(e2 ). Diese Kante ex entfernen wir. T ∪ {ex } \ {e} ist nun wieder ein Spannbaum und auch minimal, denn w(ex ) ≥ w(e). Tatsächlich gilt sogar w(ex ) = w(e2 ). c) Sei e1 = (u, v) und e2 = (w, x). Einer der beiden Knoten von e2 ist nicht zu e1 inzident, also u 6= w 6= v oder u 6= x 6= v, sonst hätten wir eine Mehrfachkante. Dieser Knoten sei o. B. d. A. x. Definiere den Schnitt S = {u, v, x}. e2 führt aus S heraus und ist außerdem eine der leichtesten herausführenden Kanten, denn nur e1 kann noch leichter sein, führt aber nicht aus S heraus. Somit kann e2 nach der Cut Property in einem minimalen Spannbaum verwendet werden. Aufgabe 5 (Durchmesser eines Graphen) Skizzieren Sie einen Algorithmus, der den Durchmesser eines ungerichteten, zusammenhängenden Graphen G = (V, E) berechnet und dabei höchstens O(nm) Zeit benötigt (n := |V |, m := |E|). Sie können dazu einen bekannten Algorithmus modifizieren und verwenden. Begründen Sie, warum Ihr Algorithmus das gewünschte Laufzeitverhalten hat. Anmerkung: Der Durchmesser D eines Graphen G ist der maximale Abstand zwischen zwei Knoten in G. Der Abstand zweier Knoten sei dabei die minimale Kantenanzahl eines Pfades zwischen diesen Knoten in G. Mathematisch: D := maxu,v∈V minP ∈P(u,v) |P |, wobei P(u, v) die Menge aller Pfade zwischen u und v ist, und |P | die Anzahl der Kanten in Pfad P . Musterlösung: Zur Lösung des Problems modifiziere man die Breitensuche derart, dass sie für einen gegebenen Startknoten s ∈ V den maximalen Abstand von s zu einem Knoten v ∈ V liefert, also einfach die höchste vergebene BFS-Nummer. Wir erhalten also eine Funktion bfs(s : NodeID) : N0 . Dann berechnet der folgende Algorithmus den Durchmesser von G: function durchmesser (V : Set of NodeID, E : Set of unordered Pair of NodeID) : N0 res := h := 0 : N0 3: for each v ∈ V do 4: h := bfs(v) 1: 2: 3 if h > res then res := h 6: end for 7: return res 5: // n Aufrufe von bfs Die Funktion bfs benötigt O(m + n) Zeit, da bei einer Ausführung von bfs jede Kante O(1)-mal angefasst wird und für jeden Knoten genau einmal der Abstand ermittelt wird. Weil G zusammenhängend ist, gilt aber m ≥ n − 1. D.h. bfs braucht O(m) Zeit, insgesamt also O(nm) Zeit. Aufgabe 6 (Hashing) Gegeben sei eine leere Hash-Tabelle mit linearem Suchen ( open hashing with linear probing“). Als ” Hashfunktion werde h(x) := x mod 10 verwendet. a) Nun werde die Operationsfolge insert(13), insert(16), insert(4), insert(14), insert(3) ausgeführt. In welchen Zustand befindet sich die Hashtabelle nach der Ausführung? Tragen Sie die Lösung in die folgende Tabelle ein: 0 1 2 3 4 5 6 7 8 9 b) Gegeben sei nun eine weitere Hash-Tabelle mit linearem Suchen ( open hashing with linear ” probing“). Die Operation remove sei dabei so implementiert, dass keine Löschmarkierungen gesetzt werden (verwenden Sie den Algorithmus aus der Vorlesung). Als Hashfunktion werde wieder h(x) := x mod 10 verwendet. Die Hashtabelle befinde sich in dem wie folgt skizzierten Zustand: 0 1 2 3 4 5 6 7 8 9 21 1 12 14 2 13 Nun werde die Operation remove(12) ausgeführt. In welchen Zustand befindet sich die Hashtabelle nach der Ausführung? Tragen Sie die Lösung in die folgende Tabelle ein: 4 0 1 2 3 4 5 6 7 8 9 Musterlösung: a) b) Aufgabe 7 0 1 0 1 21 2 2 1 3 13 4 4 3 2 4 14 5 14 5 13 6 16 7 3 8 9 6 7 8 9 (Mehrfach-Selektion) Gegeben sei eine (unsortierte) Menge M von n Elementen sowie eine totale Ordnungsrelation < auf M (die Ordnungsrelation sei in konstanter Zeit auswertbar). Skizzieren Sie einen vergleichsbasierten Algorithmus, der die Elemente mit den Rängen1 1, 2, 4, 8, . . ., 2blog2 nc berechnet und dafür im schlimmsten Fall O(n) Zeit benötigt. Dabei dürfen Sie eine Funktion select(M, k) verwenden, die das Element mit Rang k einer Menge M zurückgibt, und zwar im schlechtesten Fall in O(|M |) Zeit. Begründen Sie kurz, wieso Ihr Algorithmus das gewünschte Laufzeitverhalten aufweist. Musterlösung: 1: function logselect(M ) 2: if |M | = 0 then 3: return hi 4: end if 5: ` = 2blog2 |M |c 6: p = select(M , `) 7: M< := {a ∈ M : a < p} 8: return logselect(M< ) ◦hpi Obige Prozedur gibt die gewünschten Elemente in aufsteigender Reihenfolge aus. Zeile 7 benötigt lineare Zeit. Nach dem ersten Schritt halbiert sich |M | bei jeder Rekursion, so dass sich mittels geometrischer Reihe (oder Master-Theorem) eine insgesamt lineare Laufzeit ergibt. Aufgabe 8 (Prioritätswarteschlangen) a) Beschreiben Sie, wie man eine adressierbare Prioritätswarteschlange mittels einer doppelt verketteten Liste implementiert. Ein Listenglied repräsentiere dabei ein Element in der Schlange, ein Handle sei ein Handle eines Listenglieds. Es gelte die Invariante, dass die Liste vor und nach jeder Operation sortiert ist. Skizzieren Sie alle Operationen der Schnittstelle und geben Sie die asymptotischen Laufzeiten an. 1 Für eine endliche totalgeordnete Menge (M, <) hat ein Element x ∈ M genau dann den Rang i ∈ N, wenn m1 < m2 < · · · < x = mi < · · · < mn gilt für M = {m1 , . . . , mn }. 5 b) Beschreiben Sie, wie man eine adressierbare Prioritätswarteschlange mittels einer sortierten Folge (z. B. (a, b)-Baum) implementiert. Skizzieren Sie alle Operationen der Schnittstelle und geben Sie die asymptotischen Laufzeiten an. Musterlösung: a) Die Liste wird immer in aufsteigender Reihenfolge sortiert gehalten. Konstruktion: Lege leere Liste an. Laufzeit O(1). insert(e): Füge e nach linearer Suche an der richtigen Stelle in die Liste ein, gebe Handle auf Listenglied zurück. Laufzeit O(n). deleteMin(): Gib erstes Element zurück und lösche das Listenglied. Laufzeit O(1). decreaseKey(h, k): Ändere Schlüssel bei h und schiebe es an die richtige Stelle. Laufzeit O(n). optional: remove(h): Lösche Listenglied h. Laufzeit O(1). merge: Mische beide Listen in üblicher Weise. Laufzeit O(n1 + n2 ). b) Für die Folge wählen wir eine aufsteigende Reihenfolge. Konstruktion: Lege leere sortierte Folge an. Laufzeit O(1). insert(e): Füge e in sortierte Folge ein, gebe Handle auf Folgenglied zurück. Laufzeit O(log n). deleteMin(): Gib erstes Element zurück und lösche das Folgenglied. Laufzeit O(log n). decreaseKey(h, k): Entferne h und füge es mit neuem Schlüssel wieder ein. Laufzeit O(log n), noch besseres Verhalten möglich mit Finger-Suche. optional: remove(h): Lösche Folgenglied h. Laufzeit O(log n). Aufgabe 9 (O-Notation) a) Zeigen Sie: g(n) = O(f (n)) =⇒ O(f (n) + g(n)) = O(f (n)). b) Zeigen Sie: O(f (n)) · O(g(n)) = O(f (n) · g(n)). c) Für o(f (n)) wird auch die folgende, alternative Definition verwendet: g(n) = o(f (n)) :⇐⇒ lim |g(n)/f (n)| = 0 . n→∞ Es soll nun bewiesen werden, dass beide Definitionen äquivalent sind für g : N≥0 → R≥0 und f : N≥0 → R>0 . Zeigen Sie also: g(n) =0 n→∞ f (n) lim ⇐⇒ ∀c ∈ R>0 : ∃n0 ∈ N>0 : ∀n ≥ n0 : g(n) ≤ cf (n) Musterlösung: a) Nach Vorraussetzung ist g(n) ∈ O(f (n)). Nach Definition gibt es also c ∈ R>0 und n0 ∈ N, so dass für alle n ≥ n0 gilt: g(n) ≤ cf (n). Es ist also f (n) + g(n) ≤ f (n) + cf (n) = (1 + c)f (n) für alle n ≥ n0 . Damit gilt die Behauptung. 6 b) Zunächst: O(f (n)) · O(g(n)) bezeichnet eine Menge von Funktionen, nämlich n o h0 (n) · h00 (n) h0 (n) ∈ O(f (n)) und h00 (n) ∈ O(g(n)) . Des weiteren bezeichnet das Gleichheitszeichen hier tatsächlich eine Gleichheit (von Mengen). Es handelt sich in diesem Fall also nicht um einen Ausdruck der informellen Schreibweise, wie sie sich im Rahmen der O-Notation eingebürgert hat. ⊆ Sei also h(n) ∈ O(f (n)) · O(g(n)). Dann exisitieren h0 (n) ∈ O(f (n)) und h00 (n) ∈ O(g(n)) mit h(n) = h0 (n) · h00 (n). Weiter gibt es c0 , c00 ∈ R>0 und n00 , n000 ∈ N mit h(n) = h0 (n) · h00 (n) ≤ c0 f (n) · c00 g(n) = c0 c00 (f (n) · g(n)) für alle n ≥ max{n00 , n000 }. Also ist h(n) ∈ O(f (n) · g(n)). ⊇ Sei umgekehrt h(n) ∈ O(f (n) · g(n)), d.h. es gibt c ∈ R>0 , n0 ∈ N0 mit h(n) ≤ c · f (n)g(n) für alle n ≥ n0 . Somit gilt h(n) ≤ cf (n) für alle n ≥ n0 g(n) =⇒ h(n) ∈ O(f (n)) , g(n) zumindest wenn g(n) > 0 für alle n ≥ n0 . In diesem Fall erhalten wir also h(n) = h(n) · g(n) ∈ O(f (n)) · O(g(n)) . |{z} g(n) | {z } ∈O(g(n)) ∈O(f (n)) Was passiert aber falls g(n) doch Nullstellen jenseits von n0 besitzt? Sei also n1 ≥ n0 mit g(n1 ) = 0. Dann ist h(n1 ) ≤ c · f (n1 )g(n1 ) = 0. Da h(n) nach Definition der O-Notation aber nie negativ wird, gilt sogar h(n1 ) = 0. Insgeasamt ist also h(n) = h0 (n) · g(n) mit ( h(n) 0 g(n) für alle n mit g(n) 6= 0 . h (n) = 0 sonst Es ist aber h0 (n) ∈ O(f (n)). Also gilt h(n) = h0 (n) · g(n) ∈ O(f (n)) · O(g(n)). c) Der Grenzwert ist folgendermaßen definiert: Sei f : D → R ein Funktion mit D ⊆ R und D nach oben unbeschränkt. Dann gelte lim f (x) = a x→∞ :⇐⇒ ∀ε > 0 : ∃x0 ∈ R : ∀x ≥ x0 : |f (x) − a| ≤ ε Diese Definition erinnert in ihrer äußeren Form schon an die Definition von o(. . . ). In der Tat ist für den Beweis auch gar nicht mehr viel zu tun: Seien also g : N0 → R≥0 und f : N0 → R+ Funktionen. Dann gilt folgende Äquivalenzumformung: g(n) g(n) nach Def ≤ε =0 ⇐⇒ ∀ε > 0 : ∃x0 ∈ R : ∀n ≥ x0 : lim n→∞ f (n) f (n) g(n),f (n)≥0 ⇐⇒ Aufgabe 10 ∀ε > 0 : ∃n0 ∈ N : ∀n ≥ n0 : g(n) ≤ εf (n) (Geldwechselproblem und dynamisches Programmieren) Angenommen, Sie haben die Aufgabe, eine Wechselgeldrückgabe für Getränkeautomaten zu programmieren. Ziel ist es dabei, immer die optimale (d. h. kleinstmögliche) Anzahl von Münzen zurückzugeben. Für viele Münzwertsysteme (z. B. auch für den Euro) wird diese Aufgabe durch das GreedyVerfahren in Abbildung 1 optimal gelöst. 7 a) Im Fall des Euro ist die Menge der Münzwerte M = {1, 2, 5, 10, 20, 50, 100, 200} (alle Werte in Cent). Zeigen Sie: Der Greedy-Algorithmus aus Abbildung 1 liefert nicht immer ein optimales Ergebnis, wenn eine zusätzliche Münze im Wert von 4 Cent eingeführt wird. Nun soll mit Hilfe von dynamischem Programmieren eine Algorithmus entwickelt werden, der für jedes beliebige Münzsystem M mit 1 ∈ M stets eine optimale Lösung liefert (wäre 1 6∈ M , so könnten manche Beträge nicht dargestellt werden). b) Identifizieren Sie die optimalen Teillösungen einer optimalen Lösung. Die Optimalität dieser Teillösungen muss dabei bewiesen werden. Was sind die trivialen Teilprobleme und deren trivialoptimale Lösungen? c) Geben Sie die optimale Lösung als Rekurrenz an. d) Geben Sie den fertigen Algorithmus in Pseudo-Code an. 1: 2: 3: 4: 5: 6: 7: procedure Geldrueckgabe(rueckbetrag : N0 , M : Set of N) rest := rueckbetrag : N0 while rest > 0 do wähle m ∈ M sodass m maximal ist mit m ≤ rest print Gib eine Münze im Wert von m zurück. rest := rest − m end while Abbildung 1: Ein Greedy-Verfahren zur Bestimmung der verwendeten Münzen bei der Geldrückgabe. Alle Werte sind natürliche Zahlen und werden z. B. in Cent angegeben. Die Menge M umfasst die möglichen Münzwerte. Musterlösung: a) Der Beweis erfolgt durch ein Gegenbeispiel: Man betrachte den Rückgabebetrag 8 Cent. In diesem Fall empfiehlt der Algorithmus die Rückgabe der Münzwerte {5, 2, 1}. Die optimale Lösung ist jedoch {4, 4} (wir verwenden Multimengen). b) Probleme, Teilprobleme, Lösungen und Teillösungen. Das zu lösende Problem ist durch den Betrag b gegeben, ein Teilproblem ist ein Teilbetrag b0 ≤ b. Eine Lösung zum Betrag b wird, wie schon in Teilaufgabe a), durch ein Multimenge L von Münzwerten aus M ausgedrückt, wobei P ist eine Teilmultimenge L0 ⊆ L. Dabei löst L0 b = m∈L m gelten muss. Eine Teillösung von L P das Teilproblem, das durch den Teilbetrag b0 = m∈L0 m gegeben ist. Eine optimale Lösung ist eine Lösung L mit einer minimalen Anzahl von Elementen. Beweis der Optimalität von Teillösungen einer optimalen Lösung. Eine Teillösung L0 ⊆ L einer optimalen Lösung L ist eine optimale Lösung des zu L0 gehörenden Teilproblems. Wäre eine Teillösung L0 nämlich nicht optimal, so gäbe es eine bessere P Lösung für das von L0 P gelöste Teilproblem, also eine Teilmultimenge L00 ⊆ L mit |L00 | < |L0 | und m∈L00 m = m∈L0 m. Dann wäre aber L̃ := (L\L0 ) ∪ L00 eine bessere Lösung des von L gelösten Gesamtproblems. Widerspruch zur Optimalität von L. Triviale Probleme und triviale Lösungen. Das einzige triviale Problem ist durch den Betrag 0 gegeben, die zugehörige trivial-optimale Lösung ist die leere Multimenge ∅. c) Eine optimale Lösung für einen Betrag b ∈ N0 und eine endliche Menge von Münzwerten M wird durch die Rekurrenz minMultSet L(b − m) ∪ {m} m ∈ M ∧ m ≤ b falls b > 0 L(b) = ∅ sonst 8 gegeben (wobei minMultSet aus einer Kollektion von Multimengen eine Multimenge minimaler Größe auswählt). Man beachte, dass man in einem Rekursionschritt nicht alle Teilbeträge von b betrachten muss, sondern nur die, die um jeweils einen Münzbetrag kleiner sind. d) Die Rekurrenz aus Teilaufgabe c) kann mehr oder weniger direkt in einen Algorithmus überführt werden. Allerdings arbeitet unser Algorithmus nicht mehr rekursiv. Die Teillösungen werden vielmehr bottom-up“ mit Hilfe einer Tabelle berechnet. Außerdem verwalten wir keine Mul” timengen. Es reicht uns stattdessen, nur den Münzwert zu speichern, der zu nächstkleineren Teillösung hinzukommt. procedure GeldrueckgabeOptimal (b : N0 , M : Set of N) assert(1 ∈ M ) // Andernfalls gibt es für manche Beträge keine Lösung 3: k : Array[0..b] of {0, . . . , b} // Minimale Anz. von Münzen für jedes Teilproblem 4: coin := h0, b, . . . , bi : Array[0..b] of {0, . . . , b} // Hinzukommende Münze zum 5: // nächst kleineren Teilproblem i − coin[i] 1: 2: 6: 7: 8: 9: 10: 11: 12: 13: // Lösung bottom-up konstruieren for i = 1 to b for each d ∈ M if d ≤ i then if k[i − d] + 1 < k[i] then k[i] := k[i − d] + 1 coin[i] := d // Differenz zur nächst kleineren Teillösung i − coin[i] 14: 15: 16: 17: 18: 19: 20: // Lösung ausgeben i := b while i > 0 do print coin[i] i := i − coin[i] end while Man beachte, dass die nächst kleinere Teillösung für einen Betrag i nicht unbedingt die Lösung für den Teilbetrag i − 1 ist, sondern die Lösung für den Teilbetrag i − coin[i]. Dies liegt daran, dass zu dieser Teillösung ja gerade der Betrag coin[i] hinzugekommen ist. Aufgabe 11 (Listen) In unserer einfachen Listen-Datenstruktur ist die Bestimmung der Listenlänge in konstanter Zeit nicht möglich. Das kann durch die Einführung einer Objektvariable size behoben werden, die aktualisiert wird, sobald sich die Anzahl der Elemente verändert. Operationen, die mehrere Listen betreffen, müssen nun über diese Listen Bescheid wissen, obwohl Low-Level-Funktionen wie splice nur Handles auf die betreffenden Elemente benötigen. Der Code, um ein Element a aus Liste L in Liste L0 nach a0 zu verschieben, wäre beispielsweise: 1: procedure moveAfter (a, a0 : Handle; L, L0 : List) 2: splice(a, a, a0 ) 3: L.size – – 4: L0 .size++ Modifizieren Sie die Operationen remove, insertAfter und concat so, dass size korrekt aktualisiert wird. Musterlösung: 1: procedure remove(b : Handle; L : List) 2: moveAfter(b, freeList.head, L, freeList) 9 1: 2: 3: 4: 5: 6: procedure insertAfter (x : Element; a : Handle; L : List) checkFreeList a0 := freeList.first moveAfter(a0 ,a,freeList, L) a0 → e := x return a0 procedure concat(L, L0 : List) 2: splice(L0 .first, L0 .last, L.last) 3: L.size :=L.size + L0 .size 4: L0 .size :=0 1: O-Phase 2010 TUTOREN GESUCHT !!! Werde selbst O-Phasen-Tutor !!! Seminar vom 4. bis 6. Oktober, Tutorentag am 9. Oktober, O-Phase vom 11. bis 16. Oktober Weitere Infos über die Fachschaft Mathe/Info Ausgabe: Montag, 5.7.2010 Abgabe (nur für Zusatzpunkte): Freitag, 9.7.2010, 12:45 im Briefkasten im UG, Gebäude 50.34 10