I need a working algorithm for finding all simple cycles in an undirected graph. I know the cost can be exponential and the problem is NP-complete, but I am going to use it in a small graph (up to 20-30 vertices) and the cycles are small in number.
The following is a demo implementation in Python based on depth first search.
An outer loop scans all nodes of the graph and starts a search from every node. Node neighbours (according to the list of edges) are added to the cycle path. Recursion ends if no more non-visited neighbours can be added. A new cycle is found if the path is longer than two nodes and the next neighbour is the start of the path. To avoid duplicate cycles, the cycles are normalized by rotating the smallest node to the start. Cycles in inverted ordering are also taken into account.
This is just a naive implementation. The classical paper is: Donald B. Johnson. Finding all the elementary circuits of a directed graph. SIAM J. Comput., 4(1):77–84, 1975.
Python
graph = [[1, 3], [1, 4], [1, 5],[1,6], [2, 3],
[2, 4],[2, 5], [2, 6], [3, 5], [3,6],
[4, 5], [4, 6]]
cycles = []
def main():
global graph
global cycles
for edge in graph:
for node in edge:
findNewCycles([node])
for cy in cycles:
path = [str(node) for node in cy]
s = ",".join(path)
print(s)
def findNewCycles(path):
start_node = path[0]
next_node= None
sub = []
#visit each edge and each node of each edge
for edge in graph:
node1, node2 = edge
if start_node in edge:
if node1 == start_node:
next_node = node2
else:
next_node = node1
if not visited(next_node, path):
# neighbor node not on path yet
sub = [next_node]
sub.extend(path)
# explore extended path
findNewCycles(sub);
elif len(path) > 2 and next_node == path[-1]:
# cycle found
p = rotate_to_smallest(path);
inv = invert(p)
if isNew(p) and isNew(inv):
cycles.append(p)
def invert(path):
return rotate_to_smallest(path[::-1])
# rotate cycle path such that it begins with the smallest node
def rotate_to_smallest(path):
n = path.index(min(path))
return path[n:]+path[:n]
def isNew(path):
return not path in cycles
def visited(node, path):
return node in path
main()
print(len(cycles))
1,4,2,3
1,5,4,2,3
1,6,4,2,3
1,5,2,3
1,4,5,2,3
1,6,4,5,2,3
1,6,2,3
1,4,6,2,3
1,5,4,6,2,3
1,5,3
1,4,2,5,3
1,6,4,2,5,3
1,6,2,5,3
1,4,6,2,5,3
1,4,5,3
1,6,2,4,5,3
1,6,4,5,3
1,6,3
1,4,2,6,3
1,5,4,2,6,3
1,5,2,6,3
1,4,5,2,6,3
1,4,6,3
1,5,2,4,6,3
1,5,4,6,3
1,5,3,2,4
1,6,3,2,4
1,5,2,4
1,6,3,5,2,4
1,6,2,4
1,5,3,6,2,4
1,5,4
1,6,3,2,5,4
1,6,2,5,4
1,6,2,3,5,4
1,6,3,5,4
1,6,4
1,5,3,2,6,4
1,5,2,6,4
1,5,2,3,6,4
1,5,3,6,4
1,6,3,2,5
1,6,4,2,5
1,6,2,5
1,6,4,2,3,5
1,6,2,3,5
1,6,3,5
1,6,3,2,4,5
1,6,2,4,5
1,6,4,5
2,3,5,4
2,3,6,4
2,3,5
2,3,6,4,5
2,3,6
2,3,5,4,6
2,5,3,6,4
2,5,3,6
2,4,5,3,6
3,6,4,5
2,4,5
2,4,6
2,5,4,6
63
矩阵方法 Maple
Obviously the number of cycles of length k is closely related to the number of walks of length k that end at their own starting point. Clearly, this number of walks is the trace of the k t h k^{th} kth power of the adjacency matrix. It only remains to remove the walks that aren’t cycles and to divide out the 2*k permutations of each cycle. This formula does that:
#Number of k-cycles for a given adjacency matrix A
kCycles:= (A::Matrix, n::posint, k::And(posint, Not({1,2})))->
add(
(-1)^(k-i)*binomial(n-i,n-k) *
add(LinearAlgebra:-Trace(A[S,S]^k), S= combinat:-choose(n,i)),
i= 2..k
)/2/k
:
A:= GraphTheory:-AdjacencyMatrix(GraphTheory:-SpecialGraphs:-OctahedronGraph()):
n:= 6: #number of vertices
seq(kCycles(A, n, k), k= 3..6);
8, 15, 24, 16
add([%]); #total number of cycles
63
These results agree with VV’s explicit list of the cycles of this graph.
My little procedure above will gain a huge efficiency improvement by using remember tables for the traces, but I don’t have time right now. If someone else chooses to undertake this, recall that a remember table cannot be indexed correctly by a mutable structure like a Matrix. So, use S and k as the table indices.