Skip to content

Commit

Permalink
sagemathgh-37662: Add parameter sort_neighbors to method `init_shor…
Browse files Browse the repository at this point in the history
…t_digraph`

    
Fixes sagemath#37642.

We add parameter `sort_edges` to method `init_short_digraph` of the
`short_digraph` data structure defined in
`src/sage/graphs/base/static_sparse_graph.pxd|pyx`.
- When set to `True` (default), for each vertex, the list of neighbors
is sorted by increasing vertex labels. This enables to search for a
vertex in this list using binary search, and so in time `O(log(n))`, but
the overall running time of method  `init_short_digraph` is in `O(m +
n*log(m))`.
- When set to `False`, neighbors are not sorted. The running time of
method `init_short_digraph` is reduced to `O(n + m)`, but the running
time for searching in the list of neighbors increases to `O(m)`.

So this new parameter is particularly useful for linear time algorithms
such as `lex_BFS`.



### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [x] I have linked a relevant issue or discussion.
- [ ] I have created tests covering the changes.
- [x] I have updated the documentation accordingly.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#37662
Reported by: David Coudert
Reviewer(s):
  • Loading branch information
Release Manager committed Jun 7, 2024
2 parents 648e53a + 052c58b commit 2d12f02
Show file tree
Hide file tree
Showing 12 changed files with 136 additions and 65 deletions.
3 changes: 2 additions & 1 deletion src/sage/graphs/asteroidal_triples.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -147,7 +147,8 @@ def is_asteroidal_triple_free(G, certificate=False):
# module sage.graphs.base.static_sparse_graph
cdef list int_to_vertex = list(G)
cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex,
sort_neighbors=False)

cdef bitset_t seen
bitset_init(seen, n)
Expand Down
7 changes: 6 additions & 1 deletion src/sage/graphs/base/static_sparse_graph.pxd
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,21 @@ cdef extern from "stdlib.h":
void *bsearch(const_void *key, const_void *base, size_t nmemb,
size_t size, int(*compar)(const_void *, const_void *)) nogil

cdef extern from "search.h":
void *lfind(const_void *key, const_void *base, size_t *nmemb,
size_t size, int(*compar)(const_void *, const_void *)) nogil

ctypedef struct short_digraph_s:
uint32_t * edges
uint32_t ** neighbors
PyObject * edge_labels
int m
int n
bint sorted_neighbors

ctypedef short_digraph_s short_digraph[1]

cdef int init_short_digraph(short_digraph g, G, edge_labelled=?, vertex_list=?) except -1
cdef int init_short_digraph(short_digraph g, G, edge_labelled=?, vertex_list=?, sort_neighbors=?) except -1
cdef void free_short_digraph(short_digraph g) noexcept
cdef int init_reverse(short_digraph dst, short_digraph src) except -1
cdef int out_degree(short_digraph g, int u) noexcept
Expand Down
126 changes: 86 additions & 40 deletions src/sage/graphs/base/static_sparse_graph.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ Technical details
-----------------
* When creating a ``short_digraph`` from a ``Graph`` or ``DiGraph`` named ``G``,
the `i^{\text{th}}` vertex corresponds *by default* to ``G.vertices(sort=True)[i]``.
the `i^{\text{th}}` vertex corresponds *by default* to ``list(G)[i]``.
Using optional parameter ``vertex_list``, you can specify the order of the
vertices. Then `i^{\text{th}}` vertex will corresponds to ``vertex_list[i]``.
Expand Down Expand Up @@ -116,7 +116,7 @@ Cython functions
:widths: 30, 70
:delim: |
``init_short_digraph(short_digraph g, G)`` | Initialize ``short_digraph g`` from a Sage (Di)Graph.
``init_short_digraph(short_digraph g, G, edge_labelled, vertex_list, sort_neighbors)`` | Initialize ``short_digraph g`` from a Sage (Di)Graph.
``int n_edges(short_digraph g)`` | Return the number of edges in ``g``
``int out_degree(short_digraph g, int i)`` | Return the out-degree of vertex `i` in ``g``
``has_edge(short_digraph g, int u, int v)`` | Test the existence of an edge.
Expand Down Expand Up @@ -204,23 +204,39 @@ cdef extern from "fenv.h":
int fesetround (int)


cdef int init_short_digraph(short_digraph g, G, edge_labelled=False, vertex_list=None) except -1:
cdef int init_short_digraph(short_digraph g, G, edge_labelled=False,
vertex_list=None, sort_neighbors=True) except -1:
r"""
Initialize ``short_digraph g`` from a Sage (Di)Graph.
If ``G`` is a ``Graph`` object (and not a ``DiGraph``), an edge between two
vertices `u` and `v` is replaced by two arcs in both directions.
INPUT:
- ``g`` -- a short_digraph
- ``G`` -- a ``Graph`` or a ``DiGraph``. If ``G`` is a ``Graph`` object,
then any edge between two vertices `u` and `v` is replaced by two arcs in
both directions.
- ``edge_labelled`` -- boolean (default: ``False``); whether to store the
label of edges or not
- ``vertex_list`` -- list (default: ``None``); list of all vertices of ``G``
in some order. When given, it is used to map the vertices of the graph to
consecutive integers. Otherwise, the result of ``list(G)`` is used
instead.
- ``sort_neighbors`` -- boolean (default: ``True``); whether to ensure that
the vertices in the list of neighbors of a vertex are sorted by increasing
vertex labels. This choice may have a non-negligeable impact on the time
complexity of some methods. More precisely:
The optional argument ``vertex_list`` is assumed to be a list of all
vertices of the graph ``G`` in some order.
**Beware that no checks are made that this input is correct**.
- When set to ``True``, the time complexity for initializing ``g`` is in
`O(m + m\log{m})` and deciding if ``g`` has edge `(u, v)` can be done in
time `O(\log{m})` using binary search.
If ``vertex_list`` is given, it will be used to map vertices of
the graph to consecutive integers. Otherwise, the result of
``G.vertices(sort=True)`` will be used instead. Because this only
works if the vertices can be sorted, using ``vertex_list`` is
useful when working with possibly non-sortable objects in Python
3.
- When set to ``False``, the time complexity for initializing ``g`` is
reduced to `O(n + m)` but the time complexity for deciding if ``g`` has
edge `(u, v)` increases to `O(m)`.
"""
g.edge_labels = NULL

Expand All @@ -242,7 +258,7 @@ cdef int init_short_digraph(short_digraph g, G, edge_labelled=False, vertex_list
raise ValueError("The source graph must be either a DiGraph or a Graph object !")

cdef int i, j, v_id
cdef list vertices = vertex_list if vertex_list is not None else G.vertices(sort=True)
cdef list vertices = vertex_list if vertex_list is not None else list(G)
cdef dict v_to_id = {v: i for i, v in enumerate(vertices)}
cdef list neighbor_label
cdef list edge_labels
Expand All @@ -256,6 +272,7 @@ cdef int init_short_digraph(short_digraph g, G, edge_labelled=False, vertex_list
# Initializing the value of neighbors
g.neighbors[0] = g.edges
cdef CGraph cg = <CGraph> G._backend
g.sorted_neighbors = sort_neighbors

if not G.has_loops():
# Normal case
Expand All @@ -272,7 +289,7 @@ cdef int init_short_digraph(short_digraph g, G, edge_labelled=False, vertex_list
g.neighbors[i] = g.neighbors[i - 1] + <int> len(G.edges_incident(vertices[i - 1]))

if not edge_labelled:
for u, v in G.edge_iterator(labels=False):
for u, v in G.edge_iterator(labels=False, sort_vertices=False):
i = v_to_id[u]
j = v_to_id[v]

Expand All @@ -289,22 +306,33 @@ cdef int init_short_digraph(short_digraph g, G, edge_labelled=False, vertex_list

g.neighbors[0] = g.edges

# Sorting the neighbors
for i in range(g.n):
qsort(g.neighbors[i], g.neighbors[i + 1] - g.neighbors[i], sizeof(int), compare_uint32_p)
if sort_neighbors:
# Sorting the neighbors
for i in range(g.n):
qsort(g.neighbors[i], g.neighbors[i + 1] - g.neighbors[i], sizeof(int), compare_uint32_p)

else:
from operator import itemgetter
edge_labels = [None] * n_edges
for v in G:
neighbor_label = [(v_to_id[uu], l) if uu != v else (v_to_id[u], l)
for u, uu, l in G.edges_incident(v)]
neighbor_label.sort(key=itemgetter(0))
v_id = v_to_id[v]

for i, (j, label) in enumerate(neighbor_label):
g.neighbors[v_id][i] = j
edge_labels[(g.neighbors[v_id] + i) - g.edges] = label
if sort_neighbors:
for v in G:
neighbor_label = [(v_to_id[uu], l) if uu != v else (v_to_id[u], l)
for u, uu, l in G.edges_incident(v)]
neighbor_label.sort(key=itemgetter(0))
v_id = v_to_id[v]

for i, (j, label) in enumerate(neighbor_label):
g.neighbors[v_id][i] = j
edge_labels[(g.neighbors[v_id] + i) - g.edges] = label
else:
for v in G:
v_id = v_to_id[v]
for i, (u, uu, label) in enumerate(G.edges_incident(v)):
if v == uu:
g.neighbors[v_id][i] = v_to_id[u]
else:
g.neighbors[v_id][i] = v_to_id[uu]
edge_labels[(g.neighbors[v_id] + i) - g.edges] = label

g.edge_labels = <PyObject *> <void *> edge_labels
cpython.Py_XINCREF(g.edge_labels)
Expand Down Expand Up @@ -335,6 +363,7 @@ cdef int init_empty_copy(short_digraph dst, short_digraph src) except -1:
"""
dst.n = src.n
dst.m = src.m
dst.sorted_neighbors = src.sorted_neighbors
dst.edge_labels = NULL
cdef list edge_labels

Expand All @@ -360,7 +389,7 @@ cdef int init_reverse(short_digraph dst, short_digraph src) except -1:
if not dst.n:
return 0

# 1/3
# 1/4
#
# In a first pass, we count the in-degrees of each vertex and store it in a
# vector. With this information, we can initialize dst.neighbors to its
Expand All @@ -376,7 +405,7 @@ cdef int init_reverse(short_digraph dst, short_digraph src) except -1:
dst.neighbors[i] = dst.neighbors[i - 1] + in_degree[i - 1]
sig_free(in_degree)

# 2/3
# 2/4
#
# Second pass : we list the edges again, and add them in dst.edges. Doing
# so, we will change the value of dst.neighbors, but that is not so bad as
Expand All @@ -391,20 +420,28 @@ cdef int init_reverse(short_digraph dst, short_digraph src) except -1:

dst.neighbors[v] += 1

# 3/3
# 3/4
#
# Final step : set the correct values of dst.neighbors again. It is easy, as
# Third step : set the correct values of dst.neighbors again. It is easy, as
# the correct value of dst.neighbors[i] is actually dst.neighbors[i-1]
for i in range(src.n - 1, 0, -1):
dst.neighbors[i] = dst.neighbors[i - 1]
dst.neighbors[0] = dst.edges

# 4/4
#
# Final step : if the neighbors of src are assumed to be sorted by
# increasing labels, we do the same for dst.
if src.sorted_neighbors:
for i in range(dst.n):
qsort(dst.neighbors[i], dst.neighbors[i + 1] - dst.neighbors[i], sizeof(int), compare_uint32_p)

return 0


cdef int compare_uint32_p(const_void *a, const_void *b) noexcept:
"""
Comparison function needed for ``bsearch``.
Comparison function needed for ``bsearch`` and ``lfind``.
"""
return (<uint32_t *> a)[0] - (<uint32_t *> b)[0]

Expand All @@ -413,9 +450,19 @@ cdef inline uint32_t * has_edge(short_digraph g, int u, int v) noexcept:
r"""
Test the existence of an edge.
Assumes that the neighbors of each vertex are sorted.
Return a pointer to ``v`` in the list of neighbors of ``u`` if found and
``NULL`` otherwise.
"""
return <uint32_t *> bsearch(&v, g.neighbors[u], g.neighbors[u + 1] - g.neighbors[u], sizeof(uint32_t), compare_uint32_p)
if g.sorted_neighbors:
# The neighbors of u are sorted by increasing label. We can use binary
# search to decide if g has edge (u, v)
return <uint32_t *> bsearch(&v, g.neighbors[u], g.neighbors[u + 1] - g.neighbors[u],
sizeof(uint32_t), compare_uint32_p)

# Otherwise, we use the linear time lfind method
cdef size_t nelem = g.neighbors[u + 1] - g.neighbors[u]
return <uint32_t *> lfind(&v, g.neighbors[u], &nelem,
sizeof(uint32_t), compare_uint32_p)


cdef inline object edge_label(short_digraph g, uint32_t * edge):
Expand All @@ -424,8 +471,7 @@ cdef inline object edge_label(short_digraph g, uint32_t * edge):
"""
if not g.edge_labels:
return None
else:
return (<list> g.edge_labels)[edge - g.edges]
return (<list> g.edge_labels)[edge - g.edges]


cdef uint32_t simple_BFS(short_digraph g,
Expand Down Expand Up @@ -750,7 +796,7 @@ def tarjan_strongly_connected_components(G):
cdef MemoryAllocator mem = MemoryAllocator()
cdef list int_to_vertex = list(G)
cdef short_digraph g
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex, sort_neighbors=False)
cdef int * scc = <int*> mem.malloc(g.n * sizeof(int))
sig_on()
cdef int nscc = tarjan_strongly_connected_components_C(g, scc)
Expand Down Expand Up @@ -867,7 +913,7 @@ def strongly_connected_components_digraph(G):
cdef MemoryAllocator mem = MemoryAllocator()
cdef list int_to_vertex = list(G)
cdef short_digraph g, scc_g
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex, sort_neighbors=False)
cdef int * scc = <int*> mem.malloc(g.n * sizeof(int))
cdef int i, j, nscc
cdef list edges = []
Expand Down Expand Up @@ -932,7 +978,7 @@ def triangles_count(G):
# g is a copy of G. If G is internally a static sparse graph, we use it.
cdef list int_to_vertex = list(G)
cdef short_digraph g
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex, sort_neighbors=True)

cdef uint64_t * count = <uint64_t *> check_calloc(G.order(), sizeof(uint64_t))

Expand Down
6 changes: 3 additions & 3 deletions src/sage/graphs/centrality.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ cdef dict centrality_betweenness_C(G, numerical_type _, bint normalize=True):
mpq_init(mpq_tmp)

try:
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(g, G, edge_labelled=False, vertex_list=int_to_vertex, sort_neighbors=False)
init_reverse(bfs_dag, g)

queue = <uint32_t*> check_allocarray(n, sizeof(uint32_t))
Expand Down Expand Up @@ -690,7 +690,7 @@ def centrality_closeness_top_k(G, int k=1, int verbose=0):
# calling out_neighbors. This data structure is well documented in the
# module sage.graphs.base.static_sparse_graph
cdef list V = list(G)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=V)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=V, sort_neighbors=False)
cdef int n = sd.n
cdef int* reachL = <int*> mem.malloc(n * sizeof(int))
cdef int* reachU
Expand Down Expand Up @@ -943,7 +943,7 @@ def centrality_closeness_random_k(G, int k=1):
# Copying the whole graph as a static_sparse_graph for fast shortest
# paths computation in unweighted graph. This data structure is well
# documented in module sage.graphs.base.static_sparse_graph
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex, sort_neighbors=False)
distance = <uint32_t*> mem.malloc(n * sizeof(uint32_t))
waiting_list = <uint32_t*> mem.malloc(n * sizeof(uint32_t))
bitset_init(seen, n)
Expand Down
2 changes: 1 addition & 1 deletion src/sage/graphs/convexity_properties.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -755,7 +755,7 @@ def is_geodetic(G):
# Copy the graph as a short digraph
cdef int n = G.order()
cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G))
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G), sort_neighbors=False)

# Allocate some data structures
cdef MemoryAllocator mem = MemoryAllocator()
Expand Down
18 changes: 12 additions & 6 deletions src/sage/graphs/distances_all_pairs.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -304,7 +304,8 @@ cdef inline all_pairs_shortest_path_BFS(gg,
# Copying the whole graph to obtain the list of neighbors quicker than by
# calling out_neighbors
cdef short_digraph sd
init_short_digraph(sd, gg, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(sd, gg, edge_labelled=False, vertex_list=int_to_vertex,
sort_neighbors=False)

c_all_pairs_shortest_path_BFS(sd, predecessors, distances, eccentricity)

Expand Down Expand Up @@ -1057,7 +1058,8 @@ def eccentricity(G, algorithm="standard", vertex_list=None):
ecc = c_eccentricity(G, vertex_list=int_to_vertex)

else:
init_short_digraph(sd, G, edge_labelled=False, vertex_list=vertex_list)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=vertex_list,
sort_neighbors=False)

if algorithm == "DHV":
ecc = c_eccentricity_DHV(sd)
Expand Down Expand Up @@ -1832,7 +1834,8 @@ def diameter(G, algorithm=None, source=None):
# module sage.graphs.base.static_sparse_graph
cdef list int_to_vertex = list(G)
cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex,
sort_neighbors=False)
cdef short_digraph rev_sd # to store copy of sd with edges reversed

# and we map the source to an int in [0,n-1]
Expand Down Expand Up @@ -1934,7 +1937,8 @@ def radius_DHV(G):

cdef list int_to_vertex = list(G)
cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex)
init_short_digraph(sd, G, edge_labelled=False, vertex_list=int_to_vertex,
sort_neighbors=False)

cdef uint32_t source, ecc_source
cdef uint32_t antipode, ecc_antipode
Expand Down Expand Up @@ -2051,7 +2055,8 @@ def wiener_index(G):
# calling out_neighbors. This data structure is well documented in the
# module sage.graphs.base.static_sparse_graph
cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G))
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G),
sort_neighbors=False)

# allocated some data structures
cdef bitset_t seen
Expand Down Expand Up @@ -2363,12 +2368,13 @@ def szeged_index(G, algorithm=None):
return 0

cdef short_digraph sd
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G))
cdef uint64_t s

if algorithm is "low":
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G), sort_neighbors=True)
s = c_szeged_index_low_memory(sd)
else:
init_short_digraph(sd, G, edge_labelled=False, vertex_list=list(G), sort_neighbors=False)
s = c_szeged_index_high_memory(sd)

free_short_digraph(sd)
Expand Down
Loading

0 comments on commit 2d12f02

Please sign in to comment.