diff --git a/src/doc/en/reference/graphs/index.rst b/src/doc/en/reference/graphs/index.rst index 18ea0a34e6a..404768c0d52 100644 --- a/src/doc/en/reference/graphs/index.rst +++ b/src/doc/en/reference/graphs/index.rst @@ -122,5 +122,6 @@ Libraries of algorithms sage/graphs/connectivity sage/graphs/edge_connectivity sage/graphs/domination + sage/graphs/cycle_enumeration .. include:: ../footer.txt diff --git a/src/sage/graphs/connectivity.pyx b/src/sage/graphs/connectivity.pyx index 2411f3dd4c9..7d42f80a935 100644 --- a/src/sage/graphs/connectivity.pyx +++ b/src/sage/graphs/connectivity.pyx @@ -23,6 +23,7 @@ Here is what the module can do: :meth:`connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`blocks_and_cut_vertices` | Return the blocks and cut vertices of the graph. :meth:`blocks_and_cuts_tree` | Return the blocks-and-cuts tree of the graph. + :meth:`biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. :meth:`is_edge_cut` | Check whether the input edges form an edge cut. :meth:`is_cut_vertex` | Check whether the input vertex is a cut-vertex. @@ -812,6 +813,44 @@ def blocks_and_cuts_tree(G): g.add_edge(('B', bloc), ('C', c)) return g +def biconnected_components_subgraphs(G): + r""" + Return a list of biconnected components as graph objects. + + A biconnected component is a maximal subgraph that is biconnected, i.e., + removing any vertex does not disconnect it. + + INPUT: + + - ``G`` -- the input graph + + EXAMPLES:: + + sage: from sage.graphs.connectivity import biconnected_components_subgraphs + sage: G = Graph({0: [1, 2], 1: [0, 2], 2: [0, 1, 3], 3: [2]}) + sage: L = biconnected_components_subgraphs(G) + sage: L + [Subgraph of (): Graph on 2 vertices, Subgraph of (): Graph on 3 vertices] + sage: L[0].edges() + [(2, 3, None)] + sage: L[1].edges() + [(0, 1, None), (0, 2, None), (1, 2, None)] + + TESTS: + + If ``G`` is not a Sage graph, an error is raised:: + + sage: from sage.graphs.connectivity import biconnected_components_subgraphs + sage: biconnected_components_subgraphs('I am not a graph') + Traceback (most recent call last): + ... + TypeError: the input must be a Sage graph + """ + from sage.graphs.generic_graph import GenericGraph + if not isinstance(G, GenericGraph): + raise TypeError("the input must be a Sage graph") + + return [G.subgraph(c) for c in blocks_and_cut_vertices(G)[0]] def is_edge_cut(G, edges): """ diff --git a/src/sage/graphs/cycle_enumeration.py b/src/sage/graphs/cycle_enumeration.py new file mode 100644 index 00000000000..3d372a22d28 --- /dev/null +++ b/src/sage/graphs/cycle_enumeration.py @@ -0,0 +1,868 @@ +r""" +Cycle enumeration + +This module is meant for all functions related to cycle enumeration in graphs. + +.. csv-table:: + :class: contentstable + :widths: 30, 70 + :delim: | + + :func:`all_cycles_iterator` | Return an iterator over the cycles of ``self``. + :func:`all_simple_cycles` | Return an iterator over the simple cycles of ``self``. + +Functions +--------- +""" +# **************************************************************************** +# Copyright (C) 2025 Yuta Inoue +# David Coudert +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** +from copy import copy + + +def _all_cycles_iterator_vertex(self, vertex, starting_vertices=None, simple=False, + rooted=False, max_length=None, trivial=False, + remove_acyclic_edges=True, + weight_function=None, by_weight=False, + check_weight=True, report_weight=False): + r""" + Return an iterator over the cycles of ``self`` starting with the given + vertex in increasing length order. Each edge must have a positive weight. + + INPUT: + + - ``vertex`` -- the starting vertex of the cycle + + - ``starting_vertices`` -- iterable (default: ``None``); vertices from + which the cycles must start. If ``None``, then all vertices of the + graph can be starting points. This argument is necessary if ``rooted`` + is set to ``True``. + + - ``simple`` -- boolean (default: ``False``); if set to ``True``, then + only simple cycles are considered. A cycle is simple if the only + vertex occurring twice in it is the starting and ending one. + + - ``rooted`` -- boolean (default: ``False``); if set to False, then + cycles differing only by their starting vertex are considered the same + (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', + 'b']``). Otherwise, all cycles are enumerated. + + - ``max_length`` -- nonnegative integer (default: ``None``); the + maximum length of the enumerated paths. If set to ``None``, then all + lengths are allowed. + + - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then + the empty paths are also enumerated + + - ``remove_acyclic_edges`` -- boolean (default: ``True``); whether + acyclic edges must be removed from the graph. Used to avoid + recomputing it for each vertex + + - ``weight_function`` -- function (default: ``None``); a function that + takes as input an edge ``(u, v, l)`` and outputs its weight. If not + ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` + and ``by_weight`` is ``True``, we use the edge label ``l`` as a + weight. + + - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges + in the graph are weighted, otherwise all edges have weight 1 + + - ``check_weight`` -- boolean (default: ``True``); whether to check that + the ``weight_function`` outputs a number for each edge + + - ``report_weight`` -- boolean (default: ``False``); if ``False``, just + a cycle is returned. Otherwise a tuple of cycle length and cycle is + returned. + + OUTPUT: iterator + + EXAMPLES:: + + sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) + sage: it = g._all_cycles_iterator_vertex('a', simple=False, max_length=None) + sage: for i in range(5): print(next(it)) + ['a', 'a'] + ['a', 'a', 'a'] + ['a', 'a', 'a', 'a'] + ['a', 'a', 'a', 'a', 'a'] + ['a', 'a', 'a', 'a', 'a', 'a'] + sage: it = g._all_cycles_iterator_vertex('c', simple=False, max_length=None) + sage: for i in range(5): print(next(it)) + ['c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c'] + + sage: it = g._all_cycles_iterator_vertex('d', simple=False, max_length=None) + sage: for i in range(5): print(next(it)) + ['d', 'c', 'd'] + ['d', 'c', 'd', 'c', 'd'] + ['d', 'c', 'd', 'c', 'd', 'c', 'd'] + ['d', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd'] + ['d', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd'] + + It is possible to set a maximum length so that the number of cycles is + finite:: + + sage: it = g._all_cycles_iterator_vertex('d', simple=False, max_length=6) + sage: list(it) + [['d', 'c', 'd'], ['d', 'c', 'd', 'c', 'd'], ['d', 'c', 'd', 'c', 'd', 'c', 'd']] + + When ``simple`` is set to True, the number of cycles is finite since no vertex + but the first one can occur more than once:: + + sage: it = g._all_cycles_iterator_vertex('d', simple=True, max_length=None) + sage: list(it) + [['d', 'c', 'd']] + + By default, the empty cycle is not enumerated:: + + sage: it = g._all_cycles_iterator_vertex('d', simple=True, trivial=True) + sage: list(it) + [['d'], ['d', 'c', 'd']] + + A cycle is enumerated in increasing length order for a weighted graph:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', 2), ('a', 'e', 2), ('b', 'a', 1), ('b', 'c', 1), + ....: ('c', 'd', 2), ('d', 'b', 1), ('e', 'a', 2)]) + sage: it = g._all_cycles_iterator_vertex('a', simple=False, max_length=None, + ....: by_weight=True, report_weight=True) + sage: for i in range(7): print(next(it)) + (3, ['a', 'b', 'a']) + (4, ['a', 'e', 'a']) + (6, ['a', 'b', 'a', 'b', 'a']) + (7, ['a', 'b', 'a', 'e', 'a']) + (7, ['a', 'b', 'c', 'd', 'b', 'a']) + (7, ['a', 'e', 'a', 'b', 'a']) + (8, ['a', 'e', 'a', 'e', 'a']) + + Each edge must have a positive weight:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) + sage: next(g._all_cycles_iterator_vertex('a', simple=False, max_length=None, + ....: by_weight=True, report_weight=True)) + Traceback (most recent call last): + ... + ValueError: negative weight is not allowed + """ + if starting_vertices is None: + starting_vertices = [vertex] + # First enumerate the empty cycle + if trivial: + if report_weight: + yield (0, [vertex]) + else: + yield [vertex] + # First we remove vertices and edges that are not part of any cycle + if remove_acyclic_edges: + sccs = self.strongly_connected_components() + if len(sccs) == 1: + h = self + else: + d = {} + for id, component in enumerate(sccs): + for v in component: + d[v] = id + h = copy(self) + h.delete_edges((u, v) for u, v in h.edge_iterator(labels=False) if d[u] != d[v]) + else: + h = self + + by_weight, weight_function = self._get_weight_function(by_weight=by_weight, + weight_function=weight_function, + check_weight=check_weight) + if by_weight: + for e in h.edge_iterator(): + if weight_function(e) < 0: + raise ValueError("negative weight is not allowed") + + from heapq import heapify, heappop, heappush + heap_queue = [(0, [vertex])] + if max_length is None: + from sage.rings.infinity import Infinity + max_length = Infinity + while heap_queue: + length, path = heappop(heap_queue) + # Checks if a cycle has been found + if len(path) > 1 and path[0] == path[-1]: + if report_weight: + yield (length, path) + else: + yield path + # If simple is set to True, only simple cycles are + # allowed, Then it discards the current path + if (not simple or path.count(path[-1]) == 1): + for e in h.outgoing_edge_iterator(path[-1]): + neighbor = e[1] + # Makes sure that the current cycle is not too long. + # If cycles are not rooted, makes sure to keep only the + # minimum cycle according to the lexicographic order + if length + weight_function(e) <= max_length and \ + (rooted or neighbor not in starting_vertices or path[0] <= neighbor): + heappush(heap_queue, (length + weight_function(e), path + [neighbor])) + + +def _all_simple_cycles_iterator_edge(self, edge, max_length=None, + remove_unnecessary_edges=True, + weight_function=None, by_weight=False, + check_weight=True, report_weight=False): + r""" + Return an iterator over the **simple** cycles of ``self`` starting with the + given edge in increasing length order. Each edge must have a positive weight. + + INPUT: + + - ``edge`` -- the starting edge of the cycle. + + - ``max_length`` -- nonnegative integer (default: ``None``); the + maximum length of the enumerated paths. If set to ``None``, then all + lengths are allowed. + + - ``remove_unnecessary_edges`` -- boolean (default: ``True``); whether + unnecessary edges for enumerating simple cycles must be removed from the + graph. If ``self`` is directed, edges not in the strongly connected + component that contains ``edge`` are unnecessary. Otherwise, edges not in + the 2-connected component that contains ``edge`` are unnecessary. + + - ``weight_function`` -- function (default: ``None``); a function that + takes as input an edge ``(u, v, l)`` and outputs its weight. If not + ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` + and ``by_weight`` is ``True``, we use the edge label ``l`` as a + weight. + + - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges + in the graph are weighted, otherwise all edges have weight 1 + + - ``check_weight`` -- boolean (default: ``True``); whether to check that + the ``weight_function`` outputs a number for each edge + + - ``report_weight`` -- boolean (default: ``False``); if ``False``, just + a cycle is returned. Otherwise a tuple of cycle length and cycle is + returned. + + OUTPUT: iterator + + ALGORITHM: + + Given an edge `uv`, this algorithm extracts k-shortest `vu`-path in `G-uv` + in increasing length order by using k-shortest path algorithm. Thus, it + extracts only simple cycles. See + :math:`~sage.graphs.path_enumeration.shortest_simple_paths` for more + information. + + EXAMPLES: + + sage: g = graphs.Grid2dGraph(2, 5).to_directed() + sage: it = g._all_simple_cycles_iterator_edge(((0, 0), (0, 1), None), report_weight=True) + sage: for i in range(5): print(next(it)) + (2, [(0, 0), (0, 1), (0, 0)]) + (4, [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + (6, [(0, 0), (0, 1), (0, 2), (1, 2), (1, 1), (1, 0), (0, 0)]) + (8, [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) + (10, [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) + + The function works for undirected graphs as well:: + + sage: g = graphs.Grid2dGraph(2, 5) + sage: it = g._all_simple_cycles_iterator_edge(((0, 0), (0, 1), None), report_weight=True) + sage: for i in range(4): print(next(it)) + (4, [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) + (6, [(0, 0), (0, 1), (0, 2), (1, 2), (1, 1), (1, 0), (0, 0)]) + (8, [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) + (10, [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) + + Each edge must have a positive weight:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) + sage: next(g._all_simple_cycles_iterator_edge(('a', 'b', -2), max_length=None, by_weight=True)) + Traceback (most recent call last): + ... + ValueError: negative weight is not allowed + + TESTS: + + A graph with a loop:: + + sage: g = DiGraph(loops=True) + sage: g.add_edges([('a', 'b'), ('b', 'c'), ('c', 'a'), ('c', 'c')]) + sage: it = g._all_simple_cycles_iterator_edge(('a', 'b'), remove_unnecessary_edges=True) + sage: next(it) + ['a', 'b', 'c', 'a'] + sage: g = DiGraph(loops=True) + sage: g.add_edges([('a', 'b'), ('b', 'c'), ('c', 'c')]) + sage: it = g._all_simple_cycles_iterator_edge(('c', 'c'), remove_unnecessary_edges=True) + sage: next(it) + ['c', 'c'] + + An undirected graph with 2 biconnected components:: + + sage: g = Graph() + sage: g.add_edges([('a', 'b'), ('b', 'c'), ('c', 'a'), ('a', 'd'), ('d', 'e'), ('e', 'a')]) + sage: it = g._all_simple_cycles_iterator_edge(('a', 'b'), remove_unnecessary_edges=True) + sage: next(it) + ['a', 'b', 'c', 'a'] + """ + # First we remove vertices and edges that are not part of any cycle + if remove_unnecessary_edges: + if self.is_directed(): + components = self.strongly_connected_components_subgraphs() + else: + components = self.biconnected_components_subgraphs() + h = None + for component in components: + if component.has_edge(edge): + h = component + break + else: + # edge connects two strongly connected components, so + # no simple cycle starting with edge exists. + return + else: + h = copy(self) + # delete edge + h.delete_edge(edge) + + by_weight, weight_function = self._get_weight_function(by_weight=by_weight, + weight_function=weight_function, + check_weight=check_weight) + + if by_weight: + for e in self.edge_iterator(): + if weight_function(e) < 0: + raise ValueError("negative weight is not allowed") + + it = h.shortest_simple_paths(source=edge[1], target=edge[0], + weight_function=weight_function, + by_weight=by_weight, + check_weight=check_weight, + algorithm=('Feng' if h.is_directed() else 'Yen'), + report_edges=False, + report_weight=True) + + edge_weight = weight_function(edge) + + if max_length is None: + from sage.rings.infinity import Infinity + max_length = Infinity + for length, path in it: + if length + edge_weight > max_length: + break + + if report_weight: + yield length + edge_weight, [edge[0]] + path + else: + yield [edge[0]] + path + + +def all_cycles_iterator(self, starting_vertices=None, simple=False, + rooted=False, max_length=None, trivial=False, + weight_function=None, by_weight=False, + check_weight=True, report_weight=False, + algorithm='A'): + r""" + Return an iterator over all the cycles of ``self`` starting with one of + the given vertices. Each edge must have a positive weight. + + The cycles are enumerated in increasing length order. + + INPUT: + + - ``starting_vertices`` -- iterable (default: ``None``); vertices from + which the cycles must start. If ``None``, then all vertices of the + graph can be starting points. This argument is necessary if ``rooted`` + is set to ``True``. + + - ``simple`` -- boolean (default: ``False``); if set to ``True``, then + only simple cycles are considered. A cycle is simple if the only + vertex occurring twice in it is the starting and ending one. + + - ``rooted`` -- boolean (default: ``False``); if set to False, then + cycles differing only by their starting vertex are considered the same + (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', + 'b']``). Otherwise, all cycles are enumerated. + + - ``max_length`` -- nonnegative integer (default: ``None``); the + maximum length of the enumerated paths. If set to ``None``, then all + lengths are allowed. + + - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then + the empty paths are also enumerated + + - ``weight_function`` -- function (default: ``None``); a function that + takes as input an edge ``(u, v, l)`` and outputs its weight. If not + ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` + and ``by_weight`` is ``True``, we use the edge label ``l`` as a + weight. + + - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges + in the graph are weighted, otherwise all edges have weight 1 + + - ``check_weight`` -- boolean (default: ``True``); whether to check that + the ``weight_function`` outputs a number for each edge + + - ``report_weight`` -- boolean (default: ``False``); if ``False``, just + a cycle is returned. Otherwise a tuple of cycle length and cycle is + returned. + + - ``algorithm`` -- string (default: ``'A'``); the algorithm used to + enumerate the cycles. + + - The algorithm ``'A'`` holds cycle iterators starting with each vertex, + and output them in increasing length order. + + - The algorithm ``'B'`` holds cycle iterators starting with each edge, + and output them in increasing length order. It depends on the k-shortest + simple paths algorithm. Thus, it is not available if ``simple=False``. + + OUTPUT: iterator + + .. SEEALSO:: + + - :meth:`all_simple_cycles` + - :meth:`~sage.graphs.path_enumeration.shortest_simple_paths` + + AUTHOR: + + Alexandre Blondin Masse + + EXAMPLES:: + + sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) + sage: it = g.all_cycles_iterator() + sage: for _ in range(7): print(next(it)) + ['a', 'a'] + ['a', 'a', 'a'] + ['c', 'd', 'c'] + ['a', 'a', 'a', 'a'] + ['a', 'a', 'a', 'a', 'a'] + ['c', 'd', 'c', 'd', 'c'] + ['a', 'a', 'a', 'a', 'a', 'a'] + + There are no cycles in the empty graph and in acyclic graphs:: + + sage: g = DiGraph() + sage: it = g.all_cycles_iterator() + sage: list(it) + [] + sage: g = DiGraph({0:[1]}) + sage: it = g.all_cycles_iterator() + sage: list(it) + [] + + It is possible to restrict the starting vertices of the cycles:: + + sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) + sage: it = g.all_cycles_iterator(starting_vertices=['b', 'c']) + sage: for _ in range(3): print(next(it)) + ['c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c'] + ['c', 'd', 'c', 'd', 'c', 'd', 'c'] + + Also, one can bound the length of the cycles:: + + sage: it = g.all_cycles_iterator(max_length=3) + sage: list(it) + [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], + ['a', 'a', 'a', 'a']] + + By default, cycles differing only by their starting point are not all + enumerated, but this may be parametrized:: + + sage: it = g.all_cycles_iterator(max_length=3, rooted=False) + sage: list(it) + [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], + ['a', 'a', 'a', 'a']] + sage: it = g.all_cycles_iterator(max_length=3, rooted=True) + sage: list(it) + [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], ['d', 'c', 'd'], + ['a', 'a', 'a', 'a']] + + One may prefer to enumerate simple cycles, i.e. cycles such that the only + vertex occurring twice in it is the starting and ending one (see also + :meth:`all_simple_cycles`):: + + sage: it = g.all_cycles_iterator(simple=True) + sage: list(it) + [['a', 'a'], ['c', 'd', 'c']] + sage: g = digraphs.Circuit(4) + sage: list(g.all_cycles_iterator(simple=True)) + [[0, 1, 2, 3, 0]] + + A cycle is enumerated in increasing length order for a weighted graph:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', 2), ('a', 'e', 2), ('b', 'a', 1), ('b', 'c', 1), + ....: ('c', 'd', 2), ('d', 'b', 1), ('e', 'a', 2)]) + sage: it = g.all_cycles_iterator(simple=False, max_length=None, + ....: by_weight=True, report_weight=True) + sage: for i in range(9): print(next(it)) + (3, ['a', 'b', 'a']) + (4, ['a', 'e', 'a']) + (4, ['b', 'c', 'd', 'b']) + (6, ['a', 'b', 'a', 'b', 'a']) + (7, ['a', 'b', 'a', 'e', 'a']) + (7, ['a', 'b', 'c', 'd', 'b', 'a']) + (7, ['a', 'e', 'a', 'b', 'a']) + (8, ['a', 'e', 'a', 'e', 'a']) + (8, ['b', 'c', 'd', 'b', 'c', 'd', 'b']) + + Each edge must have a positive weight:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) + sage: next(g.all_cycles_iterator(simple=False, max_length=None, + ....: by_weight=True, report_weight=True)) + Traceback (most recent call last): + ... + ValueError: negative weight is not allowed + + The algorithm ``'B'`` works for undirected graphs as well:: + + sage: g = Graph({0: [1, 2], 1: [0, 2], 2: [0, 1, 3, 5], 3: [2, 4], 4: [3, 5], 5: [4, 2]}) + sage: for cycle in g.all_cycles_iterator(algorithm='B', simple=True, by_weight=True): + ....: print(cycle) + [0, 1, 2, 0] + [2, 3, 4, 5, 2] + + The algorithm ``'B'`` is available only when `simple=True`:: + + sage: g = DiGraph() + sage: g.add_edges([('a', 'b', 1), ('b', 'a', 1)]) + sage: next(g.all_cycles_iterator(algorithm='B', simple=False)) + ....: + Traceback (most recent call last): + ... + ValueError: The algorithm 'B' is available only when simple=True. + + The algorithm ``'A'`` is available only for directed graphs:: + + sage: g = Graph({0: [1, 2], 1: [0, 2], 2: [0, 1]}) + sage: next(g.all_cycles_iterator(algorithm='A', simple=True)) + Traceback (most recent call last): + ... + ValueError: The algorithm 'A' is available only for directed graphs. + """ + if starting_vertices is None: + starting_vertices = self + + if algorithm == 'A' and not self.is_directed(): + raise ValueError("The algorithm 'A' is available only for directed graphs.") + if algorithm == 'B' and not simple: + raise ValueError("The algorithm 'B' is available only when simple=True.") + + by_weight, weight_function = self._get_weight_function(by_weight=by_weight, + weight_function=weight_function, + check_weight=check_weight) + + if by_weight: + for e in self.edge_iterator(): + if weight_function(e) < 0: + raise ValueError("negative weight is not allowed") + + if algorithm == 'A': + if self.is_directed(): + # Since a cycle is always included in a given strongly connected + # component, we may remove edges from the graph + sccs = self.strongly_connected_components() + if len(sccs) == 1: + h = self + else: + d = {} + for id, component in enumerate(sccs): + for v in component: + d[v] = id + h = copy(self) + h.delete_edges((u, v) for u, v in h.edge_iterator(labels=False) if d[u] != d[v]) + else: + h = self + + # We create one cycles iterator per vertex. This is necessary if we + # want to iterate over cycles with increasing length. + def cycle_iter(v): + return h._all_cycles_iterator_vertex(v, + starting_vertices=starting_vertices, + simple=simple, + rooted=rooted, + max_length=max_length, + trivial=trivial, + remove_acyclic_edges=False, + weight_function=weight_function, + by_weight=by_weight, + check_weight=check_weight, + report_weight=True) + + iterators = {v: cycle_iter(v) for v in starting_vertices} + elif algorithm == 'B': + def simple_cycle_iter(hh, e): + return hh._all_simple_cycles_iterator_edge(e, + max_length=max_length, + remove_unnecessary_edges=False, + weight_function=weight_function, + by_weight=by_weight, + check_weight=check_weight, + report_weight=True) + if self.is_directed(): + def decompose(hh): + return hh.strongly_connected_components_subgraphs() + else: + def decompose(hh): + return hh.biconnected_components_subgraphs() + + components = decompose(self) + iterators = dict() + while components: + hh = components.pop() + if not hh.size(): + continue + e = next(hh.edge_iterator()) + iterators[e] = simple_cycle_iter(hh, e) + hh.delete_edge(e) + components.extend(decompose(hh)) + else: + raise ValueError(f"The algorithm {algorithm} is not valid. \ + Use the algorithm 'A' or 'B'.") + + cycles = [] + for key, it in iterators.items(): + try: + length, cycle = next(it) + cycles.append((length, cycle, key)) + except StopIteration: + pass + # Since we always extract a shortest path, using a heap + # can speed up the algorithm + from heapq import heapify, heappop, heappush + heapify(cycles) + while cycles: + # We choose the shortest available cycle + length, shortest_cycle, key = heappop(cycles) + if report_weight: + yield (length, shortest_cycle) + else: + yield shortest_cycle + # We update the cycle iterator to its next available cycle if it + # exists + try: + length, cycle = next(iterators[key]) + heappush(cycles, (length, cycle, key)) + except StopIteration: + pass + + +def all_simple_cycles(self, starting_vertices=None, rooted=False, + max_length=None, trivial=False, + weight_function=None, by_weight=False, + check_weight=True, report_weight=False, + algorithm='A'): + r""" + Return a list of all simple cycles of ``self``. The cycles are + enumerated in increasing length order. Each edge must have a + positive weight. + + INPUT: + + - ``starting_vertices`` -- iterable (default: ``None``); vertices from + which the cycles must start. If ``None``, then all vertices of the + graph can be starting points. This argument is necessary if ``rooted`` + is set to ``True``. + + - ``rooted`` -- boolean (default: ``False``); if set to False, then + cycles differing only by their starting vertex are considered the same + (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', + 'b']``). Otherwise, all cycles are enumerated. + + - ``max_length`` -- nonnegative integer (default: ``None``); the + maximum length of the enumerated paths. If set to ``None``, then all + lengths are allowed. + + - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then + the empty paths are also enumerated + + - ``weight_function`` -- function (default: ``None``); a function that + takes as input an edge ``(u, v, l)`` and outputs its weight. If not + ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` + and ``by_weight`` is ``True``, we use the edge label ``l`` as a + weight. + + - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges + in the graph are weighted, otherwise all edges have weight 1 + + - ``check_weight`` -- boolean (default: ``True``); whether to check that + the ``weight_function`` outputs a number for each edge + + - ``report_weight`` -- boolean (default: ``False``); if ``False``, just + a cycle is returned. Otherwise a tuple of cycle length and cycle is + returned. + + - ``algorithm`` -- string (default: ``'A'``); the algorithm used to + enumerate the cycles. + + - The algorithm ``'A'`` holds cycle iterators starting with each vertex, + and output them in increasing length order. + + - The algorithm ``'B'`` holds cycle iterators starting with each edge, + and output them in increasing length order. + + OUTPUT: list + + .. NOTE:: + + Although the number of simple cycles of a finite graph is always + finite, computing all its cycles may take a very long time. + + EXAMPLES:: + + sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) + sage: g.all_simple_cycles() + [['a', 'a'], ['c', 'd', 'c']] + + The directed version of the Petersen graph:: + + sage: g = graphs.PetersenGraph().to_directed() + sage: g.all_simple_cycles(max_length=4) + [[0, 1, 0], [0, 4, 0], [0, 5, 0], [1, 2, 1], [1, 6, 1], [2, 3, 2], + [2, 7, 2], [3, 4, 3], [3, 8, 3], [4, 9, 4], [5, 7, 5], [5, 8, 5], + [6, 8, 6], [6, 9, 6], [7, 9, 7]] + sage: g.all_simple_cycles(max_length=6) + [[0, 1, 0], [0, 4, 0], [0, 5, 0], [1, 2, 1], [1, 6, 1], [2, 3, 2], + [2, 7, 2], [3, 4, 3], [3, 8, 3], [4, 9, 4], [5, 7, 5], [5, 8, 5], + [6, 8, 6], [6, 9, 6], [7, 9, 7], [0, 1, 2, 3, 4, 0], + [0, 1, 2, 7, 5, 0], [0, 1, 6, 8, 5, 0], [0, 1, 6, 9, 4, 0], + [0, 4, 3, 2, 1, 0], [0, 4, 3, 8, 5, 0], [0, 4, 9, 6, 1, 0], + [0, 4, 9, 7, 5, 0], [0, 5, 7, 2, 1, 0], [0, 5, 7, 9, 4, 0], + [0, 5, 8, 3, 4, 0], [0, 5, 8, 6, 1, 0], [1, 2, 3, 8, 6, 1], + [1, 2, 7, 9, 6, 1], [1, 6, 8, 3, 2, 1], [1, 6, 9, 7, 2, 1], + [2, 3, 4, 9, 7, 2], [2, 3, 8, 5, 7, 2], [2, 7, 5, 8, 3, 2], + [2, 7, 9, 4, 3, 2], [3, 4, 9, 6, 8, 3], [3, 8, 6, 9, 4, 3], + [5, 7, 9, 6, 8, 5], [5, 8, 6, 9, 7, 5], [0, 1, 2, 3, 8, 5, 0], + [0, 1, 2, 7, 9, 4, 0], [0, 1, 6, 8, 3, 4, 0], + [0, 1, 6, 9, 7, 5, 0], [0, 4, 3, 2, 7, 5, 0], + [0, 4, 3, 8, 6, 1, 0], [0, 4, 9, 6, 8, 5, 0], + [0, 4, 9, 7, 2, 1, 0], [0, 5, 7, 2, 3, 4, 0], + [0, 5, 7, 9, 6, 1, 0], [0, 5, 8, 3, 2, 1, 0], + [0, 5, 8, 6, 9, 4, 0], [1, 2, 3, 4, 9, 6, 1], + [1, 2, 7, 5, 8, 6, 1], [1, 6, 8, 5, 7, 2, 1], + [1, 6, 9, 4, 3, 2, 1], [2, 3, 8, 6, 9, 7, 2], + [2, 7, 9, 6, 8, 3, 2], [3, 4, 9, 7, 5, 8, 3], + [3, 8, 5, 7, 9, 4, 3]] + + The complete graph (without loops) on `4` vertices:: + + sage: g = graphs.CompleteGraph(4).to_directed() + sage: g.all_simple_cycles() + [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 2, 1], [1, 3, 1], [2, 3, 2], + [0, 1, 2, 0], [0, 1, 3, 0], [0, 2, 1, 0], [0, 2, 3, 0], + [0, 3, 1, 0], [0, 3, 2, 0], [1, 2, 3, 1], [1, 3, 2, 1], + [0, 1, 2, 3, 0], [0, 1, 3, 2, 0], [0, 2, 1, 3, 0], + [0, 2, 3, 1, 0], [0, 3, 1, 2, 0], [0, 3, 2, 1, 0]] + + If the graph contains a large number of cycles, one can bound the length + of the cycles, or simply restrict the possible starting vertices of the + cycles:: + + sage: g = graphs.CompleteGraph(20).to_directed() + sage: g.all_simple_cycles(max_length=2) + [[0, 1, 0], [0, 2, 0], [0, 3, 0], [0, 4, 0], [0, 5, 0], [0, 6, 0], [0, 7, 0], + [0, 8, 0], [0, 9, 0], [0, 10, 0], [0, 11, 0], [0, 12, 0], [0, 13, 0], + [0, 14, 0], [0, 15, 0], [0, 16, 0], [0, 17, 0], [0, 18, 0], [0, 19, 0], + [1, 2, 1], [1, 3, 1], [1, 4, 1], [1, 5, 1], [1, 6, 1], [1, 7, 1], [1, 8, 1], + [1, 9, 1], [1, 10, 1], [1, 11, 1], [1, 12, 1], [1, 13, 1], [1, 14, 1], + [1, 15, 1], [1, 16, 1], [1, 17, 1], [1, 18, 1], [1, 19, 1], [2, 3, 2], + [2, 4, 2], [2, 5, 2], [2, 6, 2], [2, 7, 2], [2, 8, 2], [2, 9, 2], [2, 10, 2], + [2, 11, 2], [2, 12, 2], [2, 13, 2], [2, 14, 2], [2, 15, 2], [2, 16, 2], + [2, 17, 2], [2, 18, 2], [2, 19, 2], [3, 4, 3], [3, 5, 3], [3, 6, 3], + [3, 7, 3], [3, 8, 3], [3, 9, 3], [3, 10, 3], [3, 11, 3], [3, 12, 3], + [3, 13, 3], [3, 14, 3], [3, 15, 3], [3, 16, 3], [3, 17, 3], [3, 18, 3], + [3, 19, 3], [4, 5, 4], [4, 6, 4], [4, 7, 4], [4, 8, 4], [4, 9, 4], [4, 10, 4], + [4, 11, 4], [4, 12, 4], [4, 13, 4], [4, 14, 4], [4, 15, 4], [4, 16, 4], + [4, 17, 4], [4, 18, 4], [4, 19, 4], [5, 6, 5], [5, 7, 5], [5, 8, 5], + [5, 9, 5], [5, 10, 5], [5, 11, 5], [5, 12, 5], [5, 13, 5], [5, 14, 5], + [5, 15, 5], [5, 16, 5], [5, 17, 5], [5, 18, 5], [5, 19, 5], [6, 7, 6], + [6, 8, 6], [6, 9, 6], [6, 10, 6], [6, 11, 6], [6, 12, 6], [6, 13, 6], + [6, 14, 6], [6, 15, 6], [6, 16, 6], [6, 17, 6], [6, 18, 6], [6, 19, 6], + [7, 8, 7], [7, 9, 7], [7, 10, 7], [7, 11, 7], [7, 12, 7], [7, 13, 7], + [7, 14, 7], [7, 15, 7], [7, 16, 7], [7, 17, 7], [7, 18, 7], [7, 19, 7], + [8, 9, 8], [8, 10, 8], [8, 11, 8], [8, 12, 8], [8, 13, 8], [8, 14, 8], + [8, 15, 8], [8, 16, 8], [8, 17, 8], [8, 18, 8], [8, 19, 8], [9, 10, 9], + [9, 11, 9], [9, 12, 9], [9, 13, 9], [9, 14, 9], [9, 15, 9], [9, 16, 9], + [9, 17, 9], [9, 18, 9], [9, 19, 9], [10, 11, 10], [10, 12, 10], [10, 13, 10], + [10, 14, 10], [10, 15, 10], [10, 16, 10], [10, 17, 10], [10, 18, 10], + [10, 19, 10], [11, 12, 11], [11, 13, 11], [11, 14, 11], [11, 15, 11], + [11, 16, 11], [11, 17, 11], [11, 18, 11], [11, 19, 11], [12, 13, 12], + [12, 14, 12], [12, 15, 12], [12, 16, 12], [12, 17, 12], [12, 18, 12], + [12, 19, 12], [13, 14, 13], [13, 15, 13], [13, 16, 13], [13, 17, 13], + [13, 18, 13], [13, 19, 13], [14, 15, 14], [14, 16, 14], [14, 17, 14], + [14, 18, 14], [14, 19, 14], [15, 16, 15], [15, 17, 15], [15, 18, 15], + [15, 19, 15], [16, 17, 16], [16, 18, 16], [16, 19, 16], [17, 18, 17], + [17, 19, 17], [18, 19, 18]] + + sage: g = graphs.CompleteGraph(20).to_directed() + sage: g.all_simple_cycles(max_length=2, starting_vertices=[0]) + [[0, 1, 0], [0, 2, 0], [0, 3, 0], [0, 4, 0], [0, 5, 0], + [0, 6, 0], [0, 7, 0], [0, 8, 0], [0, 9, 0], [0, 10, 0], + [0, 11, 0], [0, 12, 0], [0, 13, 0], [0, 14, 0], [0, 15, 0], + [0, 16, 0], [0, 17, 0], [0, 18, 0], [0, 19, 0]] + + One may prefer to distinguish equivalent cycles having distinct starting + vertices (compare the following examples):: + + sage: g = graphs.CompleteGraph(4).to_directed() + sage: g.all_simple_cycles(max_length=2, rooted=False) + [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 2, 1], [1, 3, 1], [2, 3, 2]] + sage: g.all_simple_cycles(max_length=2, rooted=True) + [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 0, 1], [1, 2, 1], [1, 3, 1], + [2, 0, 2], [2, 1, 2], [2, 3, 2], [3, 0, 3], [3, 1, 3], [3, 2, 3]] + + A cycle is enumerated in increasing length order for a weighted graph:: + + sage: cycles = g.all_simple_cycles(weight_function=lambda e:e[0]+e[1], + ....: by_weight=True, report_weight=True) + sage: cycles + [(2, [0, 1, 0]), (4, [0, 2, 0]), (6, [0, 1, 2, 0]), (6, [0, 2, 1, 0]), + (6, [0, 3, 0]), (6, [1, 2, 1]), (8, [0, 1, 3, 0]), (8, [0, 3, 1, 0]), + (8, [1, 3, 1]), (10, [0, 2, 3, 0]), (10, [0, 3, 2, 0]), (10, [2, 3, 2]), + (12, [0, 1, 2, 3, 0]), (12, [0, 1, 3, 2, 0]), (12, [0, 2, 1, 3, 0]), + (12, [0, 2, 3, 1, 0]), (12, [0, 3, 1, 2, 0]), (12, [0, 3, 2, 1, 0]), + (12, [1, 2, 3, 1]), (12, [1, 3, 2, 1])] + + The algorithm ``'B'`` can be used:: + + sage: cycles_B = g.all_simple_cycles(weight_function=lambda e:e[0]+e[1], by_weight=True, + ....: report_weight=True, algorithm='B') + sage: cycles_B + [(2, [0, 1, 0]), (4, [0, 2, 0]), (6, [0, 1, 2, 0]), (6, [0, 2, 1, 0]), + (6, [0, 3, 0]), (6, [1, 2, 1]), (8, [0, 1, 3, 0]), (8, [0, 3, 1, 0]), + (8, [1, 3, 1]), (10, [0, 2, 3, 0]), (10, [0, 3, 2, 0]), (10, [2, 3, 2]), + (12, [0, 1, 3, 2, 0]), (12, [0, 1, 2, 3, 0]), (12, [0, 2, 3, 1, 0]), + (12, [0, 2, 1, 3, 0]), (12, [0, 3, 2, 1, 0]), (12, [0, 3, 1, 2, 0]), + (12, [1, 2, 3, 1]), (12, [1, 3, 2, 1])] + sage: cycles.sort() == cycles_B.sort() + True + + The algorithm ``'A'`` is available only for directed graphs:: + + sage: g = Graph({0: [1, 2], 1: [0, 2], 2: [0, 1]}) + sage: g.all_simple_cycles(algorithm='A') + Traceback (most recent call last): + ... + ValueError: The algorithm 'A' is available only for directed graphs. + """ + return list(self.all_cycles_iterator(starting_vertices=starting_vertices, + simple=True, rooted=rooted, + max_length=max_length, trivial=trivial, + weight_function=weight_function, + by_weight=by_weight, + check_weight=check_weight, + report_weight=report_weight, + algorithm=algorithm)) diff --git a/src/sage/graphs/digraph.py b/src/sage/graphs/digraph.py index 3057ebd4619..f01e71e7f21 100644 --- a/src/sage/graphs/digraph.py +++ b/src/sage/graphs/digraph.py @@ -2773,778 +2773,6 @@ def periphery(self, by_weight=False, algorithm=None, weight_function=None, return [] return [v for v in self if ecc[v] == d] - # Paths and cycles iterators - - def _all_cycles_iterator_vertex(self, vertex, starting_vertices=None, simple=False, - rooted=False, max_length=None, trivial=False, - remove_acyclic_edges=True, - weight_function=None, by_weight=False, - check_weight=True, report_weight=False): - r""" - Return an iterator over the cycles of ``self`` starting with the given - vertex in increasing length order. Each edge must have a positive weight. - - INPUT: - - - ``vertex`` -- the starting vertex of the cycle - - - ``starting_vertices`` -- iterable (default: ``None``); vertices from - which the cycles must start. If ``None``, then all vertices of the - graph can be starting points. This argument is necessary if ``rooted`` - is set to ``True``. - - - ``simple`` -- boolean (default: ``False``); if set to ``True``, then - only simple cycles are considered. A cycle is simple if the only - vertex occurring twice in it is the starting and ending one. - - - ``rooted`` -- boolean (default: ``False``); if set to False, then - cycles differing only by their starting vertex are considered the same - (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', - 'b']``). Otherwise, all cycles are enumerated. - - - ``max_length`` -- nonnegative integer (default: ``None``); the - maximum length of the enumerated paths. If set to ``None``, then all - lengths are allowed. - - - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then - the empty paths are also enumerated - - - ``remove_acyclic_edges`` -- boolean (default: ``True``); whether - acyclic edges must be removed from the graph. Used to avoid - recomputing it for each vertex - - - ``weight_function`` -- function (default: ``None``); a function that - takes as input an edge ``(u, v, l)`` and outputs its weight. If not - ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` - and ``by_weight`` is ``True``, we use the edge label ``l`` as a - weight. - - - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges - in the graph are weighted, otherwise all edges have weight 1 - - - ``check_weight`` -- boolean (default: ``True``); whether to check that - the ``weight_function`` outputs a number for each edge - - - ``report_weight`` -- boolean (default: ``False``); if ``False``, just - a cycle is returned. Otherwise a tuple of cycle length and cycle is - returned. - - OUTPUT: iterator - - EXAMPLES:: - - sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) - sage: it = g._all_cycles_iterator_vertex('a', simple=False, max_length=None) - sage: for i in range(5): print(next(it)) - ['a', 'a'] - ['a', 'a', 'a'] - ['a', 'a', 'a', 'a'] - ['a', 'a', 'a', 'a', 'a'] - ['a', 'a', 'a', 'a', 'a', 'a'] - sage: it = g._all_cycles_iterator_vertex('c', simple=False, max_length=None) - sage: for i in range(5): print(next(it)) - ['c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c'] - - sage: it = g._all_cycles_iterator_vertex('d', simple=False, max_length=None) - sage: for i in range(5): print(next(it)) - ['d', 'c', 'd'] - ['d', 'c', 'd', 'c', 'd'] - ['d', 'c', 'd', 'c', 'd', 'c', 'd'] - ['d', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd'] - ['d', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd', 'c', 'd'] - - It is possible to set a maximum length so that the number of cycles is - finite:: - - sage: it = g._all_cycles_iterator_vertex('d', simple=False, max_length=6) - sage: list(it) - [['d', 'c', 'd'], ['d', 'c', 'd', 'c', 'd'], ['d', 'c', 'd', 'c', 'd', 'c', 'd']] - - When ``simple`` is set to True, the number of cycles is finite since no vertex - but the first one can occur more than once:: - - sage: it = g._all_cycles_iterator_vertex('d', simple=True, max_length=None) - sage: list(it) - [['d', 'c', 'd']] - - By default, the empty cycle is not enumerated:: - - sage: it = g._all_cycles_iterator_vertex('d', simple=True, trivial=True) - sage: list(it) - [['d'], ['d', 'c', 'd']] - - A cycle is enumerated in increasing length order for a weighted graph:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', 2), ('a', 'e', 2), ('b', 'a', 1), ('b', 'c', 1), - ....: ('c', 'd', 2), ('d', 'b', 1), ('e', 'a', 2)]) - sage: it = g._all_cycles_iterator_vertex('a', simple=False, max_length=None, - ....: by_weight=True, report_weight=True) - sage: for i in range(7): print(next(it)) - (3, ['a', 'b', 'a']) - (4, ['a', 'e', 'a']) - (6, ['a', 'b', 'a', 'b', 'a']) - (7, ['a', 'b', 'a', 'e', 'a']) - (7, ['a', 'b', 'c', 'd', 'b', 'a']) - (7, ['a', 'e', 'a', 'b', 'a']) - (8, ['a', 'e', 'a', 'e', 'a']) - - Each edge must have a positive weight:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) - sage: next(g._all_cycles_iterator_vertex('a', simple=False, max_length=None, - ....: by_weight=True, report_weight=True)) - Traceback (most recent call last): - ... - ValueError: negative weight is not allowed - """ - if starting_vertices is None: - starting_vertices = [vertex] - # First enumerate the empty cycle - if trivial: - if report_weight: - yield (0, [vertex]) - else: - yield [vertex] - # First we remove vertices and edges that are not part of any cycle - if remove_acyclic_edges: - sccs = self.strongly_connected_components() - d = {} - for id, component in enumerate(sccs): - for v in component: - d[v] = id - h = copy(self) - h.delete_edges((u, v) for u, v in h.edge_iterator(labels=False) if d[u] != d[v]) - else: - h = self - - by_weight, weight_function = self._get_weight_function(by_weight=by_weight, - weight_function=weight_function, - check_weight=check_weight) - if by_weight: - for e in h.edge_iterator(): - if weight_function(e) < 0: - raise ValueError("negative weight is not allowed") - - from heapq import heappop, heappush - heap_queue = [(0, [vertex])] - if max_length is None: - from sage.rings.infinity import Infinity - max_length = Infinity - while heap_queue: - length, path = heappop(heap_queue) - # Checks if a cycle has been found - if len(path) > 1 and path[0] == path[-1]: - if report_weight: - yield (length, path) - else: - yield path - # If simple is set to True, only simple cycles are - # allowed, Then it discards the current path - if (not simple or path.count(path[-1]) == 1): - for e in h.outgoing_edge_iterator(path[-1]): - neighbor = e[1] - # Makes sure that the current cycle is not too long. - # If cycles are not rooted, makes sure to keep only the - # minimum cycle according to the lexicographic order - if length + weight_function(e) <= max_length and \ - (rooted or neighbor not in starting_vertices or path[0] <= neighbor): - heappush(heap_queue, (length + weight_function(e), path + [neighbor])) - - def _all_simple_cycles_iterator_edge(self, edge, max_length=None, - remove_acyclic_edges=True, - weight_function=None, by_weight=False, - check_weight=True, report_weight=False): - r""" - Return an iterator over the **simple** cycles of ``self`` starting with the - given edge in increasing length order. Each edge must have a positive weight. - - INPUT: - - - ``edge`` -- the starting edge of the cycle. - - - ``max_length`` -- nonnegative integer (default: ``None``); the - maximum length of the enumerated paths. If set to ``None``, then all - lengths are allowed. - - - ``remove_acyclic_edges`` -- boolean (default: ``True``); whether - acyclic edges must be removed from the graph. Used to avoid - recomputing it for each edge - - - ``weight_function`` -- function (default: ``None``); a function that - takes as input an edge ``(u, v, l)`` and outputs its weight. If not - ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` - and ``by_weight`` is ``True``, we use the edge label ``l`` as a - weight. - - - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges - in the graph are weighted, otherwise all edges have weight 1 - - - ``check_weight`` -- boolean (default: ``True``); whether to check that - the ``weight_function`` outputs a number for each edge - - - ``report_weight`` -- boolean (default: ``False``); if ``False``, just - a cycle is returned. Otherwise a tuple of cycle length and cycle is - returned. - - OUTPUT: iterator - - ALGORITHM: - - Given an edge `uv`, this algorithm extracts k-shortest `vu`-path in `G-uv` - in increasing length order by using k-shortest path algorithm. Thus, it - extracts only simple cycles. See - :math:`~sage.graphs.path_enumeration.shortest_simple_paths` for more - information. - - EXAMPLES: - - sage: g = graphs.Grid2dGraph(2, 5).to_directed() - sage: it = g._all_simple_cycles_iterator_edge(((0, 0), (0, 1), None), report_weight=True) - sage: for i in range(5): print(next(it)) - (2, [(0, 0), (0, 1), (0, 0)]) - (4, [(0, 0), (0, 1), (1, 1), (1, 0), (0, 0)]) - (6, [(0, 0), (0, 1), (0, 2), (1, 2), (1, 1), (1, 0), (0, 0)]) - (8, [(0, 0), (0, 1), (0, 2), (0, 3), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) - (10, [(0, 0), (0, 1), (0, 2), (0, 3), (0, 4), (1, 4), (1, 3), (1, 2), (1, 1), (1, 0), (0, 0)]) - - Each edge must have a positive weight:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) - sage: next(g._all_simple_cycles_iterator_edge(('a', 'b', -2), max_length=None, by_weight=True)) - Traceback (most recent call last): - ... - ValueError: negative weight is not allowed - - TESTS: - - A graph with a loop:: - - sage: g = DiGraph(loops=True) - sage: g.add_edges([('a', 'b'), ('b', 'c'), ('c', 'a'), ('c', 'c')]) - sage: it = g._all_simple_cycles_iterator_edge(('a', 'b'), remove_acyclic_edges=True) - sage: next(it) - ['a', 'b', 'c', 'a'] - sage: g = DiGraph(loops=True) - sage: g.add_edges([('a', 'b'), ('b', 'c'), ('c', 'c')]) - sage: it = g._all_simple_cycles_iterator_edge(('c', 'c'), remove_acyclic_edges=True) - sage: next(it) - ['c', 'c'] - """ - # First we remove vertices and edges that are not part of any cycle - if remove_acyclic_edges: - h = None - for component in self.strongly_connected_components_subgraphs(): - if component.has_edge(edge): - h = component - if h is None: - # edge connects two strongly connected components, so - # no simple cycle starting with edge exists. - return - else: - h = copy(self) - # delete edge - h.delete_edge(edge) - - by_weight, weight_function = self._get_weight_function(by_weight=by_weight, - weight_function=weight_function, - check_weight=check_weight) - - if by_weight: - for e in self.edge_iterator(): - if weight_function(e) < 0: - raise ValueError("negative weight is not allowed") - - it = h.shortest_simple_paths(source=edge[1], target=edge[0], - weight_function=weight_function, - by_weight=by_weight, - check_weight=check_weight, - algorithm='Feng', - report_edges=False, - report_weight=True) - - edge_weight = weight_function(edge) - - if max_length is None: - from sage.rings.infinity import Infinity - max_length = Infinity - for length, path in it: - if length + edge_weight > max_length: - break - - if report_weight: - yield length + edge_weight, [edge[0]] + path - else: - yield [edge[0]] + path - - def all_cycles_iterator(self, starting_vertices=None, simple=False, - rooted=False, max_length=None, trivial=False, - weight_function=None, by_weight=False, - check_weight=True, report_weight=False, - algorithm='A'): - r""" - Return an iterator over all the cycles of ``self`` starting with one of - the given vertices. Each edge must have a positive weight. - - The cycles are enumerated in increasing length order. - - INPUT: - - - ``starting_vertices`` -- iterable (default: ``None``); vertices from - which the cycles must start. If ``None``, then all vertices of the - graph can be starting points. This argument is necessary if ``rooted`` - is set to ``True``. - - - ``simple`` -- boolean (default: ``False``); if set to ``True``, then - only simple cycles are considered. A cycle is simple if the only - vertex occurring twice in it is the starting and ending one. - - - ``rooted`` -- boolean (default: ``False``); if set to False, then - cycles differing only by their starting vertex are considered the same - (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', - 'b']``). Otherwise, all cycles are enumerated. - - - ``max_length`` -- nonnegative integer (default: ``None``); the - maximum length of the enumerated paths. If set to ``None``, then all - lengths are allowed. - - - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then - the empty paths are also enumerated - - - ``weight_function`` -- function (default: ``None``); a function that - takes as input an edge ``(u, v, l)`` and outputs its weight. If not - ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` - and ``by_weight`` is ``True``, we use the edge label ``l`` as a - weight. - - - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges - in the graph are weighted, otherwise all edges have weight 1 - - - ``check_weight`` -- boolean (default: ``True``); whether to check that - the ``weight_function`` outputs a number for each edge - - - ``report_weight`` -- boolean (default: ``False``); if ``False``, just - a cycle is returned. Otherwise a tuple of cycle length and cycle is - returned. - - - ``algorithm`` -- string (default: ``'A'``); the algorithm used to - enumerate the cycles. - - - The algorithm ``'A'`` holds cycle iterators starting with each vertex, - and output them in increasing length order. - - - The algorithm ``'B'`` holds cycle iterators starting with each edge, - and output them in increasing length order. It depends on the k-shortest - simple paths algorithm. Thus, it is not available if ``simple=False``. - - OUTPUT: iterator - - .. SEEALSO:: - - - :meth:`all_simple_cycles` - - :meth:`~sage.graphs.path_enumeration.shortest_simple_paths` - - AUTHOR: - - Alexandre Blondin Masse - - EXAMPLES:: - - sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) - sage: it = g.all_cycles_iterator() - sage: for _ in range(7): print(next(it)) - ['a', 'a'] - ['a', 'a', 'a'] - ['c', 'd', 'c'] - ['a', 'a', 'a', 'a'] - ['a', 'a', 'a', 'a', 'a'] - ['c', 'd', 'c', 'd', 'c'] - ['a', 'a', 'a', 'a', 'a', 'a'] - - There are no cycles in the empty graph and in acyclic graphs:: - - sage: g = DiGraph() - sage: it = g.all_cycles_iterator() - sage: list(it) - [] - sage: g = DiGraph({0:[1]}) - sage: it = g.all_cycles_iterator() - sage: list(it) - [] - - It is possible to restrict the starting vertices of the cycles:: - - sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) - sage: it = g.all_cycles_iterator(starting_vertices=['b', 'c']) - sage: for _ in range(3): print(next(it)) - ['c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c'] - ['c', 'd', 'c', 'd', 'c', 'd', 'c'] - - Also, one can bound the length of the cycles:: - - sage: it = g.all_cycles_iterator(max_length=3) - sage: list(it) - [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], - ['a', 'a', 'a', 'a']] - - By default, cycles differing only by their starting point are not all - enumerated, but this may be parametrized:: - - sage: it = g.all_cycles_iterator(max_length=3, rooted=False) - sage: list(it) - [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], - ['a', 'a', 'a', 'a']] - sage: it = g.all_cycles_iterator(max_length=3, rooted=True) - sage: list(it) - [['a', 'a'], ['a', 'a', 'a'], ['c', 'd', 'c'], ['d', 'c', 'd'], - ['a', 'a', 'a', 'a']] - - One may prefer to enumerate simple cycles, i.e. cycles such that the only - vertex occurring twice in it is the starting and ending one (see also - :meth:`all_simple_cycles`):: - - sage: it = g.all_cycles_iterator(simple=True) - sage: list(it) - [['a', 'a'], ['c', 'd', 'c']] - sage: g = digraphs.Circuit(4) - sage: list(g.all_cycles_iterator(simple=True)) - [[0, 1, 2, 3, 0]] - - A cycle is enumerated in increasing length order for a weighted graph:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', 2), ('a', 'e', 2), ('b', 'a', 1), ('b', 'c', 1), - ....: ('c', 'd', 2), ('d', 'b', 1), ('e', 'a', 2)]) - sage: it = g.all_cycles_iterator(simple=False, max_length=None, - ....: by_weight=True, report_weight=True) - sage: for i in range(9): print(next(it)) - (3, ['a', 'b', 'a']) - (4, ['a', 'e', 'a']) - (4, ['b', 'c', 'd', 'b']) - (6, ['a', 'b', 'a', 'b', 'a']) - (7, ['a', 'b', 'a', 'e', 'a']) - (7, ['a', 'b', 'c', 'd', 'b', 'a']) - (7, ['a', 'e', 'a', 'b', 'a']) - (8, ['a', 'e', 'a', 'e', 'a']) - (8, ['b', 'c', 'd', 'b', 'c', 'd', 'b']) - - Each edge must have a positive weight:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', -2), ('b', 'a', 1)]) - sage: next(g.all_cycles_iterator(simple=False, max_length=None, - ....: by_weight=True, report_weight=True)) - Traceback (most recent call last): - ... - ValueError: negative weight is not allowed - - The algorithm ``'B'`` is available only when `simple=True`:: - - sage: g = DiGraph() - sage: g.add_edges([('a', 'b', 1), ('b', 'a', 1)]) - sage: next(g.all_cycles_iterator(algorithm='B', simple=False)) - ....: - Traceback (most recent call last): - ... - ValueError: The algorithm 'B' is available only when simple=True. - """ - if starting_vertices is None: - starting_vertices = self - # Since a cycle is always included in a given strongly connected - # component, we may remove edges from the graph - sccs = self.strongly_connected_components() - d = {} - for id, component in enumerate(sccs): - for v in component: - d[v] = id - h = copy(self) - h.delete_edges((u, v) for u, v in h.edge_iterator(labels=False) if d[u] != d[v]) - - by_weight, weight_function = self._get_weight_function(by_weight=by_weight, - weight_function=weight_function, - check_weight=check_weight) - - if by_weight: - for e in h.edge_iterator(): - if weight_function(e) < 0: - raise ValueError("negative weight is not allowed") - - if algorithm == 'A': - # We create one cycles iterator per vertex. This is necessary if we - # want to iterate over cycles with increasing length. - def cycle_iter(v): - return h._all_cycles_iterator_vertex(v, - starting_vertices=starting_vertices, - simple=simple, - rooted=rooted, - max_length=max_length, - trivial=trivial, - remove_acyclic_edges=False, - weight_function=weight_function, - by_weight=by_weight, - check_weight=check_weight, - report_weight=True) - - iterators = {v: cycle_iter(v) for v in starting_vertices} - elif algorithm == 'B': - if not simple: - raise ValueError("The algorithm 'B' is available only when simple=True.") - - 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) - - SCCS = h.strongly_connected_components_subgraphs() - iterators = dict() - 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()) - else: - raise ValueError(f"The algorithm {algorithm} is not valid. \ - Use the algorithm 'A' or 'B'.") - - cycles = [] - for key, it in iterators.items(): - try: - length, cycle = next(it) - cycles.append((length, cycle, key)) - except StopIteration: - pass - # Since we always extract a shortest path, using a heap - # can speed up the algorithm - from heapq import heapify, heappop, heappush - heapify(cycles) - while cycles: - # We choose the shortest available cycle - length, shortest_cycle, key = heappop(cycles) - if report_weight: - yield (length, shortest_cycle) - else: - yield shortest_cycle - # We update the cycle iterator to its next available cycle if it - # exists - try: - length, cycle = next(iterators[key]) - heappush(cycles, (length, cycle, key)) - except StopIteration: - pass - - def all_simple_cycles(self, starting_vertices=None, rooted=False, - max_length=None, trivial=False, - weight_function=None, by_weight=False, - check_weight=True, report_weight=False, - algorithm='A'): - r""" - Return a list of all simple cycles of ``self``. The cycles are - enumerated in increasing length order. Each edge must have a - positive weight. - - INPUT: - - - ``starting_vertices`` -- iterable (default: ``None``); vertices from - which the cycles must start. If ``None``, then all vertices of the - graph can be starting points. This argument is necessary if ``rooted`` - is set to ``True``. - - - ``rooted`` -- boolean (default: ``False``); if set to False, then - cycles differing only by their starting vertex are considered the same - (e.g. ``['a', 'b', 'c', 'a']`` and ``['b', 'c', 'a', - 'b']``). Otherwise, all cycles are enumerated. - - - ``max_length`` -- nonnegative integer (default: ``None``); the - maximum length of the enumerated paths. If set to ``None``, then all - lengths are allowed. - - - ``trivial`` -- boolean (default: ``False``); if set to ``True``, then - the empty paths are also enumerated - - - ``weight_function`` -- function (default: ``None``); a function that - takes as input an edge ``(u, v, l)`` and outputs its weight. If not - ``None``, ``by_weight`` is automatically set to ``True``. If ``None`` - and ``by_weight`` is ``True``, we use the edge label ``l`` as a - weight. - - - ``by_weight`` -- boolean (default: ``False``); if ``True``, the edges - in the graph are weighted, otherwise all edges have weight 1 - - - ``check_weight`` -- boolean (default: ``True``); whether to check that - the ``weight_function`` outputs a number for each edge - - - ``report_weight`` -- boolean (default: ``False``); if ``False``, just - a cycle is returned. Otherwise a tuple of cycle length and cycle is - returned. - - - ``algorithm`` -- string (default: ``'A'``); the algorithm used to - enumerate the cycles. - - - The algorithm ``'A'`` holds cycle iterators starting with each vertex, - and output them in increasing length order. - - - The algorithm ``'B'`` holds cycle iterators starting with each edge, - and output them in increasing length order. - - OUTPUT: list - - .. NOTE:: - - Although the number of simple cycles of a finite graph is always - finite, computing all its cycles may take a very long time. - - EXAMPLES:: - - sage: g = DiGraph({'a': ['a', 'b'], 'b': ['c'], 'c': ['d'], 'd': ['c']}, loops=True) - sage: g.all_simple_cycles() - [['a', 'a'], ['c', 'd', 'c']] - - The directed version of the Petersen graph:: - - sage: g = graphs.PetersenGraph().to_directed() - sage: g.all_simple_cycles(max_length=4) - [[0, 1, 0], [0, 4, 0], [0, 5, 0], [1, 2, 1], [1, 6, 1], [2, 3, 2], - [2, 7, 2], [3, 4, 3], [3, 8, 3], [4, 9, 4], [5, 7, 5], [5, 8, 5], - [6, 8, 6], [6, 9, 6], [7, 9, 7]] - sage: g.all_simple_cycles(max_length=6) - [[0, 1, 0], [0, 4, 0], [0, 5, 0], [1, 2, 1], [1, 6, 1], [2, 3, 2], - [2, 7, 2], [3, 4, 3], [3, 8, 3], [4, 9, 4], [5, 7, 5], [5, 8, 5], - [6, 8, 6], [6, 9, 6], [7, 9, 7], [0, 1, 2, 3, 4, 0], - [0, 1, 2, 7, 5, 0], [0, 1, 6, 8, 5, 0], [0, 1, 6, 9, 4, 0], - [0, 4, 3, 2, 1, 0], [0, 4, 3, 8, 5, 0], [0, 4, 9, 6, 1, 0], - [0, 4, 9, 7, 5, 0], [0, 5, 7, 2, 1, 0], [0, 5, 7, 9, 4, 0], - [0, 5, 8, 3, 4, 0], [0, 5, 8, 6, 1, 0], [1, 2, 3, 8, 6, 1], - [1, 2, 7, 9, 6, 1], [1, 6, 8, 3, 2, 1], [1, 6, 9, 7, 2, 1], - [2, 3, 4, 9, 7, 2], [2, 3, 8, 5, 7, 2], [2, 7, 5, 8, 3, 2], - [2, 7, 9, 4, 3, 2], [3, 4, 9, 6, 8, 3], [3, 8, 6, 9, 4, 3], - [5, 7, 9, 6, 8, 5], [5, 8, 6, 9, 7, 5], [0, 1, 2, 3, 8, 5, 0], - [0, 1, 2, 7, 9, 4, 0], [0, 1, 6, 8, 3, 4, 0], - [0, 1, 6, 9, 7, 5, 0], [0, 4, 3, 2, 7, 5, 0], - [0, 4, 3, 8, 6, 1, 0], [0, 4, 9, 6, 8, 5, 0], - [0, 4, 9, 7, 2, 1, 0], [0, 5, 7, 2, 3, 4, 0], - [0, 5, 7, 9, 6, 1, 0], [0, 5, 8, 3, 2, 1, 0], - [0, 5, 8, 6, 9, 4, 0], [1, 2, 3, 4, 9, 6, 1], - [1, 2, 7, 5, 8, 6, 1], [1, 6, 8, 5, 7, 2, 1], - [1, 6, 9, 4, 3, 2, 1], [2, 3, 8, 6, 9, 7, 2], - [2, 7, 9, 6, 8, 3, 2], [3, 4, 9, 7, 5, 8, 3], - [3, 8, 5, 7, 9, 4, 3]] - - The complete graph (without loops) on `4` vertices:: - - sage: g = graphs.CompleteGraph(4).to_directed() - sage: g.all_simple_cycles() - [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 2, 1], [1, 3, 1], [2, 3, 2], - [0, 1, 2, 0], [0, 1, 3, 0], [0, 2, 1, 0], [0, 2, 3, 0], - [0, 3, 1, 0], [0, 3, 2, 0], [1, 2, 3, 1], [1, 3, 2, 1], - [0, 1, 2, 3, 0], [0, 1, 3, 2, 0], [0, 2, 1, 3, 0], - [0, 2, 3, 1, 0], [0, 3, 1, 2, 0], [0, 3, 2, 1, 0]] - - If the graph contains a large number of cycles, one can bound the length - of the cycles, or simply restrict the possible starting vertices of the - cycles:: - - sage: g = graphs.CompleteGraph(20).to_directed() - sage: g.all_simple_cycles(max_length=2) - [[0, 1, 0], [0, 2, 0], [0, 3, 0], [0, 4, 0], [0, 5, 0], [0, 6, 0], [0, 7, 0], - [0, 8, 0], [0, 9, 0], [0, 10, 0], [0, 11, 0], [0, 12, 0], [0, 13, 0], - [0, 14, 0], [0, 15, 0], [0, 16, 0], [0, 17, 0], [0, 18, 0], [0, 19, 0], - [1, 2, 1], [1, 3, 1], [1, 4, 1], [1, 5, 1], [1, 6, 1], [1, 7, 1], [1, 8, 1], - [1, 9, 1], [1, 10, 1], [1, 11, 1], [1, 12, 1], [1, 13, 1], [1, 14, 1], - [1, 15, 1], [1, 16, 1], [1, 17, 1], [1, 18, 1], [1, 19, 1], [2, 3, 2], - [2, 4, 2], [2, 5, 2], [2, 6, 2], [2, 7, 2], [2, 8, 2], [2, 9, 2], [2, 10, 2], - [2, 11, 2], [2, 12, 2], [2, 13, 2], [2, 14, 2], [2, 15, 2], [2, 16, 2], - [2, 17, 2], [2, 18, 2], [2, 19, 2], [3, 4, 3], [3, 5, 3], [3, 6, 3], - [3, 7, 3], [3, 8, 3], [3, 9, 3], [3, 10, 3], [3, 11, 3], [3, 12, 3], - [3, 13, 3], [3, 14, 3], [3, 15, 3], [3, 16, 3], [3, 17, 3], [3, 18, 3], - [3, 19, 3], [4, 5, 4], [4, 6, 4], [4, 7, 4], [4, 8, 4], [4, 9, 4], [4, 10, 4], - [4, 11, 4], [4, 12, 4], [4, 13, 4], [4, 14, 4], [4, 15, 4], [4, 16, 4], - [4, 17, 4], [4, 18, 4], [4, 19, 4], [5, 6, 5], [5, 7, 5], [5, 8, 5], - [5, 9, 5], [5, 10, 5], [5, 11, 5], [5, 12, 5], [5, 13, 5], [5, 14, 5], - [5, 15, 5], [5, 16, 5], [5, 17, 5], [5, 18, 5], [5, 19, 5], [6, 7, 6], - [6, 8, 6], [6, 9, 6], [6, 10, 6], [6, 11, 6], [6, 12, 6], [6, 13, 6], - [6, 14, 6], [6, 15, 6], [6, 16, 6], [6, 17, 6], [6, 18, 6], [6, 19, 6], - [7, 8, 7], [7, 9, 7], [7, 10, 7], [7, 11, 7], [7, 12, 7], [7, 13, 7], - [7, 14, 7], [7, 15, 7], [7, 16, 7], [7, 17, 7], [7, 18, 7], [7, 19, 7], - [8, 9, 8], [8, 10, 8], [8, 11, 8], [8, 12, 8], [8, 13, 8], [8, 14, 8], - [8, 15, 8], [8, 16, 8], [8, 17, 8], [8, 18, 8], [8, 19, 8], [9, 10, 9], - [9, 11, 9], [9, 12, 9], [9, 13, 9], [9, 14, 9], [9, 15, 9], [9, 16, 9], - [9, 17, 9], [9, 18, 9], [9, 19, 9], [10, 11, 10], [10, 12, 10], [10, 13, 10], - [10, 14, 10], [10, 15, 10], [10, 16, 10], [10, 17, 10], [10, 18, 10], - [10, 19, 10], [11, 12, 11], [11, 13, 11], [11, 14, 11], [11, 15, 11], - [11, 16, 11], [11, 17, 11], [11, 18, 11], [11, 19, 11], [12, 13, 12], - [12, 14, 12], [12, 15, 12], [12, 16, 12], [12, 17, 12], [12, 18, 12], - [12, 19, 12], [13, 14, 13], [13, 15, 13], [13, 16, 13], [13, 17, 13], - [13, 18, 13], [13, 19, 13], [14, 15, 14], [14, 16, 14], [14, 17, 14], - [14, 18, 14], [14, 19, 14], [15, 16, 15], [15, 17, 15], [15, 18, 15], - [15, 19, 15], [16, 17, 16], [16, 18, 16], [16, 19, 16], [17, 18, 17], - [17, 19, 17], [18, 19, 18]] - - sage: g = graphs.CompleteGraph(20).to_directed() - sage: g.all_simple_cycles(max_length=2, starting_vertices=[0]) - [[0, 1, 0], [0, 2, 0], [0, 3, 0], [0, 4, 0], [0, 5, 0], - [0, 6, 0], [0, 7, 0], [0, 8, 0], [0, 9, 0], [0, 10, 0], - [0, 11, 0], [0, 12, 0], [0, 13, 0], [0, 14, 0], [0, 15, 0], - [0, 16, 0], [0, 17, 0], [0, 18, 0], [0, 19, 0]] - - One may prefer to distinguish equivalent cycles having distinct starting - vertices (compare the following examples):: - - sage: g = graphs.CompleteGraph(4).to_directed() - sage: g.all_simple_cycles(max_length=2, rooted=False) - [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 2, 1], [1, 3, 1], [2, 3, 2]] - sage: g.all_simple_cycles(max_length=2, rooted=True) - [[0, 1, 0], [0, 2, 0], [0, 3, 0], [1, 0, 1], [1, 2, 1], [1, 3, 1], - [2, 0, 2], [2, 1, 2], [2, 3, 2], [3, 0, 3], [3, 1, 3], [3, 2, 3]] - - A cycle is enumerated in increasing length order for a weighted graph:: - - sage: cycles = g.all_simple_cycles(weight_function=lambda e:e[0]+e[1], - ....: by_weight=True, report_weight=True) - sage: cycles - [(2, [0, 1, 0]), (4, [0, 2, 0]), (6, [0, 1, 2, 0]), (6, [0, 2, 1, 0]), - (6, [0, 3, 0]), (6, [1, 2, 1]), (8, [0, 1, 3, 0]), (8, [0, 3, 1, 0]), - (8, [1, 3, 1]), (10, [0, 2, 3, 0]), (10, [0, 3, 2, 0]), (10, [2, 3, 2]), - (12, [0, 1, 2, 3, 0]), (12, [0, 1, 3, 2, 0]), (12, [0, 2, 1, 3, 0]), - (12, [0, 2, 3, 1, 0]), (12, [0, 3, 1, 2, 0]), (12, [0, 3, 2, 1, 0]), - (12, [1, 2, 3, 1]), (12, [1, 3, 2, 1])] - - The algorithm ``'B'`` can be used:: - - sage: cycles_B = g.all_simple_cycles(weight_function=lambda e:e[0]+e[1], by_weight=True, - ....: report_weight=True, algorithm='B') - sage: cycles_B - [(2, [0, 1, 0]), (4, [0, 2, 0]), (6, [0, 1, 2, 0]), (6, [0, 2, 1, 0]), - (6, [0, 3, 0]), (6, [1, 2, 1]), (8, [0, 1, 3, 0]), (8, [0, 3, 1, 0]), - (8, [1, 3, 1]), (10, [0, 2, 3, 0]), (10, [0, 3, 2, 0]), (10, [2, 3, 2]), - (12, [0, 1, 3, 2, 0]), (12, [0, 1, 2, 3, 0]), (12, [0, 2, 3, 1, 0]), - (12, [0, 2, 1, 3, 0]), (12, [0, 3, 2, 1, 0]), (12, [0, 3, 1, 2, 0]), - (12, [1, 2, 3, 1]), (12, [1, 3, 2, 1])] - sage: cycles.sort() == cycles_B.sort() - True - """ - return list(self.all_cycles_iterator(starting_vertices=starting_vertices, - simple=True, rooted=rooted, - max_length=max_length, trivial=trivial, - weight_function=weight_function, - by_weight=by_weight, - check_weight=check_weight, - report_weight=report_weight, - algorithm=algorithm)) - def path_semigroup(self): """ The partial semigroup formed by the paths of this quiver. @@ -4751,3 +3979,7 @@ def _singleton_in_branching(): from sage.graphs.connectivity import strongly_connected_components_subgraphs from sage.graphs.connectivity import strongly_connected_component_containing_vertex from sage.graphs.connectivity import strong_articulation_points + from sage.graphs.cycle_enumeration import all_cycles_iterator + from sage.graphs.cycle_enumeration import all_simple_cycles + from sage.graphs.cycle_enumeration import _all_cycles_iterator_vertex + from sage.graphs.cycle_enumeration import _all_simple_cycles_iterator_edge diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py index 15522431376..1b6c24e37cb 100644 --- a/src/sage/graphs/generic_graph.py +++ b/src/sage/graphs/generic_graph.py @@ -125,6 +125,8 @@ :meth:`~GenericGraph.all_simple_paths` | Return a list of all the simple paths of ``self`` starting with one of the given vertices. :meth:`~GenericGraph.triangles_count` | Return the number of triangles in the (di)graph. :meth:`~GenericGraph.shortest_simple_paths` | Return an iterator over the simple paths between a pair of vertices. + :meth:`~GenericGraph.all_cycles_iterator` | Return an iterator over all cycles in the (di)graph. + :meth:`~GenericGraph.all_simple_cycles` | Return a list of all simple cycles in the (di)graph. **Linear algebra:** @@ -242,6 +244,7 @@ :meth:`~GenericGraph.connected_components_sizes` | Return the sizes of the connected components as a list. :meth:`~GenericGraph.blocks_and_cut_vertices` | Compute the blocks and cut vertices of the graph. :meth:`~GenericGraph.blocks_and_cuts_tree` | Compute the blocks-and-cuts tree of the graph. + :meth:`~GenericGraph.biconnected_components_subgraphs` | Return a list of biconnected components as graph objects. :meth:`~GenericGraph.is_cut_edge` | Check whether the input edge is a cut-edge or a bridge. :meth:`~GenericGraph.is_edge_cut` | Check whether the input edges form an edge cut. :meth:`~GenericGraph.is_cut_vertex` | Check whether the input vertex is a cut-vertex. @@ -25937,6 +25940,7 @@ def is_self_complementary(self): from sage.graphs.connectivity import connected_components_sizes from sage.graphs.connectivity import blocks_and_cut_vertices from sage.graphs.connectivity import blocks_and_cuts_tree + from sage.graphs.connectivity import biconnected_components_subgraphs from sage.graphs.connectivity import is_cut_edge from sage.graphs.connectivity import is_edge_cut from sage.graphs.connectivity import is_cut_vertex @@ -25960,6 +25964,9 @@ def is_self_complementary(self): from sage.graphs.traversals import lex_DFS from sage.graphs.traversals import lex_DOWN is_geodetic = LazyImport('sage.graphs.convexity_properties', 'is_geodetic') + from sage.graphs.cycle_enumeration import _all_simple_cycles_iterator_edge + from sage.graphs.cycle_enumeration import all_cycles_iterator + from sage.graphs.cycle_enumeration import all_simple_cycles def katz_matrix(self, alpha, nonedgesonly=False, vertices=None): r""" diff --git a/src/sage/graphs/meson.build b/src/sage/graphs/meson.build index 891f6d8f8be..d83cdca99dc 100644 --- a/src/sage/graphs/meson.build +++ b/src/sage/graphs/meson.build @@ -25,6 +25,7 @@ py.install_sources( 'cographs.py', 'connectivity.pxd', 'convexity_properties.pxd', + 'cycle_enumeration.py', 'digraph.py', 'digraph_generators.py', 'distances_all_pairs.pxd', diff --git a/src/sage/graphs/path_enumeration.pyx b/src/sage/graphs/path_enumeration.pyx index f77d1fd239a..52643122e56 100644 --- a/src/sage/graphs/path_enumeration.pyx +++ b/src/sage/graphs/path_enumeration.pyx @@ -734,6 +734,18 @@ def yen_k_shortest_simple_paths(self, source, target, weight_function=None, [1, 6, 9, 10, 5], [1, 6, 9, 3, 4, 5], [1, 6, 9, 11, 10, 5]] + + When ``s == t`` and ``report_edge == True`` and ``report_weight == True`` (:issue:`40247`):: + + sage: g = DiGraph([(1, 2)]) + sage: list(yen_k_shortest_simple_paths(g, 1, 1, report_edges=True, report_weight=True)) + [(0, [])] + + No path between two vertices exists:: + + sage: g = Graph(2) + sage: list(yen_k_shortest_simple_paths(g, 0, 1)) + [] """ if source not in self: raise ValueError("vertex '{}' is not in the graph".format(source)) @@ -741,12 +753,8 @@ def yen_k_shortest_simple_paths(self, source, target, weight_function=None, raise ValueError("vertex '{}' is not in the graph".format(target)) if source == target: - if report_edges: - yield [] - elif report_weight: - yield (0, [source]) - else: - yield [source] + P = [] if report_edges else [source] + yield (0, P) if report_weight else P return if self.has_loops() or self.allows_multiple_edges(): @@ -784,10 +792,6 @@ def yen_k_shortest_simple_paths(self, source, target, weight_function=None, path = shortest_path_func(source, target) # corner case if not path: - if report_weight: - yield (0, []) - else: - yield [] return cdef dict edge_labels