749 lines
27 KiB
Cython
749 lines
27 KiB
Cython
"""
|
|
Routines for traversing graphs in compressed sparse format
|
|
"""
|
|
|
|
# Author: Jake Vanderplas -- <vanderplas@astro.washington.edu>
|
|
# License: BSD, (C) 2012
|
|
|
|
import numpy as np
|
|
cimport numpy as np
|
|
|
|
from scipy.sparse import csr_matrix, isspmatrix, isspmatrix_csr, isspmatrix_csc
|
|
from ._graph_validation import validate_graph
|
|
from ._graph_tools import reconstruct_path
|
|
|
|
cimport cython
|
|
from libc cimport stdlib
|
|
|
|
DTYPE = np.float64
|
|
ctypedef np.float64_t DTYPE_t
|
|
|
|
ITYPE = np.int32
|
|
ctypedef np.int32_t ITYPE_t
|
|
|
|
# EPS is the precision of DTYPE
|
|
cdef DTYPE_t DTYPE_EPS = 1E-15
|
|
|
|
# NULL_IDX is the index used in predecessor matrices to store a non-path
|
|
cdef ITYPE_t NULL_IDX = -9999
|
|
|
|
def connected_components(csgraph, directed=True, connection='weak',
|
|
return_labels=True):
|
|
"""
|
|
connected_components(csgraph, directed=True, connection='weak',
|
|
return_labels=True)
|
|
|
|
Analyze the connected components of a sparse graph
|
|
|
|
.. versionadded:: 0.11.0
|
|
|
|
Parameters
|
|
----------
|
|
csgraph : array_like or sparse matrix
|
|
The N x N matrix representing the compressed sparse graph. The input
|
|
csgraph will be converted to csr format for the calculation.
|
|
directed : bool, optional
|
|
If True (default), then operate on a directed graph: only
|
|
move from point i to point j along paths csgraph[i, j].
|
|
If False, then find the shortest path on an undirected graph: the
|
|
algorithm can progress from point i to j along csgraph[i, j] or
|
|
csgraph[j, i].
|
|
connection : str, optional
|
|
['weak'|'strong']. For directed graphs, the type of connection to
|
|
use. Nodes i and j are strongly connected if a path exists both
|
|
from i to j and from j to i. Nodes i and j are weakly connected if
|
|
only one of these paths exists. If directed == False, this keyword
|
|
is not referenced.
|
|
return_labels : str, optional
|
|
If True (default), then return the labels for each of the connected
|
|
components.
|
|
|
|
Returns
|
|
-------
|
|
n_components: int
|
|
The number of connected components.
|
|
labels: ndarray
|
|
The length-N array of labels of the connected components.
|
|
"""
|
|
if connection.lower() not in ['weak', 'strong']:
|
|
raise ValueError("connection must be 'weak' or 'strong'")
|
|
|
|
# weak connections <=> components of undirected graph
|
|
if connection.lower() == 'weak':
|
|
directed = False
|
|
|
|
csgraph = validate_graph(csgraph, directed,
|
|
dense_output=False)
|
|
|
|
labels = np.empty(csgraph.shape[0], dtype=ITYPE)
|
|
labels.fill(NULL_IDX)
|
|
|
|
if directed:
|
|
n_components = _connected_components_directed(csgraph.indices,
|
|
csgraph.indptr,
|
|
labels)
|
|
else:
|
|
csgraph_T = csgraph.T.tocsr()
|
|
n_components = _connected_components_undirected(csgraph.indices,
|
|
csgraph.indptr,
|
|
csgraph_T.indices,
|
|
csgraph_T.indptr,
|
|
labels)
|
|
|
|
if return_labels:
|
|
return n_components, labels
|
|
else:
|
|
return n_components
|
|
|
|
|
|
def breadth_first_tree(csgraph, i_start, directed=True):
|
|
r"""
|
|
breadth_first_tree(csgraph, i_start, directed=True)
|
|
|
|
Return the tree generated by a breadth-first search
|
|
|
|
Note that a breadth-first tree from a specified node is unique.
|
|
|
|
.. versionadded:: 0.11.0
|
|
|
|
Parameters
|
|
----------
|
|
csgraph : array_like or sparse matrix
|
|
The N x N matrix representing the compressed sparse graph. The input
|
|
csgraph will be converted to csr format for the calculation.
|
|
i_start : int
|
|
The index of starting node.
|
|
directed : bool, optional
|
|
If True (default), then operate on a directed graph: only
|
|
move from point i to point j along paths csgraph[i, j].
|
|
If False, then find the shortest path on an undirected graph: the
|
|
algorithm can progress from point i to j along csgraph[i, j] or
|
|
csgraph[j, i].
|
|
|
|
Returns
|
|
-------
|
|
cstree : csr matrix
|
|
The N x N directed compressed-sparse representation of the breadth-
|
|
first tree drawn from csgraph, starting at the specified node.
|
|
|
|
Examples
|
|
--------
|
|
The following example shows the computation of a depth-first tree
|
|
over a simple four-component graph, starting at node 0::
|
|
|
|
input graph breadth first tree from (0)
|
|
|
|
(0) (0)
|
|
/ \ / \
|
|
3 8 3 8
|
|
/ \ / \
|
|
(3)---5---(1) (3) (1)
|
|
\ / /
|
|
6 2 2
|
|
\ / /
|
|
(2) (2)
|
|
|
|
In compressed sparse representation, the solution looks like this:
|
|
|
|
>>> from scipy.sparse import csr_matrix
|
|
>>> from scipy.sparse.csgraph import breadth_first_tree
|
|
>>> X = csr_matrix([[0, 8, 0, 3],
|
|
... [0, 0, 2, 5],
|
|
... [0, 0, 0, 6],
|
|
... [0, 0, 0, 0]])
|
|
>>> Tcsr = breadth_first_tree(X, 0, directed=False)
|
|
>>> Tcsr.toarray().astype(int)
|
|
array([[0, 8, 0, 3],
|
|
[0, 0, 2, 0],
|
|
[0, 0, 0, 0],
|
|
[0, 0, 0, 0]])
|
|
|
|
Note that the resulting graph is a Directed Acyclic Graph which spans
|
|
the graph. A breadth-first tree from a given node is unique.
|
|
"""
|
|
node_list, predecessors = breadth_first_order(csgraph, i_start,
|
|
directed, True)
|
|
return reconstruct_path(csgraph, predecessors, directed)
|
|
|
|
|
|
def depth_first_tree(csgraph, i_start, directed=True):
|
|
r"""
|
|
depth_first_tree(csgraph, i_start, directed=True)
|
|
|
|
Return a tree generated by a depth-first search.
|
|
|
|
Note that a tree generated by a depth-first search is not unique:
|
|
it depends on the order that the children of each node are searched.
|
|
|
|
.. versionadded:: 0.11.0
|
|
|
|
Parameters
|
|
----------
|
|
csgraph : array_like or sparse matrix
|
|
The N x N matrix representing the compressed sparse graph. The input
|
|
csgraph will be converted to csr format for the calculation.
|
|
i_start : int
|
|
The index of starting node.
|
|
directed : bool, optional
|
|
If True (default), then operate on a directed graph: only
|
|
move from point i to point j along paths csgraph[i, j].
|
|
If False, then find the shortest path on an undirected graph: the
|
|
algorithm can progress from point i to j along csgraph[i, j] or
|
|
csgraph[j, i].
|
|
|
|
Returns
|
|
-------
|
|
cstree : csr matrix
|
|
The N x N directed compressed-sparse representation of the depth-
|
|
first tree drawn from csgraph, starting at the specified node.
|
|
|
|
Examples
|
|
--------
|
|
The following example shows the computation of a depth-first tree
|
|
over a simple four-component graph, starting at node 0::
|
|
|
|
input graph depth first tree from (0)
|
|
|
|
(0) (0)
|
|
/ \ \
|
|
3 8 8
|
|
/ \ \
|
|
(3)---5---(1) (3) (1)
|
|
\ / \ /
|
|
6 2 6 2
|
|
\ / \ /
|
|
(2) (2)
|
|
|
|
In compressed sparse representation, the solution looks like this:
|
|
|
|
>>> from scipy.sparse import csr_matrix
|
|
>>> from scipy.sparse.csgraph import depth_first_tree
|
|
>>> X = csr_matrix([[0, 8, 0, 3],
|
|
... [0, 0, 2, 5],
|
|
... [0, 0, 0, 6],
|
|
... [0, 0, 0, 0]])
|
|
>>> Tcsr = depth_first_tree(X, 0, directed=False)
|
|
>>> Tcsr.toarray().astype(int)
|
|
array([[0, 8, 0, 0],
|
|
[0, 0, 2, 0],
|
|
[0, 0, 0, 6],
|
|
[0, 0, 0, 0]])
|
|
|
|
Note that the resulting graph is a Directed Acyclic Graph which spans
|
|
the graph. Unlike a breadth-first tree, a depth-first tree of a given
|
|
graph is not unique if the graph contains cycles. If the above solution
|
|
had begun with the edge connecting nodes 0 and 3, the result would have
|
|
been different.
|
|
"""
|
|
node_list, predecessors = depth_first_order(csgraph, i_start,
|
|
directed, True)
|
|
return reconstruct_path(csgraph, predecessors, directed)
|
|
|
|
|
|
def breadth_first_order(csgraph, i_start,
|
|
directed=True, return_predecessors=True):
|
|
"""
|
|
breadth_first_order(csgraph, i_start, directed=True, return_predecessors=True)
|
|
|
|
Return a breadth-first ordering starting with specified node.
|
|
|
|
Note that a breadth-first order is not unique, but the tree which it
|
|
generates is unique.
|
|
|
|
.. versionadded:: 0.11.0
|
|
|
|
Parameters
|
|
----------
|
|
csgraph : array_like or sparse matrix
|
|
The N x N compressed sparse graph. The input csgraph will be
|
|
converted to csr format for the calculation.
|
|
i_start : int
|
|
The index of starting node.
|
|
directed : bool, optional
|
|
If True (default), then operate on a directed graph: only
|
|
move from point i to point j along paths csgraph[i, j].
|
|
If False, then find the shortest path on an undirected graph: the
|
|
algorithm can progress from point i to j along csgraph[i, j] or
|
|
csgraph[j, i].
|
|
return_predecessors : bool, optional
|
|
If True (default), then return the predecesor array (see below).
|
|
|
|
Returns
|
|
-------
|
|
node_array : ndarray, one dimension
|
|
The breadth-first list of nodes, starting with specified node. The
|
|
length of node_array is the number of nodes reachable from the
|
|
specified node.
|
|
predecessors : ndarray, one dimension
|
|
Returned only if return_predecessors is True.
|
|
The length-N list of predecessors of each node in a breadth-first
|
|
tree. If node i is in the tree, then its parent is given by
|
|
predecessors[i]. If node i is not in the tree (and for the parent
|
|
node) then predecessors[i] = -9999.
|
|
"""
|
|
global NULL_IDX
|
|
csgraph = validate_graph(csgraph, directed, dense_output=False)
|
|
cdef int N = csgraph.shape[0]
|
|
|
|
cdef np.ndarray node_list = np.empty(N, dtype=ITYPE)
|
|
cdef np.ndarray predecessors = np.empty(N, dtype=ITYPE)
|
|
node_list.fill(NULL_IDX)
|
|
predecessors.fill(NULL_IDX)
|
|
|
|
if directed:
|
|
length = _breadth_first_directed(i_start,
|
|
csgraph.indices, csgraph.indptr,
|
|
node_list, predecessors)
|
|
else:
|
|
csgraph_T = csgraph.T.tocsr()
|
|
length = _breadth_first_undirected(i_start,
|
|
csgraph.indices, csgraph.indptr,
|
|
csgraph_T.indices, csgraph_T.indptr,
|
|
node_list, predecessors)
|
|
|
|
if return_predecessors:
|
|
return node_list[:length], predecessors
|
|
else:
|
|
return node_list[:length]
|
|
|
|
|
|
cdef unsigned int _breadth_first_directed(
|
|
unsigned int head_node,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors):
|
|
# Inputs:
|
|
# head_node: (input) index of the node from which traversal starts
|
|
# indices: (input) CSR indices of graph
|
|
# indptr: (input) CSR indptr of graph
|
|
# node_list: (output) breadth-first list of nodes
|
|
# predecessors: (output) list of predecessors of nodes in breadth-first
|
|
# tree. Should be initialized to NULL_IDX
|
|
# Returns:
|
|
# n_nodes: the number of nodes in the breadth-first tree
|
|
global NULL_IDX
|
|
|
|
cdef unsigned int i, pnode, cnode
|
|
cdef unsigned int i_nl, i_nl_end
|
|
cdef unsigned int N = node_list.shape[0]
|
|
|
|
node_list[0] = head_node
|
|
i_nl = 0
|
|
i_nl_end = 1
|
|
|
|
while i_nl < i_nl_end:
|
|
pnode = node_list[i_nl]
|
|
|
|
for i from indptr[pnode] <= i < indptr[pnode + 1]:
|
|
cnode = indices[i]
|
|
if (cnode == head_node):
|
|
continue
|
|
elif (predecessors[cnode] == NULL_IDX):
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
i_nl_end += 1
|
|
|
|
i_nl += 1
|
|
|
|
return i_nl
|
|
|
|
|
|
cdef unsigned int _breadth_first_undirected(
|
|
unsigned int head_node,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors):
|
|
# Inputs:
|
|
# head_node: (input) index of the node from which traversal starts
|
|
# indices1: (input) CSR indices of graph
|
|
# indptr1: (input) CSR indptr of graph
|
|
# indices2: (input) CSR indices of transposed graph
|
|
# indptr2: (input) CSR indptr of transposed graph
|
|
# node_list: (output) breadth-first list of nodes
|
|
# predecessors: (output) list of predecessors of nodes in breadth-first
|
|
# tree. Should be initialized to NULL_IDX
|
|
# Returns:
|
|
# n_nodes: the number of nodes in the breadth-first tree
|
|
global NULL_IDX
|
|
|
|
cdef unsigned int i, pnode, cnode
|
|
cdef unsigned int i_nl, i_nl_end
|
|
cdef unsigned int N = node_list.shape[0]
|
|
|
|
node_list[0] = head_node
|
|
i_nl = 0
|
|
i_nl_end = 1
|
|
|
|
while i_nl < i_nl_end:
|
|
pnode = node_list[i_nl]
|
|
|
|
for i from indptr1[pnode] <= i < indptr1[pnode + 1]:
|
|
cnode = indices1[i]
|
|
if (cnode == head_node):
|
|
continue
|
|
elif (predecessors[cnode] == NULL_IDX):
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
i_nl_end += 1
|
|
|
|
for i from indptr2[pnode] <= i < indptr2[pnode + 1]:
|
|
cnode = indices2[i]
|
|
if (cnode == head_node):
|
|
continue
|
|
elif (predecessors[cnode] == NULL_IDX):
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
i_nl_end += 1
|
|
|
|
i_nl += 1
|
|
|
|
return i_nl
|
|
|
|
|
|
def depth_first_order(csgraph, i_start,
|
|
directed=True, return_predecessors=True):
|
|
"""
|
|
depth_first_order(csgraph, i_start, directed=True, return_predecessors=True)
|
|
|
|
Return a depth-first ordering starting with specified node.
|
|
|
|
Note that a depth-first order is not unique. Furthermore, for graphs
|
|
with cycles, the tree generated by a depth-first search is not
|
|
unique either.
|
|
|
|
.. versionadded:: 0.11.0
|
|
|
|
Parameters
|
|
----------
|
|
csgraph : array_like or sparse matrix
|
|
The N x N compressed sparse graph. The input csgraph will be
|
|
converted to csr format for the calculation.
|
|
i_start : int
|
|
The index of starting node.
|
|
directed : bool, optional
|
|
If True (default), then operate on a directed graph: only
|
|
move from point i to point j along paths csgraph[i, j].
|
|
If False, then find the shortest path on an undirected graph: the
|
|
algorithm can progress from point i to j along csgraph[i, j] or
|
|
csgraph[j, i].
|
|
return_predecessors : bool, optional
|
|
If True (default), then return the predecesor array (see below).
|
|
|
|
Returns
|
|
-------
|
|
node_array : ndarray, one dimension
|
|
The breadth-first list of nodes, starting with specified node. The
|
|
length of node_array is the number of nodes reachable from the
|
|
specified node.
|
|
predecessors : ndarray, one dimension
|
|
Returned only if return_predecessors is True.
|
|
The length-N list of predecessors of each node in a breadth-first
|
|
tree. If node i is in the tree, then its parent is given by
|
|
predecessors[i]. If node i is not in the tree (and for the parent
|
|
node) then predecessors[i] = -9999.
|
|
"""
|
|
global NULL_IDX
|
|
csgraph = validate_graph(csgraph, directed, dense_output=False)
|
|
cdef int N = csgraph.shape[0]
|
|
|
|
node_list = np.empty(N, dtype=ITYPE)
|
|
predecessors = np.empty(N, dtype=ITYPE)
|
|
root_list = np.empty(N, dtype=ITYPE)
|
|
flag = np.zeros(N, dtype=ITYPE)
|
|
node_list.fill(NULL_IDX)
|
|
predecessors.fill(NULL_IDX)
|
|
root_list.fill(NULL_IDX)
|
|
|
|
if directed:
|
|
length = _depth_first_directed(i_start,
|
|
csgraph.indices, csgraph.indptr,
|
|
node_list, predecessors,
|
|
root_list, flag)
|
|
else:
|
|
csgraph_T = csgraph.T.tocsr()
|
|
length = _depth_first_undirected(i_start,
|
|
csgraph.indices, csgraph.indptr,
|
|
csgraph_T.indices, csgraph_T.indptr,
|
|
node_list, predecessors,
|
|
root_list, flag)
|
|
|
|
if return_predecessors:
|
|
return node_list[:length], predecessors
|
|
else:
|
|
return node_list[:length]
|
|
|
|
|
|
cdef unsigned int _depth_first_directed(
|
|
unsigned int head_node,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] flag):
|
|
cdef unsigned int i, j, i_nl_end, cnode, pnode
|
|
cdef unsigned int N = node_list.shape[0]
|
|
cdef int no_children, i_root
|
|
|
|
node_list[0] = head_node
|
|
root_list[0] = head_node
|
|
i_root = 0
|
|
i_nl_end = 1
|
|
flag[head_node] = 1
|
|
|
|
while i_root >= 0:
|
|
pnode = root_list[i_root]
|
|
no_children = True
|
|
for i from indptr[pnode] <= i < indptr[pnode + 1]:
|
|
cnode = indices[i]
|
|
if flag[cnode]:
|
|
continue
|
|
else:
|
|
i_root += 1
|
|
root_list[i_root] = cnode
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
flag[cnode] = 1
|
|
i_nl_end += 1
|
|
no_children = False
|
|
break
|
|
|
|
if i_nl_end == N:
|
|
break
|
|
|
|
if no_children:
|
|
i_root -= 1
|
|
|
|
return i_nl_end
|
|
|
|
|
|
cdef unsigned int _depth_first_undirected(
|
|
unsigned int head_node,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] node_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] predecessors,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] root_list,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] flag):
|
|
cdef unsigned int i, j, i_nl_end, cnode, pnode
|
|
cdef unsigned int N = node_list.shape[0]
|
|
cdef int no_children, i_root
|
|
|
|
node_list[0] = head_node
|
|
root_list[0] = head_node
|
|
i_root = 0
|
|
i_nl_end = 1
|
|
flag[head_node] = 1
|
|
|
|
while i_root >= 0:
|
|
pnode = root_list[i_root]
|
|
no_children = True
|
|
|
|
for i from indptr1[pnode] <= i < indptr1[pnode + 1]:
|
|
cnode = indices1[i]
|
|
if flag[cnode]:
|
|
continue
|
|
else:
|
|
i_root += 1
|
|
root_list[i_root] = cnode
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
flag[cnode] = 1
|
|
i_nl_end += 1
|
|
no_children = False
|
|
break
|
|
|
|
if no_children:
|
|
for i from indptr2[pnode] <= i < indptr2[pnode + 1]:
|
|
cnode = indices2[i]
|
|
if flag[cnode]:
|
|
continue
|
|
else:
|
|
i_root += 1
|
|
root_list[i_root] = cnode
|
|
node_list[i_nl_end] = cnode
|
|
predecessors[cnode] = pnode
|
|
flag[cnode] = 1
|
|
i_nl_end += 1
|
|
no_children = False
|
|
break
|
|
|
|
if i_nl_end == N:
|
|
break
|
|
|
|
if no_children:
|
|
i_root -= 1
|
|
|
|
return i_nl_end
|
|
|
|
|
|
cdef int _connected_components_directed(
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] labels):
|
|
"""
|
|
Uses an iterative version of Tarjan's algorithm to find the
|
|
strongly connected components of a directed graph represented as a
|
|
sparse matrix (scipy.sparse.csc_matrix or scipy.sparse.csr_matrix).
|
|
|
|
The algorithmic complexity is for a graph with E edges and V
|
|
vertices is O(E + V).
|
|
The storage requirement is 2*V integer arrays.
|
|
|
|
Uses an iterative version of the algorithm described here:
|
|
http://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.102.1707
|
|
"""
|
|
cdef int v, w, index, low_v, low_w, label, j
|
|
cdef int SS_head, root, stack_head, f, b
|
|
cdef int VOID = -1
|
|
cdef int END = -2
|
|
cdef int N = labels.shape[0]
|
|
cdef np.ndarray[ITYPE_t, ndim=1, mode="c"] SS, lowlinks, stack_f, stack_b
|
|
|
|
lowlinks = labels
|
|
SS = np.ndarray((N,), dtype=ITYPE)
|
|
stack_b = np.ndarray((N,), dtype=ITYPE)
|
|
stack_f = SS
|
|
|
|
# The stack of nodes which have been backtracked and are in the current SCC
|
|
SS.fill(VOID)
|
|
SS_head = END
|
|
|
|
# The array containing the lowlinks of nodes not yet assigned an SCC. Shares
|
|
# memory with the labels array, since they are not used at the same time.
|
|
lowlinks.fill(VOID)
|
|
|
|
# The DFS stack. Stored with both forwards and backwards pointers to allow
|
|
# us to move a node up to the top of the stack, as we only need to visit
|
|
# each node once. stack_f shares memory with SS, as nodes aren't put on the
|
|
# SS stack until after they've been popped from the DFS stack.
|
|
stack_head = END
|
|
stack_f.fill(VOID)
|
|
stack_b.fill(VOID)
|
|
|
|
index = 0
|
|
# Count SCC labels backwards so as not to class with lowlinks values.
|
|
label = N - 1
|
|
for v in range(N):
|
|
if lowlinks[v] == VOID:
|
|
# DFS-stack push
|
|
stack_head = v
|
|
stack_f[v] = END
|
|
stack_b[v] = END
|
|
while stack_head != END:
|
|
v = stack_head
|
|
if lowlinks[v] == VOID:
|
|
lowlinks[v] = index
|
|
index += 1
|
|
|
|
# Add successor nodes
|
|
for j from indptr[v] <= j < indptr[v+1]:
|
|
w = indices[j]
|
|
if lowlinks[w] == VOID:
|
|
# DFS-stack push
|
|
if stack_f[w] != VOID:
|
|
# w is already inside the stack, so excise it.
|
|
f = stack_f[w]
|
|
b = stack_b[w]
|
|
if b != END:
|
|
stack_f[b] = f
|
|
if f != END:
|
|
stack_b[f] = b
|
|
|
|
stack_f[w] = stack_head
|
|
stack_b[w] = END
|
|
stack_b[stack_head] = w
|
|
stack_head = w
|
|
|
|
else:
|
|
# DFS-stack pop
|
|
stack_head = stack_f[v]
|
|
if stack_head >= 0:
|
|
stack_b[stack_head] = END
|
|
stack_f[v] = VOID
|
|
stack_b[v] = VOID
|
|
|
|
root = 1 # True
|
|
low_v = lowlinks[v]
|
|
for j from indptr[v] <= j < indptr[v+1]:
|
|
low_w = lowlinks[indices[j]]
|
|
if low_w < low_v:
|
|
low_v = low_w
|
|
root = 0 # False
|
|
lowlinks[v] = low_v
|
|
|
|
if root: # Found a root node
|
|
index -= 1
|
|
# while S not empty and rindex[v] <= rindex[top[S]
|
|
while SS_head != END and lowlinks[v] <= lowlinks[SS_head]:
|
|
w = SS_head # w = pop(S)
|
|
SS_head = SS[w]
|
|
SS[w] = VOID
|
|
|
|
labels[w] = label # rindex[w] = c
|
|
index -= 1 # index = index - 1
|
|
labels[v] = label # rindex[v] = c
|
|
label -= 1 # c = c - 1
|
|
else:
|
|
SS[v] = SS_head # push(S, v)
|
|
SS_head = v
|
|
|
|
# labels count down from N-1 to zero. Modify them so they
|
|
# count upward from 0
|
|
labels *= -1
|
|
labels += (N - 1)
|
|
return (N - 1) - label
|
|
|
|
cdef int _connected_components_undirected(
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr1,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indices2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] indptr2,
|
|
np.ndarray[ITYPE_t, ndim=1, mode='c'] labels):
|
|
|
|
cdef int v, w, j, label, SS_head
|
|
cdef int N = labels.shape[0]
|
|
cdef int VOID = -1
|
|
cdef int END = -2
|
|
labels.fill(VOID)
|
|
label = 0
|
|
|
|
# Share memory for the stack and labels, since labels are only
|
|
# applied once a node has been popped from the stack.
|
|
cdef np.ndarray[ITYPE_t, ndim=1, mode="c"] SS = labels
|
|
SS_head = END
|
|
for v in range(N):
|
|
if labels[v] == VOID:
|
|
# SS.push(v)
|
|
SS_head = v
|
|
SS[v] = END
|
|
|
|
while SS_head != END:
|
|
# v = SS.pop()
|
|
v = SS_head
|
|
SS_head = SS[v]
|
|
|
|
labels[v] = label
|
|
|
|
# Push children onto the stack if they havn't been
|
|
# seen at all yet.
|
|
for j from indptr1[v] <= j < indptr1[v+1]:
|
|
w = indices1[j]
|
|
if SS[w] == VOID:
|
|
SS[w] = SS_head
|
|
SS_head = w
|
|
for j from indptr2[v] <= j < indptr2[v+1]:
|
|
w = indices2[j]
|
|
if SS[w] == VOID:
|
|
SS[w] = SS_head
|
|
SS_head = w
|
|
label += 1
|
|
|
|
return label
|