Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/networkx/algorithms/approximation/treewidth.py: 21%

94 statements  

« prev     ^ index     » next       coverage.py v7.3.2, created at 2023-10-20 07:00 +0000

1"""Functions for computing treewidth decomposition. 

2 

3Treewidth of an undirected graph is a number associated with the graph. 

4It can be defined as the size of the largest vertex set (bag) in a tree 

5decomposition of the graph minus one. 

6 

7`Wikipedia: Treewidth <https://en.wikipedia.org/wiki/Treewidth>`_ 

8 

9The notions of treewidth and tree decomposition have gained their 

10attractiveness partly because many graph and network problems that are 

11intractable (e.g., NP-hard) on arbitrary graphs become efficiently 

12solvable (e.g., with a linear time algorithm) when the treewidth of the 

13input graphs is bounded by a constant [1]_ [2]_. 

14 

15There are two different functions for computing a tree decomposition: 

16:func:`treewidth_min_degree` and :func:`treewidth_min_fill_in`. 

17 

18.. [1] Hans L. Bodlaender and Arie M. C. A. Koster. 2010. "Treewidth 

19 computations I.Upper bounds". Inf. Comput. 208, 3 (March 2010),259-275. 

20 http://dx.doi.org/10.1016/j.ic.2009.03.008 

21 

22.. [2] Hans L. Bodlaender. "Discovering Treewidth". Institute of Information 

23 and Computing Sciences, Utrecht University. 

24 Technical Report UU-CS-2005-018. 

25 http://www.cs.uu.nl 

26 

27.. [3] K. Wang, Z. Lu, and J. Hicks *Treewidth*. 

28 https://web.archive.org/web/20210507025929/http://web.eecs.utk.edu/~cphill25/cs594_spring2015_projects/treewidth.pdf 

29 

30""" 

31 

32import itertools 

33import sys 

34from heapq import heapify, heappop, heappush 

35 

36import networkx as nx 

37from networkx.utils import not_implemented_for 

38 

39__all__ = ["treewidth_min_degree", "treewidth_min_fill_in"] 

40 

41 

42@not_implemented_for("directed") 

43@not_implemented_for("multigraph") 

44@nx._dispatch 

45def treewidth_min_degree(G): 

46 """Returns a treewidth decomposition using the Minimum Degree heuristic. 

47 

48 The heuristic chooses the nodes according to their degree, i.e., first 

49 the node with the lowest degree is chosen, then the graph is updated 

50 and the corresponding node is removed. Next, a new node with the lowest 

51 degree is chosen, and so on. 

52 

53 Parameters 

54 ---------- 

55 G : NetworkX graph 

56 

57 Returns 

58 ------- 

59 Treewidth decomposition : (int, Graph) tuple 

60 2-tuple with treewidth and the corresponding decomposed tree. 

61 """ 

62 deg_heuristic = MinDegreeHeuristic(G) 

63 return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph)) 

64 

65 

66@not_implemented_for("directed") 

67@not_implemented_for("multigraph") 

68@nx._dispatch 

69def treewidth_min_fill_in(G): 

70 """Returns a treewidth decomposition using the Minimum Fill-in heuristic. 

71 

72 The heuristic chooses a node from the graph, where the number of edges 

73 added turning the neighbourhood of the chosen node into clique is as 

74 small as possible. 

75 

76 Parameters 

77 ---------- 

78 G : NetworkX graph 

79 

80 Returns 

81 ------- 

82 Treewidth decomposition : (int, Graph) tuple 

83 2-tuple with treewidth and the corresponding decomposed tree. 

84 """ 

85 return treewidth_decomp(G, min_fill_in_heuristic) 

86 

87 

88class MinDegreeHeuristic: 

89 """Implements the Minimum Degree heuristic. 

90 

91 The heuristic chooses the nodes according to their degree 

92 (number of neighbours), i.e., first the node with the lowest degree is 

93 chosen, then the graph is updated and the corresponding node is 

94 removed. Next, a new node with the lowest degree is chosen, and so on. 

95 """ 

96 

97 def __init__(self, graph): 

98 self._graph = graph 

99 

100 # nodes that have to be updated in the heap before each iteration 

101 self._update_nodes = [] 

102 

103 self._degreeq = [] # a heapq with 3-tuples (degree,unique_id,node) 

104 self.count = itertools.count() 

105 

106 # build heap with initial degrees 

107 for n in graph: 

108 self._degreeq.append((len(graph[n]), next(self.count), n)) 

109 heapify(self._degreeq) 

110 

111 def best_node(self, graph): 

112 # update nodes in self._update_nodes 

113 for n in self._update_nodes: 

114 # insert changed degrees into degreeq 

115 heappush(self._degreeq, (len(graph[n]), next(self.count), n)) 

116 

117 # get the next valid (minimum degree) node 

118 while self._degreeq: 

119 (min_degree, _, elim_node) = heappop(self._degreeq) 

120 if elim_node not in graph or len(graph[elim_node]) != min_degree: 

121 # outdated entry in degreeq 

122 continue 

123 elif min_degree == len(graph) - 1: 

124 # fully connected: abort condition 

125 return None 

126 

127 # remember to update nodes in the heap before getting the next node 

128 self._update_nodes = graph[elim_node] 

129 return elim_node 

130 

131 # the heap is empty: abort 

132 return None 

133 

134 

135def min_fill_in_heuristic(graph): 

136 """Implements the Minimum Degree heuristic. 

137 

138 Returns the node from the graph, where the number of edges added when 

139 turning the neighbourhood of the chosen node into clique is as small as 

140 possible. This algorithm chooses the nodes using the Minimum Fill-In 

141 heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses 

142 additional constant memory.""" 

143 

144 if len(graph) == 0: 

145 return None 

146 

147 min_fill_in_node = None 

148 

149 min_fill_in = sys.maxsize 

150 

151 # sort nodes by degree 

152 nodes_by_degree = sorted(graph, key=lambda x: len(graph[x])) 

153 min_degree = len(graph[nodes_by_degree[0]]) 

154 

155 # abort condition (handle complete graph) 

156 if min_degree == len(graph) - 1: 

157 return None 

158 

159 for node in nodes_by_degree: 

160 num_fill_in = 0 

161 nbrs = graph[node] 

162 for nbr in nbrs: 

163 # count how many nodes in nbrs current nbr is not connected to 

164 # subtract 1 for the node itself 

165 num_fill_in += len(nbrs - graph[nbr]) - 1 

166 if num_fill_in >= 2 * min_fill_in: 

167 break 

168 

169 num_fill_in /= 2 # divide by 2 because of double counting 

170 

171 if num_fill_in < min_fill_in: # update min-fill-in node 

172 if num_fill_in == 0: 

173 return node 

174 min_fill_in = num_fill_in 

175 min_fill_in_node = node 

176 

177 return min_fill_in_node 

178 

179 

180@nx._dispatch 

181def treewidth_decomp(G, heuristic=min_fill_in_heuristic): 

182 """Returns a treewidth decomposition using the passed heuristic. 

183 

184 Parameters 

185 ---------- 

186 G : NetworkX graph 

187 heuristic : heuristic function 

188 

189 Returns 

190 ------- 

191 Treewidth decomposition : (int, Graph) tuple 

192 2-tuple with treewidth and the corresponding decomposed tree. 

193 """ 

194 

195 # make dict-of-sets structure 

196 graph = {n: set(G[n]) - {n} for n in G} 

197 

198 # stack containing nodes and neighbors in the order from the heuristic 

199 node_stack = [] 

200 

201 # get first node from heuristic 

202 elim_node = heuristic(graph) 

203 while elim_node is not None: 

204 # connect all neighbours with each other 

205 nbrs = graph[elim_node] 

206 for u, v in itertools.permutations(nbrs, 2): 

207 if v not in graph[u]: 

208 graph[u].add(v) 

209 

210 # push node and its current neighbors on stack 

211 node_stack.append((elim_node, nbrs)) 

212 

213 # remove node from graph 

214 for u in graph[elim_node]: 

215 graph[u].remove(elim_node) 

216 

217 del graph[elim_node] 

218 elim_node = heuristic(graph) 

219 

220 # the abort condition is met; put all remaining nodes into one bag 

221 decomp = nx.Graph() 

222 first_bag = frozenset(graph.keys()) 

223 decomp.add_node(first_bag) 

224 

225 treewidth = len(first_bag) - 1 

226 

227 while node_stack: 

228 # get node and its neighbors from the stack 

229 (curr_node, nbrs) = node_stack.pop() 

230 

231 # find a bag all neighbors are in 

232 old_bag = None 

233 for bag in decomp.nodes: 

234 if nbrs <= bag: 

235 old_bag = bag 

236 break 

237 

238 if old_bag is None: 

239 # no old_bag was found: just connect to the first_bag 

240 old_bag = first_bag 

241 

242 # create new node for decomposition 

243 nbrs.add(curr_node) 

244 new_bag = frozenset(nbrs) 

245 

246 # update treewidth 

247 treewidth = max(treewidth, len(new_bag) - 1) 

248 

249 # add edge to decomposition (implicitly also adds the new node) 

250 decomp.add_edge(old_bag, new_bag) 

251 

252 return treewidth, decomp