diff --git a/src/doc/en/reference/graphs/index.rst b/src/doc/en/reference/graphs/index.rst index c549876fe31..34c5f134839 100644 --- a/src/doc/en/reference/graphs/index.rst +++ b/src/doc/en/reference/graphs/index.rst @@ -84,6 +84,7 @@ Libraries of algorithms sage/graphs/spanning_tree sage/graphs/pq_trees sage/graphs/trees + sage/graphs/matching sage/graphs/matchpoly sage/graphs/genus sage/graphs/lovasz_theta diff --git a/src/doc/en/reference/references/index.rst b/src/doc/en/reference/references/index.rst index 59b28d9e9be..190e951186d 100644 --- a/src/doc/en/reference/references/index.rst +++ b/src/doc/en/reference/references/index.rst @@ -4666,6 +4666,12 @@ REFERENCES: measures*. Publications Mathématiques de l'Institut des Hautes Études Scientifiques 98(1) (2003), pp. 167-212. +.. [LZ2001] Dingjun Lou and Ning Zhong. *A highly efficient algorithm to + determine bicritical graphs.* In: Wang, J. (eds) Computing and + Combinatorics. Lecture Notes in Computer Science, vol 2108, + pages 349--356. Springer, Berlin, Heidelberg, 2001, + :doi:`10.1007/3-540-44679-6_38`. + .. [LZ2004] S. Lando and A. Zvonkine, "Graphs on surfaces and their applications", Springer-Verlag, 2004. @@ -6510,6 +6516,10 @@ REFERENCES: .. [TTWL2009] Trebst, Troyer, Wang and Ludwig, A short introduction to Fibonacci anyon models, :arxiv:`0902.3275`. +.. [Tut1947] W.T. Tutte. *The factorization of linear graphs.* Journal of the + London Mathematical Society, vol. s1-22, issue 2, pages 107--111, + April 1947. :doi:`10.1112/jlms/s1-22.2.107`. + .. [Tur1974] \R. J. Turyn *Hadamard matrices, Baumert-Hall units, four-symbol sequences, pulse compression, and surface wave encodings*. Journal of Combinatorial Theory, Series A 16.3 (1974), pp 313–333. diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index 134fe000df9..8bef9c56285 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -4122,408 +4122,6 @@ def asc(sigma): ret += M.term(sigma.to_composition(), t**asc(sigma)) return ret - @doc_index("Leftovers") - def matching(self, value_only=False, algorithm='Edmonds', - use_edge_labels=False, solver=None, verbose=0, - *, integrality_tolerance=1e-3): - r""" - Return a maximum weighted matching of the graph represented by the list - of its edges. - - For more information, see the :wikipedia:`Matching_(graph_theory)`. - - Given a graph `G` such that each edge `e` has a weight `w_e`, a maximum - matching is a subset `S` of the edges of `G` of maximum weight such that - no two edges of `S` are incident with each other. - - As an optimization problem, it can be expressed as: - - .. MATH:: - - \mbox{Maximize : }&\sum_{e\in G.edges()} w_e b_e\\ - \mbox{Such that : }&\forall v \in G, - \sum_{(u,v)\in G.edges()} b_{(u,v)}\leq 1\\ - &\forall x\in G, b_x\mbox{ is a binary variable} - - INPUT: - - - ``value_only`` -- boolean (default: ``False``); when set to ``True``, - only the cardinal (or the weight) of the matching is returned - - - ``algorithm`` -- string (default: ``'Edmonds'``) - - - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX - - - ``'LP'`` uses a Linear Program formulation of the matching problem - - - ``use_edge_labels`` -- boolean (default: ``False``) - - - when set to ``True``, computes a weighted matching where each edge - is weighted by its label (if an edge has no label, `1` is assumed) - - - when set to ``False``, each edge has weight `1` - - - ``solver`` -- string (default: ``None``); specifies a Mixed Integer - Linear Programming (MILP) solver to be used. If set to ``None``, the - default one is used. For more information on MILP solvers and which - default solver is used, see the method :meth:`solve - ` of the class - :class:`MixedIntegerLinearProgram - `. - - - ``verbose`` -- integer (default: 0); sets the level of verbosity: - set to 0 by default, which means quiet (only useful when ``algorithm - == "LP"``) - - - ``integrality_tolerance`` -- float; parameter for use with MILP - solvers over an inexact base ring; see - :meth:`MixedIntegerLinearProgram.get_values`. - - OUTPUT: - - - When ``value_only=False`` (default), this method returns an - :class:`EdgesView` containing the edges of a maximum matching of `G`. - - - When ``value_only=True``, this method returns the sum of the - weights (default: ``1``) of the edges of a maximum matching of `G`. - The type of the output may vary according to the type of the edge - labels and the algorithm used. - - ALGORITHM: - - The problem is solved using Edmond's algorithm implemented in NetworkX, - or using Linear Programming depending on the value of ``algorithm``. - - EXAMPLES: - - Maximum matching in a Pappus Graph:: - - sage: g = graphs.PappusGraph() - sage: g.matching(value_only=True) # needs sage.networkx - 9 - - Same test with the Linear Program formulation:: - - sage: g = graphs.PappusGraph() - sage: g.matching(algorithm='LP', value_only=True) # needs sage.numerical.mip - 9 - - .. PLOT:: - - g = graphs.PappusGraph() - sphinx_plot(g.plot(edge_colors={"red":g.matching()})) - - TESTS: - - When ``use_edge_labels`` is set to ``False``, with Edmonds' algorithm - and LP formulation:: - - sage: g = Graph([(0,1,0), (1,2,999), (2,3,-5)]) - sage: sorted(g.matching()) # needs sage.networkx - [(0, 1, 0), (2, 3, -5)] - sage: sorted(g.matching(algorithm='LP')) # needs sage.numerical.mip - [(0, 1, 0), (2, 3, -5)] - - When ``use_edge_labels`` is set to ``True``, with Edmonds' algorithm and - LP formulation:: - - sage: g = Graph([(0,1,0), (1,2,999), (2,3,-5)]) - sage: g.matching(use_edge_labels=True) # needs sage.networkx - [(1, 2, 999)] - sage: g.matching(algorithm='LP', use_edge_labels=True) # needs sage.numerical.mip - [(1, 2, 999)] - - With loops and multiedges:: - - sage: edge_list = [(0,0,5), (0,1,1), (0,2,2), (0,3,3), (1,2,6) - ....: , (1,2,3), (1,3,3), (2,3,3)] - sage: g = Graph(edge_list, loops=True, multiedges=True) - sage: m = g.matching(use_edge_labels=True) # needs sage.networkx - sage: type(m) # needs sage.networkx - - sage: sorted(m) # needs sage.networkx - [(0, 3, 3), (1, 2, 6)] - - TESTS: - - If ``algorithm`` is set to anything different from ``'Edmonds'`` or - ``'LP'``, an exception is raised:: - - sage: g = graphs.PappusGraph() - sage: g.matching(algorithm='somethingdifferent') - Traceback (most recent call last): - ... - ValueError: algorithm must be set to either "Edmonds" or "LP" - """ - from sage.rings.real_mpfr import RR - - def weight(x): - if x in RR: - return x - else: - return 1 - - W = {} - L = {} - for u, v, l in self.edge_iterator(): - if u is v: - continue - fuv = frozenset((u, v)) - if fuv not in L or (use_edge_labels and W[fuv] < weight(l)): - L[fuv] = l - if use_edge_labels: - W[fuv] = weight(l) - - if algorithm == "Edmonds": - import networkx - g = networkx.Graph() - if use_edge_labels: - for (u, v), w in W.items(): - g.add_edge(u, v, weight=w) - else: - for u, v in L: - g.add_edge(u, v) - d = networkx.max_weight_matching(g) - if value_only: - if use_edge_labels: - return sum(W[frozenset(e)] for e in d) - return Integer(len(d)) - - return EdgesView(Graph([(u, v, L[frozenset((u, v))]) for u, v in d], - format='list_of_edges')) - - elif algorithm == "LP": - g = self - from sage.numerical.mip import MixedIntegerLinearProgram - # returns the weight of an edge considering it may not be - # weighted ... - p = MixedIntegerLinearProgram(maximization=True, solver=solver) - b = p.new_variable(binary=True) - if use_edge_labels: - p.set_objective(p.sum(w * b[fe] for fe, w in W.items())) - else: - p.set_objective(p.sum(b[fe] for fe in L)) - # for any vertex v, there is at most one edge incident to v in - # the maximum matching - for v in g: - p.add_constraint(p.sum(b[frozenset(e)] for e in self.edge_iterator(vertices=[v], labels=False) - if e[0] != e[1]), max=1) - - p.solve(log=verbose) - b = p.get_values(b, convert=bool, tolerance=integrality_tolerance) - if value_only: - if use_edge_labels: - return sum(w for fe, w in W.items() if b[fe]) - return Integer(sum(1 for fe in L if b[fe])) - - return EdgesView(Graph([(u, v, L[frozenset((u, v))]) - for u, v in L if b[frozenset((u, v))]], - format='list_of_edges')) - - raise ValueError('algorithm must be set to either "Edmonds" or "LP"') - - @doc_index("Leftovers") - def is_factor_critical(self, matching=None, algorithm='Edmonds', solver=None, verbose=0, - *, integrality_tolerance=0.001): - r""" - Check whether this graph is factor-critical. - - A graph of order `n` is factor-critical if every subgraph of `n-1` - vertices have a perfect matching, hence `n` must be odd. See - :wikipedia:`Factor-critical_graph` for more details. - - This method implements the algorithm proposed in [LR2004]_ and we assume - that a graph of order one is factor-critical. The time complexity of the - algorithm is linear if a near perfect matching is given as input (i.e., - a matching such that all vertices but one are incident to an edge of the - matching). Otherwise, the time complexity is dominated by the time - needed to compute a maximum matching of the graph. - - INPUT: - - - ``matching`` -- (default: ``None``) a near perfect matching of the - graph, that is a matching such that all vertices of the graph but one - are incident to an edge of the matching. It can be given using any - valid input format of :class:`~sage.graphs.graph.Graph`. - - If set to ``None``, a matching is computed using the other parameters. - - - ``algorithm`` -- string (default: ``'Edmonds'``); the algorithm to use - to compute a maximum matching of the graph among - - - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX - - - ``'LP'`` uses a Linear Program formulation of the matching problem - - - ``solver`` -- string (default: ``None``); specifies a Mixed Integer - Linear Programming (MILP) solver to be used. If set to ``None``, the - default one is used. For more information on MILP solvers and which - default solver is used, see the method :meth:`solve - ` of the class - :class:`MixedIntegerLinearProgram - `. - - - ``verbose`` -- integer (default: 0); sets the level of verbosity: - set to 0 by default, which means quiet (only useful when ``algorithm - == "LP"``) - - - ``integrality_tolerance`` -- float; parameter for use with MILP - solvers over an inexact base ring; see - :meth:`MixedIntegerLinearProgram.get_values`. - - EXAMPLES: - - Odd length cycles and odd cliques of order at least 3 are - factor-critical graphs:: - - sage: [graphs.CycleGraph(2*i + 1).is_factor_critical() for i in range(5)] # needs networkx - [True, True, True, True, True] - sage: [graphs.CompleteGraph(2*i + 1).is_factor_critical() for i in range(5)] # needs networkx - [True, True, True, True, True] - - More generally, every Hamiltonian graph with an odd number of vertices - is factor-critical:: - - sage: G = graphs.RandomGNP(15, .2) - sage: G.add_path([0..14]) - sage: G.add_edge(14, 0) - sage: G.is_hamiltonian() - True - sage: G.is_factor_critical() # needs networkx - True - - Friendship graphs are non-Hamiltonian factor-critical graphs:: - - sage: [graphs.FriendshipGraph(i).is_factor_critical() for i in range(1, 5)] # needs networkx - [True, True, True, True] - - Bipartite graphs are not factor-critical:: - - sage: G = graphs.RandomBipartite(randint(1, 10), randint(1, 10), .5) # needs numpy - sage: G.is_factor_critical() # needs numpy - False - - Graphs with even order are not factor critical:: - - sage: G = graphs.RandomGNP(10, .5) - sage: G.is_factor_critical() - False - - One can specify a matching:: - - sage: F = graphs.FriendshipGraph(4) - sage: M = F.matching() # needs networkx - sage: F.is_factor_critical(matching=M) # needs networkx - True - sage: F.is_factor_critical(matching=Graph(M)) # needs networkx - True - - TESTS: - - Giving a wrong matching:: - - sage: G = graphs.RandomGNP(15, .3) - sage: while not G.is_biconnected(): - ....: G = graphs.RandomGNP(15, .3) - sage: M = G.matching() # needs networkx - sage: G.is_factor_critical(matching=M[:-1]) # needs networkx - Traceback (most recent call last): - ... - ValueError: the input is not a near perfect matching of the graph - sage: G.is_factor_critical(matching=G.edges(sort=True)) - Traceback (most recent call last): - ... - ValueError: the input is not a matching - sage: M = [(2*i, 2*i + 1) for i in range(9)] - sage: G.is_factor_critical(matching=M) - Traceback (most recent call last): - ... - ValueError: the input is not a matching of the graph - """ - if self.order() == 1: - return True - - # The graph must have an odd number of vertices, be 2-edge connected, so - # without bridges, and not bipartite - if (not self.order() % 2 or not self.is_connected() or - list(self.bridges()) or self.is_bipartite()): - return False - - if matching: - # We check that the input matching is a valid near perfect matching - # of the graph. - M = Graph(matching) - if any(d != 1 for d in M.degree()): - raise ValueError("the input is not a matching") - if not M.is_subgraph(self, induced=False): - raise ValueError("the input is not a matching of the graph") - if (self.order() != M.order() + 1) or (self.order() != 2*M.size() + 1): - raise ValueError("the input is not a near perfect matching of the graph") - else: - # We compute a maximum matching of the graph - M = Graph(self.matching(algorithm=algorithm, solver=solver, verbose=verbose, - integrality_tolerance=integrality_tolerance)) - - # It must be a near-perfect matching - if self.order() != M.order() + 1: - return False - - # We find the unsaturated vertex u, i.e., the only vertex of the graph - # not in M - for u in self: - if u not in M: - break - - # We virtually build an M-alternating tree T - from queue import Queue - Q = Queue() - Q.put(u) - even = set([u]) - odd = set() - pred = {u: u} - rank = {u: 0} - - while not Q.empty(): - x = Q.get() - for y in self.neighbor_iterator(x): - if y in odd: - continue - elif y in even: - # Search for the nearest common ancestor t of x and y - P = [x] - R = [y] - while P[-1] != R[-1]: - if rank[P[-1]] > rank[R[-1]]: - P.append(pred[P[-1]]) - elif rank[P[-1]] < rank[R[-1]]: - R.append(pred[R[-1]]) - else: - P.append(pred[P[-1]]) - R.append(pred[R[-1]]) - t = P.pop() - R.pop() - # Set t as pred of all vertices of the chains and add - # vertices marked odd to the queue - for a in itertools.chain(P, R): - pred[a] = t - rank[a] = rank[t] + 1 - if a in odd: - even.add(a) - odd.discard(a) - Q.put(a) - else: # y has not been visited yet - z = next(M.neighbor_iterator(y)) - odd.add(y) - even.add(z) - Q.put(z) - pred[y] = x - pred[z] = y - rank[y] = rank[x] + 1 - rank[z] = rank[y] + 1 - - # The graph is factor critical if all vertices are marked even - return len(even) == self.order() - @doc_index("Algorithmically hard stuff") def has_homomorphism_to(self, H, core=False, solver=None, verbose=0, *, integrality_tolerance=1e-3): @@ -9016,221 +8614,6 @@ def ihara_zeta_function_inverse(self): T[2 * j + 1, 2 * i] = 1 return T.charpoly('t').reverse() - @doc_index("Leftovers") - def perfect_matchings(self, labels=False): - r""" - Return an iterator over all perfect matchings of the graph. - - ALGORITHM: - - Choose a vertex `v`, then recurse through all edges incident to `v`, - removing one edge at a time whenever an edge is added to a matching. - - INPUT: - - - ``labels`` -- boolean (default: ``False``); when ``True``, the edges - in each perfect matching are triples (containing the label as the - third element), otherwise the edges are pairs. - - .. SEEALSO:: - - :meth:`matching` - - EXAMPLES:: - - sage: G=graphs.GridGraph([2,3]) - sage: for m in G.perfect_matchings(): - ....: print(sorted(m)) - [((0, 0), (0, 1)), ((0, 2), (1, 2)), ((1, 0), (1, 1))] - [((0, 0), (1, 0)), ((0, 1), (0, 2)), ((1, 1), (1, 2))] - [((0, 0), (1, 0)), ((0, 1), (1, 1)), ((0, 2), (1, 2))] - - sage: G = graphs.CompleteGraph(4) - sage: for m in G.perfect_matchings(labels=True): - ....: print(sorted(m)) - [(0, 1, None), (2, 3, None)] - [(0, 2, None), (1, 3, None)] - [(0, 3, None), (1, 2, None)] - - sage: G = Graph([[1,-1,'a'], [2,-2, 'b'], [1,-2,'x'], [2,-1,'y']]) - sage: sorted(sorted(m) for m in G.perfect_matchings(labels=True)) - [[(-2, 1, 'x'), (-1, 2, 'y')], [(-2, 2, 'b'), (-1, 1, 'a')]] - - sage: G = graphs.CompleteGraph(8) - sage: mpc = G.matching_polynomial().coefficients(sparse=False)[0] # needs sage.libs.flint - sage: len(list(G.perfect_matchings())) == mpc # needs sage.libs.flint - True - - sage: G = graphs.PetersenGraph().copy(immutable=True) - sage: [sorted(m) for m in G.perfect_matchings()] - [[(0, 1), (2, 3), (4, 9), (5, 7), (6, 8)], - [(0, 1), (2, 7), (3, 4), (5, 8), (6, 9)], - [(0, 4), (1, 2), (3, 8), (5, 7), (6, 9)], - [(0, 4), (1, 6), (2, 3), (5, 8), (7, 9)], - [(0, 5), (1, 2), (3, 4), (6, 8), (7, 9)], - [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]] - - sage: list(Graph().perfect_matchings()) - [[]] - - sage: G = graphs.CompleteGraph(5) - sage: list(G.perfect_matchings()) - [] - """ - if not self: - yield [] - return - if self.order() % 2 or any(len(cc) % 2 for cc in self.connected_components(sort=False)): - return - - def rec(G): - """ - Iterator over all perfect matchings of a simple graph `G`. - """ - if not G: - yield [] - return - if G.order() % 2 == 0: - v = next(G.vertex_iterator()) - Nv = list(G.neighbor_iterator(v)) - G.delete_vertex(v) - for u in Nv: - Nu = list(G.neighbor_iterator(u)) - G.delete_vertex(u) - for partial_matching in rec(G): - partial_matching.append((u, v)) - yield partial_matching - G.add_vertex(u) - G.add_edges((u, nu) for nu in Nu) - G.add_vertex(v) - G.add_edges((v, nv) for nv in Nv) - - # We create a mutable copy of the graph and remove its loops, if any - G = self.copy(immutable=False) - G.allow_loops(False) - - # We create a mapping from frozen unlabeled edges to (labeled) edges. - # This ease for instance the manipulation of multiedges (if any) - edges = {} - for e in G.edges(sort=False, labels=labels): - f = frozenset(e[:2]) - if f in edges: - edges[f].append(e) - else: - edges[f] = [e] - - # We now get rid of multiple edges, if any - G.allow_multiple_edges(False) - - # For each unlabeled matching, we yield all its possible labelings - for m in rec(G): - yield from itertools.product(*[edges[frozenset(e)] for e in m]) - - @doc_index("Leftovers") - def has_perfect_matching(self, algorithm='Edmonds', solver=None, verbose=0, - *, integrality_tolerance=1e-3): - r""" - Return whether this graph has a perfect matching. - INPUT: - - - ``algorithm`` -- string (default: ``'Edmonds'``) - - - ``'Edmonds'`` uses Edmonds' algorithm as implemented in NetworkX to - find a matching of maximal cardinality, then check whether this - cardinality is half the number of vertices of the graph. - - - ``'LP_matching'`` uses a Linear Program to find a matching of - maximal cardinality, then check whether this cardinality is half the - number of vertices of the graph. - - - ``'LP'`` uses a Linear Program formulation of the perfect matching - problem: put a binary variable ``b[e]`` on each edge `e`, and for - each vertex `v`, require that the sum of the values of the edges - incident to `v` is 1. - - - ``solver`` -- string (default: ``None``); specifies a Mixed Integer - Linear Programming (MILP) solver to be used. If set to ``None``, the - default one is used. For more information on MILP solvers and which - default solver is used, see the method :meth:`solve - ` of the class - :class:`MixedIntegerLinearProgram - `. - - - ``verbose`` -- integer (default: 0); sets the level of verbosity: - set to 0 by default, which means quiet (only useful when - ``algorithm == "LP_matching"`` or ``algorithm == "LP"``) - - - ``integrality_tolerance`` -- float; parameter for use with MILP - solvers over an inexact base ring; see - :meth:`MixedIntegerLinearProgram.get_values`. - - OUTPUT: boolean - - EXAMPLES:: - - sage: graphs.PetersenGraph().has_perfect_matching() # needs networkx - True - sage: graphs.WheelGraph(6).has_perfect_matching() # needs networkx - True - sage: graphs.WheelGraph(5).has_perfect_matching() # needs networkx - False - sage: graphs.PetersenGraph().has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip - True - sage: graphs.WheelGraph(6).has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip - True - sage: graphs.WheelGraph(5).has_perfect_matching(algorithm='LP_matching') - False - sage: graphs.PetersenGraph().has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip - True - sage: graphs.WheelGraph(6).has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip - True - sage: graphs.WheelGraph(5).has_perfect_matching(algorithm='LP_matching') - False - - TESTS:: - - sage: G = graphs.EmptyGraph() - sage: all(G.has_perfect_matching(algorithm=algo) # needs networkx - ....: for algo in ['Edmonds', 'LP_matching', 'LP']) - True - - Be careful with isolated vertices:: - - sage: G = graphs.PetersenGraph() - sage: G.add_vertex(11) - sage: any(G.has_perfect_matching(algorithm=algo) # needs networkx - ....: for algo in ['Edmonds', 'LP_matching', 'LP']) - False - """ - if self.order() % 2: - return False - if algorithm == "Edmonds": - return len(self) == 2*self.matching(value_only=True, - use_edge_labels=False, - algorithm='Edmonds') - elif algorithm == "LP_matching": - return len(self) == 2*self.matching(value_only=True, - use_edge_labels=False, - algorithm='LP', - solver=solver, - verbose=verbose, - integrality_tolerance=integrality_tolerance) - elif algorithm == "LP": - from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException - p = MixedIntegerLinearProgram(solver=solver) - b = p.new_variable(binary=True) - for v in self: - edges = self.edges_incident(v, labels=False) - if not edges: - return False - p.add_constraint(p.sum(b[frozenset(e)] for e in edges) == 1) - try: - p.solve(log=verbose) - return True - except MIPSolverException: - return False - raise ValueError('algorithm must be set to "Edmonds", "LP_matching" or "LP"') - @doc_index("Leftovers") def effective_resistance(self, i, j, *, base_ring=None): r""" @@ -10229,6 +9612,12 @@ def bipartite_double(self, extended=False): from sage.graphs.graph_coloring import fractional_chromatic_number from sage.graphs.graph_coloring import fractional_chromatic_index from sage.graphs.hyperbolicity import hyperbolicity + from sage.graphs.matching import has_perfect_matching + from sage.graphs.matching import is_bicritical + from sage.graphs.matching import is_factor_critical + from sage.graphs.matching import is_matching_covered + from sage.graphs.matching import matching + from sage.graphs.matching import perfect_matchings _additional_categories = { @@ -10277,7 +9666,13 @@ def bipartite_double(self, extended=False): "fractional_chromatic_number" : "Coloring", "fractional_chromatic_index" : "Coloring", "geodetic_closure" : "Leftovers", - "hyperbolicity" : "Distances", + "hyperbolicity" : "Distances", + "has_perfect_matching" : "Matching", + "is_bicritical" : "Matching", + "is_factor_critical" : "Matching", + "is_matching_covered" : "Matching", + "matching" : "Matching", + "perfect_matchings" : "Matching" } __doc__ = __doc__.replace("{INDEX_OF_METHODS}", gen_thematic_rest_table_index(Graph, _additional_categories)) diff --git a/src/sage/graphs/matching.py b/src/sage/graphs/matching.py new file mode 100644 index 00000000000..457ccc16a75 --- /dev/null +++ b/src/sage/graphs/matching.py @@ -0,0 +1,1641 @@ +r""" +Matching + +This module implements the functions pertaining to matching of undirected +graphs. A *matching* in a graph is a set of pairwise nonadjacent links +(nonloop edges). In other words, a matching in a graph is the edge set of an +1-regular subgraph. A matching is called a *perfect* *matching* if it the +subgraph generated by a set of matching edges spans the graph, i.e. it's the +edge set of an 1-regular spanning subgraph. + +The implemented methods are listed below: + +.. csv-table:: + :class: contentstable + :widths: 30, 70 + :delim: | + + :meth:`~has_perfect_matching` | Return whether the graph has a perfect matching + :meth:`~is_bicritical` | Check if the graph is bicritical + :meth:`~is_factor_critical` | Check whether the graph is factor-critical + :meth:`~is_matching_covered` | Check if the graph is matching covered + :meth:`~matching` | Return a maximum weighted matching of the graph represented by the list of its edges + :meth:`~perfect_matchings` | Return an iterator over all perfect matchings of the graph + :meth:`~M_alternating_even_mark` | Return the vertices reachable from the provided vertex via an even alternating path starting with a non-matching edge + +AUTHORS: + +- Robert L. Miller (2006-10-22): initial implementations + +- Janmenjaya Panda (2024-06-17): added + :meth:`~M_alternating_even_mark`, + :meth:`~is_bicritical` and + :meth:`~is_matching_covered` + + +Methods +------- +""" + +# **************************************************************************** +# Copyright (C) 2006 Robert L. Miller +# 2024 Janmenjaya Panda +# +# 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 sage.rings.integer import Integer +from sage.graphs.views import EdgesView + + +def has_perfect_matching(G, algorithm='Edmonds', solver=None, verbose=0, + *, integrality_tolerance=1e-3): + r""" + Return whether the graph has a perfect matching + + INPUT: + + - ``algorithm`` -- string (default: ``'Edmonds'``) + + - ``'Edmonds'`` uses Edmonds' algorithm as implemented in NetworkX to + find a matching of maximal cardinality, then check whether this + cardinality is half the number of vertices of the graph. + + - ``'LP_matching'`` uses a Linear Program to find a matching of + maximal cardinality, then check whether this cardinality is half the + number of vertices of the graph. + + - ``'LP'`` uses a Linear Program formulation of the perfect matching + problem: put a binary variable ``b[e]`` on each edge `e`, and for + each vertex `v`, require that the sum of the values of the edges + incident to `v` is 1. + + - ``solver`` -- string (default: ``None``); specifies a Mixed Integer + Linear Programming (MILP) solver to be used. If set to ``None``, the + default one is used. For more information on MILP solvers and which + default solver is used, see the method :meth:`solve + ` of the class + :class:`MixedIntegerLinearProgram + `. + + - ``verbose`` -- integer (default: 0); sets the level of verbosity: + set to 0 by default, which means quiet (only useful when + ``algorithm == "LP_matching"`` or ``algorithm == "LP"``) + + - ``integrality_tolerance`` -- float; parameter for use with MILP + solvers over an inexact base ring; see + :meth:`MixedIntegerLinearProgram.get_values`. + + OUTPUT: boolean + + EXAMPLES:: + + sage: graphs.PetersenGraph().has_perfect_matching() # needs networkx + True + sage: graphs.WheelGraph(6).has_perfect_matching() # needs networkx + True + sage: graphs.WheelGraph(5).has_perfect_matching() # needs networkx + False + sage: graphs.PetersenGraph().has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip + True + sage: graphs.WheelGraph(6).has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip + True + sage: graphs.WheelGraph(5).has_perfect_matching(algorithm='LP_matching') + False + sage: graphs.PetersenGraph().has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip + True + sage: graphs.WheelGraph(6).has_perfect_matching(algorithm='LP_matching') # needs sage.numerical.mip + True + sage: graphs.WheelGraph(5).has_perfect_matching(algorithm='LP_matching') + False + + TESTS:: + + sage: G = graphs.EmptyGraph() + sage: all(G.has_perfect_matching(algorithm=algo) # needs networkx + ....: for algo in ['Edmonds', 'LP_matching', 'LP']) + True + + Be careful with isolated vertices:: + + sage: G = graphs.PetersenGraph() + sage: G.add_vertex(11) + sage: any(G.has_perfect_matching(algorithm=algo) # needs networkx + ....: for algo in ['Edmonds', 'LP_matching', 'LP']) + False + """ + if G.order() % 2: + return False + + if algorithm == "Edmonds": + return len(G) == 2*G.matching(value_only=True, + use_edge_labels=False, + algorithm='Edmonds') + elif algorithm == "LP_matching": + return len(G) == 2*G.matching(value_only=True, + use_edge_labels=False, + algorithm='LP', + solver=solver, + verbose=verbose, + integrality_tolerance=integrality_tolerance) + elif algorithm == "LP": + from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException + p = MixedIntegerLinearProgram(solver=solver) + b = p.new_variable(binary=True) + for v in G: + edges = G.edges_incident(v, labels=False) + if not edges: + return False + p.add_constraint(p.sum(b[frozenset(e)] for e in edges) == 1) + try: + p.solve(log=verbose) + return True + except MIPSolverException: + return False + raise ValueError('algorithm must be set to "Edmonds", "LP_matching" or "LP"') + + +def is_bicritical(G, matching=None, algorithm='Edmonds', coNP_certificate=False, + solver=None, verbose=0, *, integrality_tolerance=0.001): + r""" + Check if the graph is bicritical + + A nontrivial graph `G` is *bicritical* if `G - u - v` has a perfect + matching for any two distinct vertices `u` and `v` of `G`. Bicritical + graphs are special kind of matching covered graphs. Each maximal barrier of + a bicritical graph is a singleton. Thus, for a bicritical graph, the + canonical partition of the vertex set is the set of sets where each set is + an indiviudal vertex. Three-connected bicritical graphs, aka *bricks*, play + an important role in the theory of matching covered graphs. + + This method implements the algorithm proposed in [LZ2001]_ and we + assume that a connected graph of order two is bicritical, whereas a + disconnected graph of the same order is not. The time complexity of + the algorithm is `\mathcal{O}(|V| \cdot |E|)`, if a perfect matching of + the graph is given, where `|V|` and `|E|` are the order and the size of + the graph respectively. Otherwise, time complexity may be dominated by + the time needed to compute a maximum matching of the graph. + + Note that a :class:`ValueError` is returned if the graph has loops or if + the graph is trivial, i.e., it has at most one vertex. + + INPUT: + + - ``matching`` -- (default: ``None``); a perfect matching of the + graph, that can be given using any valid input format of + :class:`~sage.graphs.graph.Graph`. + + If set to ``None``, a matching is computed using the other parameters. + + - ``algorithm`` -- string (default: ``'Edmonds'``); the algorithm to be + used to compute a maximum matching of the graph among + + - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX, + + - ``'LP'`` uses a Linear Program formulation of the matching problem. + + - ``coNP_certificate`` -- boolean (default: ``False``); if set to + ``True`` a set of pair of vertices (say `u` and `v`) is returned such + that `G - u - v` does not have a perfect matching if `G` is not + bicritical or otherwise ``None`` is returned. + + - ``solver`` -- string (default: ``None``); specify a Mixed Integer + Linear Programming (MILP) solver to be used. If set to ``None``, the + default one is used. For more information on MILP solvers and which + default solver is used, see the method :meth:`solve + ` of the class + :class:`MixedIntegerLinearProgram + `. + + - ``verbose`` -- integer (default: ``0``); sets the level of verbosity: + set to 0 by default, which means quiet (only useful when ``algorithm + == 'LP'``). + + - ``integrality_tolerance`` -- float; parameter for use with MILP + solvers over an inexact base ring; see + :meth:`MixedIntegerLinearProgram.get_values`. + + OUTPUT: + + - A boolean indicating whether the graph is bicritical or not. + + - If ``coNP_certificate`` is set to ``True``, a set of pair of vertices + is returned in case the graph is not bicritical otherwise ``None`` is + returned. + + EXAMPLES: + + The Petersen graph is bicritical:: + + sage: G = graphs.PetersenGraph() + sage: G.is_bicritical() + True + + A graph (without a self-loop) is bicritical if and only if the underlying + simple graph is bicritical:: + + sage: G = graphs.PetersenGraph() + sage: G.allow_multiple_edges(True) + sage: G.add_edge(0, 5) + sage: G.is_bicritical() + True + + A nontrivial circular ladder graph whose order is not divisible by 4 is bicritical:: + + sage: G = graphs.CircularLadderGraph(5) + sage: G.is_bicritical() + True + + The graph obtained by splicing two bicritical graph is also bicritical. + For instance, `K_4` with one extra (multiple) edge (say `G := K_4^+`) is + bicritical. Let `H := K_4^+ \odot K_4^+` such that `H` is free of multiple + edge. The graph `H` is also bicritical:: + + sage: G = graphs.CompleteGraph(4) + sage: G.allow_multiple_edges(True) + sage: G.add_edge(0, 1) + sage: G.is_bicritical() + True + sage: H = Graph() + sage: H.add_edges([ + ....: (0, 1), (0, 2), (0, 3), (0, 4), (1, 2), + ....: (1, 5), (2, 5), (3, 4), (3, 5), (4, 5) + ....: ]) + sage: H.is_bicritical() + True + + A graph (of order more than two) with more that one component is not bicritical:: + + sage: cycle1 = graphs.CycleGraph(4) + sage: cycle2 = graphs.CycleGraph(6) + sage: cycle2.relabel(lambda v: v + 4) + sage: G = Graph() + sage: G.add_edges(cycle1.edges() + cycle2.edges()) + sage: len(G.connected_components(sort=False)) + 2 + sage: G.is_bicritical() + False + + A graph (of order more than two) with a cut-vertex is not bicritical:: + + sage: G = graphs.CycleGraph(6) + sage: G.add_edges([(5, 6), (5, 7), (6, 7)]) + sage: G.is_cut_vertex(5) + True + sage: G.has_perfect_matching() + True + sage: G.is_bicritical() + False + + A connected graph of order two is assumed to be bicritical, whereas the + disconnected graph of the same order is not:: + + sage: G = graphs.CompleteBipartiteGraph(1, 1) + sage: G.is_bicritical() + True + sage: G = graphs.CompleteBipartiteGraph(2, 0) + sage: G.is_bicritical() + False + + A bipartite graph of order three or more is not bicritical:: + + sage: G = graphs.CompleteBipartiteGraph(3, 3) + sage: G.has_perfect_matching() + True + sage: G.is_bicritical() + False + + One may specify a matching:: + + sage: G = graphs.WheelGraph(10) + sage: M = G.matching() + sage: G.is_bicritical(matching=M) + True + sage: H = graphs.HexahedralGraph() + sage: N = H.matching() + sage: H.is_bicritical(matching=N) + False + + One may ask for a co-`\mathcal{NP}` certificate:: + + sage: G = graphs.CompleteGraph(14) + sage: G.is_bicritical(coNP_certificate=True) + (True, None) + sage: H = graphs.CircularLadderGraph(20) + sage: M = H.matching() + sage: H.is_bicritical(matching=M, coNP_certificate=True) + (False, {0, 2}) + + TESTS: + + If the graph is trivial:: + + sage: G = Graph() + sage: G.is_bicritical() + Traceback (most recent call last): + ... + ValueError: the graph is trivial + sage: H = graphs.CycleGraph(1) + sage: H.is_bicritical() + Traceback (most recent call last): + ... + ValueError: the graph is trivial + + Providing with a wrong matching:: + + sage: G = graphs.CompleteGraph(6) + sage: M = Graph(G.matching()) + sage: M.add_edges([(0, 1), (0, 2)]) + sage: G.is_bicritical(matching=M) + Traceback (most recent call last): + ... + ValueError: the input is not a matching + sage: N = Graph(G.matching()) + sage: N.add_edge(6, 7) + sage: G.is_bicritical(matching=N) + Traceback (most recent call last): + ... + ValueError: the input is not a matching of the graph + sage: J = Graph() + sage: J.add_edges([(0, 1), (2, 3)]) + sage: G.is_bicritical(matching=J) + Traceback (most recent call last): + ... + ValueError: the input is not a perfect matching of the graph + + Providing with a graph with a self-loop:: + + sage: G = graphs.CompleteGraph(4) + sage: G.allow_loops(True) + sage: G.add_edge(0, 0) + sage: G.is_bicritical() + Traceback (most recent call last): + ... + ValueError: This method is not known to work on graphs with loops. + Perhaps this method can be updated to handle them, but in the meantime + if you want to use it please disallow loops using allow_loops(). + + REFERENCES: + + - [LM2024]_ + + - [LZ2001]_ + + .. SEEALSO:: + :meth:`~sage.graphs.graph.Graph.is_factor_critical`, + :meth:`~sage.graphs.graph.Graph.is_matching_covered` + + AUTHORS: + + - Janmenjaya Panda (2024-06-17) + """ + # The graph must be simple + G._scream_if_not_simple(allow_multiple_edges=True) + + # The graph must be nontrivial + if G.order() < 2: + raise ValueError("the graph is trivial") + + # A graph of order two is assumed to be bicritical + if G.order() == 2: + if G.is_connected(): + return (True, None) if coNP_certificate else True + else: + return (False, None) if coNP_certificate else False + + # The graph must have an even number of vertices + if G.order() % 2: + return (False, set(list(G)[:2])) if coNP_certificate else False + + # The graph must be connected + if not G.is_connected(): + if not coNP_certificate: + return False + + components = G.connected_components(sort=False) + + # Check if there is an odd component with at least three vertices + for component in components: + if len(component) % 2 and len(component) > 2: + return (False, set(component[:2])) + + # Check if there are at least two even components + components_of_even_order = [component for component in components if len(component) % 2 == 0] + if len(components_of_even_order) > 1: + return (False, set([components_of_even_order[0][0], components_of_even_order[1][0]])) + + # Or otherwise there is at most one even component with at least two trivial odd components + u, v = None, None + + for component in components: + if u is not None and not len(component) % 2: + v = component[0] + return (False, set([u, v])) + elif len(component) == 1: + u = component[0] + + # Bipartite graphs of order at least three are not bicritical + if G.is_bipartite(): + if not coNP_certificate: + return False + + A, B = G.bipartite_sets() + + if len(A) > 1: + return (False, set(list(A)[:2])) + return (False, set(list(B)[:2])) + + # A graph (without a self-loop) is bicritical if and only if the underlying + # simple graph is bicritical + G_simple = G.to_simple() + + from sage.graphs.graph import Graph + if matching: + # The input matching must be a valid perfect matching of the graph + M = Graph(matching) + if any(d != 1 for d in M.degree()): + raise ValueError("the input is not a matching") + if any(not G_simple.has_edge(edge) for edge in M.edge_iterator()): + raise ValueError("the input is not a matching of the graph") + if (G_simple.order() != M.order()) or (G_simple.order() != 2*M.size()): + raise ValueError("the input is not a perfect matching of the graph") + else: + # A maximum matching of the graph is computed + M = Graph(G_simple.matching(algorithm=algorithm, solver=solver, verbose=verbose, + integrality_tolerance=integrality_tolerance)) + + # It must be a perfect matching + if G_simple.order() != M.order(): + u, v = next(M.edge_iterator(labels=False)) + return (False, set([u, v])) if coNP_certificate else False + + # G is bicritical if and only if for each vertex u with its M-matched neighbor being v, + # every vertex of the graph distinct from v must be reachable from u through an even length + # M-alternating uv-path starting with an edge not in M and ending with an edge in M + + for u in G_simple: + v = next(M.neighbor_iterator(u)) + + even = M_alternating_even_mark(G_simple, u, M) + + for w in G_simple: + if w != v and w not in even: + return (False, set([v, w])) if coNP_certificate else False + + return (True, None) if coNP_certificate else True + + +def is_factor_critical(G, matching=None, algorithm='Edmonds', solver=None, verbose=0, + *, integrality_tolerance=0.001): + r""" + Check whether the graph is factor-critical. + + A graph of order `n` is *factor-critical* if every subgraph of `n-1` + vertices have a perfect matching, hence `n` must be odd. See + :wikipedia:`Factor-critical_graph` for more details. + + This method implements the algorithm proposed in [LR2004]_ and we assume + that a graph of order one is factor-critical. The time complexity of the + algorithm is linear if a near perfect matching is given as input (i.e., + a matching such that all vertices but one are incident to an edge of the + matching). Otherwise, the time complexity is dominated by the time + needed to compute a maximum matching of the graph. + + INPUT: + + - ``matching`` -- (default: ``None``); a near perfect matching of the + graph, that is a matching such that all vertices of the graph but one + are incident to an edge of the matching. It can be given using any + valid input format of :class:`~sage.graphs.graph.Graph`. + + If set to ``None``, a matching is computed using the other parameters. + + - ``algorithm`` -- string (default: ``'Edmonds'``); the algorithm to use + to compute a maximum matching of the graph among + + - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX + + - ``'LP'`` uses a Linear Program formulation of the matching problem + + - ``solver`` -- string (default: ``None``); specifies a Mixed Integer + Linear Programming (MILP) solver to be used. If set to ``None``, the + default one is used. For more information on MILP solvers and which + default solver is used, see the method :meth:`solve + ` of the class + :class:`MixedIntegerLinearProgram + `. + + - ``verbose`` -- integer (default: 0); sets the level of verbosity: + set to 0 by default, which means quiet (only useful when ``algorithm + == "LP"``) + + - ``integrality_tolerance`` -- float; parameter for use with MILP + solvers over an inexact base ring; see + :meth:`MixedIntegerLinearProgram.get_values`. + + EXAMPLES: + + Odd length cycles and odd cliques of order at least 3 are + factor-critical graphs:: + + sage: [graphs.CycleGraph(2*i + 1).is_factor_critical() for i in range(5)] # needs networkx + [True, True, True, True, True] + sage: [graphs.CompleteGraph(2*i + 1).is_factor_critical() for i in range(5)] # needs networkx + [True, True, True, True, True] + + More generally, every Hamiltonian graph with an odd number of vertices + is factor-critical:: + + sage: G = graphs.RandomGNP(15, .2) + sage: G.add_path([0..14]) + sage: G.add_edge(14, 0) + sage: G.is_hamiltonian() + True + sage: G.is_factor_critical() # needs networkx + True + + Friendship graphs are non-Hamiltonian factor-critical graphs:: + + sage: [graphs.FriendshipGraph(i).is_factor_critical() for i in range(1, 5)] # needs networkx + [True, True, True, True] + + Bipartite graphs are not factor-critical:: + + sage: G = graphs.RandomBipartite(randint(1, 10), randint(1, 10), .5) # needs numpy + sage: G.is_factor_critical() # needs numpy + False + + Graphs with even order are not factor critical:: + + sage: G = graphs.RandomGNP(10, .5) + sage: G.is_factor_critical() + False + + One can specify a matching:: + + sage: F = graphs.FriendshipGraph(4) + sage: M = F.matching() # needs networkx + sage: F.is_factor_critical(matching=M) # needs networkx + True + sage: F.is_factor_critical(matching=Graph(M)) # needs networkx + True + + TESTS: + + Giving a wrong matching:: + + sage: G = graphs.RandomGNP(15, .3) + sage: while not G.is_biconnected(): + ....: G = graphs.RandomGNP(15, .3) + sage: M = G.matching() # needs networkx + sage: G.is_factor_critical(matching=M[:-1]) # needs networkx + Traceback (most recent call last): + ... + ValueError: the input is not a near perfect matching of the graph + sage: G.is_factor_critical(matching=G.edges(sort=True)) + Traceback (most recent call last): + ... + ValueError: the input is not a matching + sage: M = [(2*i, 2*i + 1) for i in range(9)] + sage: G.is_factor_critical(matching=M) + Traceback (most recent call last): + ... + ValueError: the input is not a matching of the graph + """ + if G.order() == 1: + return True + + # The graph must have an odd number of vertices, be 2-edge connected, so + # without bridges, and not bipartite + if (not G.order() % 2 or not G.is_connected() or + list(G.bridges()) or G.is_bipartite()): + return False + + from sage.graphs.graph import Graph + if matching: + # We check that the input matching is a valid near perfect matching + # of the graph. + M = Graph(matching) + if any(d != 1 for d in M.degree()): + raise ValueError("the input is not a matching") + if not M.is_subgraph(G, induced=False): + raise ValueError("the input is not a matching of the graph") + if (G.order() != M.order() + 1) or (G.order() != 2*M.size() + 1): + raise ValueError("the input is not a near perfect matching of the graph") + else: + # We compute a maximum matching of the graph + M = Graph(G.matching(algorithm=algorithm, solver=solver, verbose=verbose, + integrality_tolerance=integrality_tolerance)) + + # It must be a near-perfect matching + if G.order() != M.order() + 1: + return False + + # We find the unsaturated vertex u, i.e., the only vertex of the graph + # not in M + for u in G: + if u not in M: + break + + # We virtually build an M-alternating tree T + from queue import Queue + Q = Queue() + Q.put(u) + even = set([u]) + odd = set() + pred = {u: u} + rank = {u: 0} + + while not Q.empty(): + x = Q.get() + for y in G.neighbor_iterator(x): + if y in odd: + continue + elif y in even: + # Search for the nearest common ancestor t of x and y + P = [x] + R = [y] + while P[-1] != R[-1]: + if rank[P[-1]] > rank[R[-1]]: + P.append(pred[P[-1]]) + elif rank[P[-1]] < rank[R[-1]]: + R.append(pred[R[-1]]) + else: + P.append(pred[P[-1]]) + R.append(pred[R[-1]]) + t = P.pop() + R.pop() + # Set t as pred of all vertices of the chains and add + # vertices marked odd to the queue + import itertools + + for a in itertools.chain(P, R): + pred[a] = t + rank[a] = rank[t] + 1 + if a in odd: + even.add(a) + odd.discard(a) + Q.put(a) + else: # y has not been visited yet + z = next(M.neighbor_iterator(y)) + odd.add(y) + even.add(z) + Q.put(z) + pred[y] = x + pred[z] = y + rank[y] = rank[x] + 1 + rank[z] = rank[y] + 1 + + # The graph is factor critical if all vertices are marked even + return len(even) == G.order() + + +def is_matching_covered(G, matching=None, algorithm='Edmonds', coNP_certificate=False, + solver=None, verbose=0, *, integrality_tolerance=0.001): + r""" + Check if the graph is matching covered. + + A connected nontrivial graph wherein each edge participates in some + perfect matching is called a *matching* *covered* *graph*. + + If a perfect matching of the graph is provided, for bipartite graph, + this method implements a linear time algorithm as proposed in [LM2024]_ + that is based on the following theorem: + + Given a connected bipartite graph `G[A, B]` with a perfect matching + `M`. Construct a directed graph `D` from `G` such that `V(D) := V(G)` + and for each edge in `G` direct the corresponding edge from `A` to `B` + in `D`, if it is in `M` or otherwise direct it from `B` to `A`. The + graph `G` is matching covered if and only if `D` is strongly connected. + + For nonbipartite graph, if a perfect matching of the graph is provided, + this method implements an `\mathcal{O}(|V| \cdot |E|)` algorithm, where + `|V|` and `|E|` are the order and the size of the graph respectively. + This implementation is inspired by the `M`-`alternating` `tree` `search` + method explained in [LZ2001]_. For nonbipartite graph, the + implementation is based on the following theorem: + + Given a nonbipartite graph `G` with a perfect matching `M`. The + graph `G` is matching covered if and only if for each edge `uv` + not in `M`, there exists an `M`-`alternating` odd length `uv`-path + starting and ending with edges not in `M`. + + The time complexity may be dominated by the time needed to compute a + maximum matching of the graph, in case a perfect matching is not + provided. Also, note that for a disconnected or a trivial or a + graph with a loop, a :class:`ValueError` is returned. + + INPUT: + + - ``matching`` -- (default: ``None``); a perfect matching of the + graph, that can be given using any valid input format of + :class:`~sage.graphs.graph.Graph`. + + If set to ``None``, a matching is computed using the other parameters. + + - ``algorithm`` -- string (default: ``'Edmonds'``); the algorithm to be + used to compute a maximum matching of the graph among + + - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX, + + - ``'LP'`` uses a Linear Program formulation of the matching problem. + + - ``coNP_certificate`` -- boolean (default: ``False``); if set to + ``True`` an edge of the graph, that does not participate in any + perfect matching, is returned if `G` is not matching covered or + otherwise ``None`` is returned. + + - ``solver`` -- string (default: ``None``); specify a Mixed Integer + Linear Programming (MILP) solver to be used. If set to ``None``, the + default one is used. For more information on MILP solvers and which + default solver is used, see the method :meth:`solve + ` of the class + :class:`MixedIntegerLinearProgram + `. + + - ``verbose`` -- integer (default: ``0``); sets the level of verbosity: + set to 0 by default, which means quiet (only useful when ``algorithm + == 'LP'``). + + - ``integrality_tolerance`` -- float; parameter for use with MILP + solvers over an inexact base ring; see + :meth:`MixedIntegerLinearProgram.get_values`. + + OUTPUT: + + - A boolean indicating whether the graph is matching covered or not. + + - If ``coNP_certificate`` is set to ``True``, an edge is returned in + case the graph is not matching covered otherwise ``None`` is + returned. + + EXAMPLES: + + The Petersen graph is matching covered:: + + sage: G = graphs.PetersenGraph() + sage: G.is_matching_covered() + True + + A graph (without a self-loop) is matching covered if and only if the + underlying simple graph is matching covered:: + + sage: G = graphs.PetersenGraph() + sage: G.allow_multiple_edges(True) + sage: G.add_edge(0, 5) + sage: G.is_matching_covered() + True + + A corollary to Tutte's fundamental result [Tut1947]_, as a + strengthening of Petersen's Theorem, states that every 2-connected + cubic graph is matching covered:: + + sage: G = Graph() + sage: G.add_edges([ + ....: (0, 1), (0, 2), (0, 3), + ....: (1, 2), (1, 4), (2, 4), + ....: (3, 5), (3, 6), (4, 7), + ....: (5, 6), (5, 7), (6, 7) + ....: ]) + sage: G.vertex_connectivity() + 2 + sage: degree_sequence = G.degree_sequence() + sage: min(degree_sequence) == max(degree_sequence) == 3 + True + sage: G.is_matching_covered() + True + + A connected bipartite graph `G[A, B]`, with `|A| = |B| \geq 2`, is + matching covered if and only if `|N(X)| \geq |X| + 1`, for all + `X \subset A` such that `1 \leq |X| \leq |A| - 1`. For instance, + the Hexahedral graph is matching covered, but not the path graphs on + even number of vertices, even though they have a perfect matching:: + + sage: G = graphs.HexahedralGraph() + sage: G.is_bipartite() + True + sage: G.is_matching_covered() + True + sage: P = graphs.PathGraph(10) + sage: P.is_bipartite() + True + sage: M = Graph(P.matching()) + sage: set(P) == set(M) + True + sage: P.is_matching_covered() + False + + A connected bipartite graph `G[A, B]` of order six or more is matching + covered if and only if `G - a - b` has a perfect matching for some + vertex `a` in `A` and some vertex `b` in `B`:: + + sage: G = graphs.CircularLadderGraph(8) + sage: G.is_bipartite() + True + sage: G.is_matching_covered() + True + sage: A, B = G.bipartite_sets() + sage: # needs random + sage: import random + sage: a = random.choice(list(A)) + sage: b = random.choice(list(B)) + sage: G.delete_vertices([a, b]) + sage: M = Graph(G.matching()) + sage: set(M) == set(G) + True + sage: cycle1 = graphs.CycleGraph(4) + sage: cycle2 = graphs.CycleGraph(6) + sage: cycle2.relabel(lambda v: v + 4) + sage: H = Graph() + sage: H.add_edges(cycle1.edges() + cycle2.edges()) + sage: H.add_edge(3, 4) + sage: H.is_bipartite() + True + sage: H.is_matching_covered() + False + sage: H.delete_vertices([3, 4]) + sage: N = Graph(H.matching()) + sage: set(N) == set(H) + False + + One may specify a matching:: + + sage: G = graphs.WheelGraph(20) + sage: M = Graph(G.matching()) + sage: G.is_matching_covered(matching=M) + True + sage: J = graphs.CycleGraph(4) + sage: J.add_edge(0, 2) + sage: N = J.matching() + sage: J.is_matching_covered(matching=N) + False + + One may ask for a co-`\mathcal{NP}` certificate:: + + sage: G = graphs.CompleteGraph(14) + sage: G.is_matching_covered(coNP_certificate=True) + (True, None) + sage: H = graphs.PathGraph(20) + sage: M = H.matching() + sage: H.is_matching_covered(matching=M, coNP_certificate=True) + (False, (1, 2, None)) + + TESTS: + + If the graph is not connected:: + + sage: cycle1 = graphs.CycleGraph(4) + sage: cycle2 = graphs.CycleGraph(6) + sage: cycle2.relabel(lambda v: v + 4) + sage: G = Graph() + sage: G.add_edges(cycle1.edges() + cycle2.edges()) + sage: len(G.connected_components(sort=False)) + 2 + sage: G.is_matching_covered() + Traceback (most recent call last): + ... + ValueError: the graph is not connected + + If the graph is trivial:: + + sage: G = Graph() + sage: G.is_matching_covered() + Traceback (most recent call last): + ... + ValueError: the graph is trivial + sage: H = graphs.CycleGraph(1) + sage: H.is_matching_covered() + Traceback (most recent call last): + ... + ValueError: the graph is trivial + + Providing with a wrong matching:: + + sage: G = graphs.CompleteGraph(6) + sage: M = Graph(G.matching()) + sage: M.add_edges([(0, 1), (0, 2)]) + sage: G.is_matching_covered(matching=M) + Traceback (most recent call last): + ... + ValueError: the input is not a matching + sage: N = Graph(G.matching()) + sage: N.add_edge(6, 7) + sage: G.is_matching_covered(matching=N) + Traceback (most recent call last): + ... + ValueError: the input is not a matching of the graph + sage: J = Graph() + sage: J.add_edges([(0, 1), (2, 3)]) + sage: G.is_matching_covered(matching=J) + Traceback (most recent call last): + ... + ValueError: the input is not a perfect matching of the graph + + Providing with a graph with a self-loop:: + + sage: G = graphs.PetersenGraph() + sage: G.allow_loops(True) + sage: G.add_edge(0, 0) + sage: G.is_matching_covered() + Traceback (most recent call last): + ... + ValueError: This method is not known to work on graphs with loops. Perhaps this method can be updated to handle them, but in the meantime if you want to use it please disallow loops using allow_loops(). + + REFERENCES: + + - [LM2024]_ + + - [LZ2001]_ + + - [Tut1947]_ + + .. SEEALSO:: + :meth:`~sage.graphs.graph.Graph.is_factor_critical`, + :meth:`~sage.graphs.graph.Graph.is_bicritical` + + AUTHORS: + + - Janmenjaya Panda (2024-06-23) + """ + G._scream_if_not_simple(allow_multiple_edges=True) + + # The graph must be nontrivial + if G.order() < 2: + raise ValueError("the graph is trivial") + + # The graph must be connected + if not G.is_connected(): + raise ValueError("the graph is not connected") + + # The graph must have an even order + if G.order() % 2: + return (False, next(G.edge_iterator())) if coNP_certificate else False + + # If the underlying simple graph is a complete graph of order two, + # the graph is matching covered + if G.order() == 2: + return (True, None) if coNP_certificate else True + + # A graph (without a self-loop) is matching covered if and only if the + # underlying simple graph is matching covered + G_simple = G.to_simple() + + from sage.graphs.graph import Graph + if matching: + # The input matching must be a valid perfect matching of the graph + M = Graph(matching) + if any(d != 1 for d in M.degree()): + raise ValueError("the input is not a matching") + if any(not G_simple.has_edge(edge) for edge in M.edge_iterator()): + raise ValueError("the input is not a matching of the graph") + if (G_simple.order() != M.order()) or (G_simple.order() != 2*M.size()): + raise ValueError("the input is not a perfect matching of the graph") + else: + # A maximum matching of the graph is computed + M = Graph(G_simple.matching(algorithm=algorithm, solver=solver, verbose=verbose, + integrality_tolerance=integrality_tolerance)) + + # It must be a perfect matching + if G_simple.order() != M.order(): + return (False, next(M.edge_iterator())) if coNP_certificate else False + + # Biparite graph: + # + # Given a connected bipartite graph G[A, B] with a perfect matching M. + # Construct a directed graph D from G such that V(D) := V(G) and + # for each edge in G direct the corresponding edge from A to B in D, + # if it is in M or otherwise direct it from B to A. The graph G is + # matching covered if and only if D is strongly connected. + + if G_simple.is_bipartite(): + A, _ = G_simple.bipartite_sets() + color = dict() + + for u in G_simple: + color[u] = 0 if u in A else 1 + + from sage.graphs.digraph import DiGraph + H = DiGraph() + + for u, v in G_simple.edge_iterator(labels=False): + if color[u]: + u, v = v, u + + if M.has_edge(u, v): + H.add_edge(u, v) + else: + H.add_edge(v, u) + + # Check if H is strongly connected using Kosaraju's algorithm + def dfs(J, v, visited, orientation): + stack = [v] # a stack of vertices + + while stack: + v = stack.pop() + + if v not in visited: + visited[v] = True + + if orientation == 'in': + for u in J.neighbors_out(v): + if u not in visited: + stack.append(u) + + elif orientation == 'out': + for u in J.neighbors_in(v): + if u not in visited: + stack.append(u) + else: + raise ValueError('Unknown orientation') + + root = next(H.vertex_iterator()) + + visited_in = {} + dfs(H, root, visited_in, 'in') + + visited_out = {} + dfs(H, root, visited_out, 'out') + + for edge in H.edge_iterator(): + u, v, _ = edge + if (u not in visited_in) or (v not in visited_out): + if not M.has_edge(edge): + return (False, edge) if coNP_certificate else False + + return (True, None) if coNP_certificate else True + + # Nonbipartite graph: + # + # Given a nonbipartite graph G with a perfect matching M. The graph G is + # matching covered if and only if for each edge uv not in M, there exists + # an M-alternating odd length uv-path starting and ending with edges not + # in M. + + for u in G_simple: + v = next(M.neighbor_iterator(u)) + + even = M_alternating_even_mark(G_simple, u, M) + + for w in G_simple.neighbor_iterator(v): + if w != u and w not in even: + return (False, (v, w)) if coNP_certificate else False + + return (True, None) if coNP_certificate else True + + +def matching(G, value_only=False, algorithm='Edmonds', + use_edge_labels=False, solver=None, verbose=0, + *, integrality_tolerance=1e-3): + r""" + Return a maximum weighted matching of the graph represented by the list + of its edges + + For more information, see the :wikipedia:`Matching_(graph_theory)`. + + Given a graph `G` such that each edge `e` has a weight `w_e`, a maximum + matching is a subset `S` of the edges of `G` of maximum weight such that + no two edges of `S` are incident with each other. + + As an optimization problem, it can be expressed as: + + .. MATH:: + + \mbox{Maximize : }&\sum_{e\in G.edges()} w_e b_e\\ + \mbox{Such that : }&\forall v \in G, + \sum_{(u,v)\in G.edges()} b_{(u,v)}\leq 1\\ + &\forall x\in G, b_x\mbox{ is a binary variable} + + INPUT: + + - ``value_only`` -- boolean (default: ``False``); when set to ``True``, + only the cardinal (or the weight) of the matching is returned + + - ``algorithm`` -- string (default: ``'Edmonds'``) + + - ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX + + - ``'LP'`` uses a Linear Program formulation of the matching problem + + - ``use_edge_labels`` -- boolean (default: ``False``) + + - when set to ``True``, computes a weighted matching where each edge + is weighted by its label (if an edge has no label, `1` is assumed) + + - when set to ``False``, each edge has weight `1` + + - ``solver`` -- string (default: ``None``); specifies a Mixed Integer + Linear Programming (MILP) solver to be used. If set to ``None``, the + default one is used. For more information on MILP solvers and which + default solver is used, see the method :meth:`solve + ` of the class + :class:`MixedIntegerLinearProgram + `. + + - ``verbose`` -- integer (default: 0); sets the level of verbosity: + set to 0 by default, which means quiet (only useful when ``algorithm + == "LP"``) + + - ``integrality_tolerance`` -- float; parameter for use with MILP + solvers over an inexact base ring; see + :meth:`MixedIntegerLinearProgram.get_values`. + + OUTPUT: + + - When ``value_only=False`` (default), this method returns an + :class:`EdgesView` containing the edges of a maximum matching of `G`. + + - When ``value_only=True``, this method returns the sum of the + weights (default: ``1``) of the edges of a maximum matching of `G`. + The type of the output may vary according to the type of the edge + labels and the algorithm used. + + ALGORITHM: + + The problem is solved using Edmond's algorithm implemented in NetworkX, + or using Linear Programming depending on the value of ``algorithm``. + + EXAMPLES: + + Maximum matching in a Pappus Graph:: + + sage: g = graphs.PappusGraph() + sage: g.matching(value_only=True) # needs sage.networkx + 9 + + Same test with the Linear Program formulation:: + + sage: g = graphs.PappusGraph() + sage: g.matching(algorithm='LP', value_only=True) # needs sage.numerical.mip + 9 + + .. PLOT:: + + g = graphs.PappusGraph() + sphinx_plot(g.plot(edge_colors={"red":g.matching()})) + + TESTS: + + When ``use_edge_labels`` is set to ``False``, with Edmonds' algorithm + and LP formulation:: + + sage: g = Graph([(0,1,0), (1,2,999), (2,3,-5)]) + sage: sorted(g.matching()) # needs sage.networkx + [(0, 1, 0), (2, 3, -5)] + sage: sorted(g.matching(algorithm='LP')) # needs sage.numerical.mip + [(0, 1, 0), (2, 3, -5)] + + When ``use_edge_labels`` is set to ``True``, with Edmonds' algorithm and + LP formulation:: + + sage: g = Graph([(0,1,0), (1,2,999), (2,3,-5)]) + sage: g.matching(use_edge_labels=True) # needs sage.networkx + [(1, 2, 999)] + sage: g.matching(algorithm='LP', use_edge_labels=True) # needs sage.numerical.mip + [(1, 2, 999)] + + With loops and multiedges:: + + sage: edge_list = [(0,0,5), (0,1,1), (0,2,2), (0,3,3), (1,2,6) + ....: , (1,2,3), (1,3,3), (2,3,3)] + sage: g = Graph(edge_list, loops=True, multiedges=True) + sage: m = g.matching(use_edge_labels=True) # needs sage.networkx + sage: type(m) # needs sage.networkx + + sage: sorted(m) # needs sage.networkx + [(0, 3, 3), (1, 2, 6)] + + TESTS: + + If ``algorithm`` is set to anything different from ``'Edmonds'`` or + ``'LP'``, an exception is raised:: + + sage: g = graphs.PappusGraph() + sage: g.matching(algorithm='somethingdifferent') + Traceback (most recent call last): + ... + ValueError: algorithm must be set to either "Edmonds" or "LP" + """ + from sage.rings.real_mpfr import RR + + def weight(x): + if x in RR: + return x + else: + return 1 + + W = {} + L = {} + for u, v, l in G.edge_iterator(): + if u is v: + continue + fuv = frozenset((u, v)) + if fuv not in L or (use_edge_labels and W[fuv] < weight(l)): + L[fuv] = l + if use_edge_labels: + W[fuv] = weight(l) + + if algorithm == "Edmonds": + import networkx + g = networkx.Graph() + if use_edge_labels: + for (u, v), w in W.items(): + g.add_edge(u, v, weight=w) + else: + for u, v in L: + g.add_edge(u, v) + d = networkx.max_weight_matching(g) + if value_only: + if use_edge_labels: + return sum(W[frozenset(e)] for e in d) + return Integer(len(d)) + + from sage.graphs.graph import Graph + return EdgesView(Graph([(u, v, L[frozenset((u, v))]) for u, v in d], + format='list_of_edges')) + + elif algorithm == "LP": + g = G + from sage.numerical.mip import MixedIntegerLinearProgram + # returns the weight of an edge considering it may not be + # weighted ... + p = MixedIntegerLinearProgram(maximization=True, solver=solver) + b = p.new_variable(binary=True) + if use_edge_labels: + p.set_objective(p.sum(w * b[fe] for fe, w in W.items())) + else: + p.set_objective(p.sum(b[fe] for fe in L)) + # for any vertex v, there is at most one edge incident to v in + # the maximum matching + for v in g: + p.add_constraint(p.sum(b[frozenset(e)] for e in G.edge_iterator(vertices=[v], labels=False) + if e[0] != e[1]), max=1) + + p.solve(log=verbose) + b = p.get_values(b, convert=bool, tolerance=integrality_tolerance) + if value_only: + if use_edge_labels: + return sum(w for fe, w in W.items() if b[fe]) + return Integer(sum(1 for fe in L if b[fe])) + + from sage.graphs.graph import Graph + return EdgesView(Graph([(u, v, L[frozenset((u, v))]) + for u, v in L if b[frozenset((u, v))]], + format='list_of_edges')) + + raise ValueError('algorithm must be set to either "Edmonds" or "LP"') + + +def perfect_matchings(G, labels=False): + r""" + Return an iterator over all perfect matchings of the graph + + ALGORITHM: + + Choose a vertex `v`, then recurse through all edges incident to `v`, + removing one edge at a time whenever an edge is added to a matching. + + INPUT: + + - ``labels`` -- boolean (default: ``False``); when ``True``, the edges + in each perfect matching are triples (containing the label as the + third element), otherwise the edges are pairs. + + .. SEEALSO:: + + :meth:`matching` + + EXAMPLES:: + + sage: G=graphs.GridGraph([2,3]) + sage: for m in G.perfect_matchings(): + ....: print(sorted(m)) + [((0, 0), (0, 1)), ((0, 2), (1, 2)), ((1, 0), (1, 1))] + [((0, 0), (1, 0)), ((0, 1), (0, 2)), ((1, 1), (1, 2))] + [((0, 0), (1, 0)), ((0, 1), (1, 1)), ((0, 2), (1, 2))] + + sage: G = graphs.CompleteGraph(4) + sage: for m in G.perfect_matchings(labels=True): + ....: print(sorted(m)) + [(0, 1, None), (2, 3, None)] + [(0, 2, None), (1, 3, None)] + [(0, 3, None), (1, 2, None)] + + sage: G = Graph([[1,-1,'a'], [2,-2, 'b'], [1,-2,'x'], [2,-1,'y']]) + sage: sorted(sorted(m) for m in G.perfect_matchings(labels=True)) + [[(-2, 1, 'x'), (-1, 2, 'y')], [(-2, 2, 'b'), (-1, 1, 'a')]] + + sage: G = graphs.CompleteGraph(8) + sage: mpc = G.matching_polynomial().coefficients(sparse=False)[0] # needs sage.libs.flint + sage: len(list(G.perfect_matchings())) == mpc # needs sage.libs.flint + True + + sage: G = graphs.PetersenGraph().copy(immutable=True) + sage: [sorted(m) for m in G.perfect_matchings()] + [[(0, 1), (2, 3), (4, 9), (5, 7), (6, 8)], + [(0, 1), (2, 7), (3, 4), (5, 8), (6, 9)], + [(0, 4), (1, 2), (3, 8), (5, 7), (6, 9)], + [(0, 4), (1, 6), (2, 3), (5, 8), (7, 9)], + [(0, 5), (1, 2), (3, 4), (6, 8), (7, 9)], + [(0, 5), (1, 6), (2, 7), (3, 8), (4, 9)]] + + sage: list(Graph().perfect_matchings()) + [[]] + + sage: G = graphs.CompleteGraph(5) + sage: list(G.perfect_matchings()) + [] + """ + if not G: + yield [] + return + if G.order() % 2 or any(len(cc) % 2 for cc in G.connected_components(sort=False)): + return + + def rec(G): + """ + Iterator over all perfect matchings of a simple graph `G`. + """ + if not G: + yield [] + return + if G.order() % 2 == 0: + v = next(G.vertex_iterator()) + Nv = list(G.neighbor_iterator(v)) + G.delete_vertex(v) + for u in Nv: + Nu = list(G.neighbor_iterator(u)) + G.delete_vertex(u) + for partial_matching in rec(G): + partial_matching.append((u, v)) + yield partial_matching + G.add_vertex(u) + G.add_edges((u, nu) for nu in Nu) + G.add_vertex(v) + G.add_edges((v, nv) for nv in Nv) + + # We create a mutable copy of the graph and remove its loops, if any + G_copy = G.copy(immutable=False) + G_copy.allow_loops(False) + + # We create a mapping from frozen unlabeled edges to (labeled) edges. + # This ease for instance the manipulation of multiedges (if any) + edges = {} + for e in G_copy.edges(sort=False, labels=labels): + f = frozenset(e[:2]) + if f in edges: + edges[f].append(e) + else: + edges[f] = [e] + + # We now get rid of multiple edges, if any + G_copy.allow_multiple_edges(False) + + # For each unlabeled matching, we yield all its possible labelings + import itertools + + for m in rec(G_copy): + yield from itertools.product(*[edges[frozenset(e)] for e in m]) + + +def M_alternating_even_mark(G, vertex, matching): + r""" + Return the vertices reachable from ``vertex`` via an even alternating path + starting with a non-matching edge + + This method implements the algorithm proposed in [LR2004]_. Note that + the complexity of the algorithm is linear in number of edges. + + INPUT: + + - ``vertex`` -- a vertex of the graph + + - ``matching`` -- a matching of the graph; it can be given using any + valid input format of :class:`~sage.graphs.graph.Graph` + + OUTPUT: + + - ``even`` -- the set of vertices each of which is reachable from the + provided vertex through a path starting with an edge not in the + matching and ending with an edge in the matching; note that a note that a + :class:`ValueError` is returned if the graph is not simple + + EXAMPLES: + + Show the list of required vertices for a graph `G` with a matching `M` + for a vertex `u`:: + + sage: G = graphs.CycleGraph(3) + sage: M = G.matching() + sage: M + [(0, 2, None)] + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S0 = M_alternating_even_mark(G, 0, M) + sage: S0 + {0} + sage: S1 = M_alternating_even_mark(G, 1, M) + sage: S1 + {0, 1, 2} + + The result is equivalent for the underlying simple graph of the provided + graph, if the other parameters provided are the same:: + + sage: G = graphs.CompleteBipartiteGraph(3, 3) + sage: G.allow_multiple_edges(True) + sage: G.add_edge(0, 3) + sage: M = G.matching() + sage: u = 0 + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S = M_alternating_even_mark(G, u, M) + sage: S + {0, 1, 2} + sage: T = M_alternating_even_mark(G.to_simple(), u, M) + sage: T + {0, 1, 2} + + For a factor critical graph `G` (for instance, a wheel graph of an odd + order) with a near perfect matching `M` and `u` being the (unique) + `M`-exposed vertex, each vertex in `G` is reachable from `u` through an + even length `M`-alternating path as described above:: + + sage: G = graphs.WheelGraph(11) + sage: M = Graph(G.matching()) + sage: G.is_factor_critical(M) + True + sage: for v in G: + ....: if v not in M: + ....: break + ....: + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S = M_alternating_even_mark(G, v, M) + sage: S == set(G) + True + + For a matching covered graph `G` (for instance, `K_4 \odot K_{3,3}`) with a + perfect matching `M` and for some vertex `u` with `v` being its `M`-matched + neighbor, each neighbor of `v` is reachable from `u` through an even length + `M`-alternating path as described above:: + + sage: G = Graph() + sage: G.add_edges([ + ....: (0, 2), (0, 3), (0, 4), (1, 2), + ....: (1, 3), (1, 4), (2, 5), (3, 6), + ....: (4, 7), (5, 6), (5, 7), (6, 7) + ....: ]) + sage: M = Graph(G.matching()) + sage: G.is_matching_covered(M) + True + sage: u = 0 + sage: v = next(M.neighbor_iterator(u)) + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S = M_alternating_even_mark(G, u, M) + sage: (set(G.neighbor_iterator(v))).issubset(S) + True + + For a bicritical graph `G` (for instance, the Petersen graph) with a + perfect matching `M` and for some vertex `u` with its `M`-matched neighbor + being `v`, each vertex of the graph distinct from `v` is reachable from `u` + through an even length `M`-alternating path as described above:: + + sage: G = graphs.PetersenGraph() + sage: M = Graph(G.matching()) + sage: G.is_bicritical(M) + True + sage: import random + sage: u = random.choice(list(G)) # needs random + sage: v = next(M.neighbor_iterator(u)) + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S = M_alternating_even_mark(G, u, M) + sage: S == (set(G) - {v}) + True + + TESTS: + + Giving a wrong vertex:: + + sage: G = graphs.HexahedralGraph() + sage: M = G.matching() + sage: u = G.order() + sage: from sage.graphs.matching import M_alternating_even_mark + sage: S = M_alternating_even_mark(G, u, M) + Traceback (most recent call last): + ... + ValueError: '8' is not a vertex of the graph + + Giving a wrong matching:: + + sage: from sage.graphs.matching import M_alternating_even_mark + sage: G = graphs.CompleteGraph(6) + sage: M = [(0, 1), (0, 2)] + sage: u = 0 + sage: S = M_alternating_even_mark(G, u, M) + Traceback (most recent call last): + ... + ValueError: the input is not a matching + sage: G = graphs.CompleteBipartiteGraph(3, 3) + sage: M = [(2*i, 2*i + 1) for i in range(4)] + sage: u = 0 + sage: S = M_alternating_even_mark(G, u, M) + Traceback (most recent call last): + ... + ValueError: the input is not a matching of the graph + + REFERENCES: + + - [LR2004]_ + + .. SEEALSO:: + :meth:`~sage.graphs.graph.Graph.is_factor_critical`, + :meth:`~sage.graphs.graph.Graph.is_matching_covered`, + :meth:`~sage.graphs.graph.Graph.is_bicritical` + + AUTHORS: + + - Janmenjaya Panda (2024-06-17) + """ + # The input vertex must be a valid vertex of the graph + if vertex not in G: + raise ValueError("'{}' is not a vertex of the graph".format(vertex)) + + # The result is equivalent for the underlying simple graph of the provided + # graph. So, the underlying simple graph is considered for implementational + # simplicity. + G_simple = G.to_simple() + + # The input matching must be a valid matching of the graph + from sage.graphs.graph import Graph + M = Graph(matching) + if any(d != 1 for d in M.degree()): + raise ValueError("the input is not a matching") + if any(not G_simple.has_edge(edge) for edge in M.edge_iterator()): + raise ValueError("the input is not a matching of the graph") + + # Build an M-alternating tree T rooted at vertex + import itertools + from queue import Queue + + q = Queue() + q.put(vertex) + + even = set([vertex]) + odd = set() + predecessor = {vertex: vertex} + rank = {vertex: 0} + + if vertex in M: + u = next(M.neighbor_iterator(vertex)) + predecessor[u] = None + rank[u] = -1 + odd.add(u) + + while not q.empty(): + x = q.get() + for y in G_simple.neighbor_iterator(x): + if y in odd: + continue + elif y in even: + # Search t := LCA(x, y) + ancestor_x = [x] + ancestor_y = [y] + + # Loop over until the nearest common ancestor of x and y is reached + while ancestor_x[-1] != ancestor_y[-1]: + if rank[ancestor_x[-1]] > rank[ancestor_y[-1]]: + ancestor_x.append(predecessor[ancestor_x[-1]]) + elif rank[ancestor_x[-1]] < rank[ancestor_y[-1]]: + ancestor_y.append(predecessor[ancestor_y[-1]]) + else: + ancestor_x.append(predecessor[ancestor_x[-1]]) + ancestor_y.append(predecessor[ancestor_y[-1]]) + + lcs = ancestor_x.pop() + ancestor_y.pop() + # Set t as pred of all vertices of the chains and add + # vertices marked odd to the queue + next_rank_to_lcs_rank = rank[lcs] + 1 + for a in itertools.chain(ancestor_x, ancestor_y): + predecessor[a] = lcs + rank[a] = next_rank_to_lcs_rank + + if a in odd: + even.add(a) + odd.discard(a) + q.put(a) + + elif y in M: + # y has not been visited yet + z = next(M.neighbor_iterator(y)) + odd.add(y) + even.add(z) + q.put(z) + + predecessor[y] = x + predecessor[z] = y + + rank[y] = rank[x] + 1 + rank[z] = rank[y] + 1 + + return even \ No newline at end of file