Graphen

Werbung
Graphen
Mathematisch sind Graphen definiert als:
G = (V, E) mit V Menge von Knoten, E ⊆ V × V Menge von Kanten
Wie können Graphen nun in Haskell implementiert werden? Die Datentypen leiten wir
direkt aus der mathematischen Definition ab, zusätzlich fügen wir noch Beschriftungen
zu Knoten und Kanten hinzu:
module Graph where
type
type
type
type
Graph a b = (Nodes a, Edges b)
NodeId
= Int
Nodes a
= [(NodeId, a)]
Edges b
= [(NodeId, b, NodeId)]
Wir stellen zunächst Überlegungen zur Schnittstelle an, um die Implementierung und
Effizienzbetrachtungen kümmern wir uns später.
Viele (imperative) Graphalgorithmen arbeiten mit Markierungen. Ähnliches wäre auch
in unserem Framework über Beschriftungen möglich, z.B. durch Erweiterung um eine
boolesche Komponente. Allerdings entspricht dies nicht dem üblichen induktiven Programmieren in funktionalen Sprachen.
Schöner wäre eine induktive Darstellung der Graphen, wie:
• leerer Graph (Konstruktor emptyGraph)
• Graph, der aus einem Knoten (mit seinem Kontext, den ein- und ausgehenden
Kanten) und einem Restgraph besteht (Konstruktor &v, wobei v die zugehörige
Knotennummer ist)
Mit dieser Darstellung ließe sich eine Tiefensuche wie folgt implementieren (in Pseudocode):
dfs
dfs
dfs
dfs
:: [NodeId] -> Graph a b -> [NodeId]
[]
_
= []
(v:vs) (c &v g) = v : dfs (succs c ++ vs) g
(_:vs) g
= dfs vs g
Problematisch ist natürlich das doppelte Vorkommen von v in den Pattern (NichtLinearität) und der parametrisierte Konstruktor &. Als Lösung kann dieses Matching
mit Hilfe einer Funktion umgesetzt werden:
type Context a b = ([(NodeId, b)], a, [(NodeId, b)])
match :: NodeId -> Graph a b
-> Maybe (Context a b, -- Kontext
1
Graph a b)
-- Restgraph
dfs :: [NodeId] -> Graph a b -> [NodeId]
dfs []
_ = []
dfs (v:vs) g = case match v g of
Nothing
-> dfs vs g
Just ((_ ,_ ,succs), g’) -> v : dfs (map fst succs ++ vs) g’
Es fehlt noch die Definition der Funktion match, die wir durch Suchen des Knotens und
Aufsammeln der Vorgänger- und Nachfolgerknoten implementieren.
match n (nodes, edges) = do
a <- lookup n nodes
let ctxt = ( [(m, b) | (m, b, n’) <- edges, n’ == n]
, a
, [(m, b) | (n’, b, m) <- edges, n’ == n] )
grph = ( filter ((/= n) . fst) nodes
, [ e | e@(m, _, m’) <- edges, m /= n, m’ /= n] )
return (ctxt, grph)
Für die Konstruktion von Graphen definieren wir einige Funktionen:
addNode :: NodeId -> a -> Graph a b -> Graph a b
addNode n a (nodes, edges) =
maybe ((n, a) : nodes, edges)
(error $ "Node " ++ show n ++ "already in graph")
(lookup n nodes)
addEdge :: NodeId -> b -> NodeId -> Graph a b -> Graph a b
addEdge n b m (nodes, edges) =
maybe (errNode n)
(\_ -> maybe (errNode m)
(const (nodes, (n, b, m) : edges))
(lookup m nodes))
(lookup n nodes)
where errNode n = error $ "Node " ++ show n ++ " not in graph"
addNodeWithSuccs :: NodeId -> a -> [(NodeId, b)]
-> Graph a b -> Graph a b
addNodeWithSuccs n a succs = foldr (.) (addNode n a)
[addEdge n b m | (m, b) <- succs]
Beachte: Knoten / Kanten können nun mittels match in anderer Reihenfolge entnommen werden als sie hinzugefügt wurden.
In unserer Implementierung soll es mehr auf die Idee der Schnittstelle ankommen, die
Repräsentation eines Graphen ist so noch ineffizient. Eine effizientere Darstellung ist
2
beispielsweise möglich unter Verwendung von Braunbäumen oder höhenbalancierten
Suchbäumen, die NodeIds auf Vorgänger-/Nachfolgerknoten abbilden. Hierdurch wird
die Implementierung von match komplizierter, ist aber effizient möglich. Dadurch wird
das Hinzufügen von Knoten/Kanten und das Matchen logarithmisch in der Graphgröße.
Eine weitere Verbesserungsmöglichkeit: Die NodeIds sind nicht abstrakt, sondern müssen
durch die Anwendung (und damit den Programmierer) generiert werden. Dies lässt sich
durch eine monadische Erweiterung der Graph-Konstruktion um einen NodeId-Zustand
verbessern, sodass Graphen wie folgt konstruiert werden können:
g = do
n <- addNode "a"
m <- addNode "b"
addEdge n 42 m
3
Herunterladen