123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463 |
- """
- Eulerian circuits and graphs.
- """
- from itertools import combinations
- import networkx as nx
- from ..utils import arbitrary_element, not_implemented_for
- __all__ = [
- "is_eulerian",
- "eulerian_circuit",
- "eulerize",
- "is_semieulerian",
- "has_eulerian_path",
- "eulerian_path",
- ]
- def is_eulerian(G):
- """Returns True if and only if `G` is Eulerian.
- A graph is *Eulerian* if it has an Eulerian circuit. An *Eulerian
- circuit* is a closed walk that includes each edge of a graph exactly
- once.
- Graphs with isolated vertices (i.e. vertices with zero degree) are not
- considered to have Eulerian circuits. Therefore, if the graph is not
- connected (or not strongly connected, for directed graphs), this function
- returns False.
- Parameters
- ----------
- G : NetworkX graph
- A graph, either directed or undirected.
- Examples
- --------
- >>> nx.is_eulerian(nx.DiGraph({0: [3], 1: [2], 2: [3], 3: [0, 1]}))
- True
- >>> nx.is_eulerian(nx.complete_graph(5))
- True
- >>> nx.is_eulerian(nx.petersen_graph())
- False
- If you prefer to allow graphs with isolated vertices to have Eulerian circuits,
- you can first remove such vertices and then call `is_eulerian` as below example shows.
- >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
- >>> G.add_node(3)
- >>> nx.is_eulerian(G)
- False
- >>> G.remove_nodes_from(list(nx.isolates(G)))
- >>> nx.is_eulerian(G)
- True
- """
- if G.is_directed():
- # Every node must have equal in degree and out degree and the
- # graph must be strongly connected
- return all(
- G.in_degree(n) == G.out_degree(n) for n in G
- ) and nx.is_strongly_connected(G)
- # An undirected Eulerian graph has no vertices of odd degree and
- # must be connected.
- return all(d % 2 == 0 for v, d in G.degree()) and nx.is_connected(G)
- def is_semieulerian(G):
- """Return True iff `G` is semi-Eulerian.
- G is semi-Eulerian if it has an Eulerian path but no Eulerian circuit.
- See Also
- --------
- has_eulerian_path
- is_eulerian
- """
- return has_eulerian_path(G) and not is_eulerian(G)
- def _find_path_start(G):
- """Return a suitable starting vertex for an Eulerian path.
- If no path exists, return None.
- """
- if not has_eulerian_path(G):
- return None
- if is_eulerian(G):
- return arbitrary_element(G)
- if G.is_directed():
- v1, v2 = (v for v in G if G.in_degree(v) != G.out_degree(v))
- # Determines which is the 'start' node (as opposed to the 'end')
- if G.out_degree(v1) > G.in_degree(v1):
- return v1
- else:
- return v2
- else:
- # In an undirected graph randomly choose one of the possibilities
- start = [v for v in G if G.degree(v) % 2 != 0][0]
- return start
- def _simplegraph_eulerian_circuit(G, source):
- if G.is_directed():
- degree = G.out_degree
- edges = G.out_edges
- else:
- degree = G.degree
- edges = G.edges
- vertex_stack = [source]
- last_vertex = None
- while vertex_stack:
- current_vertex = vertex_stack[-1]
- if degree(current_vertex) == 0:
- if last_vertex is not None:
- yield (last_vertex, current_vertex)
- last_vertex = current_vertex
- vertex_stack.pop()
- else:
- _, next_vertex = arbitrary_element(edges(current_vertex))
- vertex_stack.append(next_vertex)
- G.remove_edge(current_vertex, next_vertex)
- def _multigraph_eulerian_circuit(G, source):
- if G.is_directed():
- degree = G.out_degree
- edges = G.out_edges
- else:
- degree = G.degree
- edges = G.edges
- vertex_stack = [(source, None)]
- last_vertex = None
- last_key = None
- while vertex_stack:
- current_vertex, current_key = vertex_stack[-1]
- if degree(current_vertex) == 0:
- if last_vertex is not None:
- yield (last_vertex, current_vertex, last_key)
- last_vertex, last_key = current_vertex, current_key
- vertex_stack.pop()
- else:
- triple = arbitrary_element(edges(current_vertex, keys=True))
- _, next_vertex, next_key = triple
- vertex_stack.append((next_vertex, next_key))
- G.remove_edge(current_vertex, next_vertex, next_key)
- def eulerian_circuit(G, source=None, keys=False):
- """Returns an iterator over the edges of an Eulerian circuit in `G`.
- An *Eulerian circuit* is a closed walk that includes each edge of a
- graph exactly once.
- Parameters
- ----------
- G : NetworkX graph
- A graph, either directed or undirected.
- source : node, optional
- Starting node for circuit.
- keys : bool
- If False, edges generated by this function will be of the form
- ``(u, v)``. Otherwise, edges will be of the form ``(u, v, k)``.
- This option is ignored unless `G` is a multigraph.
- Returns
- -------
- edges : iterator
- An iterator over edges in the Eulerian circuit.
- Raises
- ------
- NetworkXError
- If the graph is not Eulerian.
- See Also
- --------
- is_eulerian
- Notes
- -----
- This is a linear time implementation of an algorithm adapted from [1]_.
- For general information about Euler tours, see [2]_.
- References
- ----------
- .. [1] J. Edmonds, E. L. Johnson.
- Matching, Euler tours and the Chinese postman.
- Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
- .. [2] https://en.wikipedia.org/wiki/Eulerian_path
- Examples
- --------
- To get an Eulerian circuit in an undirected graph::
- >>> G = nx.complete_graph(3)
- >>> list(nx.eulerian_circuit(G))
- [(0, 2), (2, 1), (1, 0)]
- >>> list(nx.eulerian_circuit(G, source=1))
- [(1, 2), (2, 0), (0, 1)]
- To get the sequence of vertices in an Eulerian circuit::
- >>> [u for u, v in nx.eulerian_circuit(G)]
- [0, 2, 1]
- """
- if not is_eulerian(G):
- raise nx.NetworkXError("G is not Eulerian.")
- if G.is_directed():
- G = G.reverse()
- else:
- G = G.copy()
- if source is None:
- source = arbitrary_element(G)
- if G.is_multigraph():
- for u, v, k in _multigraph_eulerian_circuit(G, source):
- if keys:
- yield u, v, k
- else:
- yield u, v
- else:
- yield from _simplegraph_eulerian_circuit(G, source)
- def has_eulerian_path(G, source=None):
- """Return True iff `G` has an Eulerian path.
- An Eulerian path is a path in a graph which uses each edge of a graph
- exactly once. If `source` is specified, then this function checks
- whether an Eulerian path that starts at node `source` exists.
- A directed graph has an Eulerian path iff:
- - at most one vertex has out_degree - in_degree = 1,
- - at most one vertex has in_degree - out_degree = 1,
- - every other vertex has equal in_degree and out_degree,
- - and all of its vertices belong to a single connected
- component of the underlying undirected graph.
- If `source` is not None, an Eulerian path starting at `source` exists if no
- other node has out_degree - in_degree = 1. This is equivalent to either
- there exists an Eulerian circuit or `source` has out_degree - in_degree = 1
- and the conditions above hold.
- An undirected graph has an Eulerian path iff:
- - exactly zero or two vertices have odd degree,
- - and all of its vertices belong to a single connected component.
- If `source` is not None, an Eulerian path starting at `source` exists if
- either there exists an Eulerian circuit or `source` has an odd degree and the
- conditions above hold.
- Graphs with isolated vertices (i.e. vertices with zero degree) are not considered
- to have an Eulerian path. Therefore, if the graph is not connected (or not strongly
- connected, for directed graphs), this function returns False.
- Parameters
- ----------
- G : NetworkX Graph
- The graph to find an euler path in.
- source : node, optional
- Starting node for path.
- Returns
- -------
- Bool : True if G has an Eulerian path.
- Examples
- --------
- If you prefer to allow graphs with isolated vertices to have Eulerian path,
- you can first remove such vertices and then call `has_eulerian_path` as below example shows.
- >>> G = nx.Graph([(0, 1), (1, 2), (0, 2)])
- >>> G.add_node(3)
- >>> nx.has_eulerian_path(G)
- False
- >>> G.remove_nodes_from(list(nx.isolates(G)))
- >>> nx.has_eulerian_path(G)
- True
- See Also
- --------
- is_eulerian
- eulerian_path
- """
- if nx.is_eulerian(G):
- return True
- if G.is_directed():
- ins = G.in_degree
- outs = G.out_degree
- # Since we know it is not eulerian, outs - ins must be 1 for source
- if source is not None and outs[source] - ins[source] != 1:
- return False
- unbalanced_ins = 0
- unbalanced_outs = 0
- for v in G:
- if ins[v] - outs[v] == 1:
- unbalanced_ins += 1
- elif outs[v] - ins[v] == 1:
- unbalanced_outs += 1
- elif ins[v] != outs[v]:
- return False
- return (
- unbalanced_ins <= 1 and unbalanced_outs <= 1 and nx.is_weakly_connected(G)
- )
- else:
- # We know it is not eulerian, so degree of source must be odd.
- if source is not None and G.degree[source] % 2 != 1:
- return False
- # Sum is 2 since we know it is not eulerian (which implies sum is 0)
- return sum(d % 2 == 1 for v, d in G.degree()) == 2 and nx.is_connected(G)
- def eulerian_path(G, source=None, keys=False):
- """Return an iterator over the edges of an Eulerian path in `G`.
- Parameters
- ----------
- G : NetworkX Graph
- The graph in which to look for an eulerian path.
- source : node or None (default: None)
- The node at which to start the search. None means search over all
- starting nodes.
- keys : Bool (default: False)
- Indicates whether to yield edge 3-tuples (u, v, edge_key).
- The default yields edge 2-tuples
- Yields
- ------
- Edge tuples along the eulerian path.
- Warning: If `source` provided is not the start node of an Euler path
- will raise error even if an Euler Path exists.
- """
- if not has_eulerian_path(G, source):
- raise nx.NetworkXError("Graph has no Eulerian paths.")
- if G.is_directed():
- G = G.reverse()
- if source is None or nx.is_eulerian(G) is False:
- source = _find_path_start(G)
- if G.is_multigraph():
- for u, v, k in _multigraph_eulerian_circuit(G, source):
- if keys:
- yield u, v, k
- else:
- yield u, v
- else:
- yield from _simplegraph_eulerian_circuit(G, source)
- else:
- G = G.copy()
- if source is None:
- source = _find_path_start(G)
- if G.is_multigraph():
- if keys:
- yield from reversed(
- [(v, u, k) for u, v, k in _multigraph_eulerian_circuit(G, source)]
- )
- else:
- yield from reversed(
- [(v, u) for u, v, k in _multigraph_eulerian_circuit(G, source)]
- )
- else:
- yield from reversed(
- [(v, u) for u, v in _simplegraph_eulerian_circuit(G, source)]
- )
- @not_implemented_for("directed")
- def eulerize(G):
- """Transforms a graph into an Eulerian graph.
- If `G` is Eulerian the result is `G` as a MultiGraph, otherwise the result is a smallest
- (in terms of the number of edges) multigraph whose underlying simple graph is `G`.
- Parameters
- ----------
- G : NetworkX graph
- An undirected graph
- Returns
- -------
- G : NetworkX multigraph
- Raises
- ------
- NetworkXError
- If the graph is not connected.
- See Also
- --------
- is_eulerian
- eulerian_circuit
- References
- ----------
- .. [1] J. Edmonds, E. L. Johnson.
- Matching, Euler tours and the Chinese postman.
- Mathematical programming, Volume 5, Issue 1 (1973), 111-114.
- .. [2] https://en.wikipedia.org/wiki/Eulerian_path
- .. [3] http://web.math.princeton.edu/math_alive/5/Notes1.pdf
- Examples
- --------
- >>> G = nx.complete_graph(10)
- >>> H = nx.eulerize(G)
- >>> nx.is_eulerian(H)
- True
- """
- if G.order() == 0:
- raise nx.NetworkXPointlessConcept("Cannot Eulerize null graph")
- if not nx.is_connected(G):
- raise nx.NetworkXError("G is not connected")
- odd_degree_nodes = [n for n, d in G.degree() if d % 2 == 1]
- G = nx.MultiGraph(G)
- if len(odd_degree_nodes) == 0:
- return G
- # get all shortest paths between vertices of odd degree
- odd_deg_pairs_paths = [
- (m, {n: nx.shortest_path(G, source=m, target=n)})
- for m, n in combinations(odd_degree_nodes, 2)
- ]
- # use the number of vertices in a graph + 1 as an upper bound on
- # the maximum length of a path in G
- upper_bound_on_max_path_length = len(G) + 1
- # use "len(G) + 1 - len(P)",
- # where P is a shortest path between vertices n and m,
- # as edge-weights in a new graph
- # store the paths in the graph for easy indexing later
- Gp = nx.Graph()
- for n, Ps in odd_deg_pairs_paths:
- for m, P in Ps.items():
- if n != m:
- Gp.add_edge(
- m, n, weight=upper_bound_on_max_path_length - len(P), path=P
- )
- # find the minimum weight matching of edges in the weighted graph
- best_matching = nx.Graph(list(nx.max_weight_matching(Gp)))
- # duplicate each edge along each path in the set of paths in Gp
- for m, n in best_matching.edges():
- path = Gp[m][n]["path"]
- G.add_edges_from(nx.utils.pairwise(path))
- return G
|