nonisomorphic_trees.py 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. """
  2. Implementation of the Wright, Richmond, Odlyzko and McKay (WROM)
  3. algorithm for the enumeration of all non-isomorphic free trees of a
  4. given order. Rooted trees are represented by level sequences, i.e.,
  5. lists in which the i-th element specifies the distance of vertex i to
  6. the root.
  7. """
  8. __all__ = ["nonisomorphic_trees", "number_of_nonisomorphic_trees"]
  9. import networkx as nx
  10. def nonisomorphic_trees(order, create="graph"):
  11. """Returns a list of nonisomporphic trees
  12. Parameters
  13. ----------
  14. order : int
  15. order of the desired tree(s)
  16. create : graph or matrix (default="Graph)
  17. If graph is selected a list of trees will be returned,
  18. if matrix is selected a list of adjancency matrix will
  19. be returned
  20. Returns
  21. -------
  22. G : List of NetworkX Graphs
  23. M : List of Adjacency matrices
  24. References
  25. ----------
  26. """
  27. if order < 2:
  28. raise ValueError
  29. # start at the path graph rooted at its center
  30. layout = list(range(order // 2 + 1)) + list(range(1, (order + 1) // 2))
  31. while layout is not None:
  32. layout = _next_tree(layout)
  33. if layout is not None:
  34. if create == "graph":
  35. yield _layout_to_graph(layout)
  36. elif create == "matrix":
  37. yield _layout_to_matrix(layout)
  38. layout = _next_rooted_tree(layout)
  39. def number_of_nonisomorphic_trees(order):
  40. """Returns the number of nonisomorphic trees
  41. Parameters
  42. ----------
  43. order : int
  44. order of the desired tree(s)
  45. Returns
  46. -------
  47. length : Number of nonisomorphic graphs for the given order
  48. References
  49. ----------
  50. """
  51. return sum(1 for _ in nonisomorphic_trees(order))
  52. def _next_rooted_tree(predecessor, p=None):
  53. """One iteration of the Beyer-Hedetniemi algorithm."""
  54. if p is None:
  55. p = len(predecessor) - 1
  56. while predecessor[p] == 1:
  57. p -= 1
  58. if p == 0:
  59. return None
  60. q = p - 1
  61. while predecessor[q] != predecessor[p] - 1:
  62. q -= 1
  63. result = list(predecessor)
  64. for i in range(p, len(result)):
  65. result[i] = result[i - p + q]
  66. return result
  67. def _next_tree(candidate):
  68. """One iteration of the Wright, Richmond, Odlyzko and McKay
  69. algorithm."""
  70. # valid representation of a free tree if:
  71. # there are at least two vertices at layer 1
  72. # (this is always the case because we start at the path graph)
  73. left, rest = _split_tree(candidate)
  74. # and the left subtree of the root
  75. # is less high than the tree with the left subtree removed
  76. left_height = max(left)
  77. rest_height = max(rest)
  78. valid = rest_height >= left_height
  79. if valid and rest_height == left_height:
  80. # and, if left and rest are of the same height,
  81. # if left does not encompass more vertices
  82. if len(left) > len(rest):
  83. valid = False
  84. # and, if they have the same number or vertices,
  85. # if left does not come after rest lexicographically
  86. elif len(left) == len(rest) and left > rest:
  87. valid = False
  88. if valid:
  89. return candidate
  90. else:
  91. # jump to the next valid free tree
  92. p = len(left)
  93. new_candidate = _next_rooted_tree(candidate, p)
  94. if candidate[p] > 2:
  95. new_left, new_rest = _split_tree(new_candidate)
  96. new_left_height = max(new_left)
  97. suffix = range(1, new_left_height + 2)
  98. new_candidate[-len(suffix) :] = suffix
  99. return new_candidate
  100. def _split_tree(layout):
  101. """Returns a tuple of two layouts, one containing the left
  102. subtree of the root vertex, and one containing the original tree
  103. with the left subtree removed."""
  104. one_found = False
  105. m = None
  106. for i in range(len(layout)):
  107. if layout[i] == 1:
  108. if one_found:
  109. m = i
  110. break
  111. else:
  112. one_found = True
  113. if m is None:
  114. m = len(layout)
  115. left = [layout[i] - 1 for i in range(1, m)]
  116. rest = [0] + [layout[i] for i in range(m, len(layout))]
  117. return (left, rest)
  118. def _layout_to_matrix(layout):
  119. """Create the adjacency matrix for the tree specified by the
  120. given layout (level sequence)."""
  121. result = [[0] * len(layout) for i in range(len(layout))]
  122. stack = []
  123. for i in range(len(layout)):
  124. i_level = layout[i]
  125. if stack:
  126. j = stack[-1]
  127. j_level = layout[j]
  128. while j_level >= i_level:
  129. stack.pop()
  130. j = stack[-1]
  131. j_level = layout[j]
  132. result[i][j] = result[j][i] = 1
  133. stack.append(i)
  134. return result
  135. def _layout_to_graph(layout):
  136. """Create a NetworkX Graph for the tree specified by the
  137. given layout(level sequence)"""
  138. G = nx.Graph()
  139. stack = []
  140. for i in range(len(layout)):
  141. i_level = layout[i]
  142. if stack:
  143. j = stack[-1]
  144. j_level = layout[j]
  145. while j_level >= i_level:
  146. stack.pop()
  147. j = stack[-1]
  148. j_level = layout[j]
  149. G.add_edge(i, j)
  150. stack.append(i)
  151. return G