src/directed-graph.lisp @ 7e80eda84170
v1.4.0
Add an explicit condition hierarchy
Fixes https://github.com/sjl/cl-digraph/issues/5
author |
Steve Losh <steve@stevelosh.com> |
date |
Sun, 14 Mar 2021 21:31:34 -0400 |
parents |
884333cfb6fb |
children |
4c06758934a2 |
(in-package :digraph)
;;;; Utils --------------------------------------------------------------------
(defun make-hash-table-portably (&key (size 0) test hash-function)
(apply #'make-hash-table
:test test
:size size
;; Don't explode if the implementation doesn't support :hash-function.
:allow-other-keys t
(when hash-function
(list :hash-function hash-function))))
;;;; Errors -------------------------------------------------------------------
(defgeneric vertex-involved (condition)
(:documentation "Retrieve the vertex involved in the condition."))
(define-condition digraph-error (error) ()
(:documentation "Base condition for digraph-related errors."))
(define-condition topological-sort-cycle (digraph-error)
((vertex-involved% :initarg :vertex-involved :reader vertex-involved))
(:report
(lambda (c stream)
(format stream "Cycle detected during topological sort involving vertex ~S."
(vertex-involved c))))
(:documentation
"An error signaled when topologically sorting a graph that contains a cycle.
`vertex-involved` can be used to retrieve one of the vertices involved in a
cycle. Which vertex in the cycle is chosen is arbitrary."))
(define-condition missing-vertex (digraph-error)
((vertex-involved% :initarg :vertex-involved :reader vertex-involved))
(:documentation "Base condition for errors signaled when inserting an edge with a vertex missing."))
(define-condition missing-predecessor (missing-vertex) ()
(:report
(lambda (c stream)
(format stream
"Cannot add edge with predecessor ~S because it is not in the graph."
(vertex-involved c))))
(:documentation
"An error signaled when trying to insert an edge whose predecessor is not in the graph.
`vertex-involved` can be used to retrieve the offending predecessor."))
(define-condition missing-successor (missing-vertex) ()
(:report
(lambda (c stream)
(format stream
"Cannot add edge with successor ~S because it is not in the graph."
(vertex-involved c))))
(:documentation
"An error signaled when trying to insert an edge whose successor is not in the graph.
`vertex-involved` can be used to retrieve the offending successor."))
;;;; Data ---------------------------------------------------------------------
(defclass digraph ()
((nodes :initarg :nodes :reader digraph-nodes)
(test :initarg :test :reader digraph-test)
(hash-function :initarg :hash-function :reader digraph-hash-function))
(:documentation "A directed graph. Use `make-digraph` to create one."))
(defun make-digraph (&key initial-vertices
(test #'eql)
(hash-function nil))
"Create and return a new digraph.
`initial-vertices` can be a sequence of vertices to add to the graph.
`test` should be one of the hash table equality predicates.
If your Lisp implementation supports the `:hash-function` argument for
creating hash tables with custom predicates, you can specify one with
`hash-function`.
"
(let ((digraph (make-instance 'digraph
:nodes (make-hash-table-portably
:test test
:size (length initial-vertices)
:hash-function hash-function)
:test test
:hash-function hash-function)))
(map nil (curry #'insert-vertex digraph) initial-vertices)
digraph))
(defmethod print-object ((d digraph) stream)
(print-unreadable-object (d stream :type t :identity t)
(format stream "~:S" (hash-table-keys (digraph-nodes d)))))
(defmacro pred (digraph object)
`(car (gethash ,object (digraph-nodes ,digraph))))
(defmacro succ (digraph object)
`(cdr (gethash ,object (digraph-nodes ,digraph))))
(defmacro do-vertices ((symbol digraph) &body body)
`(loop :for ,symbol :being :the hash-keys :of (digraph-nodes ,digraph)
:do (progn ,@body)))
(defmacro do-edges ((predecessor-symbol successor-symbol digraph) &body body)
(with-gensyms (succs)
`(loop
:for ,predecessor-symbol :being :the hash-keys :of (digraph-nodes ,digraph)
:using (hash-value (nil . ,succs))
:do (loop :for ,successor-symbol :in ,succs :do (progn ,@body)))))
;;;; Basic API ----------------------------------------------------------------
(defun emptyp (digraph)
"Return `t` if `digraph` has no vertices or edges, `nil` otherwise."
(zerop (hash-table-count (digraph-nodes digraph))))
(defun vertices (digraph)
"Return a fresh list of the vertices of `digraph`."
(hash-table-keys (digraph-nodes digraph)))
(defun edges (digraph)
"Return a fresh list of the edges of `digraph`.
Each edge will be a cons of the form `(predecessor . successor)`.
"
(map-edges #'cons digraph))
(defun predecessors (digraph vertex)
"Return a fresh list of the predecessors of `vertex`."
(copy-list (pred digraph vertex)))
(defun successors (digraph vertex)
"Return a fresh list of the successors of `vertex`."
(copy-list (succ digraph vertex)))
(defun neighbors (digraph vertex)
"Return a fresh list of the neighbors of `vertex`."
(union (predecessors digraph vertex)
(successors digraph vertex)
:test (digraph-test digraph)))
(defun contains-vertex-p (digraph vertex)
"Return whether the graph contains `vertex`."
(nth-value 1 (gethash vertex (digraph-nodes digraph))))
(defun contains-edge-p (digraph predecessor successor)
"Return whether the graph contains an edge from `predecessor` to `successor`."
(ensure-boolean (member successor (succ digraph predecessor)
:test (digraph-test digraph))))
(defun insert-vertex (digraph vertex)
"Insert `vertex` into the graph if it is not already a member.
Returns `t` if the vertex was already in the graph, or `nil` if it was
inserted.
"
(nth-value 1 (ensure-gethash vertex (digraph-nodes digraph)
(cons nil nil))))
(defun insert-edge (digraph predecessor successor)
"Insert an edge from `predecessor` to `successor` if not already present.
Returns `t` if the edge was already in the graph, or `nil` if it was
inserted.
The `predecessor` and `successor` vertices must already exist in the graph.
If `predecessor` is not in the graph a `missing-predecessor` error will be
signaled. Otherwise, if `successor` is not in the graph, a `missing-successor`
error will be signaled.
"
(unless (contains-vertex-p digraph predecessor)
(error 'missing-predecessor :vertex-involved predecessor))
(unless (contains-vertex-p digraph successor)
(error 'missing-successor :vertex-involved successor))
(prog1
(contains-edge-p digraph predecessor successor)
(pushnew predecessor (pred digraph successor) :test (digraph-test digraph))
(pushnew successor (succ digraph predecessor) :test (digraph-test digraph))))
(defun insert-chain (digraph predecessor successor &rest later-successors)
"Insert edges between a series of vertices.
Give a series of vertices `V0 V1 ... Vn`, edges between each will be inserted
if not already present:
V0 -> V1 -> ... -> Vn
All vertices must exist in the graph already.
Returns `nil`.
"
(insert-edge digraph predecessor successor)
(when later-successors
(apply #'insert-chain digraph successor later-successors)))
(defun arbitrary-vertex (digraph)
"Return an arbitrary vertex of `digraph` and `t`.
If the digraph is empty, `(values nil nil)` will be returned instead.
"
(do-vertices (vertex digraph)
(return-from arbitrary-vertex (values vertex t)))
(values nil nil))
(defun remove-edge (digraph predecessor successor)
"Remove an edge from `predecessor` to `successor` if present.
Returns `t` if there was such an edge, or `nil` if not.
"
(if (contains-edge-p digraph predecessor successor)
(progn
(removef (succ digraph predecessor) successor :test (digraph-test digraph))
(removef (pred digraph successor) predecessor :test (digraph-test digraph))
t)
nil))
(defun remove-vertex (digraph vertex)
"Remove `vertex` from the graph if present.
If there are any edges to/from `vertex` they will be automatically removed.
Returns `t` if there was such a vertex, or `nil` if not.
"
(if (contains-vertex-p digraph vertex)
(let ((ps (pred digraph vertex))
(ss (succ digraph vertex))
(test (digraph-test digraph)))
(loop :for p :in ps :do (removef (succ digraph p) vertex :test test))
(loop :for s :in ss :do (removef (pred digraph s) vertex :test test))
(remhash vertex (digraph-nodes digraph))
t)
nil))
(defun degree (digraph vertex)
"Return the number of neighbors of `vertex`."
(length (neighbors digraph vertex)))
(defun degree-in (digraph vertex)
"Return the number of predecessors of `vertex`."
(length (pred digraph vertex)))
(defun degree-out (digraph vertex)
"Return the number of successors of `vertex`."
(length (succ digraph vertex)))
(defun count-vertices (digraph)
"Return the number of vertices in `digraph`."
(hash-table-count (digraph-nodes digraph)))
(defun count-edges (digraph)
"Return the number of edges in `digraph`."
(let ((result 0))
(do-edges (nil nil digraph) (incf result))
result))
(defun rootp (digraph vertex)
"Return whether `vertex` is a root vertex in `digraph`."
(null (pred digraph vertex)))
(defun leafp (digraph vertex)
"Return whether `vertex` is a leaf vertex in `digraph`."
(null (succ digraph vertex)))
;;;; Iteration ----------------------------------------------------------------
(defun mapc-vertices (function digraph)
"Call `function` on each vertex in `digraph`.
The order in which the vertices are processed is unspecified.
Returns `nil`.
"
(do-vertices (v digraph) (funcall function v)))
(defun mapc-edges (function digraph)
"Call `function` on each edge in `digraph`.
For each edge, `function` will be called once with two arguments:
(function predecessor successor)
The order in which the edges are processed is unspecified.
Returns `nil`.
"
(do-edges (p s digraph) (funcall function p s)))
(defun map-vertices (function digraph)
"Return a fresh list with the results of calling `function` on each vertex.
The order of the resulting list is unspecified.
"
(let ((result nil))
(do-vertices (v digraph) (push (funcall function v) result))
result))
(defun map-edges (function digraph)
"Return a fresh list with the results of calling `function` on each edge.
For each edge, `function` will be called once with two arguments:
(function predecessor successor)
The order of the resulting list is unspecified.
"
(let ((result nil))
(do-edges (p s digraph) (push (funcall function p s) result))
result))
(defun find-vertex-if (function digraph)
(do-vertices (v digraph)
(when (funcall function v)
(return-from find-vertex-if v))))
;;;; Copying ------------------------------------------------------------------
(defun copy-digraph (digraph)
"Create a fresh copy of `digraph`.
The vertex objects themselves are not copied, but everything else is.
"
;; todo make this faster, but at least this works
(let ((copy (make-digraph :test (digraph-test digraph)
:hash-function (digraph-hash-function digraph)
:initial-vertices (vertices digraph))))
(do-edges (p s digraph) (insert-edge copy p s))
copy))
;;;; Traversal ----------------------------------------------------------------
;;; Adapted from http://algorithms.wtf/
(defun mapc-depth-first (function digraph start-vertex)
"Apply `function` to the vertices of a depth-first traversal of `digraph`.
Returns `nil`.
Vertices are processed in depth-first order, beginning at `start-vertex`.
Cycles in the graph will not be traversed into.
"
(let ((seen nil))
(labels ((recur (vertex)
(when (not (member vertex seen :test (digraph-test digraph)))
(push vertex seen)
(funcall function vertex)
(mapcar #'recur (succ digraph vertex)))))
(when (contains-vertex-p digraph start-vertex)
(recur start-vertex))))
nil)
(defun mapc-breadth-first (function digraph start-vertex)
"Apply `function` to the vertices of a breadth-first traversal of `digraph`.
Returns `nil`.
Vertices are processed in breadth-first order, beginning at `start-vertex`.
Cycles in the graph will not be traversed into.
"
(let ((seen nil)
(remaining nil))
(labels ((recur (vertex)
(when (not (member vertex seen :test (digraph-test digraph)))
(push vertex seen)
(funcall function vertex)
;;; todo maybe use jpl queues here...
(appendf remaining (succ digraph vertex)))
(when remaining
(recur (pop remaining)))))
(when (contains-vertex-p digraph start-vertex)
(recur start-vertex))))
nil)
(defun map-depth-first (function digraph start-vertex)
"Apply `function` to the vertices of a breadth-first traversal of `digraph`.
Returns a fresh list with the results.
Vertices are processed in depth-first order, beginning at `start-vertex`, and
the resulting list has this order as well.
Cycles in the graph will not be traversed into.
"
(let ((result nil))
(mapc-depth-first (lambda (v) (push (funcall function v) result))
digraph start-vertex)
(nreverse result)))
(defun map-breadth-first (function digraph start-vertex)
"Apply `function` to the vertices of a breadth-first traversal of `digraph`.
Returns a fresh list with the results.
Vertices are processed in breadth-first order, beginning at `start-vertex`,
and the resulting list has this order as well.
Cycles in the graph will not be traversed into.
"
(let ((result nil))
(mapc-breadth-first (lambda (v) (push (funcall function v) result))
digraph start-vertex)
(nreverse result)))
(defun roots (digraph)
"Return all root vertices in `digraph`.
This is currently O(vertices).
A root is a vertex with no incoming edges (i.e. in-degree 0).
"
(remove-if-not (curry #'rootp digraph)
(vertices digraph)))
(defun leafs (digraph)
"Return all leaf vertices in `digraph`.
This is currently O(vertices).
A root is a vertex with no outgoing edges (i.e. out-degree 0).
"
(remove-if-not (curry #'leafp digraph)
(vertices digraph)))
(declaim (inline topological-sort%))
(defun topological-sort% (function digraph)
(let ((status (make-hash-table-portably
:test (digraph-test digraph)
:hash-function (digraph-hash-function digraph))))
(labels
((visit (vertex)
(ecase (gethash vertex status :new)
(:active (error 'topological-sort-cycle :vertex-involved vertex))
(:new (recur vertex))
(:done nil)))
(recur (vertex)
(setf (gethash vertex status) :active)
(mapc #'visit (succ digraph vertex))
(setf (gethash vertex status) :done)
(funcall function vertex)))
(mapc #'visit (roots digraph)))
status))
(defun topological-sort (digraph)
"Return a fresh list of the vertices of `digraph` in topological order.
Edges are treated as meaning \"depends on\", so an edge `A --> B` means \"A
depends on B\" and that B must come before A in the resulting list. Aside
from this restriction, the order of the resulting list is arbitrary.
The order in which the vertices are processed is unspecified.
A `topological-sort-cycle` error will be signaled if the graph contains
a cycle.
"
(let* ((result nil)
(seen (topological-sort% (lambda (v) (push v result)) digraph)))
;; Make sure there are no rootless cycles.
(if (= (hash-table-count seen) (count-vertices digraph))
(nreverse result)
(error 'topological-sort-cycle
:vertex-involved (find-vertex-if
(lambda (v) (not (gethash v seen)))
digraph)))))
(defun reachablep (digraph start target &key (strategy :breadth-first))
"Return `t` if it is possible to reach `target` from `start`, otherwise `nil`.
All vertices are reachable from themselves.
Otherwise a `target` is reachable from `start` if a directed path exists from
the start to the target.
`strategy` will be used to determine how to traverse the graph when searching
for a path, and can be one of `:breadth-first` or `:depth-first`.
"
(let* ((traverse (ccase strategy
(:breadth-first #'mapc-breadth-first)
(:depth-first #'mapc-depth-first)))
(test (digraph-test digraph))
(check (lambda (vertex)
(when (funcall test vertex target)
(return-from reachablep t)))))
(funcall traverse check digraph start)
nil))