250 lines
7.9 KiB
Python
250 lines
7.9 KiB
Python
|
"""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
|