Technische Universität München Institut für Informatik Prof. Tobias Nipkow, Ph. D. Martin Strecker WS 2003/2004 11. Dezember 2003 Aufgabenblatt 6 Praktikum Spezifikation und Verifikation 1 Spannende Graphentheorie In dieser Aufgabe geht es darum, für einen Graphen eine Menge aufspannender Bäume zu berechnen, d.h. eine Menge von Bäumen, • deren Knotenmenge die Knotenmenge des Graphen ist und • deren Kantenmenge eine Teilmenge der Kantenmenge des Graphen ist. Ein Vorteil der Repräsentation eines Graphens durch aufspannender Bäume ist, daß sich die Bäume leichter traversieren lassen (keine Zyklen möglich). Der unten vorgestellte Algorithmus ist zudem Grundlage einiger interessanter Algorithmen, z.B. zur Berechnung stark zusammenhängender Teilgraphen. 1.1 Graphen Die hier betrachteten gerichteten Graphen werden dargestellt als Listen von Paaren (Knoten, Liste der Nachfolgeknoten). Der in Abb. ?? gezeigte Graph wird also repräsentiert durch [(0, [6, 9]), (1, [0, 8]), (2, [4, 7]), (4, [3, 7, 9]), (5, [8]), (6, [1, 5])]. 0 9 1 6 8 5 2 4 3 Abbildung 1: Beispielgraph types ’v graph = "(’v × ’v list) list" 7 Die folgenden Funktionen berechnen jeweils die Menge (repräsentiert als Liste) der Knoten und der Kanten des Graphen und der Nachfolgeknoten eines gegebenen Knoten: constdefs vertices :: "’a graph ⇒ ’a list" "vertices g == remdups ((map fst g) @ (flatten (map snd g)))" edges :: "’a graph ⇒ (’a × ’a) list" "edges g == flatten (map ( λ (v, vs). (map ( λ v’. (v, v’)) vs)) g)" succs :: "[’a graph, ’a] ⇒ ’a list" "succs g x == (if_none (assoc g x) [])" Aufgabe: Zeigen Sie folgendes Lemma: lemma succs_vertices: "set (succs g x) ⊆ set (vertices g)" . . . 1.2 Bäume Die folgende Definition für den Datentyp der Bäume geht über die bisher verwendeten Datentypdefinitionen hinaus, da er gegenseitig induktiv ist (die induktiven Datentypen Liste und Baum wechseln sich gegenseitig ab). Dies schlägt sich unter anderem darin nieder, wie primitive Rekursion und Induktion durchgeführt werden. Sollten Sie mehr Information hierzu benötigen, so lesen Sie den Abschnitt 3.4 im Tutorial. datatype ’a tree = Node ’a "’a tree list" consts tr_vertices :: "’a tree ⇒ ’a list" tr_list_vertices :: "’a tree list ⇒ ’a list" primrec "tr_vertices (Node x trs) = x # (tr_list_vertices trs)" "tr_list_vertices [] = []" "tr_list_vertices (t#ts) = (tr_vertices t) @ (tr_list_vertices ts)" consts tr_top :: "’a tree ⇒ ’a" primrec "tr_top (Node x trs) = x" consts tr_edges :: "’a tree ⇒ (’a × ’a) list" tr_list_edges :: "’a tree list ⇒ (’a × ’a) list" primrec "tr_edges (Node x trs) = (map ( λ t. (x, tr_top t)) trs) @ (tr_list_edges trs)" 2 "tr_list_edges [] = []" "tr_list_edges (t#ts) = (tr_edges t) @ (tr_list_edges ts)" 1.3 Tiefensuche in Graphen Die Funktion dfsm führt Tiefensuche in einem Graphen durch und markiert dabei die bereits besuchten Knoten. Sie gibt ein Paar zurück, dessen erste Komponente eine Liste der aufspannenden Bäume des Graphen ist und dessen zweite Komponente eine Liste der bei der Traversierung markierten Knoten ist. Etwas mehr im Detail ist das Vorgehen folgendes: Die Funktion hat Parameter g (den Graphen), ms (die Liste der bereits markierten Knoten des Graphen) und eine Arbeitsliste xs der noch zu besuchenden Knoten. Ist die Arbeitsliste abgearbeitet, wird eine leere Baumliste und die Menge der markierten Knoten zurückgegeben. Ist noch mindestens ein Knoten x zu besuchen und ist dieser in der Menge der Knoten des Graphen (siehe weiter unten), so wird geprüft, ob der Knoten bereits markiert ist. Ist das der Fall, so werden die restlichen noch zu besuchenden Knoten xs abgearbeitet. Ist der Knoten jedoch nicht markiert, werden zuerst rekursiv alle Nachfolgeknoten von x aufgesucht. Man erhält eine Liste von von x aus erreichbaren Teilbäumen tsx und die jetzt insgesamt besuchten Knoten msx zurück. Mit dieser neuen Markierung arbeitet man in einem zweiten Aufruf alle weiteren Knoten in xs ab und erhält wieder eine Baumliste und eine neue Markierung. Die Funktion gibt die (geeignet kombinierten) Teilbaumlisten und die neue Markierung zurück. Aufgabe: Machen Sie sich das Vorgehen des Algorithmus an dem Beispiel aus Abb. ?? klar. Die durchgezogenen Kanten sind die Kanten von aufspannenden Bäumen des Graphen. Gibt es auch andere aufspannende Bäume, und wenn ja, welche berechnet der Algorithmus? consts dfsm :: "((’a graph × ’a list) × ’a list) ⇒ (’a tree list × ’a list)" recdef dfsm "measure ( λ (g, ms). card (set (vertices g) - set ms)) <*lex*> measure length" "dfsm ((g, ms), []) = ([], ms)" "dfsm ((g, ms), x#xs) = (if ( ¬ x mem (vertices g)) then ([], ms) else (if (x mem ms) then dfsm ((g, ms), xs) else (let (tsx, msx) = dfsm ((g, (x # ms)), succs g x); (tsxs, msxs) = dfsm ((g, msx @ x # ms), xs) 3 in ((Node x tsx) # tsxs, msxs))))" ( hints recdef_simp: mem_set dfsm_card intro: psubset_card_mono) Zur leichteren Handhabung verwenden wir folgende Simplifikationsregeln und außerdem das Induktionsprädikat dfsm_induct_simp anstelle des von Isabelle generierten. lemma dfsm_Nil: "dfsm ((g, ms), []) = ([], ms)" by simp lemma dfsm_Cons: "dfsm ((g, ms), x # xs) = (if ( ¬ x mem (vertices g)) then ([], ms) else if x mem ms then dfsm ((g, ms), xs) else (let (tsx, msx) = dfsm ((g, x # ms), succs g x); (tsxs, msxs) = dfsm ((g, msx @ x # ms), xs) in ((Node x tsx) # tsxs, msxs)))" by (simp add: dfsm_card mem_set measure_def inv_image_def lex_prod_def) Die Funktion dfs, an der wir primär interessiert sind, ruft dfsm mit einem Graphen, einer leeren Liste von Markierungen und der Liste aller Knoten als Arbeitsliste auf: constdefs dfs :: "’a graph ⇒ ’a tree list" "dfs g == fst (dfsm ((g, []), vertices g))" Aus den Isabelle-Definitionen kann ML-Code generiert werden, dessen Ausführung wesentlich effizienter ist als die Vereinfachung durch den Isabelle-Simplifier. Aufgabe: Um den generierten Code auszuprobieren, gehen Sie so vor: • Starten Sie eine Isabelle-Shell mit isabelle (Kleinbuchstaben!). Üblicherweise werden in dem generierten Code spezielle Funktionen verwendet, die in einer StandardML Laufzeitumgebung nicht vorhanden sind. • Laden Sie die ML-Datei mit use "dfs.ML"; • Rufen Sie die Funktion dfs mit verschiedenen Eingaben auf, z.B. dfs [(0, [1]), (1, [2]), (2, [0])] ; 1.4 Beweise Im folgenden sollen drei Aussagen gezeigt werden, aus denen die Hauptaussage folgt: dfs berechnet eine Liste aufspannender Bäume, d.h. die Knoten der Bäume sind gleich der Knoten des Graphen und die Kanten der Bäume sind eine Untermenge der Kanten des Graphen. 4 Die erste Aussage stellt eine Beziehung zwischen den Knoten der Bäume und den Knoten des Graphen her. lemma marking_vertices: "set xs ⊆ set (vertices g) −→ (let (ts, ms’) = dfsm ((g, ms), xs) in set ms’ - set ms ⊆ set (tr_list_vertices ts) ∧ set (tr_list_vertices ts) ⊆ set (vertices g) ∧ tr_top ‘ set ts ⊆ set xs )" . . . Die folgenden Beweise haben eine ähnliche Struktur. Beachten Sie insbesondere, wie geschachtelte let -Ausdrücke mithilfe von subgoal_tac aufgelöst werden. Aufgabe: Zeigen Sie: dfsm markiert zusätzlich zu den bereits markierten Knoten doch diejenigen der Arbeitsliste. Damit der Beweis gelingt, muß angenommen werden, daß in der Arbeitsliste nur Knoten des Graphen vorkommen: lemma marking_increases: "set xs ⊆ set (vertices g) −→ (let (ts, ms’) = dfsm ((g, ms), xs) in set ms ∪ set xs ⊆ set ms’)" . . . Aufgabe: Zeigen Sie: Die Kanten der Bäume ergeben eine Teilmenge der Kanten des Graphen: lemma tr_list_edges_edges: "set xs ⊆ set (vertices g) −→ (let (ts, ms’) = dfsm ((g, ms), xs) in set (tr_list_edges ts) ⊆ set (edges g))" . . . Aufgabe: Kombinieren Sie jetzt die obigen Lemmas zum Beweis der Hauptaussage: theorem spanning_trees: "let ts = dfs g in set (tr_list_vertices ts) = set (vertices g) ∧ set (tr_list_edges ts) ⊆ set (edges g)" . . . Abgabe: 7. Januar 2004 5