Repository: sagemath/sage

Issue:
Iterator over the distance-k dominating sets
### 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.

### Problem Description

[Question 67263 ](https://ask.sagemath.org/question/67263/is-there-any-code-to-list-all-minimal-k-distance-dominating-sets-of-a-simple-graph-g-in-sagemath/) on [ask.sagemath.org](https://ask.sagemath.org/) asks for a method to list all minimal distance-k dominating sets of a graph.


### Proposed Solution

To iterate over the minimal distance k dominating sets, we can modify the working copy of the graph in method `minimal_dominating_sets`, adding edges between vertices at distance at most k.

To iterate over minimum distance k dominating set, we can create a new method `dominating_sets` and use constraints generation to prevent finding twice a minimum dominating set.

### Alternatives Considered

None.

### Additional Information

The default value of parameter `work_on_copy` is `True`, and so by default the input graph is modified. This is a bad choice. We should at least change the default value to `False`. 


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
@@ -54,7 +54,8 @@
 
 # ****************************************************************************
 #       Copyright (C) 2009 Nathann Cohen <nathann.cohen@gmail.com>
-#                     2019 Jean-Florent Raymond  <j-florent.raymond@uca.fr>
+#                     2019 Jean-Florent Raymond <j-florent.raymond@uca.fr>
+#                     2023 David Coudert <david.coudert@inria.fr>
 #
 # 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
@@ -583,7 +584,7 @@ def _aux_with_rep(H, to_dom, u_next):
                 break
 
 
-def minimal_dominating_sets(G, to_dominate=None, work_on_copy=True):
+def minimal_dominating_sets(G, to_dominate=None, work_on_copy=False, k=1):
     r"""
     Return an iterator over the minimal dominating sets of a graph.
 
@@ -594,10 +595,12 @@ def minimal_dominating_sets(G, to_dominate=None, work_on_copy=True):
     - ``to_dominate`` -- vertex iterable or ``None`` (default: ``None``);
       the set of vertices to be dominated.
 
-    - ``work_on_copy`` -- boolean (default: ``True``); whether or not to work on
+    - ``work_on_copy`` -- boolean (default: ``False``); whether or not to work on
       a copy of the input graph; if set to ``False``, the input graph will be
       modified (relabeled).
 
+    - ``k`` -- a non-negative integer (default: ``1``); the domination distance
+
     OUTPUT:
 
     An iterator over the inclusion-minimal sets of vertices of ``G``.
@@ -660,6 +663,31 @@ def minimal_dominating_sets(G, to_dominate=None, work_on_copy=True):
         sage: len(ll) == len(pp) and all(x in pp for x in ll) and all(x in ll for x in pp)
         True
 
+    Listing minimal distance-`k` dominating sets::
+
+        sage: G = graphs.Grid2dGraph(2, 3)
+        sage: list(G.minimal_dominating_sets(k=0))
+        [{(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)}]
+        sage: list(G.minimal_dominating_sets(k=1))
+        [{(0, 0), (0, 2), (1, 1)},
+         {(0, 1), (1, 1)},
+         {(0, 0), (0, 1), (0, 2)},
+         {(0, 2), (1, 0)},
+         {(0, 0), (1, 2)},
+         {(0, 1), (1, 0), (1, 2)},
+         {(1, 0), (1, 1), (1, 2)}]
+        sage: list(G.minimal_dominating_sets(k=2))
+        [{(0, 0), (1, 2)},
+         {(0, 2), (1, 2)},
+         {(1, 0), (1, 2)},
+         {(0, 1)},
+         {(0, 0), (0, 2)},
+         {(0, 2), (1, 0)},
+         {(0, 0), (1, 0)},
+         {(1, 1)}]
+        sage: list(G.minimal_dominating_sets(k=3))
+        [{(0, 0)}, {(0, 1)}, {(0, 2)}, {(1, 0)}, {(1, 1)}, {(1, 2)}]
+
     TESTS:
 
     The empty graph is handled correctly::
@@ -701,8 +729,21 @@ def minimal_dominating_sets(G, to_dominate=None, work_on_copy=True):
         ....:     return True
         sage: check_uniqueness(graphs.RandomGNP(9, 0.5))
         True
-    """
 
+    Asking for a negative distance::
+
+        sage: next(Graph(1).minimal_dominating_sets(k=-1))
+        Traceback (most recent call last):
+        ...
+        ValueError: the domination distance must be a non-negative integer
+
+    Trying to dominate vertices that are not part of the graph::
+
+        sage: next(Graph(1).minimal_dominating_sets(to_dominate=['foo']))
+        Traceback (most recent call last):
+        ...
+        ValueError: vertex (foo) is not a vertex of the graph
+    """
     def tree_search(H, plng, dom, i):
         r"""
         Enumerate minimal dominating sets recursively.
@@ -768,15 +809,30 @@ def tree_search(H, plng, dom, i):
     int_to_vertex = list(G)
     vertex_to_int = {u: i for i, u in enumerate(int_to_vertex)}
 
-    if work_on_copy:
-        G.relabel(perm=vertex_to_int)
+    if to_dominate is None:
+        vertices_to_dominate = set(range(G.order()))
     else:
-        G = G.relabel(perm=vertex_to_int, inplace=False)
+        for u in to_dominate:
+            if u not in G:
+                raise ValueError(f"vertex ({u}) is not a vertex of the graph")
+        vertices_to_dominate = {vertex_to_int[u] for u in to_dominate}
 
-    if to_dominate is None:
-        vertices_to_dominate = set(G)
+    if k < 0:
+        raise ValueError("the domination distance must be a non-negative integer")
+    elif not k:
+        yield set(int_to_vertex) if to_dominate is None else set(to_dominate)
+        return
+    elif k > 1:
+        # We build a graph H with an edge between u and v if these vertices are
+        # at distance at most k in G
+        H = G.__class__(G.order())
+        for u, ui in vertex_to_int.items():
+            H.add_edges((ui, vertex_to_int[v]) for v in G.breadth_first_search(u, distance=k) if u != v)
+        G = H
+    elif work_on_copy:
+        G.relabel(perm=vertex_to_int)
     else:
-        vertices_to_dominate = set(to_dominate)
+        G = G.relabel(perm=vertex_to_int, inplace=False)
 
     if not vertices_to_dominate:
         # base case: vertices_to_dominate is empty


Test patch: