treewidth.py 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """Functions for computing treewidth decomposition.
  2. Treewidth of an undirected graph is a number associated with the graph.
  3. It can be defined as the size of the largest vertex set (bag) in a tree
  4. decomposition of the graph minus one.
  5. `Wikipedia: Treewidth <https://en.wikipedia.org/wiki/Treewidth>`_
  6. The notions of treewidth and tree decomposition have gained their
  7. attractiveness partly because many graph and network problems that are
  8. intractable (e.g., NP-hard) on arbitrary graphs become efficiently
  9. solvable (e.g., with a linear time algorithm) when the treewidth of the
  10. input graphs is bounded by a constant [1]_ [2]_.
  11. There are two different functions for computing a tree decomposition:
  12. :func:`treewidth_min_degree` and :func:`treewidth_min_fill_in`.
  13. .. [1] Hans L. Bodlaender and Arie M. C. A. Koster. 2010. "Treewidth
  14. computations I.Upper bounds". Inf. Comput. 208, 3 (March 2010),259-275.
  15. http://dx.doi.org/10.1016/j.ic.2009.03.008
  16. .. [2] Hans L. Bodlaender. "Discovering Treewidth". Institute of Information
  17. and Computing Sciences, Utrecht University.
  18. Technical Report UU-CS-2005-018.
  19. http://www.cs.uu.nl
  20. .. [3] K. Wang, Z. Lu, and J. Hicks *Treewidth*.
  21. https://web.archive.org/web/20210507025929/http://web.eecs.utk.edu/~cphill25/cs594_spring2015_projects/treewidth.pdf
  22. """
  23. import itertools
  24. import sys
  25. from heapq import heapify, heappop, heappush
  26. import networkx as nx
  27. from networkx.utils import not_implemented_for
  28. __all__ = ["treewidth_min_degree", "treewidth_min_fill_in"]
  29. @not_implemented_for("directed")
  30. @not_implemented_for("multigraph")
  31. def treewidth_min_degree(G):
  32. """Returns a treewidth decomposition using the Minimum Degree heuristic.
  33. The heuristic chooses the nodes according to their degree, i.e., first
  34. the node with the lowest degree is chosen, then the graph is updated
  35. and the corresponding node is removed. Next, a new node with the lowest
  36. degree is chosen, and so on.
  37. Parameters
  38. ----------
  39. G : NetworkX graph
  40. Returns
  41. -------
  42. Treewidth decomposition : (int, Graph) tuple
  43. 2-tuple with treewidth and the corresponding decomposed tree.
  44. """
  45. deg_heuristic = MinDegreeHeuristic(G)
  46. return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph))
  47. @not_implemented_for("directed")
  48. @not_implemented_for("multigraph")
  49. def treewidth_min_fill_in(G):
  50. """Returns a treewidth decomposition using the Minimum Fill-in heuristic.
  51. The heuristic chooses a node from the graph, where the number of edges
  52. added turning the neighbourhood of the chosen node into clique is as
  53. small as possible.
  54. Parameters
  55. ----------
  56. G : NetworkX graph
  57. Returns
  58. -------
  59. Treewidth decomposition : (int, Graph) tuple
  60. 2-tuple with treewidth and the corresponding decomposed tree.
  61. """
  62. return treewidth_decomp(G, min_fill_in_heuristic)
  63. class MinDegreeHeuristic:
  64. """Implements the Minimum Degree heuristic.
  65. The heuristic chooses the nodes according to their degree
  66. (number of neighbours), i.e., first the node with the lowest degree is
  67. chosen, then the graph is updated and the corresponding node is
  68. removed. Next, a new node with the lowest degree is chosen, and so on.
  69. """
  70. def __init__(self, graph):
  71. self._graph = graph
  72. # nodes that have to be updated in the heap before each iteration
  73. self._update_nodes = []
  74. self._degreeq = [] # a heapq with 3-tuples (degree,unique_id,node)
  75. self.count = itertools.count()
  76. # build heap with initial degrees
  77. for n in graph:
  78. self._degreeq.append((len(graph[n]), next(self.count), n))
  79. heapify(self._degreeq)
  80. def best_node(self, graph):
  81. # update nodes in self._update_nodes
  82. for n in self._update_nodes:
  83. # insert changed degrees into degreeq
  84. heappush(self._degreeq, (len(graph[n]), next(self.count), n))
  85. # get the next valid (minimum degree) node
  86. while self._degreeq:
  87. (min_degree, _, elim_node) = heappop(self._degreeq)
  88. if elim_node not in graph or len(graph[elim_node]) != min_degree:
  89. # outdated entry in degreeq
  90. continue
  91. elif min_degree == len(graph) - 1:
  92. # fully connected: abort condition
  93. return None
  94. # remember to update nodes in the heap before getting the next node
  95. self._update_nodes = graph[elim_node]
  96. return elim_node
  97. # the heap is empty: abort
  98. return None
  99. def min_fill_in_heuristic(graph):
  100. """Implements the Minimum Degree heuristic.
  101. Returns the node from the graph, where the number of edges added when
  102. turning the neighbourhood of the chosen node into clique is as small as
  103. possible. This algorithm chooses the nodes using the Minimum Fill-In
  104. heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses
  105. additional constant memory."""
  106. if len(graph) == 0:
  107. return None
  108. min_fill_in_node = None
  109. min_fill_in = sys.maxsize
  110. # sort nodes by degree
  111. nodes_by_degree = sorted(graph, key=lambda x: len(graph[x]))
  112. min_degree = len(graph[nodes_by_degree[0]])
  113. # abort condition (handle complete graph)
  114. if min_degree == len(graph) - 1:
  115. return None
  116. for node in nodes_by_degree:
  117. num_fill_in = 0
  118. nbrs = graph[node]
  119. for nbr in nbrs:
  120. # count how many nodes in nbrs current nbr is not connected to
  121. # subtract 1 for the node itself
  122. num_fill_in += len(nbrs - graph[nbr]) - 1
  123. if num_fill_in >= 2 * min_fill_in:
  124. break
  125. num_fill_in /= 2 # divide by 2 because of double counting
  126. if num_fill_in < min_fill_in: # update min-fill-in node
  127. if num_fill_in == 0:
  128. return node
  129. min_fill_in = num_fill_in
  130. min_fill_in_node = node
  131. return min_fill_in_node
  132. def treewidth_decomp(G, heuristic=min_fill_in_heuristic):
  133. """Returns a treewidth decomposition using the passed heuristic.
  134. Parameters
  135. ----------
  136. G : NetworkX graph
  137. heuristic : heuristic function
  138. Returns
  139. -------
  140. Treewidth decomposition : (int, Graph) tuple
  141. 2-tuple with treewidth and the corresponding decomposed tree.
  142. """
  143. # make dict-of-sets structure
  144. graph = {n: set(G[n]) - {n} for n in G}
  145. # stack containing nodes and neighbors in the order from the heuristic
  146. node_stack = []
  147. # get first node from heuristic
  148. elim_node = heuristic(graph)
  149. while elim_node is not None:
  150. # connect all neighbours with each other
  151. nbrs = graph[elim_node]
  152. for u, v in itertools.permutations(nbrs, 2):
  153. if v not in graph[u]:
  154. graph[u].add(v)
  155. # push node and its current neighbors on stack
  156. node_stack.append((elim_node, nbrs))
  157. # remove node from graph
  158. for u in graph[elim_node]:
  159. graph[u].remove(elim_node)
  160. del graph[elim_node]
  161. elim_node = heuristic(graph)
  162. # the abort condition is met; put all remaining nodes into one bag
  163. decomp = nx.Graph()
  164. first_bag = frozenset(graph.keys())
  165. decomp.add_node(first_bag)
  166. treewidth = len(first_bag) - 1
  167. while node_stack:
  168. # get node and its neighbors from the stack
  169. (curr_node, nbrs) = node_stack.pop()
  170. # find a bag all neighbors are in
  171. old_bag = None
  172. for bag in decomp.nodes:
  173. if nbrs <= bag:
  174. old_bag = bag
  175. break
  176. if old_bag is None:
  177. # no old_bag was found: just connect to the first_bag
  178. old_bag = first_bag
  179. # create new node for decomposition
  180. nbrs.add(curr_node)
  181. new_bag = frozenset(nbrs)
  182. # update treewidth
  183. treewidth = max(treewidth, len(new_bag) - 1)
  184. # add edge to decomposition (implicitly also adds the new node)
  185. decomp.add_edge(old_bag, new_bag)
  186. return treewidth, decomp