123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398 |
- """Functions for encoding and decoding trees.
- Since a tree is a highly restricted form of graph, it can be represented
- concisely in several ways. This module includes functions for encoding
- and decoding trees in the form of nested tuples and Prüfer
- sequences. The former requires a rooted tree, whereas the latter can be
- applied to unrooted trees. Furthermore, there is a bijection from Prüfer
- sequences to labeled trees.
- """
- from collections import Counter
- from itertools import chain
- import networkx as nx
- from networkx.utils import not_implemented_for
- __all__ = [
- "from_nested_tuple",
- "from_prufer_sequence",
- "NotATree",
- "to_nested_tuple",
- "to_prufer_sequence",
- ]
- class NotATree(nx.NetworkXException):
- """Raised when a function expects a tree (that is, a connected
- undirected graph with no cycles) but gets a non-tree graph as input
- instead.
- """
- @not_implemented_for("directed")
- def to_nested_tuple(T, root, canonical_form=False):
- """Returns a nested tuple representation of the given tree.
- The nested tuple representation of a tree is defined
- recursively. The tree with one node and no edges is represented by
- the empty tuple, ``()``. A tree with ``k`` subtrees is represented
- by a tuple of length ``k`` in which each element is the nested tuple
- representation of a subtree.
- Parameters
- ----------
- T : NetworkX graph
- An undirected graph object representing a tree.
- root : node
- The node in ``T`` to interpret as the root of the tree.
- canonical_form : bool
- If ``True``, each tuple is sorted so that the function returns
- a canonical form for rooted trees. This means "lighter" subtrees
- will appear as nested tuples before "heavier" subtrees. In this
- way, each isomorphic rooted tree has the same nested tuple
- representation.
- Returns
- -------
- tuple
- A nested tuple representation of the tree.
- Notes
- -----
- This function is *not* the inverse of :func:`from_nested_tuple`; the
- only guarantee is that the rooted trees are isomorphic.
- See also
- --------
- from_nested_tuple
- to_prufer_sequence
- Examples
- --------
- The tree need not be a balanced binary tree::
- >>> T = nx.Graph()
- >>> T.add_edges_from([(0, 1), (0, 2), (0, 3)])
- >>> T.add_edges_from([(1, 4), (1, 5)])
- >>> T.add_edges_from([(3, 6), (3, 7)])
- >>> root = 0
- >>> nx.to_nested_tuple(T, root)
- (((), ()), (), ((), ()))
- Continuing the above example, if ``canonical_form`` is ``True``, the
- nested tuples will be sorted::
- >>> nx.to_nested_tuple(T, root, canonical_form=True)
- ((), ((), ()), ((), ()))
- Even the path graph can be interpreted as a tree::
- >>> T = nx.path_graph(4)
- >>> root = 0
- >>> nx.to_nested_tuple(T, root)
- ((((),),),)
- """
- def _make_tuple(T, root, _parent):
- """Recursively compute the nested tuple representation of the
- given rooted tree.
- ``_parent`` is the parent node of ``root`` in the supertree in
- which ``T`` is a subtree, or ``None`` if ``root`` is the root of
- the supertree. This argument is used to determine which
- neighbors of ``root`` are children and which is the parent.
- """
- # Get the neighbors of `root` that are not the parent node. We
- # are guaranteed that `root` is always in `T` by construction.
- children = set(T[root]) - {_parent}
- if len(children) == 0:
- return ()
- nested = (_make_tuple(T, v, root) for v in children)
- if canonical_form:
- nested = sorted(nested)
- return tuple(nested)
- # Do some sanity checks on the input.
- if not nx.is_tree(T):
- raise nx.NotATree("provided graph is not a tree")
- if root not in T:
- raise nx.NodeNotFound(f"Graph {T} contains no node {root}")
- return _make_tuple(T, root, None)
- def from_nested_tuple(sequence, sensible_relabeling=False):
- """Returns the rooted tree corresponding to the given nested tuple.
- The nested tuple representation of a tree is defined
- recursively. The tree with one node and no edges is represented by
- the empty tuple, ``()``. A tree with ``k`` subtrees is represented
- by a tuple of length ``k`` in which each element is the nested tuple
- representation of a subtree.
- Parameters
- ----------
- sequence : tuple
- A nested tuple representing a rooted tree.
- sensible_relabeling : bool
- Whether to relabel the nodes of the tree so that nodes are
- labeled in increasing order according to their breadth-first
- search order from the root node.
- Returns
- -------
- NetworkX graph
- The tree corresponding to the given nested tuple, whose root
- node is node 0. If ``sensible_labeling`` is ``True``, nodes will
- be labeled in breadth-first search order starting from the root
- node.
- Notes
- -----
- This function is *not* the inverse of :func:`to_nested_tuple`; the
- only guarantee is that the rooted trees are isomorphic.
- See also
- --------
- to_nested_tuple
- from_prufer_sequence
- Examples
- --------
- Sensible relabeling ensures that the nodes are labeled from the root
- starting at 0::
- >>> balanced = (((), ()), ((), ()))
- >>> T = nx.from_nested_tuple(balanced, sensible_relabeling=True)
- >>> edges = [(0, 1), (0, 2), (1, 3), (1, 4), (2, 5), (2, 6)]
- >>> all((u, v) in T.edges() or (v, u) in T.edges() for (u, v) in edges)
- True
- """
- def _make_tree(sequence):
- """Recursively creates a tree from the given sequence of nested
- tuples.
- This function employs the :func:`~networkx.tree.join` function
- to recursively join subtrees into a larger tree.
- """
- # The empty sequence represents the empty tree, which is the
- # (unique) graph with a single node. We mark the single node
- # with an attribute that indicates that it is the root of the
- # graph.
- if len(sequence) == 0:
- return nx.empty_graph(1)
- # For a nonempty sequence, get the subtrees for each child
- # sequence and join all the subtrees at their roots. After
- # joining the subtrees, the root is node 0.
- return nx.tree.join([(_make_tree(child), 0) for child in sequence])
- # Make the tree and remove the `is_root` node attribute added by the
- # helper function.
- T = _make_tree(sequence)
- if sensible_relabeling:
- # Relabel the nodes according to their breadth-first search
- # order, starting from the root node (that is, the node 0).
- bfs_nodes = chain([0], (v for u, v in nx.bfs_edges(T, 0)))
- labels = {v: i for i, v in enumerate(bfs_nodes)}
- # We would like to use `copy=False`, but `relabel_nodes` doesn't
- # allow a relabel mapping that can't be topologically sorted.
- T = nx.relabel_nodes(T, labels)
- return T
- @not_implemented_for("directed")
- def to_prufer_sequence(T):
- r"""Returns the Prüfer sequence of the given tree.
- A *Prüfer sequence* is a list of *n* - 2 numbers between 0 and
- *n* - 1, inclusive. The tree corresponding to a given Prüfer
- sequence can be recovered by repeatedly joining a node in the
- sequence with a node with the smallest potential degree according to
- the sequence.
- Parameters
- ----------
- T : NetworkX graph
- An undirected graph object representing a tree.
- Returns
- -------
- list
- The Prüfer sequence of the given tree.
- Raises
- ------
- NetworkXPointlessConcept
- If the number of nodes in `T` is less than two.
- NotATree
- If `T` is not a tree.
- KeyError
- If the set of nodes in `T` is not {0, …, *n* - 1}.
- Notes
- -----
- There is a bijection from labeled trees to Prüfer sequences. This
- function is the inverse of the :func:`from_prufer_sequence`
- function.
- Sometimes Prüfer sequences use nodes labeled from 1 to *n* instead
- of from 0 to *n* - 1. This function requires nodes to be labeled in
- the latter form. You can use :func:`~networkx.relabel_nodes` to
- relabel the nodes of your tree to the appropriate format.
- This implementation is from [1]_ and has a running time of
- $O(n)$.
- See also
- --------
- to_nested_tuple
- from_prufer_sequence
- References
- ----------
- .. [1] Wang, Xiaodong, Lei Wang, and Yingjie Wu.
- "An optimal algorithm for Prufer codes."
- *Journal of Software Engineering and Applications* 2.02 (2009): 111.
- <https://doi.org/10.4236/jsea.2009.22016>
- Examples
- --------
- There is a bijection between Prüfer sequences and labeled trees, so
- this function is the inverse of the :func:`from_prufer_sequence`
- function:
- >>> edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]
- >>> tree = nx.Graph(edges)
- >>> sequence = nx.to_prufer_sequence(tree)
- >>> sequence
- [3, 3, 3, 4]
- >>> tree2 = nx.from_prufer_sequence(sequence)
- >>> list(tree2.edges()) == edges
- True
- """
- # Perform some sanity checks on the input.
- n = len(T)
- if n < 2:
- msg = "Prüfer sequence undefined for trees with fewer than two nodes"
- raise nx.NetworkXPointlessConcept(msg)
- if not nx.is_tree(T):
- raise nx.NotATree("provided graph is not a tree")
- if set(T) != set(range(n)):
- raise KeyError("tree must have node labels {0, ..., n - 1}")
- degree = dict(T.degree())
- def parents(u):
- return next(v for v in T[u] if degree[v] > 1)
- index = u = next(k for k in range(n) if degree[k] == 1)
- result = []
- for i in range(n - 2):
- v = parents(u)
- result.append(v)
- degree[v] -= 1
- if v < index and degree[v] == 1:
- u = v
- else:
- index = u = next(k for k in range(index + 1, n) if degree[k] == 1)
- return result
- def from_prufer_sequence(sequence):
- r"""Returns the tree corresponding to the given Prüfer sequence.
- A *Prüfer sequence* is a list of *n* - 2 numbers between 0 and
- *n* - 1, inclusive. The tree corresponding to a given Prüfer
- sequence can be recovered by repeatedly joining a node in the
- sequence with a node with the smallest potential degree according to
- the sequence.
- Parameters
- ----------
- sequence : list
- A Prüfer sequence, which is a list of *n* - 2 integers between
- zero and *n* - 1, inclusive.
- Returns
- -------
- NetworkX graph
- The tree corresponding to the given Prüfer sequence.
- Notes
- -----
- There is a bijection from labeled trees to Prüfer sequences. This
- function is the inverse of the :func:`from_prufer_sequence` function.
- Sometimes Prüfer sequences use nodes labeled from 1 to *n* instead
- of from 0 to *n* - 1. This function requires nodes to be labeled in
- the latter form. You can use :func:`networkx.relabel_nodes` to
- relabel the nodes of your tree to the appropriate format.
- This implementation is from [1]_ and has a running time of
- $O(n)$.
- References
- ----------
- .. [1] Wang, Xiaodong, Lei Wang, and Yingjie Wu.
- "An optimal algorithm for Prufer codes."
- *Journal of Software Engineering and Applications* 2.02 (2009): 111.
- <https://doi.org/10.4236/jsea.2009.22016>
- See also
- --------
- from_nested_tuple
- to_prufer_sequence
- Examples
- --------
- There is a bijection between Prüfer sequences and labeled trees, so
- this function is the inverse of the :func:`to_prufer_sequence`
- function:
- >>> edges = [(0, 3), (1, 3), (2, 3), (3, 4), (4, 5)]
- >>> tree = nx.Graph(edges)
- >>> sequence = nx.to_prufer_sequence(tree)
- >>> sequence
- [3, 3, 3, 4]
- >>> tree2 = nx.from_prufer_sequence(sequence)
- >>> list(tree2.edges()) == edges
- True
- """
- n = len(sequence) + 2
- # `degree` stores the remaining degree (plus one) for each node. The
- # degree of a node in the decoded tree is one more than the number
- # of times it appears in the code.
- degree = Counter(chain(sequence, range(n)))
- T = nx.empty_graph(n)
- # `not_orphaned` is the set of nodes that have a parent in the
- # tree. After the loop, there should be exactly two nodes that are
- # not in this set.
- not_orphaned = set()
- index = u = next(k for k in range(n) if degree[k] == 1)
- for v in sequence:
- T.add_edge(u, v)
- not_orphaned.add(u)
- degree[v] -= 1
- if v < index and degree[v] == 1:
- u = v
- else:
- index = u = next(k for k in range(index + 1, n) if degree[k] == 1)
- # At this point, there must be exactly two orphaned nodes; join them.
- orphans = set(T) - not_orphaned
- u, v = orphans
- T.add_edge(u, v)
- return T
|