treewidth.py 8.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255
  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. @nx._dispatchable(returns_graph=True)
  32. def treewidth_min_degree(G):
  33. """Returns a treewidth decomposition using the Minimum Degree heuristic.
  34. The heuristic chooses the nodes according to their degree, i.e., first
  35. the node with the lowest degree is chosen, then the graph is updated
  36. and the corresponding node is removed. Next, a new node with the lowest
  37. degree is chosen, and so on.
  38. Parameters
  39. ----------
  40. G : NetworkX graph
  41. Returns
  42. -------
  43. Treewidth decomposition : (int, Graph) tuple
  44. 2-tuple with treewidth and the corresponding decomposed tree.
  45. """
  46. deg_heuristic = MinDegreeHeuristic(G)
  47. return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph))
  48. @not_implemented_for("directed")
  49. @not_implemented_for("multigraph")
  50. @nx._dispatchable(returns_graph=True)
  51. def treewidth_min_fill_in(G):
  52. """Returns a treewidth decomposition using the Minimum Fill-in heuristic.
  53. The heuristic chooses a node from the graph, where the number of edges
  54. added turning the neighborhood of the chosen node into clique is as
  55. small as possible.
  56. Parameters
  57. ----------
  58. G : NetworkX graph
  59. Returns
  60. -------
  61. Treewidth decomposition : (int, Graph) tuple
  62. 2-tuple with treewidth and the corresponding decomposed tree.
  63. """
  64. return treewidth_decomp(G, min_fill_in_heuristic)
  65. class MinDegreeHeuristic:
  66. """Implements the Minimum Degree heuristic.
  67. The heuristic chooses the nodes according to their degree
  68. (number of neighbors), i.e., first the node with the lowest degree is
  69. chosen, then the graph is updated and the corresponding node is
  70. removed. Next, a new node with the lowest degree is chosen, and so on.
  71. """
  72. def __init__(self, graph):
  73. self._graph = graph
  74. # nodes that have to be updated in the heap before each iteration
  75. self._update_nodes = []
  76. self._degreeq = [] # a heapq with 3-tuples (degree,unique_id,node)
  77. self.count = itertools.count()
  78. # build heap with initial degrees
  79. for n in graph:
  80. self._degreeq.append((len(graph[n]), next(self.count), n))
  81. heapify(self._degreeq)
  82. def best_node(self, graph):
  83. # update nodes in self._update_nodes
  84. for n in self._update_nodes:
  85. # insert changed degrees into degreeq
  86. heappush(self._degreeq, (len(graph[n]), next(self.count), n))
  87. # get the next valid (minimum degree) node
  88. while self._degreeq:
  89. (min_degree, _, elim_node) = heappop(self._degreeq)
  90. if elim_node not in graph or len(graph[elim_node]) != min_degree:
  91. # outdated entry in degreeq
  92. continue
  93. elif min_degree == len(graph) - 1:
  94. # fully connected: abort condition
  95. return None
  96. # remember to update nodes in the heap before getting the next node
  97. self._update_nodes = graph[elim_node]
  98. return elim_node
  99. # the heap is empty: abort
  100. return None
  101. def min_fill_in_heuristic(graph_dict):
  102. """Implements the Minimum Degree heuristic.
  103. graph_dict: dict keyed by node to sets of neighbors (no self-loops)
  104. Returns the node from the graph, where the number of edges added when
  105. turning the neighborhood of the chosen node into clique is as small as
  106. possible. This algorithm chooses the nodes using the Minimum Fill-In
  107. heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses
  108. additional constant memory.
  109. """
  110. if len(graph_dict) == 0:
  111. return None
  112. min_fill_in_node = None
  113. min_fill_in = sys.maxsize
  114. # sort nodes by degree
  115. nodes_by_degree = sorted(graph_dict, key=lambda x: len(graph_dict[x]))
  116. min_degree = len(graph_dict[nodes_by_degree[0]])
  117. # abort condition (handle complete graph)
  118. if min_degree == len(graph_dict) - 1:
  119. return None
  120. for node in nodes_by_degree:
  121. num_fill_in = 0
  122. nbrs = graph_dict[node]
  123. for nbr in nbrs:
  124. # count how many nodes in nbrs current nbr is not connected to
  125. # subtract 1 for the node itself
  126. num_fill_in += len(nbrs - graph_dict[nbr]) - 1
  127. if num_fill_in >= 2 * min_fill_in:
  128. break
  129. num_fill_in /= 2 # divide by 2 because of double counting
  130. if num_fill_in < min_fill_in: # update min-fill-in node
  131. if num_fill_in == 0:
  132. return node
  133. min_fill_in = num_fill_in
  134. min_fill_in_node = node
  135. return min_fill_in_node
  136. @nx._dispatchable(returns_graph=True)
  137. def treewidth_decomp(G, heuristic=min_fill_in_heuristic):
  138. """Returns a treewidth decomposition using the passed heuristic.
  139. Parameters
  140. ----------
  141. G : NetworkX graph
  142. heuristic : heuristic function
  143. Returns
  144. -------
  145. Treewidth decomposition : (int, Graph) tuple
  146. 2-tuple with treewidth and the corresponding decomposed tree.
  147. """
  148. # make dict-of-sets structure
  149. graph_dict = {n: set(G[n]) - {n} for n in G}
  150. # stack containing nodes and neighbors in the order from the heuristic
  151. node_stack = []
  152. # get first node from heuristic
  153. elim_node = heuristic(graph_dict)
  154. while elim_node is not None:
  155. # connect all neighbors with each other
  156. nbrs = graph_dict[elim_node]
  157. for u, v in itertools.permutations(nbrs, 2):
  158. if v not in graph_dict[u]:
  159. graph_dict[u].add(v)
  160. # push node and its current neighbors on stack
  161. node_stack.append((elim_node, nbrs))
  162. # remove node from graph_dict
  163. for u in graph_dict[elim_node]:
  164. graph_dict[u].remove(elim_node)
  165. del graph_dict[elim_node]
  166. elim_node = heuristic(graph_dict)
  167. # the abort condition is met; put all remaining nodes into one bag
  168. decomp = nx.Graph()
  169. first_bag = frozenset(graph_dict.keys())
  170. decomp.add_node(first_bag)
  171. treewidth = len(first_bag) - 1
  172. while node_stack:
  173. # get node and its neighbors from the stack
  174. (curr_node, nbrs) = node_stack.pop()
  175. # find a bag all neighbors are in
  176. old_bag = None
  177. for bag in decomp.nodes:
  178. if nbrs <= bag:
  179. old_bag = bag
  180. break
  181. if old_bag is None:
  182. # no old_bag was found: just connect to the first_bag
  183. old_bag = first_bag
  184. # create new node for decomposition
  185. nbrs.add(curr_node)
  186. new_bag = frozenset(nbrs)
  187. # update treewidth
  188. treewidth = max(treewidth, len(new_bag) - 1)
  189. # add edge to decomposition (implicitly also adds the new node)
  190. decomp.add_edge(old_bag, new_bag)
  191. return treewidth, decomp