nim-lib/graphs.nim

1485 lines
43 KiB
Nim
Raw Normal View History

2026-03-20 18:34:42 +00:00
import linear,iops, sets, tables, algorithm, heapqueue, random
export linear
type GraphRepr = enum
AdjMatrix
Sparse
Dense
type Graph* = ref object
#representation of vertices
V*: int ##number of vertices.
capacity: int #number of possible vertices.
E*: int ##number of edges.
directed*: bool
representation: GraphRepr
#AdjMatrix
adjMatrix: matrix[int]
#Sparse / Dense
adjList: seq[HashSet[int]]
inverseAdjList: seq[HashSet[int]] #only used for digraph
func contains*(g: Graph, v, w: int): bool =
##Test if v ==> w is an edge in the graph g.
if v >= g.V or w >= g.V: return false
case g.representation:
of AdjMatrix:
return g.adjMatrix[v, w] > 0
of Sparse:
return w in g.adjList[v]
of Dense:
return not (w in g.adjList[v])
func outDegree*(g: Graph, v: int): int =
if v >= g.V: return 0
case g.representation:
of AdjMatrix:
for w in 0..<g.V:
result += g.adjMatrix[v, w]
return result
of Sparse:
return g.adjList[v].len
of Dense:
return g.V - g.adjList[v].len
func inDegree*(g: Graph, v: int): int =
if v >= g.V: return 0
if not g.directed: return g.outDegree(v) #easier
case g.representation:
of AdjMatrix:
for w in 0..<g.V:
result += g.adjMatrix[w, v]
return result
of Sparse:
return g.inverseAdjList[v].len
of Dense:
return g.V - g.inverseAdjList[v].len
func degree*(g: Graph, v: int): int =
if v >= g.V: return 0
if g.directed: return g.inDegree(v) + g.outDegree(v)
else: return g.outDegree(v)
func edgeDensity*(g: Graph): float =
var E = g.E.float
var V = g.V.float
if g.directed:
return E / (V * (V-1))
else:
return 2*E / (V * (V-1))
func adjacencyMatrix*(g: Graph): matrix[int] =
##Returns the V x V adjacency matrix for g.
##This may be different from the internal adjacency matrix if g.capacity > g.V.
result.initMatrix(g.V, g.V)
case g.representation
of AdjMatrix:
#trim it down
for v in 0..<g.V:
for w in 0..<g.V:
if v != w:
result[v, w] = g.adjMatrix[v, w]
of Sparse:
for v in 0..<g.V:
for w in g.adjList[v]:
if v != w:
result[v, w] += 1
of Dense:
for v in 0..<g.V:
for w in 0..<g.V:
if v != w:
result[v, w] = 1
for w in g.adjList[v]:
result[v, w] -= 1
func initGraph*(V: int = 0): Graph =
##This returns a graph with V vertices and 0 edges.
new(result)
result.V = V
result.capacity = V
result.E = 0
result.directed = false
#empty graph - thus Sparse.
result.representation = Sparse
result.adjList = newSeq[HashSet[int]](V)
for i in 0..<V:
result.adjList[i] = initHashSet[int]()
func initDenseGraph*(V: int = 0): Graph =
new(result)
result.V = V
result.capacity = V
result.E = (V*(V-1)) div 2
result.directed = false
#the densest dense graph is the complete graph.
result.representation = Dense
result.adjList = newSeq[HashSet[int]](V)
for i in 0..<V:
result.adjList[i] = initHashSet[int]()
func initDigraph*(V: int = 0): Graph =
new(result)
result.V = V
result.capacity = V
result.E = 0
result.directed = true
#empty graph - thus Sparse.
result.representation = Sparse
result.adjList = newSeq[HashSet[int]](V)
result.inverseAdjList = newSeq[HashSet[int]](V)
for i in 0..<V:
result.adjList[i] = initHashSet[int]()
result.inverseAdjList[i] = initHashSet[int]()
func initDenseDigraph*(V: int = 0): Graph =
new(result)
result.V = V
result.capacity = V
result.E = (V*(V-1)) div 2
result.directed = true
#the densest dense graph is the complete graph.
result.representation = Dense
result.adjList = newSeq[HashSet[int]](V)
result.inverseAdjList = newSeq[HashSet[int]](V)
for i in 0..<V:
result.adjList[i] = initHashSet[int]()
result.inverseAdjList[i] = initHashSet[int]()
proc regularize(g: var Graph) =
#actually implement this later lmao
return
#[
AdjMatrix:
O(V^2) space
O(1) time to check/modify (v, w)
O(1) time to get adjacency matrix
O(V) time to get deg(v)
Converting into:
Sparse/Dense:
O(V^2) time
Sparse/Dense:
O(2E) space
O(deg(v)) to check/modify (v, w)
O(V^2) time to get adjacency matrix
O(1) time to get deg(v)
Converting into:
Dense/Sparse:
O(V^2) time
AdjMatrix:
O(V^2) time
]#
# var density = g.edgeDensity()
# case g.representation:
# of AdjMatrix:
# if density <= 0.05:
# #switch to sparse
# # echo "Switching to sparse (density is ", density, ")"
# g.representation = Sparse
# g.adjList = newSeq[HashSet[int]](g.capacity)
# for i in 0..<g.capacity:
# g.adjList[i] = initHashSet[int]()
# for v in 0..<g.V:
# for w in 0..<g.V:
# for i in 1..g.adjMatrix[v, w]:
# g.adjList[v].incl w
# if g.directed:
# g.inverseAdjList = newSeq[HashSet[int]](g.capacity)
# for i in 0..<g.capacity:
# g.inverseAdjList[i] = initHashSet[int]()
# for v in 0..<g.V:
# for w in 0..<g.V:
# for i in 1..g.adjMatrix[v, w]:
# g.inverseAdjList[w].incl v
# g.adjMatrix = nil
# if density >= 0.95:
# return #TODO TESTING
# #switch to dense
# #automatically reduce capacity as much as possible
# g.representation = Dense
# g.capacity = g.V
# g.adjList = newSeq[HashSet[int]](g.V)
# for i in 0..<g.V:
# g.adjList[i] = initHashSet[int]()
# for v in 0..<g.V:
# for w in 0..<g.V:
# if g.adjMatrix[v, w] == 0:
# g.adjList[v].incl w
# if g.directed:
# g.inverseAdjList = newSeq[HashSet[int]](g.V)
# for i in 0..<g.V:
# g.inverseAdjList[i] = initHashSet[int]()
# for v in 0..<g.V:
# for w in 0..<g.V:
# if g.adjMatrix[v, w] == 0:
# g.inverseAdjList[w].incl v
# g.adjMatrix = nil
# of Sparse:
# if density >= 0.10:
# # echo "Switching to adjmatrix (density is ", density, ")"
# #convert to AdjMatrix
# g.adjMatrix = g.adjacencyMatrix()
# g.capacity = g.V #cuts down capacity automatically
# g.adjList = @[]
# if g.directed: g.inverseAdjList = @[]
# g.representation = AdjMatrix
# of Dense:
# if density <= 0.90:
# # echo "Switching to adjmatrix (density is ", density, ")"
# #convert to AdjMatrix
# g.adjMatrix = g.adjacencyMatrix()
# g.capacity = g.V #cuts down capacity automatically
# g.adjList = @[]
# if g.directed: g.inverseAdjList = @[]
# g.representation = AdjMatrix
proc enlarge(g: var Graph, capNew: int) =
var capNew = capNew
if capNew == 0: capNew = 1
# echo "Graph (", g.representation, ") expanding. Capacity ", g.capacity, " ==> ", capNew
case g.representation:
of AdjMatrix:
var newMatrix: matrix[int]
newMatrix.initMatrix(capNew, capNew)
for v in 0..<g.capacity:
for w in 0..<g.capacity:
newMatrix[v, w] = g.adjMatrix[v, w]
g.adjMatrix = newMatrix
of Sparse:
for v in g.capacity..<capNew:
g.adjList.add initHashSet[int]()
if g.directed:
g.inverseAdjList.add initHashSet[int]()
of Dense:
var all = initHashSet[int]()
for i in 0..<capNew: all.incl i
for v in g.capacity..<capNew:
g.adjList.add all
if g.directed:
g.inverseAdjList.add all
for i in 0..<g.capacity:
for v in g.capacity..<capNew:
g.adjList[i].incl v
if g.directed:
g.inverseAdjList[i].incl v
g.capacity = capNew
proc connect*(g: var Graph, v, w: int) =
if g.contains(v, w) or v==w: return
#verify bounds
while v >= g.capacity or w >= g.capacity:
g.enlarge(g.capacity * 2)
if v >= g.V or w >= g.V:
#increase g.V
g.V = max(v, w) + 1
inc g.E
if g.directed:
case g.representation:
of AdjMatrix:
g.adjMatrix[v, w] = 1
of Sparse:
g.adjList[v].incl w
g.inverseAdjList[w].incl v
of Dense: #remove (v, w) if it exists, and remove (w, v) from inverseAdjList
g.adjList[v].excl w
g.inverseAdjList[w].excl v
else: #undirected
case g.representation:
of AdjMatrix:
g.adjMatrix[v, w] = 1
g.adjMatrix[w, v] = 1
of Sparse:
g.adjList[v].incl w
g.adjList[w].incl v
of Dense: #remove (v, w) and (w, v) if it exists
g.adjList[v].excl(w)
g.adjList[w].excl(v)
g.regularize()
proc disconnect*(g: var Graph, v, w: int) =
if not g.contains(v, w): return
#verify bounds
while v >= g.capacity or w >= g.capacity:
g.enlarge(g.capacity * 2)
if v >= g.V or w >= g.V:
#increase g.V
g.V = max(v, w) + 1
dec g.E
if g.directed:
case g.representation:
of AdjMatrix:
g.adjMatrix[v, w] = 0
of Sparse:
g.adjList[v].excl w
g.inverseAdjList[w].excl v
of Dense:
g.adjList[v].incl w
g.inverseAdjList[w].incl v
else: #undirected
case g.representation:
of AdjMatrix:
g.adjMatrix[v, w] = 0
g.adjMatrix[w, v] = 0
of Sparse:
g.adjList[v].excl w
g.adjList[w].excl v
of Dense: #remove (v, w) and (w, v) if it exists
g.adjList[v].incl(w)
g.adjList[w].incl(v)
g.regularize()
template defineGraph*(graphName: untyped, V: untyped, body: untyped): untyped =
var `graphName` {.inject.} = initGraph(V)
block:
proc `==>`(v, w: int) {.used.} =
`graphName`.connect(v, w)
proc cycle(vs: openArray[int]) {.used.} =
for i in 0..vs.high:
vs[i] ==> vs[(i+1) mod vs.len]
body
template defineDigraph*(graphName: untyped, V: untyped, body: untyped): untyped =
var `graphName` {.inject.} = initDigraph(V)
proc `==>`(v, w: int) {.used.} =
`graphName`.connect(v, w)
proc cycle(vs: openArray[int]) {.used.} =
for i in 0..vs.high:
vs[i] ==> vs[(i+1) mod vs.len]
body
#[
=====================
ADVANCED OPERATIONS
=====================
]#
iterator edges*(g: Graph): (int, int) =
case g.representation:
of AdjMatrix:
for v in 0..<g.V:
var wStart = 0
if not g.directed: wStart = v+1
for w in wStart..<g.V:
if w == v: continue
for i in 1..g.adjMatrix[v, w]: yield (v, w)
of Sparse:
for v in countdown(g.V - 1, 0):
for w in g.adjList[v]:
if w > v or g.directed: yield (v, w)
of Dense:
var forbidden = newSeq[bool](g.V)
for v in 0..<g.V:
for w in 0..<g.V: forbidden[w] = false
for w in g.adjList[v]: forbidden[w] = true
var wStart = 0
if not g.directed: wStart = v+1
for w in wStart..<g.V:
if not forbidden[w]: yield (v, w)
iterator edges*(g: Graph, v: int): int =
##Iterates over the edges (v, w), where you specify v.
case g.representation:
of AdjMatrix:
for w in 0..<g.V:
if w == v: continue
for i in 1..g.adjMatrix[v, w]: yield w
of Sparse:
for w in g.adjList[v]: yield w
of Dense:
var forbidden = newSeq[bool](g.V)
for w in 0..<g.V: forbidden[w] = false
for w in g.adjList[v]: forbidden[w] = true
for w in 0..<g.V:
if not forbidden[w] and w != v: yield w
iterator preimage*(g: Graph, v: int): int =
##Iterates over the edges (w, v), where you specify v.
##This is the same as g.edges(v) if g is undirected.
if not g.directed:
for w in g.edges(v): yield w
case g.representation:
of AdjMatrix:
for w in 0..<g.V:
if w == v: continue
for i in 1..g.adjMatrix[w, v]: yield w
of Sparse:
for w in 0..<g.V:
if v in g.adjList[w]: yield w
of Dense:
var forbidden = newSeq[bool](g.V)
for w in 0..<g.V: forbidden[w] = false
for w in g.inverseAdjList[v]: forbidden[w] = true
for w in 0..<g.V:
if not forbidden[w] and w != v: yield w
proc shortestPathLengths*(g: Graph): matrix[int] =
##result[v, w] is the lowest length path from v to w, or -1 if none exists.
##Uses Floyd-Warshall, runs in O(V^3).
result.initMatrix(g.V, g.V)
for v in 0..<g.V:
for w in 0..<g.V:
result[v, w] = -1
for (v, w) in g.edges:
result[v, w] = 1
for v in 0..<g.V:
result[v, v] = 0
for k in 0..<g.V:
for i in 0..<g.V:
for j in 0..<g.V:
if result[i, k] >= 0 and result[k, j] >= 0 and (result[i, j] < 0 or result[i, j] > result[i, k] + result[k, j]):
result[i, j] = result[i, k] + result[k, j]
proc shortestPathCosts*[T](g: Graph, weights: matrix[T]): matrix[T] =
##Given the set of edge weights, result[v, w] is the lowest cost path from v to w.
##Uses Floyd-Warshall, runs in O(V^3).
result.initMatrix(g.V, g.V)
for v in 0..<g.V:
for w in 0..<g.V:
result[v, w] = Inf
for (v, w) in g.edges:
result[v, w] = weights[v, w]
for v in 0..<g.V:
result[v, v] = weights[0, 0] - weights[0, 0]
for k in 0..<g.V:
for i in 0..<g.V:
for j in 0..<g.V:
if result[i, j] > result[i, k] + result[k, j]:
result[i, j] = result[i, k] + result[k, j]
proc clone*(g: Graph): Graph =
new(result)
result.representation = g.representation
result.directed = g.directed
result.V = g.V
result.capacity = g.capacity
result.E = g.E
case g.representation
of AdjMatrix:
result.adjMatrix = g.adjMatrix.copy
of Sparse, Dense:
result.adjList = g.adjList
proc countEdges(g: Graph): int =
#used internally
case g.representation:
of AdjMatrix:
for u in 0..<g.V:
for v in 0..<g.V:
if g.adjMatrix[u, v] > 0: result += 1
of Sparse:
for u in 0..<g.V:
result += g.adjList[u].len
of Dense:
for u in 0..<g.V:
result += g.V - g.adjList[u].len
if not g.directed: return result shr 1
return result
proc contract*(g: Graph, v, w: int, simple: bool = true): Graph =
var gc = g.clone
if v == w: return gc
var (v, w) = (v, w)
if v > w:
(v, w) = (w, v)
#relabel w as v
#relabel g.V - 1 as w
#track number of edges lost
#[
any edge v -> w or w -> v will be lost.
any edge u -> w will be turned into an edge u -> v.
any edge w -> u will be turned into an edge v -> u.
]#
case gc.representation
of AdjMatrix:
for u in 0..<gc.V:
if u == v: continue
if gc.adjMatrix[w, u] > 0:
gc.adjMatrix[v, u] = 1
if gc.adjMatrix[u, w] > 0:
gc.adjMatrix[u, w] = 0
gc.adjMatrix[u, v] = 1
for u in 0..<gc.V:
gc.adjMatrix[w, u] = gc.adjMatrix[gc.V - 1, u]
gc.adjMatrix[gc.V - 1, u] = 0
of Sparse:
gc.adjList[w].excl v
gc.adjList[v].excl w
for u in gc.adjList[w]:
if u == v: continue
#edge w ==> u
#change it to an edge v ==> u
gc.adjList[v].incl u
for u in 0..<gc.V:
if u == v: continue
if w in gc.adjList[u]:
#edge u ==> w
#change it to an edge u ==> v
gc.adjList[u].excl w
gc.adjList[u].incl v
gc.adjList[w] = gc.adjList[gc.V - 1]
for u in 0..<gc.V:
if gc.V - 1 in gc.adjList[u]:
gc.adjList[u].excl(g.V - 1)
gc.adjList[u].incl w
gc.adjList[gc.V - 1] = initHashSet[int]()
of Dense:
for u in gc.adjList[w]:
#edge w ==> u
#change it to an edge v ==> u
gc.adjList[v].incl u
for u in 0..<g.V:
if w in gc.adjList[u]:
#edge u ==> w
#change it to an edge u ==> v
gc.adjList[u].excl w
gc.adjList[u].incl v
gc.adjList[w] = gc.adjList[gc.V - 1]
gc.adjList[gc.V - 1] = initHashSet[int]()
#TODO for adj and dense
dec gc.V
gc.E = gc.countEdges
return gc
proc `/`*(g: Graph, vw: (int, int)): Graph = g.contract(vw[0], vw[1])
proc `*`*(g, h: Graph): Graph =
##Returns the direct product of the graphs.
if not g.directed and not h.directed:
defineGraph(output, g.V * h.V):
for e1 in g.edges:
for e2 in h.edges:
e1[0] * h.V + e2[0] ==> e1[1] * h.V + e2[1]
e1[1] * h.V + e2[0] ==> e1[0] * h.V + e2[1]
return output
elif g.directed and h.directed:
defineDigraph(output, g.V * h.V):
for e1 in g.edges:
for e2 in h.edges:
e1[0] * h.V + e2[0] ==> e1[1] * h.V + e2[1]
return output
else:
echo "Graph product not yet implemented."
proc directProduct*(g, h: Graph): Graph = g*h
proc cartesianProduct*(g, h: Graph): Graph =
##Returns the cartesian product of the graphs.
if not g.directed and not h.directed:
defineGraph(output, g.V * h.V):
for e1 in g.edges:
for v in 0..<h.V:
e1[0] * h.V + v ==> e1[1] * h.V + v
for e1 in h.edges:
for v in 0..<g.V:
e1[0] + v * h.V ==> e1[1] + v * h.V
return output
elif g.directed and h.directed:
defineDigraph(output, g.V * h.V):
for e1 in g.edges:
for v in 0..<h.V:
e1[0] * h.V + v ==> e1[1] * h.V + v
for e1 in h.edges:
for v in 0..<g.V:
e1[0] + v * h.V ==> e1[1] + v * h.V
return output
proc lexProduct*(g, h: Graph): Graph =
##Returns the lexicographical product of the graphs.
if not g.directed and not h.directed:
defineGraph(output, g.V * h.V):
for e1 in g.edges:
for v in 0..<h.V:
for w in 0..<h.V:
e1[0] * h.V + v ==> e1[1] * h.V + w
for e1 in h.edges:
for v in 0..<g.V:
e1[0] + v * h.V ==> e1[1] + v * h.V
return output
elif g.directed and h.directed:
defineDigraph(output, g.V * h.V):
for e1 in g.edges:
for v in 0..<h.V:
for w in 0..<h.V:
e1[0] * h.V + v ==> e1[1] * h.V + w
for e1 in h.edges:
for v in 0..<g.V:
e1[0] + v * h.V ==> e1[1] + v * h.V
return output
else:
echo "Graph product not yet implemented."
func `+`*(g, h: Graph): Graph =
##Returns the disjoint sum of the graphs g and h.
if g.directed == h.directed:
result = g.clone
for e in h.edges:
result.connect e[0] + g.V, e[1] + g.V
#else: TODO
proc union*(g, h: Graph): Graph =
##Returns the union of the graphs g and h.
if g.directed == h.directed:
result = g.clone
for e in h.edges:
result.connect e[0], e[1]
else:
echo "Graph union not yet implemented."
func complement*(g: Graph): Graph =
##Returns the graph with the same vertices, but with opposite edges.
result = g.clone
case g.representation:
of AdjMatrix:
for v in 0..<g.V:
for w in 0..<g.V:
if w == v: continue
result.adjMatrix[v, w] = 1 - result.adjMatrix[v, w]
of Sparse:
result.representation = Dense
of Dense:
result.representation = Sparse
result.E = ((g.V * (g.V - 1)) shr 1) - g.E
func toDigraph*(g: Graph): Graph =
##Returns the directed version of the graph g.
##If g is directed it will return a clone of g.
if g.directed: return g.clone
case g.representation:
of AdjMatrix:
result = g.clone
result.directed = true
of Sparse:
result = initDigraph(g.V)
for e in g.edges:
result.connect e[0], e[1]
result.connect e[1], e[0]
of Dense:
result = initDenseDigraph(g.V)
#do this manually because there isn't a great g.nonEdges
for v in countdown(g.V - 1, 0):
for w in g.adjList[v]:
result.disconnect v, w
result.disconnect w, v
return result
func toGraph*(g: Graph): Graph = #mostly copied from toDigraph.
##Returns the UNDIRECTED version of the graph g.
##The edge v <-> w will be present iff v ==> w or w ==> v.
##If g is undirected it will return a clone of g.
if not g.directed: return g.clone
case g.representation:
of AdjMatrix:
result = g.clone
for v in 0..<g.V:
for w in v..<g.V:
result.adjMatrix[v, w] = max(result.adjMatrix[v, w], result.adjMatrix[w, v])
result.directed = false
of Sparse:
result = initGraph(g.V)
for e in g.edges:
result.connect e[0], e[1]
of Dense:
result = initDenseGraph(g.V)
#do this manually because there isn't a great g.nonEdges
for v in countdown(g.V - 1, 0):
for w in g.adjList[v]:
result.disconnect v, w
return result
func wolframFormat*(g: Graph): string =
##Converts the graph into a string parseable by Wolfram Language.
result = "Graph[{"
var comma = false
for e in g.edges:
if comma: result &= ","
else: comma = true
if g.directed: result &= $(e[0]) & "->" & $(e[1])
else: result &= $(e[0]) & "<->" & $(e[1])
return result & "}]"
func lineGraph*(g: Graph): Graph =
##This returns a graph (isomorphic to) the graph whose vertices are the edges of the original graph,
##and two new vertices are connected if the corresponding edges are incident at a vertex.
var g = g.toGraph() #forget direction
result = initGraph(g.E)
#label each edge in a matrix
var edgeLabels: matrix[int]
block:
var i = 0
edgeLabels.initMatrix(g.V, g.V)
for r in 0..<g.V:
for c in 0..<g.V:
edgeLabels[r, c] = -1
for e in g.edges:
edgeLabels[e[0], e[1]] = i
edgeLabels[e[1], e[0]] = i
inc i
#edges are labeled
for v in 0..<g.V:
var clique = newSeq[int]()
for e in g.edges(v): clique.add e
for w in 0..clique.high:
for u in w+1..clique.high:
result.connect edgeLabels[v, clique[w]], edgeLabels[v, clique[u]]
return result
func del*(g: Graph, v: int): Graph =
##Returns the graph obtained by removing the vertex v.
##The vertex g.V - 1 will be relabeled.
#TODO
func inducedSubgraph*(g: Graph, vertices: openArray[int]): Graph =
##Returns the graph obtained by keeping only those vertices.
##All vertices will be relabeled.
#TODO
func degreeSequence*(g: Graph): seq[int] =
##Returns the sequence of degrees of the vertices of g.
##The vertex v has degree result[v].
result = newSeq[int](g.V)
case g.representation:
of AdjMatrix:
for v in 0..<g.V:
for w in 0..<g.V:
result[v] += g.adjacencyMatrix[v, w]
of Sparse:
for v in 0..<g.V:
result[v] = g.adjList[v].len
if g.directed: result[v] += g.inverseAdjList[v].len
of Dense:
for v in 0..<g.V:
result[v] = g.V - g.adjList[v].len
func degreeMultiset*(g: Graph): seq[int] =
##Returns the sorted sequence of degrees of the vertices of g.
##In this implementation they are listed from smallest to largest.
result = g.degreeSequence
result.sort
proc connectedComponents*(g: Graph): seq[Graph] =
##Returns a sequence of connected graphs whose disjoint union is isomorphic to g.
result = @[]
var ids = newSeq[int](g.V)
var seen = newSeq[bool](g.V)
for v in 0..<g.V:
if seen[v]: continue
var component: Graph
if g.directed: component = initDigraph(1)
else: component = initGraph(1)
var chunk = initHashSet[int]()
chunk.incl v
#ids[v] = 0
var u = 0
while chunk.len > 0:
var w = chunk.pop
if seen[w]: continue
ids[w] = u
inc u
for z in g.edges(w):
if seen[z]: component.connect ids[w], ids[z]
else:
chunk.incl z
for z in g.preimage(w):
if seen[z]: component.connect ids[z], ids[w]
else: chunk.incl z
seen[w] = true
result.add component
func spanningForest*(g: Graph): seq[Graph] =
##Returns one spanning tree for each connected component of g.
#TODO
func grundyValues*(g: Graph): seq[int] =
##Treat the vertices as states of an impartial game.
##Then this returns the grundy values of the states of the game.
##result[v] = 0 if and only if v is a losing state under normal play.
##This assumes that g is an acyclic directed graph.
if not g.directed:
return @[]
result = newSeq[int](g.V)
var following = newSeq[seq[int]](g.V)
for v in 0..<g.V: following[v] = @[]
var toProcess = initHashSet[int]()
for v in 0..<g.V:
if g.outDegree(v) == 0: toProcess.incl v
while toProcess.len > 0:
for v in toProcess:
if g.outDegree(v) == following[v].len:
#we can compute the grundy value of v and add its preimage
var u = 0
while u in following[v]: inc u
result[v] = u
for w in g.preimage(v):
following[w].add u
toProcess.incl w
toProcess.excl v
break
#[
===============
GRAPH QUERIES
===============
]#
type partial = tuple
assignments: seq[int]
remaining: HashSet[int]
func findSubgraph*(g, h: Graph): seq[int] =
##Returns an isomorphic copy of g inside of h, or @[] if none exists.
##This is usually hard to do.
if g.V > h.V: return @[]
if g.E > h.E: return @[]
if g.E == 0: return @[0] #after this point we know h.E > 0
var states = newSeq[partial]() #a state is an assignment of the first _ vertices of g to vertices of h.
block:
var initial: partial
initial.assignments = @[]
initial.remaining = initHashSet[int]()
for v in 0..<h.V: initial.remaining.incl v
states.add initial
var gdegs = g.degreeSequence
var hdegs = h.degreeSequence
while states.len > 0:
var state = states.pop
for v in state.remaining:
if hdegs[v] >= gdegs[state.assignments.len]:
#try to use v next
var useable = true
for i, u in state.assignments.pairs:
if g.contains(i, state.assignments.len) and not h.contains(state.assignments[i], v):
useable = false
break
if useable:
var stateNew: partial
stateNew.assignments = state.assignments
stateNew.assignments.add v
if stateNew.assignments.len == g.V:
return stateNew.assignments
stateNew.remaining = state.remaining
stateNew.remaining.excl v
states.add stateNew
return @[]
func findInducedSubgraph*(g, h: Graph): seq[int] =
##TODO
return @[]
func findIsomorphism*(g, h: Graph): seq[int] =
##Returns an isomorphism between g and h, or @[] if none exists.
##This is usually hard to do.
if g.V != h.V: return @[]
if g.E != h.E: return @[]
var gdegs = g.degreeMultiset
var hdegs = h.degreeMultiset
for i in 0..<g.V:
if gDegs[i] != hDegs[i]: return @[]
var states = newSeq[partial]() #a state is an assignment of the first _ vertices of g to vertices of h.
block:
var initial: partial
initial.assignments = @[]
initial.remaining = initHashSet[int]()
for v in 0..<h.V: initial.remaining.incl v
states.add initial
gdegs = g.degreeSequence
hdegs = h.degreeSequence
while states.len > 0:
var state = states.pop
for v in state.remaining:
if hdegs[v] == gdegs[state.assignments.len]:
#try to use v next
var useable = true
for i, u in state.assignments.pairs:
if g.contains(i, state.assignments.len) and not h.contains(state.assignments[i], v):
useable = false
break
if useable:
var stateNew: partial
stateNew.assignments = state.assignments
stateNew.assignments.add v
if stateNew.assignments.len == g.V:
return stateNew.assignments
stateNew.remaining = state.remaining
stateNew.remaining.excl v
states.add stateNew
return @[]
func subgraphQ*(g, h: Graph): bool = findSubgraph(g, h).len > 0
func inducedSubgraphQ*(g, h: Graph): bool = findSubgraph(g, h).len > 0 #TODO
func isomorphicQ*(g, h: Graph): bool = findIsomorphism(g, h).len > 0
proc swapVertices*(g: var Graph, v, w: int) =
##Swaps the labels of vertices v and w.
if g.directed:
echo "Not implemented for directed graphs."
quit(-1)
if v == w: return
case g.representation:
of AdjMatrix:
#todo
return
of Sparse:
for u in (g.adjList[v] - g.adjList[w]):
if u == w: continue
g.adjList[u].excl v
g.adjList[u].incl w
for u in (g.adjList[w] - g.adjList[v]):
if u == v: continue
g.adjList[u].excl w
g.adjList[u].incl v
swap g.adjList[v], g.adjList[w]
of Dense:
#todo
return
proc relabel*(g: var Graph) =
##Relabels g so that the degree string is increasing.
var list = g.degreeSequence
var list2 = list.sorted
for i in 0..<g.V:
if list[i] != list2[i]:
for j in i+1..<g.V:
if list[j] == list2[i]:
swap list[i], list[j]
g.swapVertices(i, j)
var chromaticTable* = initTable[string, poly[int64]]()
# var seenGraphs* = newSeq[seq[(Graph, poly[int64])]](12)
proc chromaticPolynomial*(g: Graph): poly[int64] =
if g.directed:
echo "Trying to get chrom poly of directed graph. Exiting."
quit(-1)
if g.E == 0:
return createPoly(@[1'i64]).shift(g.V)
if g.E == 1:
return createPoly(@[-1'i64, 1'i64]).shift(g.V - 1)
if g.E == 2 and g.V == 3:
return createPoly(@[0'i64, 1'i64, -2'i64, 1'i64])
var singletons = 0
# for v in 0..<g.V:
# if g.degree(v) == 0: inc singletons
if g.E == ((g.V - singletons) * (g.V - singletons - 1)) div 2:
var factor = createPoly(@[1'i64]).shift(1)
result = factor
for i in 1..<g.V - singletons:
factor = factor - 1
result = result * factor
return result.shift(singletons)
# var g = g
# if rand(1.0) > 0.01: g.relabel
var id = $g.V
for e in g.edges:
id = id & $e
if chromaticTable.hasKey(id): return chromaticTable[id]
# if g.V < seenGraphs.len:
# for (h, ph) in seenGraphs[g.V]:
# if g.isomorphicQ(h):
# # chromaticTable[id] = ph
# return ph
if g.edgeDensity <= 1.0: #change when dense graphs are implemented:
for v in countdown(g.V - 1, 0):
for w in g.edges(v):
#only need the first edge we find
var h = g.clone
h.disconnect(v, w)
var q = g / (v, w)
var hp = h.chromaticPolynomial
var qp = q.chromaticPolynomial
# quit(-1)
result = hp - qp
chromaticTable[id] = result
# if g.V < seenGraphs.len:
# seenGraphs[g.V].add (g, result)
return result
else:
#find first not-edge
for v in countdown(g.V - 1, 0):
for w in countdown(v - 1, 0):
if not g.contains(v, w):
#only need the first edge we find
var h = g.clone
h.connect(v, w)
var q = g / (v, w)
var hp = h.chromaticPolynomial
var qp = q.chromaticPolynomial
result = hp + qp
chromaticTable[id] = result
return result
# echo g.V, ", ", g.E
var chromaticTableMod = initTable[string, poly[zmod]]()
proc chromaticPolynomialMod*(g: Graph, m: int64): poly[zmod] =
if g.directed:
quit(-1)
if g.E == 0:
return createPoly(@[(1'i64, m)]).shift(g.V)
if g.E == 1:
var x = createPoly(@[(1'i64, m)]).shift(1)
if g.V == 2: return x * (x - (1'i64, m))
return x.shift(g.V - 2) * (x - (1'i64, m))
if g.E == (g.V * (g.V - 1)) div 2:
var factor = createPoly(@[(1'i64, m)]).shift(1)
result = factor
for i in 1..<g.V:
factor = factor - (1'i64, m)
result = result * factor
return result
var id = $g.V
for e in g.edges: id = id & $e
if chromaticTableMod.hasKey(id): return chromaticTableMod[id]
if g.edgeDensity <= 0.5:
for (v, w) in g.edges:
#only need the first edge we find
var h = g.clone
h.disconnect(v, w)
var q = g / (v, w)
var hp = h.chromaticPolynomialMod(m)
var qp = q.chromaticPolynomialMod(m)
# quit(-1)
result = hp - qp
chromaticTableMod[id] = result
return result
else:
#find first not-edge
for v in countdown(g.V - 1, 0):
# if g.degree(v) == 0: continue
for w in countdown(v - 1, 0):
if not g.contains(v, w):
#only need the first edge we find
var h = g.clone
h.connect(v, w)
var q = g / (v, w)
var hp = h.chromaticPolynomialMod(m)
var qp = q.chromaticPolynomialMod(m)
# quit(-1)
result = hp + qp
chromaticTableMod[id] = result
return result
proc chromaticNumber*(g: Graph): int =
##Returns the chromatic number of g.
##Will leave behind chromatic polynomial memoization data.
##Use sanitizeChromaticTables() if you wish to clear it.
var p = g.chromaticPolynomial
result = 1
while (p <~ result) == 0: inc result
proc sanitizeChromaticTables* =
chromaticTable.clear
chromaticTableMod.clear
func acyclicQ*(g: Graph): bool =
##Returns whether g is acyclic (a tree).
if g.E <= 2: return true
var unmarked = initHashSet[int]()
for v in 0..<g.V:
unmarked.incl v
var traversed: matrix[bool]
traversed.initMatrix(g.V, g.V)
while unmarked.len > 0:
var batch = initHashSet[int]()
batch.incl unmarked.pop
while batch.len > 0:
var batchNew = initHashSet[int]()
for v in batch:
for w in g.edges(v):
if not (w in unmarked) and not traversed[v, w]: return false
traversed[v, w] = true
if not g.directed: traversed[w, v] = true
if w in unmarked:
batchNew.incl w
unmarked.excl w
batch = batchNew
return true
func bipartiteQ*(g: Graph): bool =
##Returns whether g is bipartite.
##This is equivalent to g having no odd cycle.
var unmarked = initHashSet[int]()
for v in 0..<g.V:
unmarked.incl v
var red = newSeq[bool](g.V)
while unmarked.len > 0:
var batch = initHashSet[int]()
batch.incl unmarked.pop
while batch.len > 0:
var batchNew = initHashSet[int]()
for v in batch:
for w in g.edges(v):
if w in unmarked: batchNew.incl w
elif red[v] == red[w]: return false
red[w] = not red[v]
unmarked.excl w
batch = batchNew
return true
func shortestPaths*(g: Graph): matrix[seq[int]] =
##Returns a sequence of vertices which form a shortest path from v to w, or an empty seq if none exists.
##result[v, v] will tell you the shortest circuit starting and ending at v.
result.initMatrix(g.V, g.V)
for v in 0..<g.V:
for w in 0..<g.V:
result[v, w] = newSeq[int]()
for (v, w) in g.edges:
result[v, w] = @[v, w]
if not g.directed:
result[w, v] = @[w, v]
#if you uncomment this it will not find the shortest circuits v -> ... -> v.
# for v in 0..<g.V:
# result[v, v] = @[v]
for k in 0..<g.V:
for i in 0..<g.V:
for j in 0..<g.V:
if result[i, k].len > 0 and result[k, j].len > 0 and (result[i, j].len == 0 or result[i, j].len > result[i, k].len + result[k, j].len):
result[i, j] = result[i, k][0..^2] & result[k, j]
proc connectedQ*(g: Graph): bool =
##Returns whether g is connected.
if g.E <= 1: return true
var lengths = g.shortestPaths
for v in 0..<g.V:
for w in 0..<g.V:
if v == w: continue
if lengths[v, w].len == 0:
echo v, ", ", w
return false
return true
func completeGraph*(n: int): Graph = initDenseGraph(n)
proc completeBipartiteGraph*(n: int, m: int): Graph =
defineGraph(output, n+m):
for v in 0..<n:
for w in n..<n+m:
v ==> w
return output
func cycleGraph*(n: int): Graph =
defineGraph(output, n):
for v in 0..<n:
v ==> (v + 1) mod n
return output
func starGraph*(n: int): Graph = completeBipartiteGraph(1, n)
func cubicGraph*(n: int): Graph =
##Returns the n dimensional cube graph.
defineGraph(output, 1 shl n):
for v in 0..<(1 shl n):
var z = 1
for i in 1..n:
v ==> (v xor z)
z = z shl 1
return output
func petersenGraph*: Graph =
defineGraph(output, 10):
cycle [0, 1, 2, 3, 4]
cycle [5, 7, 9, 6, 8]
for i in 0..4:
i ==> i+5
return output
func octahedralGraph*: Graph =
defineGraph(output, 6):
cycle [0, 1, 2]
cycle [3, 4, 5]
for i in 0..2:
i ==> i + 3
i ==> ((i + 1) mod 3) + 3
return output
#generated by Mathematica
func dodecahedralGraph*: Graph =
defineGraph(output, 20):
0 ==> 13
0 ==> 14
0 ==> 15
1 ==> 4
1 ==> 5
1 ==> 12
2 ==> 6
2 ==> 13
2 ==> 18
3 ==> 7
3 ==> 14
3 ==> 19
4 ==> 10
4 ==> 18
5 ==> 11
5 ==> 19
6 ==> 10
6 ==> 15
7 ==> 11
7 ==> 15
8 ==> 9
8 ==> 13
8 ==> 16
9 ==> 14
9 ==> 17
10 ==> 11
12 ==> 16
12 ==> 17
16 ==> 18
17 ==> 19
return output
func icosahedralGraph*: Graph =
defineGraph(output, 12):
0 ==> 2
0 ==> 4
0 ==> 5
0 ==> 8
0 ==> 9
1 ==> 3
1 ==> 6
1 ==> 7
1 ==> 10
1 ==> 11
2 ==> 6
2 ==> 7
2 ==> 8
2 ==> 9
3 ==> 4
3 ==> 5
3 ==> 10
3 ==> 11
4 ==> 5
4 ==> 8
4 ==> 10
5 ==> 9
5 ==> 11
6 ==> 7
6 ==> 8
6 ==> 10
7 ==> 9
7 ==> 11
8 ==> 10
9 ==> 11
return output
func moserSpindleGraph*: Graph =
defineGraph(output, 7):
0 ==> 1
0 ==> 4
0 ==> 6
1 ==> 2
1 ==> 5
2 ==> 3
2 ==> 5
3 ==> 4
3 ==> 5
3 ==> 6
4 ==> 6
return output
func wheelGraph*(n: int): Graph =
result = cycleGraph(n-1)
for v in 0..<(n-1):
result.connect(v, n-1)
func pathGraph*(n: int): Graph =
defineGraph(output, n):
for i in 0..<(n-1):
i ==> i+1
return output
func gridGraph*(r, c: int): Graph =
##Returns the rectangular r x c grid graph.
defineGraph(output, r*c):
for i in 0..<r:
for j in 0..<c:
if i < (r-1):
i*c + j ==> (i+1)*c + j
if j < (c-1):
i*c + j ==> i*c + (j+1)
return output
func rookGraph*(m, n: int): Graph = cartesianProduct(completeGraph(m), completeGraph(n))
#also edge graph of completeBipartiteGraph(m, n)
func torusGridGraph*(m, n: int): Graph = cartesianProduct(cycleGraph(m), cycleGraph(n))
func turanGraph*(n, r: int): Graph =
result = initGraph(1)
var q = n div r
var s = n mod r
var at = 0
for u in 1..s:
#each group is size (q+1)
for v in at..<at + (q+1):
for w in 0..<at:
result.connect(v, w)
for w in at+(q+1)..<n:
result.connect(v, w)
at += q+1
for u in s+1..r:
#each group is size q
for v in at..<at + q:
for w in 0..<at:
result.connect(v, w)
for w in at+q..<n:
result.connect(v, w)
at += q
func prismGraph*(n: int): Graph = pathGraph(2).cartesianProduct(cycleGraph(n))
proc `$`*(g: Graph): string =
##This attempts to name the graph.
##If it can't come up with a name then it will describe its properties.
if g.directed:
#do something else
#TODO
discard 0
result = ""
var components = g.connectedComponents
proc recognizeComponent(h: Graph): string =
#recognize a connected graph
if h.V == 1: return "Singleton"
elif h.V == 2: return "Path[2]"
elif h.V == 3 and h.E == 2: return "Path[3]"
elif h.V == 3 and h.E == 3: return "Triangle"
elif h.V == 4 and h.E == 4 and h.isomorphicQ(cycleGraph(4)): return "Square"
elif h.V == 4 and h.E == 6: return "Tetrahedron (K[4])"
elif h.V == 5 and h.E == 5 and h.isomorphicQ(cycleGraph(5)): return "Pentagon"
elif h.V == 6 and h.E == 6 and h.isomorphicQ(cycleGraph(6)): return "Hexagon"
elif h.V == h.E and h.isomorphicQ(cycleGraph(h.V)): return "C[" & $h.V & "]"
elif h.V == 8 and h.E == 12 and h.isomorphicQ(cubicGraph(3)): return "Cube"
elif h.V == 12 and h.E == 24 and h.isomorphicQ(cubicGraph(3).lineGraph): return "Cuboctahedron"
elif h.V == 16 and h.E == 32 and h.isomorphicQ(cubicGraph(4)): return "Tesseract"
#test for other cube graphs
for l in 5..8:
if h.V == (1 shl l) and h.E == l * (1 shl (l-1)) and h.isomorphicQ(cubicGraph(l)): return $h.V & "-Cube"
if h.V == 4 and h.E == 3 and h.isomorphicQ(starGraph(3)): return "Claw"
elif h.E == h.V - 1 and h.isomorphicQ(starGraph(h.V - 1)): return "Star[" & $(h.V - 1) & "]"
elif h.V == 10 and h.E == 15 and h.isomorphicQ(petersenGraph()): return "Petersen"
elif h.V == 6 and h.E == 12 and h.isomorphicQ(octahedralGraph()): return "Octahedron"
elif h.E == h.V - 1 and h.isomorphicQ(pathGraph(h.V)): return "Path[" & $h.V & "]"
elif h.V == 20 and h.E == 30 and h.isomorphicQ(dodecahedralGraph()): return "Dodecahedron"
elif h.V == 30 and h.E == 60 and h.isomorphicQ(dodecahedralGraph().lineGraph): return "Icosidodecahedron"
elif h.V == 12 and h.E == 30 and h.isomorphicQ(icosahedralGraph()): return "Icosahedron"
elif h.V == 30 and h.E == 120 and h.isomorphicQ(icosahedralGraph().lineGraph): return "Icosahedral Line Graph"
elif h.E == 2 * (h.V - 1) and h.isomorphicQ(wheelGraph(h.V)): return "Wheel[" & $h.V & "]"
elif h.V == 7 and h.E == 11 and h.isomorphicQ(moserSpindleGraph()): return "Moser Spindle"
#test for grid graph
for r in 1..h.V:
if r*r > h.V: break
if h.V mod r == 0:
var c = h.V div r
if h.E == 2*r*c - r - c and h.isomorphicQ(gridGraph(r, c)):
return "Grid[" & $r & ", " & $c & "]"
#test for complete graph
if 2 * h.E == h.V * (h.V - 1): return "K[" & $h.V & "]"
#test for complete bipartite graph
if h.V * h.V - 4 * h.E >= 0:
var u = h.V * h.V - 4 * h.E
var urt = isqrt(u)
if urt * urt == u and (h.V + urt) mod 2 == 0:
var r = (h.V - urt) div 2
var c = (h.V + urt) div 2
if r >= 1 and c >= 1 and h.isomorphicQ(completeBipartiteGraph(r.int, c.int)):
return "CompleteBipartite[" & $r & ", " & $c & "]"
#test for prism
if h.V * 3 == h.E * 2 and h.isomorphicQ(prismGraph(h.V div 2)): return "Prism[" & $(h.V div 2) & "]"
#test for torus grid graph
if h.E == 2 * h.V:
for p in 1..h.V:
if p*p > h.V: break
if h.V mod p == 0:
var q = h.V div p
if h.isomorphicQ(torusGridGraph(p, q)):
return $p & " x " & $q & " Torus Grid Graph / " & $p & " - " & $q & " Duoprism"
#do not test for turan graph. sorry :[
#test for rook graph
for r in 1..h.V:
if r*r > h.V: break
if h.V mod r == 0:
var c = h.V div r
if h.E == ((r*c*(r+c)) div 2) - r*c and h.isomorphicQ(rookGraph(r, c)):
return "Rook[" & $r & ", " & $c & "]"
#hasn't been recognized.. just describe it
result = "V = " & $h.V & ", E = " & $h.E & ", "
if h.bipartiteQ: result &= "Bipartite, "
if h.acyclicQ: result &= "Acyclic, "
result &= "with degree multiset " & $h.degreeMultiset & "."
for i, h in components.pairs:
var part = ""
if components.len > 1: part = "Component " & $i & ": "
part &= recognizeComponent(h)
if i != components.high: part &= "\n"
result &= part
proc characteristicPoly*(g: Graph): poly[int64] =
##Returns the characteristic polynomial of a graph.
var m = g.adjacencyMatrix
castCopyMat(m, m64, int64)
return m64.characteristicPoly
type candidateMIS = tuple
chosen: seq[int]
remaining: HashSet[int]
proc `<`(x, y: candidateMIS): bool = x.chosen.len > y.chosen.len
proc maximalIndependentSet*(g: Graph): seq[int] =
var maxSoFar = 0
var selections: seq[int]
var stk = initHeapQueue[candidateMIS]()
block:
var initial = initHashSet[int]()
for v in 0..<g.V: initial.incl v
stk.push (@[], initial)
while stk.len > 0:
var current = stk.pop
# echo current
if maxSoFar >= current.chosen.len + current.remaining.len: continue
if maxSoFar < current.chosen.len:
maxSoFar = current.chosen.len
selections = current.chosen
if current.remaining.len == 0: continue
var remainingNew = current.remaining
block:
var v = remainingNew.pop
stk.push (current.chosen, remainingNew)
for w in g.edges(v):
remainingNew.excl w
var chosenNew = current.chosen
chosenNew.add v
stk.push (chosenNew, remainingNew)
return selections