Repository: sagemath/sage

Issue:
Maximum leaf number parameter
### Problem Description

The Max Leaf Number (MLN) represents the maximum number of leaf nodes in any spanning tree of a graph G. It is an incredibly useful graph parameter, particularly when constructing wireless sensor networks. It is also a minimal upper bound for both bandwidth and distance to linear forest.

Currently, SageMath does not provide a function to calculate the MLN. However, now that Minimum Connected Dominating Set (MCDS) is implemented, such a function can be easily added.

### Proposed Solution

The MLN of a graph can be calculated by generating a MCDS. This is because the MLN is the size of the difference set between the graph and the MCDS (or equivalently, the order of the graph minus the size of the dominating set).

### Alternatives Considered

The MLN could also be calculated through generating a Maximum Leaf Spanning Tree; however, this would require implementing an algorithm from scratch and has no time complexity advantage.

### Additional Information

_No response_

### Is there an existing issue for this?

- [X] I have searched the existing issues for a bug report that matches the one I want to file, without success.


Discussion:

Resolving patch:
diff --git a/src/sage/graphs/domination.py b/src/sage/graphs/domination.py
--- a/src/sage/graphs/domination.py
+++ b/src/sage/graphs/domination.py
@@ -17,6 +17,7 @@
     :meth:`~is_redundant` | Check whether a set of vertices has redundant vertices (with respect to domination).
     :meth:`~private_neighbors` | Return the private neighbors of a vertex with respect to other vertices.
     :meth:`~greedy_dominating_set` | Return a greedy distance-`k` dominating set of the graph.
+    :meth:`~maximum_leaf_number` | Return the maximum leaf number of the graph.
 
 
 EXAMPLES:
@@ -1284,3 +1285,81 @@ def greedy_dominating_set(G, k=1, vertices=None, ordering=None, return_sets=Fals
         return dom
     else:
         return list(dom)
+
+
+def maximum_leaf_number(G, solver=None, verbose=0, integrality_tolerance=1e-3):
+    r"""
+    Return the maximum leaf number of the graph.
+
+    The maximum leaf number is the maximum possible number of leaves of a
+    spanning tree of `G`. This is also the cardinality of the complement of a
+    minimum connected dominating set.
+    See the :wikipedia:`Connected_dominating_set`.
+
+    The MLN of a graph with less than 2 vertices is 0, while the MLN of a connected
+    graph with 2 or 3 vertices is 1 or 2 respectively.
+
+    INPUT:
+
+    - ``G`` -- a Graph
+
+    - ``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
+      <sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
+      :class:`MixedIntegerLinearProgram
+      <sage.numerical.mip.MixedIntegerLinearProgram>`.
+
+    - ``verbose`` -- integer (default: ``0``); sets the level of verbosity. Set
+      to 0 by default, which means quiet.
+
+    - ``integrality_tolerance`` -- float; parameter for use with MILP solvers
+      over an inexact base ring; see
+      :meth:`MixedIntegerLinearProgram.get_values`.
+
+    EXAMPLES:
+
+    Empty graph::
+
+        sage: G = Graph()
+        sage: G.maximum_leaf_number()
+        0
+
+    Petersen graph::
+
+        sage: G = graphs.PetersenGraph()
+        sage: G.maximum_leaf_number()
+        6
+
+    TESTS:
+
+    One vertex::
+
+        sage: G = Graph(1)
+        sage: G.maximum_leaf_number()
+        0
+
+    Two vertices::
+
+        sage: G = graphs.PathGraph(2)
+        sage: G.maximum_leaf_number()
+        1
+
+    Unconnected graph::
+
+        sage: G = Graph(2)
+        sage: G.maximum_leaf_number()
+        Traceback (most recent call last):
+        ...
+        ValueError: the graph must be connected
+    """
+    if G.order() <= 1:
+        return 0
+    if not G.is_connected():
+        raise ValueError('the graph must be connected')
+    if G.order() <= 3:
+        return G.order() - 1
+    return G.order() - dominating_set(G, connected=True, value_only=True,
+                                      solver=solver, verbose=verbose,
+                                      integrality_tolerance=integrality_tolerance)
diff --git a/src/sage/graphs/generic_graph.py b/src/sage/graphs/generic_graph.py
--- a/src/sage/graphs/generic_graph.py
+++ b/src/sage/graphs/generic_graph.py
@@ -315,6 +315,7 @@
     :meth:`~GenericGraph.disjoint_routed_paths` | Return a set of disjoint routed paths.
     :meth:`~GenericGraph.dominating_set` | Return a minimum dominating set of the graph
     :meth:`~GenericGraph.greedy_dominating_set` | Return a greedy distance-`k` dominating set of the graph.
+    :meth:`~GenericGraph.maximum_leaf_number` | Return the maximum leaf number of the graph.
     :meth:`~GenericGraph.subgraph_search` | Return a copy of ``G`` in ``self``.
     :meth:`~GenericGraph.subgraph_search_count` | Return the number of labelled occurrences of ``G`` in ``self``.
     :meth:`~GenericGraph.subgraph_search_iterator` | Return an iterator over the labelled copies of ``G`` in ``self``.
@@ -24407,6 +24408,7 @@ def is_self_complementary(self):
     from sage.graphs.domination import dominating_sets
     from sage.graphs.domination import dominating_set
     from sage.graphs.domination import greedy_dominating_set
+    from sage.graphs.domination import maximum_leaf_number
     from sage.graphs.base.static_dense_graph import connected_subgraph_iterator
     rooted_product = LazyImport('sage.graphs.graph_decompositions.graph_products', 'rooted_product')
     from sage.graphs.path_enumeration import shortest_simple_paths


Test patch: