Skip to content

add simple cycle enumeration by k shortest simple path algorithm #40145

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 7 commits into from
Jun 1, 2025

Conversation

kappybar
Copy link
Contributor

Add another algorithm of enumearting simple cycles in digraph, which is based on k shortest simple path algorithm.

For each edge e=uv, delete e from the original graph and enumerate vu simple path in increasing length order by k shortest simple path algorithm to obtain simple cycles that contain e in increasing length order.

Using iterators for each edge e and priority queue makes it possible to enumerate simple cycles in increasing length order.

📝 Checklist

  • The title is concise and informative.
  • The description explains in detail what this PR is about.
  • I have linked a relevant issue or discussion.
  • I have created tests covering the changes.
  • I have updated the documentation and checked the documentation preview.

⌛ Dependencies

#40114

@dcoudert
Copy link
Contributor

I think that the algorithm is not what we want: If I understand well, with the current proposal, you start an iterator from each edge of the graph, so some cycles may be reported more than once.

The correct algorithm should be like:

  1. take edge e=uv and remove it from G
  2. use an kSSP iterator to enumerate all vu paths in G-uv, and so the cycles containing uv
  3. decompose G-uv into strongly connected components and apply the same algorithm from step 1 on each SCC.

We now get a collection of iterators that you can put in a priority queue to enumerate the cycles in the right order.

Comment on lines 3055 to 3060
if not rooted:
for v in starting_vertices:
if v < edge[0]:
h.delete_vertex(v)
if v == edge[1]:
return
Copy link
Contributor Author

@kappybar kappybar May 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your comments!

I understand your algorithm intends not to output the same cycle more than once. My implemented algorithm also try to do that by deleting vertices (if rooted=False). If rooted=True, it outputs the same cycle with a different start vertex but I think it is a correct behavior.

How do you think about it?

Copy link
Contributor

@dcoudert dcoudert May 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal of my algorithm is not only to output a cycle only once. It is also to work on smaller digraphs. Indeed, once en iterator has been initialized for edge e, that edge is removed from the digraph, the digraph is decomposed into strongly connected components, and we proceed in each SCC.
This algorithm shall initialize fewer iterators (only 1 if the input digraph is a circuit for instance), and each iterator will operate in smaller digraphs.

Consider the following:

def simple_cycle_iter(hh, e):
    return hh._all_simple_cycles_iterator_edge(e,
                                               max_length=max_length,
                                               remove_acyclic_edges=False,
                                               weight_function=weight_function,
                                               by_weight=by_weight,
                                               check_weight=check_weight,
                                               report_weight=True)

SCCC = h.strongly_connected_components_subgraphs()
iterators= {}
while SCCS:
    hh = SCCS.pop()
    if not hh.size():
        continue
    e = next(hh.edge_iterator())
    iterators[e] = simple_cycle_iter(hh, e)
    hh.delete_edge(e)
    SCCS.extend(hh.strongly_connected_components_subgraphs())

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand it. I will modify the current algorithm to new one.

Copy link
Contributor

@dcoudert dcoudert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you check what happen when the digraph has loops or multiple edges ? Method strongly_connected_components considers that a vertex with a loop is a strongly connected component.

@kappybar
Copy link
Contributor Author

I think it works well for a graph with a loop, but for a graph with a multiedge, shortest_simple_paths function throw the following error.

g = DiGraph(multiedges=True)
g.add_edges([('a', 'b'), ('b', 'c'), ('b', 'c')])
it = g.shortest_simple_paths(source='a', target='c', algorithm='Feng')
next(it)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-8-bc1ab118995a> in ?()
----> 1 next(it)

sage/graphs/path_enumeration.pyx in ?()
--> 556         self = self.to_simple(to_undirected=False, keep_label='min', immutable=False)

~/Documents/gsoc2025/sage/sage/src/sage/graphs/generic_graph.py in ?(self, to_undirected, keep_label, immutable)
  19790             g = Graph(self, immutable=False)
  19791         else:
  19792             g = self.copy(immutable=False)
  19793         g.allow_loops(False)
> 19794         g.allow_multiple_edges(False, keep_label=keep_label)
  19795         if immutable is None:
  19796             immutable = self.is_immutable()
  19797         if immutable:

~/Documents/gsoc2025/sage/sage/src/sage/graphs/generic_graph.py in ?(self, new, check, keep_label)
   3942                 else:
   3943                     # This edge has already been seen: we have to remove
   3944                     # something from the graph.
   3945                     oldl = seen[u, v]
-> 3946                     if (keep_min and l < oldl) or (keep_max and l > oldl):
   3947                         # Keep the new edge, delete the old one
   3948                         self.delete_edge(u, v, oldl)
   3949                         seen[u, v] = l

TypeError: '<' not supported between instances of 'NoneType' and 'NoneType'

Is it expected behavior? If not, changing kepp_label='min' to keep_label='any' in

G = self.to_simple(to_undirected=False, keep_label='min', immutable=False)

will fix this issue.

Copy link

Documentation preview for this PR (built with commit acb7e42; changes) is ready! 🎉
This preview will update shortly after each push to this PR.

@dcoudert
Copy link
Contributor

changing to any might not be the best choice. In fact, it seems that the case of graphs with multiple edges has not yet been considered in these methods. may be we could let this case open for the moment. We will see later if we have a suitable solution.

Copy link
Contributor

@dcoudert dcoudert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM. I think this is a nice contribution.

@kappybar
Copy link
Contributor Author

Thank your for review. I fixed typo in documentation.

Copy link
Contributor

@dcoudert dcoudert left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM.

@vbraun vbraun merged commit b4f28f8 into sagemath:develop Jun 1, 2025
15 of 22 checks passed
@dcoudert dcoudert added the gsoc: 2025 Tag for GSoC2025 issues/PRs label Jun 11, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants