1"""
2Eulerian circuits and graphs.
3"""
4
5from itertools import combinations
6
7import networkx as nx
8
9from ..utils import arbitrary_element, not_implemented_for
10
11__all__ = [
12 "is_eulerian",
13 "eulerian_circuit",
14 "eulerize",
15 "is_semieulerian",
16 "has_eulerian_path",
17 "eulerian_path",
18]
19
20
21@nx._dispatchable
22def is_eulerian(G):
23 """Returns True if and only if `G` is Eulerian.
24
25 A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian
26 circuit* is a closed walk that includes each edge of a graph exactly
27 once.
28
29 Graphs with isolated vertices (i.e. vertices with zero degree) are not
30 considered to have Eulerian circuits. Therefore, if the graph is not
31 connected (or not strongly connected, for directed graphs), this function
32 returns False.
33
34 Parameters
35 ----------
36 G : NetworkX graph
37 A graph, either directed or undirected.
38
39 Examples
40 --------
41 >>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]}))
42 True
43 >>> nx.is_eulerian(nx.complete_graph(5))
44 True
45 >>> nx.is_eulerian(nx.petersen_graph())
46 False
47
48 If you prefer to allow graphs with isolated vertices to have Eulerian circuits,
49 you can first remove such vertices and then call `is_eulerian` as below example shows.
50
51 >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
52 >>> G.add_node(3)
53 >>> nx.is_eulerian(G)
54 False
55
56 >>> G.remove_nodes_from(list(nx.isolates(G)))
57 >>> nx.is_eulerian(G)
58 True
59
60
61 """
62 if G.is_directed():
63 # Every node must have equal in degree and out degree and the
64 # graph must be strongly connected
65 return all(
66 G.in_degree(n) == G.out_degree(n) for n in G
67 ) and nx.is_strongly_connected(G)
68 # An undirected Eulerian graph has no vertices of odd degree and
69 # must be connected.
70 return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G)
71
72
73@nx._dispatchable
74def is_semieulerian(G):
75 """Return True iff `G` is semi-Eulerian.
76
77 G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit.
78
79 See Also
80 --------
81 has_eulerian_path
82 is_eulerian
83 """
84 return has_eulerian_path(G) and not is_eulerian(G)
85
86
87def _find_path_start(G):
88 """Return a suitable starting vertex for an Eulerian path.
89
90 If no path exists, return None.
91 """
92 if not has_eulerian_path(G):
93 return None
94
95 if is_eulerian(G):
96 return arbitrary_element(G)
97
98 if G.is_directed():
99 v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v))
100 # Determines which is the 'start' node (as opposed to the 'end')
101 if G.out_degree(v1) > G.in_degree(v1):
102 return v1
103 else:
104 return v2
105
106 else:
107 # In an undirected graph randomly choose one of the possibilities
108 start = [v for v in G if G.degree(v) % 2 != 0][0]
109 return start
110
111
112def _simplegraph_eulerian_circuit(G, source):
113 if G.is_directed():
114 degree = G.out_degree
115 edges = G.out_edges
116 else:
117 degree = G.degree
118 edges = G.edges
119 vertex_stack = [source]
120 last_vertex = None
121 while vertex_stack:
122 current_vertex = vertex_stack[-1]
123 if degree(current_vertex) == 0:
124 if last_vertex is not None:
125 yield (last_vertex, current_vertex)
126 last_vertex = current_vertex
127 vertex_stack.pop()
128 else:
129 _, next_vertex = arbitrary_element(edges(current_vertex))
130 vertex_stack.append(next_vertex)
131 G.remove_edge(current_vertex, next_vertex)
132
133
134def _multigraph_eulerian_circuit(G, source):
135 if G.is_directed():
136 degree = G.out_degree
137 edges = G.out_edges
138 else:
139 degree = G.degree
140 edges = G.edges
141 vertex_stack = [(source, None)]
142 last_vertex = None
143 last_key = None
144 while vertex_stack:
145 current_vertex, current_key = vertex_stack[-1]
146 if degree(current_vertex) == 0:
147 if last_vertex is not None:
148 yield (last_vertex, current_vertex, last_key)
149 last_vertex, last_key = current_vertex, current_key
150 vertex_stack.pop()
151 else:
152 triple = arbitrary_element(edges(current_vertex, keys=True))
153 _, next_vertex, next_key = triple
154 vertex_stack.append((next_vertex, next_key))
155 G.remove_edge(current_vertex, next_vertex, next_key)
156
157
158@nx._dispatchable
159def eulerian_circuit(G, source=None, keys=False):
160 """Returns an iterator over the edges of an Eulerian circuit in `G`.
161
162 An *Eulerian circuit* is a closed walk that includes each edge of a
163 graph exactly once.
164
165 Parameters
166 ----------
167 G : NetworkX graph
168 A graph, either directed or undirected.
169
170 source : node, optional
171 Starting node for circuit.
172
173 keys : bool
174 If False, edges generated by this function will be of the form
175 ``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``.
176 This option is ignored unless `G` is a multigraph.
177
178 Returns
179 -------
180 edges : iterator
181 An iterator over edges in the Eulerian circuit.
182
183 Raises
184 ------
185 NetworkXError
186 If the graph is not Eulerian.
187
188 See Also
189 --------
190 is_eulerian
191
192 Notes
193 -----
194 This is a linear time implementation of an algorithm adapted from [1]_.
195
196 For general information about Euler tours, see [2]_.
197
198 References
199 ----------
200 .. [1] J. Edmonds, E. L. Johnson.
201 Matching, Euler tours and the Chinese postman.
202 Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
203 .. [2] https://en.wikipedia.org/wiki/Eulerian_path
204
205 Examples
206 --------
207 To get an Eulerian circuit in an undirected graph::
208
209 >>> G = nx.complete_graph(3)
210 >>> list(nx.eulerian_circuit(G))
211 [(0, 2), (2, 1), (1, 0)]
212 >>> list(nx.eulerian_circuit(G, source=1))
213 [(1, 2), (2, 0), (0, 1)]
214
215 To get the sequence of vertices in an Eulerian circuit::
216
217 >>> [u for u, v in nx.eulerian_circuit(G)]
218 [0, 2, 1]
219
220 """
221 if not is_eulerian(G):
222 raise nx.NetworkXError("G is not Eulerian.")
223 if G.is_directed():
224 G = G.reverse()
225 else:
226 G = G.copy()
227 if source is None:
228 source = arbitrary_element(G)
229 if G.is_multigraph():
230 for u, v, k in _multigraph_eulerian_circuit(G, source):
231 if keys:
232 yield u, v, k
233 else:
234 yield u, v
235 else:
236 yield from _simplegraph_eulerian_circuit(G, source)
237
238
239@nx._dispatchable
240def has_eulerian_path(G, source=None):
241 """Return True iff `G` has an Eulerian path.
242
243 An Eulerian path is a path in a graph which uses each edge of a graph
244 exactly once. If `source` is specified, then this function checks
245 whether an Eulerian path that starts at node `source` exists.
246
247 A directed graph has an Eulerian path iff:
248 - at most one vertex has out_degree - in_degree = 1,
249 - at most one vertex has in_degree - out_degree = 1,
250 - every other vertex has equal in_degree and out_degree,
251 - and all of its vertices belong to a single connected
252 component of the underlying undirected graph.
253
254 If `source` is not None, an Eulerian path starting at `source` exists if no
255 other node has out_degree - in_degree = 1. This is equivalent to either
256 there exists an Eulerian circuit or `source` has out_degree - in_degree = 1
257 and the conditions above hold.
258
259 An undirected graph has an Eulerian path iff:
260 - exactly zero or two vertices have odd degree,
261 - and all of its vertices belong to a single connected component.
262
263 If `source` is not None, an Eulerian path starting at `source` exists if
264 either there exists an Eulerian circuit or `source` has an odd degree and the
265 conditions above hold.
266
267 Graphs with isolated vertices (i.e. vertices with zero degree) are not considered
268 to have an Eulerian path. Therefore, if the graph is not connected (or not strongly
269 connected, for directed graphs), this function returns False.
270
271 Parameters
272 ----------
273 G : NetworkX Graph
274 The graph to find an euler path in.
275
276 source : node, optional
277 Starting node for path.
278
279 Returns
280 -------
281 Bool : True if G has an Eulerian path.
282
283 Examples
284 --------
285 If you prefer to allow graphs with isolated vertices to have Eulerian path,
286 you can first remove such vertices and then call `has_eulerian_path` as below example shows.
287
288 >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
289 >>> G.add_node(3)
290 >>> nx.has_eulerian_path(G)
291 False
292
293 >>> G.remove_nodes_from(list(nx.isolates(G)))
294 >>> nx.has_eulerian_path(G)
295 True
296
297 See Also
298 --------
299 is_eulerian
300 eulerian_path
301 """
302 if nx.is_eulerian(G):
303 return True
304
305 if G.is_directed():
306 ins = G.in_degree
307 outs = G.out_degree
308 # Since we know it is not eulerian, outs - ins must be 1 for source
309 if source is not None and outs[source] - ins[source] != 1:
310 return False
311
312 unbalanced_ins = 0
313 unbalanced_outs = 0
314 for v in G:
315 if ins[v] - outs[v] == 1:
316 unbalanced_ins += 1
317 elif outs[v] - ins[v] == 1:
318 unbalanced_outs += 1
319 elif ins[v] != outs[v]:
320 return False
321
322 return (
323 unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
324 )
325 else:
326 # We know it is not eulerian, so degree of source must be odd.
327 if source is not None and G.degree[source] % 2 != 1:
328 return False
329
330 # Sum is 2 since we know it is not eulerian (which implies sum is 0)
331 return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
332
333
334@nx._dispatchable
335def eulerian_path(G, source=None, keys=False):
336 """Return an iterator over the edges of an Eulerian path in `G`.
337
338 Parameters
339 ----------
340 G : NetworkX Graph
341 The graph in which to look for an eulerian path.
342 source : node or None (default: None)
343 The node at which to start the search. None means search over all
344 starting nodes.
345 keys : Bool (default: False)
346 Indicates whether to yield edge 3-tuples (u, v, edge_key).
347 The default yields edge 2-tuples
348
349 Yields
350 ------
351 Edge tuples along the eulerian path.
352
353 Warning: If `source` provided is not the start node of an Euler path
354 will raise error even if an Euler Path exists.
355 """
356 if not has_eulerian_path(G, source):
357 raise nx.NetworkXError("Graph has no Eulerian paths.")
358 if G.is_directed():
359 G = G.reverse()
360 if source is None or nx.is_eulerian(G) is False:
361 source = _find_path_start(G)
362 if G.is_multigraph():
363 for u, v, k in _multigraph_eulerian_circuit(G, source):
364 if keys:
365 yield u, v, k
366 else:
367 yield u, v
368 else:
369 yield from _simplegraph_eulerian_circuit(G, source)
370 else:
371 G = G.copy()
372 if source is None:
373 source = _find_path_start(G)
374 if G.is_multigraph():
375 if keys:
376 yield from reversed(
377 [(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
378 )
379 else:
380 yield from reversed(
381 [(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
382 )
383 else:
384 yield from reversed(
385 [(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
386 )
387
388
389@not_implemented_for("directed")
390@nx._dispatchable(returns_graph=True)
391def eulerize(G):
392 """Transforms a graph into an Eulerian graph.
393
394 If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest
395 (in terms of the number of edges) multigraph whose underlying simple graph is `G`.
396
397 Parameters
398 ----------
399 G : NetworkX graph
400 An undirected graph
401
402 Returns
403 -------
404 G : NetworkX multigraph
405
406 Raises
407 ------
408 NetworkXError
409 If the graph is not connected.
410
411 See Also
412 --------
413 is_eulerian
414 eulerian_circuit
415
416 References
417 ----------
418 .. [1] J. Edmonds, E. L. Johnson.
419 Matching, Euler tours and the Chinese postman.
420 Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
421 .. [2] https://en.wikipedia.org/wiki/Eulerian_path
422 .. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf
423
424 Examples
425 --------
426 >>> G = nx.complete_graph(10)
427 >>> H = nx.eulerize(G)
428 >>> nx.is_eulerian(H)
429 True
430
431 """
432 if G.order() == 0:
433 raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph")
434 if not nx.is_connected(G):
435 raise nx.NetworkXError("G is not connected")
436 odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1]
437 G = nx.MultiGraph(G)
438 if len(odd_degree_nodes) == 0:
439 return G
440
441 # get all shortest paths between vertices of odd degree
442 odd_deg_pairs_paths = [
443 (m, {n: nx.shortest_path(G, source=m, target=n)})
444 for m, n in combinations(odd_degree_nodes, 2)
445 ]
446
447 # use the number of vertices in a graph + 1 as an upper bound on
448 # the maximum length of a path in G
449 upper_bound_on_max_path_length = len(G) + 1
450
451 # use "len(G) + 1 - len(P)",
452 # where P is a shortest path between vertices n and m,
453 # as edge-weights in a new graph
454 # store the paths in the graph for easy indexing later
455 Gp = nx.Graph()
456 for n, Ps in odd_deg_pairs_paths:
457 for m, P in Ps.items():
458 if n != m:
459 Gp.add_edge(
460 m, n, weight=upper_bound_on_max_path_length - len(P), path=P
461 )
462
463 # find the minimum weight matching of edges in the weighted graph
464 best_matching = nx.Graph(list(nx.max_weight_matching(Gp)))
465
466 # duplicate each edge along each path in the set of paths in Gp
467 for m, n in best_matching.edges():
468 path = Gp[m][n]["path"]
469 G.add_edges_from(nx.utils.pairwise(path))
470 return G