|
|
""" |
|
|
======================== |
|
|
Cycle finding algorithms |
|
|
======================== |
|
|
""" |
|
|
|
|
|
from collections import defaultdict |
|
|
|
|
|
import networkx as nx |
|
|
from networkx.utils import not_implemented_for, pairwise |
|
|
|
|
|
__all__ = [ |
|
|
"cycle_basis", |
|
|
"simple_cycles", |
|
|
"recursive_simple_cycles", |
|
|
"find_cycle", |
|
|
"minimum_cycle_basis", |
|
|
] |
|
|
|
|
|
|
|
|
@not_implemented_for("directed") |
|
|
@not_implemented_for("multigraph") |
|
|
def cycle_basis(G, root=None): |
|
|
"""Returns a list of cycles which form a basis for cycles of G. |
|
|
|
|
|
A basis for cycles of a network is a minimal collection of |
|
|
cycles such that any cycle in the network can be written |
|
|
as a sum of cycles in the basis. Here summation of cycles |
|
|
is defined as "exclusive or" of the edges. Cycle bases are |
|
|
useful, e.g. when deriving equations for electric circuits |
|
|
using Kirchhoff's Laws. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : NetworkX Graph |
|
|
root : node, optional |
|
|
Specify starting node for basis. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
A list of cycle lists. Each cycle list is a list of nodes |
|
|
which forms a cycle (loop) in G. |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> G = nx.Graph() |
|
|
>>> nx.add_cycle(G, [0, 1, 2, 3]) |
|
|
>>> nx.add_cycle(G, [0, 3, 4, 5]) |
|
|
>>> print(nx.cycle_basis(G, 0)) |
|
|
[[3, 4, 5, 0], [1, 2, 3, 0]] |
|
|
|
|
|
Notes |
|
|
----- |
|
|
This is adapted from algorithm CACM 491 [1]_. |
|
|
|
|
|
References |
|
|
---------- |
|
|
.. [1] Paton, K. An algorithm for finding a fundamental set of |
|
|
cycles of a graph. Comm. ACM 12, 9 (Sept 1969), 514-518. |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
simple_cycles |
|
|
""" |
|
|
gnodes = set(G.nodes()) |
|
|
cycles = [] |
|
|
while gnodes: |
|
|
if root is None: |
|
|
root = gnodes.pop() |
|
|
stack = [root] |
|
|
pred = {root: root} |
|
|
used = {root: set()} |
|
|
while stack: |
|
|
z = stack.pop() |
|
|
zused = used[z] |
|
|
for nbr in G[z]: |
|
|
if nbr not in used: |
|
|
pred[nbr] = z |
|
|
stack.append(nbr) |
|
|
used[nbr] = {z} |
|
|
elif nbr == z: |
|
|
cycles.append([z]) |
|
|
elif nbr not in zused: |
|
|
pn = used[nbr] |
|
|
cycle = [nbr, z] |
|
|
p = pred[z] |
|
|
while p not in pn: |
|
|
cycle.append(p) |
|
|
p = pred[p] |
|
|
cycle.append(p) |
|
|
cycles.append(cycle) |
|
|
used[nbr].add(z) |
|
|
gnodes -= set(pred) |
|
|
root = None |
|
|
return cycles |
|
|
|
|
|
|
|
|
@not_implemented_for("undirected") |
|
|
def simple_cycles(G): |
|
|
"""Find simple cycles (elementary circuits) of a directed graph. |
|
|
|
|
|
A `simple cycle`, or `elementary circuit`, is a closed path where |
|
|
no node appears twice. Two elementary circuits are distinct if they |
|
|
are not cyclic permutations of each other. |
|
|
|
|
|
This is a nonrecursive, iterator/generator version of Johnson's |
|
|
algorithm [1]_. There may be better algorithms for some cases [2]_ [3]_. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : NetworkX DiGraph |
|
|
A directed graph |
|
|
|
|
|
Yields |
|
|
------ |
|
|
list of nodes |
|
|
Each cycle is represented by a list of nodes along the cycle. |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] |
|
|
>>> G = nx.DiGraph(edges) |
|
|
>>> sorted(nx.simple_cycles(G)) |
|
|
[[0], [0, 1, 2], [0, 2], [1, 2], [2]] |
|
|
|
|
|
To filter the cycles so that they don't include certain nodes or edges, |
|
|
copy your graph and eliminate those nodes or edges before calling. |
|
|
For example, to exclude self-loops from the above example: |
|
|
|
|
|
>>> H = G.copy() |
|
|
>>> H.remove_edges_from(nx.selfloop_edges(G)) |
|
|
>>> sorted(nx.simple_cycles(H)) |
|
|
[[0, 1, 2], [0, 2], [1, 2]] |
|
|
|
|
|
Notes |
|
|
----- |
|
|
The implementation follows pp. 79-80 in [1]_. |
|
|
|
|
|
The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ |
|
|
elementary circuits. |
|
|
|
|
|
References |
|
|
---------- |
|
|
.. [1] Finding all the elementary circuits of a directed graph. |
|
|
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. |
|
|
https://doi.org/10.1137/0204007 |
|
|
.. [2] Enumerating the cycles of a digraph: a new preprocessing strategy. |
|
|
G. Loizou and P. Thanish, Information Sciences, v. 27, 163-182, 1982. |
|
|
.. [3] A search strategy for the elementary cycles of a directed graph. |
|
|
J.L. Szwarcfiter and P.E. Lauer, BIT NUMERICAL MATHEMATICS, |
|
|
v. 16, no. 2, 192-204, 1976. |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
cycle_basis |
|
|
""" |
|
|
|
|
|
def _unblock(thisnode, blocked, B): |
|
|
stack = {thisnode} |
|
|
while stack: |
|
|
node = stack.pop() |
|
|
if node in blocked: |
|
|
blocked.remove(node) |
|
|
stack.update(B[node]) |
|
|
B[node].clear() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
subG = type(G)(G.edges()) |
|
|
sccs = [scc for scc in nx.strongly_connected_components(subG) if len(scc) > 1] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for v in subG: |
|
|
if subG.has_edge(v, v): |
|
|
yield [v] |
|
|
subG.remove_edge(v, v) |
|
|
|
|
|
while sccs: |
|
|
scc = sccs.pop() |
|
|
sccG = subG.subgraph(scc) |
|
|
|
|
|
startnode = scc.pop() |
|
|
|
|
|
path = [startnode] |
|
|
blocked = set() |
|
|
closed = set() |
|
|
blocked.add(startnode) |
|
|
B = defaultdict(set) |
|
|
stack = [(startnode, list(sccG[startnode]))] |
|
|
while stack: |
|
|
thisnode, nbrs = stack[-1] |
|
|
if nbrs: |
|
|
nextnode = nbrs.pop() |
|
|
if nextnode == startnode: |
|
|
yield path[:] |
|
|
closed.update(path) |
|
|
|
|
|
elif nextnode not in blocked: |
|
|
path.append(nextnode) |
|
|
stack.append((nextnode, list(sccG[nextnode]))) |
|
|
closed.discard(nextnode) |
|
|
blocked.add(nextnode) |
|
|
continue |
|
|
|
|
|
if not nbrs: |
|
|
if thisnode in closed: |
|
|
_unblock(thisnode, blocked, B) |
|
|
else: |
|
|
for nbr in sccG[thisnode]: |
|
|
if thisnode not in B[nbr]: |
|
|
B[nbr].add(thisnode) |
|
|
stack.pop() |
|
|
|
|
|
path.pop() |
|
|
|
|
|
H = subG.subgraph(scc) |
|
|
sccs.extend(scc for scc in nx.strongly_connected_components(H) if len(scc) > 1) |
|
|
|
|
|
|
|
|
@not_implemented_for("undirected") |
|
|
def recursive_simple_cycles(G): |
|
|
"""Find simple cycles (elementary circuits) of a directed graph. |
|
|
|
|
|
A `simple cycle`, or `elementary circuit`, is a closed path where |
|
|
no node appears twice. Two elementary circuits are distinct if they |
|
|
are not cyclic permutations of each other. |
|
|
|
|
|
This version uses a recursive algorithm to build a list of cycles. |
|
|
You should probably use the iterator version called simple_cycles(). |
|
|
Warning: This recursive version uses lots of RAM! |
|
|
It appears in NetworkX for pedagogical value. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : NetworkX DiGraph |
|
|
A directed graph |
|
|
|
|
|
Returns |
|
|
------- |
|
|
A list of cycles, where each cycle is represented by a list of nodes |
|
|
along the cycle. |
|
|
|
|
|
Example: |
|
|
|
|
|
>>> edges = [(0, 0), (0, 1), (0, 2), (1, 2), (2, 0), (2, 1), (2, 2)] |
|
|
>>> G = nx.DiGraph(edges) |
|
|
>>> nx.recursive_simple_cycles(G) |
|
|
[[0], [2], [0, 1, 2], [0, 2], [1, 2]] |
|
|
|
|
|
Notes |
|
|
----- |
|
|
The implementation follows pp. 79-80 in [1]_. |
|
|
|
|
|
The time complexity is $O((n+e)(c+1))$ for $n$ nodes, $e$ edges and $c$ |
|
|
elementary circuits. |
|
|
|
|
|
References |
|
|
---------- |
|
|
.. [1] Finding all the elementary circuits of a directed graph. |
|
|
D. B. Johnson, SIAM Journal on Computing 4, no. 1, 77-84, 1975. |
|
|
https://doi.org/10.1137/0204007 |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
simple_cycles, cycle_basis |
|
|
""" |
|
|
|
|
|
def _unblock(thisnode): |
|
|
"""Recursively unblock and remove nodes from B[thisnode].""" |
|
|
if blocked[thisnode]: |
|
|
blocked[thisnode] = False |
|
|
while B[thisnode]: |
|
|
_unblock(B[thisnode].pop()) |
|
|
|
|
|
def circuit(thisnode, startnode, component): |
|
|
closed = False |
|
|
path.append(thisnode) |
|
|
blocked[thisnode] = True |
|
|
for nextnode in component[thisnode]: |
|
|
if nextnode == startnode: |
|
|
result.append(path[:]) |
|
|
closed = True |
|
|
elif not blocked[nextnode]: |
|
|
if circuit(nextnode, startnode, component): |
|
|
closed = True |
|
|
if closed: |
|
|
_unblock(thisnode) |
|
|
else: |
|
|
for nextnode in component[thisnode]: |
|
|
if thisnode not in B[nextnode]: |
|
|
B[nextnode].append(thisnode) |
|
|
path.pop() |
|
|
return closed |
|
|
|
|
|
path = [] |
|
|
blocked = defaultdict(bool) |
|
|
B = defaultdict(list) |
|
|
result = [] |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for v in G: |
|
|
if G.has_edge(v, v): |
|
|
result.append([v]) |
|
|
G.remove_edge(v, v) |
|
|
|
|
|
|
|
|
|
|
|
ordering = dict(zip(G, range(len(G)))) |
|
|
for s in ordering: |
|
|
|
|
|
subgraph = G.subgraph(node for node in G if ordering[node] >= ordering[s]) |
|
|
|
|
|
|
|
|
strongcomp = nx.strongly_connected_components(subgraph) |
|
|
mincomp = min(strongcomp, key=lambda ns: min(ordering[n] for n in ns)) |
|
|
component = G.subgraph(mincomp) |
|
|
if len(component) > 1: |
|
|
|
|
|
startnode = min(component, key=ordering.__getitem__) |
|
|
for node in component: |
|
|
blocked[node] = False |
|
|
B[node][:] = [] |
|
|
dummy = circuit(startnode, startnode, component) |
|
|
return result |
|
|
|
|
|
|
|
|
def find_cycle(G, source=None, orientation=None): |
|
|
"""Returns a cycle found via depth-first traversal. |
|
|
|
|
|
The cycle is a list of edges indicating the cyclic path. |
|
|
Orientation of directed edges is controlled by `orientation`. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : graph |
|
|
A directed/undirected graph/multigraph. |
|
|
|
|
|
source : node, list of nodes |
|
|
The node from which the traversal begins. If None, then a source |
|
|
is chosen arbitrarily and repeatedly until all edges from each node in |
|
|
the graph are searched. |
|
|
|
|
|
orientation : None | 'original' | 'reverse' | 'ignore' (default: None) |
|
|
For directed graphs and directed multigraphs, edge traversals need not |
|
|
respect the original orientation of the edges. |
|
|
When set to 'reverse' every edge is traversed in the reverse direction. |
|
|
When set to 'ignore', every edge is treated as undirected. |
|
|
When set to 'original', every edge is treated as directed. |
|
|
In all three cases, the yielded edge tuples add a last entry to |
|
|
indicate the direction in which that edge was traversed. |
|
|
If orientation is None, the yielded edge has no direction indicated. |
|
|
The direction is respected, but not reported. |
|
|
|
|
|
Returns |
|
|
------- |
|
|
edges : directed edges |
|
|
A list of directed edges indicating the path taken for the loop. |
|
|
If no cycle is found, then an exception is raised. |
|
|
For graphs, an edge is of the form `(u, v)` where `u` and `v` |
|
|
are the tail and head of the edge as determined by the traversal. |
|
|
For multigraphs, an edge is of the form `(u, v, key)`, where `key` is |
|
|
the key of the edge. When the graph is directed, then `u` and `v` |
|
|
are always in the order of the actual directed edge. |
|
|
If orientation is not None then the edge tuple is extended to include |
|
|
the direction of traversal ('forward' or 'reverse') on that edge. |
|
|
|
|
|
Raises |
|
|
------ |
|
|
NetworkXNoCycle |
|
|
If no cycle was found. |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
In this example, we construct a DAG and find, in the first call, that there |
|
|
are no directed cycles, and so an exception is raised. In the second call, |
|
|
we ignore edge orientations and find that there is an undirected cycle. |
|
|
Note that the second call finds a directed cycle while effectively |
|
|
traversing an undirected graph, and so, we found an "undirected cycle". |
|
|
This means that this DAG structure does not form a directed tree (which |
|
|
is also known as a polytree). |
|
|
|
|
|
>>> G = nx.DiGraph([(0, 1), (0, 2), (1, 2)]) |
|
|
>>> nx.find_cycle(G, orientation="original") |
|
|
Traceback (most recent call last): |
|
|
... |
|
|
networkx.exception.NetworkXNoCycle: No cycle found. |
|
|
>>> list(nx.find_cycle(G, orientation="ignore")) |
|
|
[(0, 1, 'forward'), (1, 2, 'forward'), (0, 2, 'reverse')] |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
simple_cycles |
|
|
""" |
|
|
if not G.is_directed() or orientation in (None, "original"): |
|
|
|
|
|
def tailhead(edge): |
|
|
return edge[:2] |
|
|
|
|
|
elif orientation == "reverse": |
|
|
|
|
|
def tailhead(edge): |
|
|
return edge[1], edge[0] |
|
|
|
|
|
elif orientation == "ignore": |
|
|
|
|
|
def tailhead(edge): |
|
|
if edge[-1] == "reverse": |
|
|
return edge[1], edge[0] |
|
|
return edge[:2] |
|
|
|
|
|
explored = set() |
|
|
cycle = [] |
|
|
final_node = None |
|
|
for start_node in G.nbunch_iter(source): |
|
|
if start_node in explored: |
|
|
|
|
|
continue |
|
|
|
|
|
edges = [] |
|
|
|
|
|
seen = {start_node} |
|
|
|
|
|
active_nodes = {start_node} |
|
|
previous_head = None |
|
|
|
|
|
for edge in nx.edge_dfs(G, start_node, orientation): |
|
|
|
|
|
tail, head = tailhead(edge) |
|
|
if head in explored: |
|
|
|
|
|
continue |
|
|
if previous_head is not None and tail != previous_head: |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
while True: |
|
|
try: |
|
|
popped_edge = edges.pop() |
|
|
except IndexError: |
|
|
edges = [] |
|
|
active_nodes = {tail} |
|
|
break |
|
|
else: |
|
|
popped_head = tailhead(popped_edge)[1] |
|
|
active_nodes.remove(popped_head) |
|
|
|
|
|
if edges: |
|
|
last_head = tailhead(edges[-1])[1] |
|
|
if tail == last_head: |
|
|
break |
|
|
edges.append(edge) |
|
|
|
|
|
if head in active_nodes: |
|
|
|
|
|
cycle.extend(edges) |
|
|
final_node = head |
|
|
break |
|
|
else: |
|
|
seen.add(head) |
|
|
active_nodes.add(head) |
|
|
previous_head = head |
|
|
|
|
|
if cycle: |
|
|
break |
|
|
else: |
|
|
explored.update(seen) |
|
|
|
|
|
else: |
|
|
assert len(cycle) == 0 |
|
|
raise nx.exception.NetworkXNoCycle("No cycle found.") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
for i, edge in enumerate(cycle): |
|
|
tail, head = tailhead(edge) |
|
|
if tail == final_node: |
|
|
break |
|
|
|
|
|
return cycle[i:] |
|
|
|
|
|
|
|
|
@not_implemented_for("directed") |
|
|
@not_implemented_for("multigraph") |
|
|
def minimum_cycle_basis(G, weight=None): |
|
|
"""Returns a minimum weight cycle basis for G |
|
|
|
|
|
Minimum weight means a cycle basis for which the total weight |
|
|
(length for unweighted graphs) of all the cycles is minimum. |
|
|
|
|
|
Parameters |
|
|
---------- |
|
|
G : NetworkX Graph |
|
|
weight: string |
|
|
name of the edge attribute to use for edge weights |
|
|
|
|
|
Returns |
|
|
------- |
|
|
A list of cycle lists. Each cycle list is a list of nodes |
|
|
which forms a cycle (loop) in G. Note that the nodes are not |
|
|
necessarily returned in a order by which they appear in the cycle |
|
|
|
|
|
Examples |
|
|
-------- |
|
|
>>> G = nx.Graph() |
|
|
>>> nx.add_cycle(G, [0, 1, 2, 3]) |
|
|
>>> nx.add_cycle(G, [0, 3, 4, 5]) |
|
|
>>> print([sorted(c) for c in nx.minimum_cycle_basis(G)]) |
|
|
[[0, 1, 2, 3], [0, 3, 4, 5]] |
|
|
|
|
|
References: |
|
|
[1] Kavitha, Telikepalli, et al. "An O(m^2n) Algorithm for |
|
|
Minimum Cycle Basis of Graphs." |
|
|
http://link.springer.com/article/10.1007/s00453-007-9064-z |
|
|
[2] de Pina, J. 1995. Applications of shortest path methods. |
|
|
Ph.D. thesis, University of Amsterdam, Netherlands |
|
|
|
|
|
See Also |
|
|
-------- |
|
|
simple_cycles, cycle_basis |
|
|
""" |
|
|
|
|
|
return sum( |
|
|
(_min_cycle_basis(G.subgraph(c), weight) for c in nx.connected_components(G)), |
|
|
[], |
|
|
) |
|
|
|
|
|
|
|
|
def _min_cycle_basis(comp, weight): |
|
|
cb = [] |
|
|
|
|
|
|
|
|
|
|
|
spanning_tree_edges = list(nx.minimum_spanning_edges(comp, weight=None, data=False)) |
|
|
edges_excl = [frozenset(e) for e in comp.edges() if e not in spanning_tree_edges] |
|
|
N = len(edges_excl) |
|
|
|
|
|
|
|
|
set_orth = [{edge} for edge in edges_excl] |
|
|
for k in range(N): |
|
|
|
|
|
new_cycle = _min_cycle(comp, set_orth[k], weight=weight) |
|
|
cb.append(list(set().union(*new_cycle))) |
|
|
|
|
|
|
|
|
base = set_orth[k] |
|
|
set_orth[k + 1 :] = [ |
|
|
orth ^ base if len(orth & new_cycle) % 2 else orth |
|
|
for orth in set_orth[k + 1 :] |
|
|
] |
|
|
return cb |
|
|
|
|
|
|
|
|
def _min_cycle(G, orth, weight=None): |
|
|
""" |
|
|
Computes the minimum weight cycle in G, |
|
|
orthogonal to the vector orth as per [p. 338, 1] |
|
|
""" |
|
|
T = nx.Graph() |
|
|
|
|
|
nodes_idx = {node: idx for idx, node in enumerate(G.nodes())} |
|
|
idx_nodes = {idx: node for node, idx in nodes_idx.items()} |
|
|
|
|
|
nnodes = len(nodes_idx) |
|
|
|
|
|
|
|
|
|
|
|
for u, v, data in G.edges(data=True): |
|
|
uidx, vidx = nodes_idx[u], nodes_idx[v] |
|
|
edge_w = data.get(weight, 1) |
|
|
if frozenset((u, v)) in orth: |
|
|
T.add_edges_from( |
|
|
[(uidx, nnodes + vidx), (nnodes + uidx, vidx)], weight=edge_w |
|
|
) |
|
|
else: |
|
|
T.add_edges_from( |
|
|
[(uidx, vidx), (nnodes + uidx, nnodes + vidx)], weight=edge_w |
|
|
) |
|
|
|
|
|
all_shortest_pathlens = dict(nx.shortest_path_length(T, weight=weight)) |
|
|
cross_paths_w_lens = { |
|
|
n: all_shortest_pathlens[n][nnodes + n] for n in range(nnodes) |
|
|
} |
|
|
|
|
|
|
|
|
start = min(cross_paths_w_lens, key=cross_paths_w_lens.get) |
|
|
end = nnodes + start |
|
|
min_path = nx.shortest_path(T, source=start, target=end, weight="weight") |
|
|
|
|
|
|
|
|
min_path_nodes = [node if node < nnodes else node - nnodes for node in min_path] |
|
|
|
|
|
mcycle_pruned = _path_to_cycle(min_path_nodes) |
|
|
|
|
|
return {frozenset((idx_nodes[u], idx_nodes[v])) for u, v in mcycle_pruned} |
|
|
|
|
|
|
|
|
def _path_to_cycle(path): |
|
|
""" |
|
|
Removes the edges from path that occur even number of times. |
|
|
Returns a set of edges |
|
|
""" |
|
|
edges = set() |
|
|
for edge in pairwise(path): |
|
|
|
|
|
edges ^= {edge} |
|
|
return edges |
|
|
|