1"""Functions for computing dominating sets in a graph."""
2
3import math
4from heapq import heappop, heappush
5from itertools import chain, count
6
7import networkx as nx
8
9__all__ = [
10 "dominating_set",
11 "is_dominating_set",
12 "connected_dominating_set",
13 "is_connected_dominating_set",
14]
15
16
17@nx._dispatchable
18def dominating_set(G, start_with=None):
19 r"""Finds a dominating set for the graph G.
20
21 A *dominating set* for a graph with node set *V* is a subset *D* of
22 *V* such that every node not in *D* is adjacent to at least one
23 member of *D* [1]_.
24
25 Parameters
26 ----------
27 G : NetworkX graph
28
29 start_with : node (default=None)
30 Node to use as a starting point for the algorithm.
31
32 Returns
33 -------
34 D : set
35 A dominating set for G.
36
37 Notes
38 -----
39 This function is an implementation of algorithm 7 in [2]_ which
40 finds some dominating set, not necessarily the smallest one.
41
42 See also
43 --------
44 is_dominating_set
45
46 References
47 ----------
48 .. [1] https://en.wikipedia.org/wiki/Dominating_set
49
50 .. [2] Abdol-Hossein Esfahanian. Connectivity Algorithms.
51 http://www.cse.msu.edu/~cse835/Papers/Graph_connectivity_revised.pdf
52
53 """
54 all_nodes = set(G)
55 if start_with is None:
56 start_with = nx.utils.arbitrary_element(all_nodes)
57 if start_with not in G:
58 raise nx.NetworkXError(f"node {start_with} is not in G")
59 dominating_set = {start_with}
60 dominated_nodes = set(G[start_with])
61 remaining_nodes = all_nodes - dominated_nodes - dominating_set
62 while remaining_nodes:
63 # Choose an arbitrary node and determine its undominated neighbors.
64 v = remaining_nodes.pop()
65 undominated_nbrs = set(G[v]) - dominating_set
66 # Add the node to the dominating set and the neighbors to the
67 # dominated set. Finally, remove all of those nodes from the set
68 # of remaining nodes.
69 dominating_set.add(v)
70 dominated_nodes |= undominated_nbrs
71 remaining_nodes -= undominated_nbrs
72 return dominating_set
73
74
75@nx._dispatchable
76def is_dominating_set(G, nbunch):
77 """Checks if `nbunch` is a dominating set for `G`.
78
79 A *dominating set* for a graph with node set *V* is a subset *D* of
80 *V* such that every node not in *D* is adjacent to at least one
81 member of *D* [1]_.
82
83 Parameters
84 ----------
85 G : NetworkX graph
86
87 nbunch : iterable
88 An iterable of nodes in the graph `G`.
89
90 Returns
91 -------
92 dominating : bool
93 True if `nbunch` is a dominating set of `G`, false otherwise.
94
95 See also
96 --------
97 dominating_set
98
99 References
100 ----------
101 .. [1] https://en.wikipedia.org/wiki/Dominating_set
102
103 """
104 testset = {n for n in nbunch if n in G}
105 nbrs = set(chain.from_iterable(G[n] for n in testset))
106 return len(set(G) - testset - nbrs) == 0
107
108
109@nx.utils.not_implemented_for("directed")
110@nx._dispatchable
111def connected_dominating_set(G):
112 """Returns a connected dominating set.
113
114 A *dominating set* for a graph *G* with node set *V* is a subset *D* of *V*
115 such that every node not in *D* is adjacent to at least one member of *D*
116 [1]_. A *connected dominating set* is a dominating set *C* that induces a
117 connected subgraph of *G* [2]_.
118 Note that connected dominating sets are not unique in general and that there
119 may be other connected dominating sets.
120
121 Parameters
122 ----------
123 G : NewtorkX graph
124 Undirected connected graph.
125
126 Returns
127 -------
128 connected_dominating_set : set
129 A dominating set of nodes which induces a connected subgraph of G.
130
131 Raises
132 ------
133 NetworkXNotImplemented
134 If G is directed.
135
136 NetworkXError
137 If G is disconnected.
138
139 Examples
140 ________
141 >>> G = nx.Graph(
142 ... [
143 ... (1, 2),
144 ... (1, 3),
145 ... (1, 4),
146 ... (1, 5),
147 ... (1, 6),
148 ... (2, 7),
149 ... (3, 8),
150 ... (4, 9),
151 ... (5, 10),
152 ... (6, 11),
153 ... (7, 12),
154 ... (8, 12),
155 ... (9, 12),
156 ... (10, 12),
157 ... (11, 12),
158 ... ]
159 ... )
160 >>> nx.connected_dominating_set(G)
161 {1, 2, 3, 4, 5, 6, 7}
162
163 Notes
164 -----
165 This function implements Algorithm I in its basic version as described
166 in [3]_. The idea behind the algorithm is the following: grow a tree *T*,
167 starting from a node with maximum degree. Throughout the growing process,
168 nonleaf nodes in *T* are our connected dominating set (CDS), leaf nodes in
169 *T* are marked as "seen" and nodes in G that are not yet in *T* are marked as
170 "unseen". We maintain a max-heap of all "seen" nodes, and track the number
171 of "unseen" neighbors for each node. At each step we pop the heap top -- a
172 "seen" (leaf) node with maximal number of "unseen" neighbors, add it to the
173 CDS and mark all its "unseen" neighbors as "seen". For each one of the newly
174 created "seen" nodes, we also decrement the number of "unseen" neighbors for
175 all its neighbors. The algorithm terminates when there are no more "unseen"
176 nodes.
177 Runtime complexity of this implementation is $O(|E|*log|V|)$ (amortized).
178
179 References
180 ----------
181 .. [1] https://en.wikipedia.org/wiki/Dominating_set
182 .. [2] https://en.wikipedia.org/wiki/Connected_dominating_set
183 .. [3] Guha, S. and Khuller, S.
184 *Approximation Algorithms for Connected Dominating Sets*,
185 Algorithmica, 20, 374-387, 1998.
186
187 """
188 if len(G) == 0:
189 return set()
190
191 if not nx.is_connected(G):
192 raise nx.NetworkXError("G must be a connected graph")
193
194 if len(G) == 1:
195 return set(G)
196
197 G_succ = G._adj # For speed-up
198
199 # Use the count c to avoid comparing nodes
200 c = count()
201
202 # Keep track of the number of unseen nodes adjacent to each node
203 unseen_degree = dict(G.degree)
204
205 # Find node with highest degree and update its neighbors
206 (max_deg_node, max_deg) = max(unseen_degree.items(), key=lambda x: x[1])
207 for nbr in G_succ[max_deg_node]:
208 unseen_degree[nbr] -= 1
209
210 # Initially all nodes except max_deg_node are unseen
211 unseen = set(G) - {max_deg_node}
212
213 # We want a max-heap of the unseen-degree using heapq, which is a min-heap
214 # So we store the negative of the unseen-degree
215 seen = [(-max_deg, next(c), max_deg_node)]
216
217 connected_dominating_set = set()
218
219 # Main loop
220 while unseen:
221 (neg_deg, cnt, u) = heappop(seen)
222 # Check if u's unseen-degree changed while in the heap
223 if -neg_deg > unseen_degree[u]:
224 heappush(seen, (-unseen_degree[u], cnt, u))
225 continue
226 # Mark all u's unseen neighbors as seen and add them to the heap
227 for v in G_succ[u]:
228 if v in unseen:
229 unseen.remove(v)
230 for nbr in G_succ[v]:
231 unseen_degree[nbr] -= 1
232 heappush(seen, (-unseen_degree[v], next(c), v))
233 # Add u to the dominating set
234 connected_dominating_set.add(u)
235
236 return connected_dominating_set
237
238
239@nx.utils.not_implemented_for("directed")
240@nx._dispatchable
241def is_connected_dominating_set(G, nbunch):
242 """Checks if `nbunch` is a connected dominating set for `G`.
243
244 A *dominating set* for a graph *G* with node set *V* is a subset *D* of
245 *V* such that every node not in *D* is adjacent to at least one
246 member of *D* [1]_. A *connected dominating set* is a dominating
247 set *C* that induces a connected subgraph of *G* [2]_.
248
249 Parameters
250 ----------
251 G : NetworkX graph
252 Undirected graph.
253
254 nbunch : iterable
255 An iterable of nodes in the graph `G`.
256
257 Returns
258 -------
259 connected_dominating : bool
260 True if `nbunch` is connected dominating set of `G`, false otherwise.
261
262 References
263 ----------
264 .. [1] https://en.wikipedia.org/wiki/Dominating_set
265 .. [2] https://en.wikipedia.org/wiki/Connected_dominating_set
266
267 """
268 return nx.is_dominating_set(G, nbunch) and nx.is_connected(nx.subgraph(G, nbunch))