123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249 |
- """Functions for computing treewidth decomposition.
- Treewidth of an undirected graph is a number associated with the graph.
- It can be defined as the size of the largest vertex set (bag) in a tree
- decomposition of the graph minus one.
- `Wikipedia: Treewidth <https://en.wikipedia.org/wiki/Treewidth>`_
- The notions of treewidth and tree decomposition have gained their
- attractiveness partly because many graph and network problems that are
- intractable (e.g., NP-hard) on arbitrary graphs become efficiently
- solvable (e.g., with a linear time algorithm) when the treewidth of the
- input graphs is bounded by a constant [1]_ [2]_.
- There are two different functions for computing a tree decomposition:
- :func:`treewidth_min_degree` and :func:`treewidth_min_fill_in`.
- .. [1] Hans L. Bodlaender and Arie M. C. A. Koster. 2010. "Treewidth
- computations I.Upper bounds". Inf. Comput. 208, 3 (March 2010),259-275.
- http://dx.doi.org/10.1016/j.ic.2009.03.008
- .. [2] Hans L. Bodlaender. "Discovering Treewidth". Institute of Information
- and Computing Sciences, Utrecht University.
- Technical Report UU-CS-2005-018.
- http://www.cs.uu.nl
- .. [3] K. Wang, Z. Lu, and J. Hicks *Treewidth*.
- https://web.archive.org/web/20210507025929/http://web.eecs.utk.edu/~cphill25/cs594_spring2015_projects/treewidth.pdf
- """
- import itertools
- import sys
- from heapq import heapify, heappop, heappush
- import networkx as nx
- from networkx.utils import not_implemented_for
- __all__ = ["treewidth_min_degree", "treewidth_min_fill_in"]
- @not_implemented_for("directed")
- @not_implemented_for("multigraph")
- def treewidth_min_degree(G):
- """Returns a treewidth decomposition using the Minimum Degree heuristic.
- The heuristic chooses the nodes according to their degree, i.e., first
- the node with the lowest degree is chosen, then the graph is updated
- and the corresponding node is removed. Next, a new node with the lowest
- degree is chosen, and so on.
- Parameters
- ----------
- G : NetworkX graph
- Returns
- -------
- Treewidth decomposition : (int, Graph) tuple
- 2-tuple with treewidth and the corresponding decomposed tree.
- """
- deg_heuristic = MinDegreeHeuristic(G)
- return treewidth_decomp(G, lambda graph: deg_heuristic.best_node(graph))
- @not_implemented_for("directed")
- @not_implemented_for("multigraph")
- def treewidth_min_fill_in(G):
- """Returns a treewidth decomposition using the Minimum Fill-in heuristic.
- The heuristic chooses a node from the graph, where the number of edges
- added turning the neighbourhood of the chosen node into clique is as
- small as possible.
- Parameters
- ----------
- G : NetworkX graph
- Returns
- -------
- Treewidth decomposition : (int, Graph) tuple
- 2-tuple with treewidth and the corresponding decomposed tree.
- """
- return treewidth_decomp(G, min_fill_in_heuristic)
- class MinDegreeHeuristic:
- """Implements the Minimum Degree heuristic.
- The heuristic chooses the nodes according to their degree
- (number of neighbours), i.e., first the node with the lowest degree is
- chosen, then the graph is updated and the corresponding node is
- removed. Next, a new node with the lowest degree is chosen, and so on.
- """
- def __init__(self, graph):
- self._graph = graph
- # nodes that have to be updated in the heap before each iteration
- self._update_nodes = []
- self._degreeq = [] # a heapq with 3-tuples (degree,unique_id,node)
- self.count = itertools.count()
- # build heap with initial degrees
- for n in graph:
- self._degreeq.append((len(graph[n]), next(self.count), n))
- heapify(self._degreeq)
- def best_node(self, graph):
- # update nodes in self._update_nodes
- for n in self._update_nodes:
- # insert changed degrees into degreeq
- heappush(self._degreeq, (len(graph[n]), next(self.count), n))
- # get the next valid (minimum degree) node
- while self._degreeq:
- (min_degree, _, elim_node) = heappop(self._degreeq)
- if elim_node not in graph or len(graph[elim_node]) != min_degree:
- # outdated entry in degreeq
- continue
- elif min_degree == len(graph) - 1:
- # fully connected: abort condition
- return None
- # remember to update nodes in the heap before getting the next node
- self._update_nodes = graph[elim_node]
- return elim_node
- # the heap is empty: abort
- return None
- def min_fill_in_heuristic(graph):
- """Implements the Minimum Degree heuristic.
- Returns the node from the graph, where the number of edges added when
- turning the neighbourhood of the chosen node into clique is as small as
- possible. This algorithm chooses the nodes using the Minimum Fill-In
- heuristic. The running time of the algorithm is :math:`O(V^3)` and it uses
- additional constant memory."""
- if len(graph) == 0:
- return None
- min_fill_in_node = None
- min_fill_in = sys.maxsize
- # sort nodes by degree
- nodes_by_degree = sorted(graph, key=lambda x: len(graph[x]))
- min_degree = len(graph[nodes_by_degree[0]])
- # abort condition (handle complete graph)
- if min_degree == len(graph) - 1:
- return None
- for node in nodes_by_degree:
- num_fill_in = 0
- nbrs = graph[node]
- for nbr in nbrs:
- # count how many nodes in nbrs current nbr is not connected to
- # subtract 1 for the node itself
- num_fill_in += len(nbrs - graph[nbr]) - 1
- if num_fill_in >= 2 * min_fill_in:
- break
- num_fill_in /= 2 # divide by 2 because of double counting
- if num_fill_in < min_fill_in: # update min-fill-in node
- if num_fill_in == 0:
- return node
- min_fill_in = num_fill_in
- min_fill_in_node = node
- return min_fill_in_node
- def treewidth_decomp(G, heuristic=min_fill_in_heuristic):
- """Returns a treewidth decomposition using the passed heuristic.
- Parameters
- ----------
- G : NetworkX graph
- heuristic : heuristic function
- Returns
- -------
- Treewidth decomposition : (int, Graph) tuple
- 2-tuple with treewidth and the corresponding decomposed tree.
- """
- # make dict-of-sets structure
- graph = {n: set(G[n]) - {n} for n in G}
- # stack containing nodes and neighbors in the order from the heuristic
- node_stack = []
- # get first node from heuristic
- elim_node = heuristic(graph)
- while elim_node is not None:
- # connect all neighbours with each other
- nbrs = graph[elim_node]
- for u, v in itertools.permutations(nbrs, 2):
- if v not in graph[u]:
- graph[u].add(v)
- # push node and its current neighbors on stack
- node_stack.append((elim_node, nbrs))
- # remove node from graph
- for u in graph[elim_node]:
- graph[u].remove(elim_node)
- del graph[elim_node]
- elim_node = heuristic(graph)
- # the abort condition is met; put all remaining nodes into one bag
- decomp = nx.Graph()
- first_bag = frozenset(graph.keys())
- decomp.add_node(first_bag)
- treewidth = len(first_bag) - 1
- while node_stack:
- # get node and its neighbors from the stack
- (curr_node, nbrs) = node_stack.pop()
- # find a bag all neighbors are in
- old_bag = None
- for bag in decomp.nodes:
- if nbrs <= bag:
- old_bag = bag
- break
- if old_bag is None:
- # no old_bag was found: just connect to the first_bag
- old_bag = first_bag
- # create new node for decomposition
- nbrs.add(curr_node)
- new_bag = frozenset(nbrs)
- # update treewidth
- treewidth = max(treewidth, len(new_bag) - 1)
- # add edge to decomposition (implicitly also adds the new node)
- decomp.add_edge(old_bag, new_bag)
- return treewidth, decomp
|