euler.py 14 KB

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