|
306 | 306 | :meth:`~GenericGraph.feedback_vertex_set` | Compute the minimum feedback vertex set of a (di)graph.
|
307 | 307 | :meth:`~GenericGraph.multiway_cut` | Return a minimum edge multiway cut
|
308 | 308 | :meth:`~GenericGraph.max_cut` | Return a maximum edge cut of the graph.
|
| 309 | + :meth:`~GenericGraph.longest_cycle` | Return the longest (induced) cycle of ``self``. |
309 | 310 | :meth:`~GenericGraph.longest_path` | Return a longest path of ``self``.
|
310 | 311 | :meth:`~GenericGraph.traveling_salesman_problem` | Solve the traveling salesman problem (TSP)
|
311 | 312 | :meth:`~GenericGraph.is_hamiltonian` | Test whether the current graph is Hamiltonian.
|
@@ -7874,6 +7875,312 @@ def good_edge(e):
|
7874 | 7875 |
|
7875 | 7876 | return val
|
7876 | 7877 |
|
| 7878 | + def longest_cycle(self, induced=False, use_edge_labels=False, |
| 7879 | + solver=None, verbose=0, *, integrality_tolerance=0.001): |
| 7880 | + r""" |
| 7881 | + Return the longest (induced) cycle of ``self``. |
| 7882 | + |
| 7883 | + This method uses an integer linear programming formulation based on |
| 7884 | + subtour elimination constraints to find the longest cycle. This cycle is |
| 7885 | + *elementary* (or *simple*), and so without repeated vertices. When |
| 7886 | + searching for an *induced* cycle (i.e., a cycle without chord), it uses |
| 7887 | + in addition cycle elimination constraints as proposed in [MMRS2022]_. |
| 7888 | + |
| 7889 | + We assume that the longest cycle in graph has at least 3 vertices and at |
| 7890 | + least 2 in a digraph. The longest induced cycle as at least 4 vertices |
| 7891 | + in both a graph and a digraph. |
| 7892 | + |
| 7893 | + .. NOTE:: |
| 7894 | + |
| 7895 | + Graphs and digraphs with loops or multiple edges are currently not |
| 7896 | + accepted. It is certainly possible to extend the method to accept |
| 7897 | + them. |
| 7898 | + |
| 7899 | + INPUT: |
| 7900 | + |
| 7901 | + - ``induced`` -- boolean (default: ``False``); whether to return the |
| 7902 | + longest induced cycle or the longest cycle |
| 7903 | + |
| 7904 | + - ``use_edge_labels`` -- boolean (default: ``False``); whether to |
| 7905 | + compute a cycle with maximum weight where the weight of an edge is |
| 7906 | + defined by its label (a label set to ``None`` or ``{}`` being |
| 7907 | + considered as a weight of `1`), or to compute a cycle with the largest |
| 7908 | + possible number of edges (i.e., edge weights are set to 1) |
| 7909 | + |
| 7910 | + - ``solver`` -- string (default: ``None``); specify a Mixed Integer |
| 7911 | + Linear Programming (MILP) solver to be used. If set to ``None``, the |
| 7912 | + default one is used. For more information on MILP solvers and which |
| 7913 | + default solver is used, see the method :meth:`solve |
| 7914 | + <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class |
| 7915 | + :class:`MixedIntegerLinearProgram |
| 7916 | + <sage.numerical.mip.MixedIntegerLinearProgram>`. |
| 7917 | + |
| 7918 | + - ``verbose`` -- integer (default: ``0``); sets the level of |
| 7919 | + verbosity; set to ``0`` by default, which means quiet |
| 7920 | + |
| 7921 | + - ``integrality_tolerance`` -- float; parameter for use with MILP |
| 7922 | + solvers over an inexact base ring; see |
| 7923 | + :meth:`MixedIntegerLinearProgram.get_values` |
| 7924 | + |
| 7925 | + OUTPUT: |
| 7926 | + |
| 7927 | + A subgraph of ``self`` corresponding to a (directed if ``self`` is |
| 7928 | + directed) longest (induced) cycle. If ``use_edge_labels == True``, a |
| 7929 | + pair ``weight, cycle`` is returned. |
| 7930 | + |
| 7931 | + EXAMPLES: |
| 7932 | + |
| 7933 | + Longest (induced) cycle of a graph:: |
| 7934 | + |
| 7935 | + sage: G = graphs.Grid2dGraph(3, 4) |
| 7936 | + sage: G.longest_cycle(induced=False) |
| 7937 | + longest cycle from 2D Grid Graph for [3, 4]: Graph on 12 vertices |
| 7938 | + sage: G.longest_cycle(induced=True) |
| 7939 | + longest induced cycle from 2D Grid Graph for [3, 4]: Graph on 10 vertices |
| 7940 | + |
| 7941 | + Longest (induced) cycle in a digraph:: |
| 7942 | + |
| 7943 | + sage: D = digraphs.Circuit(8) |
| 7944 | + sage: D.add_edge(0, 2) |
| 7945 | + sage: D.longest_cycle(induced=False) |
| 7946 | + longest cycle from Circuit: Digraph on 8 vertices |
| 7947 | + sage: D.longest_cycle(induced=True) |
| 7948 | + longest induced cycle from Circuit: Digraph on 7 vertices |
| 7949 | + sage: D.add_edge(1, 0) |
| 7950 | + sage: D.longest_cycle(induced=False) |
| 7951 | + longest cycle from Circuit: Digraph on 8 vertices |
| 7952 | + sage: D.longest_cycle(induced=True) |
| 7953 | + longest induced cycle from Circuit: Digraph on 7 vertices |
| 7954 | + sage: D.add_edge(2, 0) |
| 7955 | + sage: D.longest_cycle(induced=False) |
| 7956 | + longest cycle from Circuit: Digraph on 8 vertices |
| 7957 | + sage: D.longest_cycle(induced=True) |
| 7958 | + longest induced cycle from Circuit: Digraph on 0 vertices |
| 7959 | + |
| 7960 | + Longest (induced) cycle when considering edge weights:: |
| 7961 | + |
| 7962 | + sage: D = digraphs.Circuit(15) |
| 7963 | + sage: for u, v in D.edges(labels=False): |
| 7964 | + ....: D.set_edge_label(u, v, 1) |
| 7965 | + sage: D.add_edge(0, 10, 50) |
| 7966 | + sage: D.add_edge(11, 1, 1) |
| 7967 | + sage: D.add_edge(13, 0, 1) |
| 7968 | + sage: D.longest_cycle(induced=False, use_edge_labels=False) |
| 7969 | + longest cycle from Circuit: Digraph on 15 vertices |
| 7970 | + sage: D.longest_cycle(induced=False, use_edge_labels=True) |
| 7971 | + (55, longest cycle from Circuit: Digraph on 6 vertices) |
| 7972 | + sage: D.longest_cycle(induced=True, use_edge_labels=False) |
| 7973 | + longest induced cycle from Circuit: Digraph on 11 vertices |
| 7974 | + sage: D.longest_cycle(induced=True, use_edge_labels=True) |
| 7975 | + (54, longest induced cycle from Circuit: Digraph on 5 vertices) |
| 7976 | + |
| 7977 | + TESTS: |
| 7978 | + |
| 7979 | + Small cases:: |
| 7980 | + |
| 7981 | + sage: Graph().longest_cycle() |
| 7982 | + longest cycle: Graph on 0 vertices |
| 7983 | + sage: Graph(1).longest_cycle() |
| 7984 | + longest cycle: Graph on 0 vertices |
| 7985 | + sage: Graph([(0, 1)]).longest_cycle() |
| 7986 | + longest cycle: Graph on 0 vertices |
| 7987 | + sage: Graph([(0, 1), (1, 2)]).longest_cycle() |
| 7988 | + longest cycle: Graph on 0 vertices |
| 7989 | + sage: Graph([(0, 1), (1, 2), (0, 2)]).longest_cycle() |
| 7990 | + longest cycle: Graph on 3 vertices |
| 7991 | + sage: Graph([(0, 1), (1, 2), (0, 2)]).longest_cycle(induced=True) |
| 7992 | + longest induced cycle: Graph on 0 vertices |
| 7993 | + sage: DiGraph().longest_cycle() |
| 7994 | + longest cycle: Digraph on 0 vertices |
| 7995 | + sage: DiGraph(1).longest_cycle() |
| 7996 | + longest cycle: Digraph on 0 vertices |
| 7997 | + sage: DiGraph([(0, 1), (1, 0)]).longest_cycle() |
| 7998 | + longest cycle: Digraph on 2 vertices |
| 7999 | + sage: DiGraph([(0, 1), (1, 0)]).longest_cycle(induced=True) |
| 8000 | + longest induced cycle: Digraph on 0 vertices |
| 8001 | + |
| 8002 | + Disconnected digraph:: |
| 8003 | + |
| 8004 | + sage: D = digraphs.Circuit(5) + digraphs.Circuit(4) |
| 8005 | + sage: D.longest_cycle() |
| 8006 | + longest cycle from Subgraph of (Circuit disjoint_union Circuit): Digraph on 5 vertices |
| 8007 | + sage: D.longest_cycle(induced=True) |
| 8008 | + longest induced cycle from Subgraph of (Circuit disjoint_union Circuit): Digraph on 5 vertices |
| 8009 | + """ |
| 8010 | + self._scream_if_not_simple() |
| 8011 | + G = self |
| 8012 | + st = f" from {G.name()}" if G.name() else "" |
| 8013 | + name = f"longest{' induced' if induced else ''} cycle{st}" |
| 8014 | + |
| 8015 | + # Helper functions to manipulate weights |
| 8016 | + if use_edge_labels: |
| 8017 | + def weight(e): |
| 8018 | + return 1 if (len(e) < 3 or e[2] is None) else e[2] |
| 8019 | + |
| 8020 | + def total_weight(gg): |
| 8021 | + return sum(weight(e) for e in gg.edge_iterator()) |
| 8022 | + else: |
| 8023 | + def weight(e): |
| 8024 | + return 1 |
| 8025 | + |
| 8026 | + def total_weight(gg): |
| 8027 | + return gg.order() |
| 8028 | + |
| 8029 | + directed = G.is_directed() |
| 8030 | + immutable = G.is_immutable() |
| 8031 | + if directed: |
| 8032 | + from sage.graphs.digraph import DiGraph as MyGraph |
| 8033 | + blocks = G.strongly_connected_components() |
| 8034 | + else: |
| 8035 | + from sage.graphs.graph import Graph as MyGraph |
| 8036 | + blocks = G.blocks_and_cut_vertices()[0] |
| 8037 | + |
| 8038 | + # Deal with graphs with multiple biconnected components |
| 8039 | + if len(blocks) > 1: |
| 8040 | + best = MyGraph(name=name, immutable=immutable) |
| 8041 | + best_w = 0 |
| 8042 | + for block in blocks: |
| 8043 | + if induced and len(block) < 4: |
| 8044 | + continue |
| 8045 | + h = G.subgraph(vertices=block) |
| 8046 | + C = h.longest_cycle(induced=induced, |
| 8047 | + use_edge_labels=use_edge_labels, |
| 8048 | + solver=solver, verbose=verbose, |
| 8049 | + integrality_tolerance=integrality_tolerance) |
| 8050 | + if total_weight(C) > best_w: |
| 8051 | + best = C |
| 8052 | + best_w = total_weight(C) |
| 8053 | + return (best_w, best) if use_edge_labels else best |
| 8054 | + |
| 8055 | + # We now know that the graph is biconnected or that the digraph is |
| 8056 | + # strongly connected. |
| 8057 | + |
| 8058 | + if ((induced and G.order() < 4) or |
| 8059 | + (not induced and ((directed and G.order() < 2) or |
| 8060 | + (not directed and G.order() < 3)))): |
| 8061 | + if use_edge_labels: |
| 8062 | + return 0, MyGraph(name=name, immutable=immutable) |
| 8063 | + return MyGraph(name=name, immutable=immutable) |
| 8064 | + if (not induced and ((directed and G.order() == 2) or |
| 8065 | + (not directed and G.order() == 3))): |
| 8066 | + answer = G.copy() |
| 8067 | + answer.name(name) |
| 8068 | + if use_edge_labels: |
| 8069 | + return total_weight(answer), answer |
| 8070 | + return answer |
| 8071 | + |
| 8072 | + # Helper functions to index edges |
| 8073 | + if directed: |
| 8074 | + def F(e): |
| 8075 | + return e[:2] |
| 8076 | + else: |
| 8077 | + def F(e): |
| 8078 | + return frozenset(e[:2]) |
| 8079 | + |
| 8080 | + from sage.numerical.mip import MixedIntegerLinearProgram |
| 8081 | + from sage.numerical.mip import MIPSolverException |
| 8082 | + |
| 8083 | + p = MixedIntegerLinearProgram(maximization=True, |
| 8084 | + solver=solver, |
| 8085 | + constraint_generation=True) |
| 8086 | + |
| 8087 | + # We need one binary variable per vertex and per edge |
| 8088 | + vertex = p.new_variable(binary=True) |
| 8089 | + edge = p.new_variable(binary=True) |
| 8090 | + |
| 8091 | + # Objective function: maximize the size of the cycle |
| 8092 | + p.set_objective(p.sum(weight(e) * edge[F(e)] for e in G.edge_iterator())) |
| 8093 | + |
| 8094 | + # We select as many vertices as edges |
| 8095 | + p.add_constraint(p.sum(edge[F(e)] for e in G.edge_iterator()) |
| 8096 | + == p.sum(vertex[u] for u in G)) |
| 8097 | + |
| 8098 | + if directed: |
| 8099 | + # If a vertex is selected, one of its incoming (resp. outgoing) edge |
| 8100 | + # must be selected, and none of them otherwise |
| 8101 | + for u in G: |
| 8102 | + p.add_constraint(p.sum(edge[F(e)] for e in G.outgoing_edge_iterator(u)) |
| 8103 | + <= vertex[u]) |
| 8104 | + p.add_constraint(p.sum(edge[F(e)] for e in G.incoming_edge_iterator(u)) |
| 8105 | + <= vertex[u]) |
| 8106 | + else: |
| 8107 | + # If a vertex is selected, two of its incident edges must be |
| 8108 | + # selected, and none of them otherwise |
| 8109 | + for u in G: |
| 8110 | + p.add_constraint(p.sum(edge[F(e)] for e in G.edge_iterator(u)) |
| 8111 | + <= 2 * vertex[u]) |
| 8112 | + |
| 8113 | + if induced: |
| 8114 | + # An edge is selected if its end vertices are. |
| 8115 | + # We use the linearization of the quadratic constraint |
| 8116 | + # vertex[u] * vertex[v] == edge[F((u, v))] |
| 8117 | + for e in G.edge_iterator(): |
| 8118 | + f = F(e) |
| 8119 | + u, v = f |
| 8120 | + p.add_constraint(edge[f] <= vertex[u]) |
| 8121 | + p.add_constraint(edge[f] <= vertex[v]) |
| 8122 | + p.add_constraint(vertex[u] + vertex[v] <= edge[f] + 1) |
| 8123 | + |
| 8124 | + # An induced cycle has at least 4 vertices |
| 8125 | + p.add_constraint(p.sum(vertex[u] for u in G), min=4) |
| 8126 | + |
| 8127 | + best = MyGraph(name=name, immutable=immutable) |
| 8128 | + best_w = 0 |
| 8129 | + |
| 8130 | + # We add cut constraints for as long as we find solutions |
| 8131 | + while True: |
| 8132 | + try: |
| 8133 | + p.solve(log=verbose) |
| 8134 | + except MIPSolverException: |
| 8135 | + # No (new) solution found |
| 8136 | + break |
| 8137 | + |
| 8138 | + # We build the Graph representing the current solution |
| 8139 | + b_val = p.get_values(edge, convert=bool, tolerance=integrality_tolerance) |
| 8140 | + edges = (e for e in G.edge_iterator() if b_val[F(e)]) |
| 8141 | + h = MyGraph(edges, format='list_of_edges', name=name, immutable=immutable) |
| 8142 | + if not h: |
| 8143 | + # No new solution found |
| 8144 | + break |
| 8145 | + |
| 8146 | + # If there is only one cycle, we are done ! |
| 8147 | + if directed: |
| 8148 | + cc = h.strongly_connected_components() |
| 8149 | + else: |
| 8150 | + cc = h.connected_components(sort=False) |
| 8151 | + if len(cc) == 1: |
| 8152 | + if total_weight(h) > best_w: |
| 8153 | + best = h |
| 8154 | + best_w = total_weight(best) |
| 8155 | + break |
| 8156 | + |
| 8157 | + # Otherwise, we add subtour elimination constraints |
| 8158 | + for c in cc: |
| 8159 | + if not (induced and len(c) < 4): |
| 8160 | + hh = h.subgraph(vertices=c) |
| 8161 | + if total_weight(hh) > best_w: |
| 8162 | + best = hh |
| 8163 | + best.name(name) |
| 8164 | + best_w = total_weight(best) |
| 8165 | + |
| 8166 | + # Add subtour elimination constraints |
| 8167 | + if directed: |
| 8168 | + p.add_constraint(p.sum(edge[F(e)] for e in G.edge_boundary(c)), min=1) |
| 8169 | + c = set(c) |
| 8170 | + cbar = (v for v in G if v not in c) |
| 8171 | + p.add_constraint(p.sum(edge[F(e)] for e in G.edge_boundary(cbar, c)), min=1) |
| 8172 | + else: |
| 8173 | + p.add_constraint(p.sum(edge[F(e)] for e in G.edge_boundary(c)), min=2) |
| 8174 | + |
| 8175 | + if induced: |
| 8176 | + # We eliminate this cycle |
| 8177 | + p.add_constraint(p.sum(vertex[u] for u in c) <= len(c) - 1) |
| 8178 | + |
| 8179 | + # We finally set the positions of the vertices and return the result |
| 8180 | + if G.get_pos(): |
| 8181 | + best.set_pos({u: pp for u, pp in G.get_pos().items() if u in best}) |
| 8182 | + return (best_w, best) if use_edge_labels else best |
| 8183 | + |
7877 | 8184 | def longest_path(self, s=None, t=None, use_edge_labels=False, algorithm="MILP",
|
7878 | 8185 | solver=None, verbose=0, *, integrality_tolerance=1e-3):
|
7879 | 8186 | r"""
|
|
0 commit comments