# HG changeset patch
# User Nathann Cohen
# Date 1276375484 -7200
# Node ID 0117059ea902dd0c13072866775bb439e112895c
# Parent 9c2c2ec3f1fb7d18e921163ddb1d8bce97983a39
trac 7563 -- PQ-Trees and Graph.is_interval
diff -r 9c2c2ec3f1fb -r 0117059ea902 doc/en/reference/graphs.rst
--- a/doc/en/reference/graphs.rst Tue Jun 08 14:19:43 2010 +0200
+++ b/doc/en/reference/graphs.rst Sat Jun 12 22:44:44 2010 +0200
@@ -19,3 +19,5 @@
sage/graphs/base/c_graph
sage/graphs/base/sparse_graph
sage/graphs/base/dense_graph
+
+ sage/graphs/pq_trees.py
\ No newline at end of file
diff -r 9c2c2ec3f1fb -r 0117059ea902 sage/graphs/generic_graph.py
--- a/sage/graphs/generic_graph.py Tue Jun 08 14:19:43 2010 +0200
+++ b/sage/graphs/generic_graph.py Sat Jun 12 22:44:44 2010 +0200
@@ -7140,6 +7140,146 @@
g.delete_vertex(v)
return True
+ def is_interval(self, certificate = False):
+ r"""
+ Check whether self is an interval graph
+
+ INPUT:
+
+ - ``certificate`` (boolean) -- The function returns ``True``
+ or ``False`` according to the graph, when ``certificate =
+ False`` (default). When ``certificate = True`` and the graph
+ is an interval graph, a dictionary whose keys are the
+ vertices and values are pairs of integers are returned
+ instead of ``True``. They correspond to an embedding of the
+ interval graph, each vertex being represented by an interval
+ going from the first of the two values to the second.
+
+ ALGORITHM:
+
+ Through the use of PQ-Trees
+
+ AUTHOR :
+
+ Nathann Cohen (implementation)
+
+ EXAMPLES:
+
+ A Petersen Graph is not chordal, nor car it be an interval
+ graph ::
+
+ sage: g = graphs.PetersenGraph()
+ sage: g.is_interval()
+ False
+
+ Though we can build intervals from the corresponding random
+ generator::
+
+ sage: g = graphs.RandomInterval(20)
+ sage: g.is_interval()
+ True
+
+ This method can also return, given an interval graph, a
+ possible embedding (we can actually compute all of them
+ through the PQ-Tree structures)::
+
+ sage: g = Graph(':S__@_@A_@AB_@AC_@ACD_@ACDE_ACDEF_ACDEFG_ACDEGH_ACDEGHI_ACDEGHIJ_ACDEGIJK_ACDEGIJKL_ACDEGIJKLMaCEGIJKNaCEGIJKNaCGIJKNPaCIP')
+ sage: d = g.is_interval(certificate = True)
+ sage: print d # not tested
+ {0: (0, 20), 1: (1, 9), 2: (2, 36), 3: (3, 5), 4: (4, 38), 5: (6, 21), 6: (7, 27), 7: (8, 12), 8: (10, 29), 9: (11, 16), 10: (13, 39), 11: (14, 31), 12: (15, 32), 13: (17, 23), 14: (18, 22), 15: (19, 33), 16: (24, 25), 17: (26, 35), 18: (28, 30), 19: (34, 37)}
+
+ From this embedding, we can clearly build an interval graph
+ isomorphic to the previous one::
+
+ sage: g2 = graphs.IntervalGraph(d.values())
+ sage: g2.is_isomorphic(g)
+ True
+
+ .. SEEALSO::
+
+ - :mod:`Interval Graph Recognition `.
+
+ - :meth:`PQ `
+ -- Implementation of PQ-Trees.
+
+ """
+
+ # An interval graph first is a chordal graph. Without this,
+ # there is no telling how we should find its maximal cliques,
+ # by the way :-)
+
+ if not self.is_chordal():
+ return False
+
+ from sage.sets.set import Set
+ from sage.combinat.subset import Subsets
+
+ # First, we need to gather the list of maximal cliques, which
+ # is easy as the graph is chordal
+
+ cliques = []
+ clique_subsets = []
+
+ # As we will be deleting vertices ...
+ g = self.copy()
+
+ for cc in self.connected_components_subgraphs():
+
+ # We pick a perfect elimination order for every connected
+ # component. We will then iteratively take the last vertex
+ # in the order (a simplicial vertex) and consider the
+ # clique it forms with its neighbors. If we do not have an
+ # inclusion-wise larger clique in our list, we add it !
+
+ peo = cc.lex_BFS()
+
+
+
+ while peo:
+ v = peo.pop()
+ clique = Set( [v] + cc.neighbors(v))
+ cc.delete_vertex(v)
+
+ if not any([clique in cs for cs in clique_subsets]):
+ cliques.append(clique)
+ clique_subsets.append(Subsets(clique))
+
+
+ from sage.graphs.pq_trees import reorder_sets
+
+ try:
+ ordered_sets = reorder_sets(cliques)
+ if not certificate:
+ return True
+
+ except ValueError:
+ return False
+
+ # We are now listing the maximal cliques in the given order,
+ # and keeping track of the vertices appearing/disappearing
+
+ current = set([])
+ beg = {}
+ end = {}
+
+ i = 0
+
+ ordered_sets.append([])
+ for S in map(set,ordered_sets):
+ for v in current-S:
+ end[v] = i
+ i = i + 1
+
+ for v in S-current:
+ beg[v] = i
+ i = i + 1
+
+ current = S
+
+
+ return dict([(v, (beg[v], end[v])) for v in self])
+
+
def is_clique(self, vertices=None, directed_clique=False):
"""
Returns True if the set ``vertices`` is a clique, False
diff -r 9c2c2ec3f1fb -r 0117059ea902 sage/graphs/pq_trees.py
--- /dev/null Thu Jan 01 00:00:00 1970 +0000
+++ b/sage/graphs/pq_trees.py Sat Jun 12 22:44:44 2010 +0200
@@ -0,0 +1,1005 @@
+r"""
+PQ-Trees
+
+This module implements PQ-Trees and methods to help recognise Interval Graphs.
+
+Class and Methods
+-----------------
+
+"""
+
+# Constants, to make the code more readable
+
+FULL = 2
+PARTIAL = 1
+EMPTY = 0
+ALIGNED = True
+UNALIGNED = False
+
+##########################################################################
+# Some Lambda Functions #
+# #
+# As the elements of a PQ-Tree can be either P-Trees, Q-Trees, or the #
+# sets themselves (the leaves), the following lambda function are #
+# meant to be applied both on PQ-Trees and Sets, and mimic for the #
+# latter the behaviour we expect from the corresponding methods #
+# defined in class PQ #
+##########################################################################
+
+set_contiguous = lambda tree, x : (
+ tree.set_contiguous(x) if isinstance(tree, PQ) else
+ ((FULL, ALIGNED) if x in tree
+ else (EMPTY, ALIGNED)))
+
+new_P = lambda liste : P(liste) if len(liste) > 1 else liste[0]
+new_Q = lambda liste : Q(liste) if len(liste) > 1 else liste[0]
+
+flatten = lambda x : x.flatten() if isinstance(x, PQ) else x
+
+impossible_msg = "Impossible"
+
+def reorder_sets(sets):
+ r"""
+ Reorders a collection of sets such that each element appears on an
+ interval.
+
+ Given a collection of sets `C = S_1,...,S_k` on a ground set `X`,
+ this function attempts to reorder them in such a way that `\forall
+ x \in X` and `i 2 or
+ (n_PARTIAL_UNALIGNED >= 1 and n_EMPTY != self.cardinality() -1)):
+
+ raise ValueError(impossible_msg)
+
+ # From now on, there are at most two pq-trees which are partially filled
+ # If there is one which is not aligned to the right, all the others are empty
+
+ #########################################################
+ # 1/2 #
+ # #
+ # Several easy cases where we can decide without paying #
+ # attention #
+ #########################################################
+
+ # All the children are FULL
+ elif n_FULL == self.cardinality():
+ return FULL, True
+
+ # All the children are empty
+ elif n_EMPTY == self.cardinality():
+ return EMPTY, True
+
+ # There is a PARTIAL UNALIGNED element (and all the others are
+ # empty as we checked before
+
+ elif n_PARTIAL_UNALIGNED == 1:
+ return (PARTIAL, UNALIGNED)
+
+ # If there is just one partial element and all the others are
+ # empty, we just reorder the set to put it at the right end
+
+ elif (n_PARTIAL_ALIGNED == 1 and
+ n_EMPTY == self.cardinality()-1):
+
+ self._children = set_EMPTY + set_PARTIAL_ALIGNED
+ return (PARTIAL, ALIGNED)
+
+
+ ################################################################
+ # 2/2 #
+ # #
+ # From now on, there are at most two partial pq-trees and all #
+ # of them have v aligned to their right #
+ # #
+ # We now want to order them in such a way that all the #
+ # elements containing v are located on the right #
+ ################################################################
+
+ else:
+
+ self._children = []
+
+ # We first move the empty elements to the left, if any
+
+ if n_EMPTY > 0:
+ self._children.extend(set_EMPTY)
+
+ # If there is one partial element we but have to add it to
+ # the sequence, then add all the full elements
+
+ # We must also make sure these elements will not be
+ # reordered in such a way that the elements containing v
+ # are not contiguous
+
+ # ==> We create a Q-tree
+
+ if n_PARTIAL_ALIGNED < 2:
+
+ new = []
+
+
+ # add the partial element, if any
+ if n_PARTIAL_ALIGNED == 1:
+
+ subtree = set_PARTIAL_ALIGNED[0]
+ new.extend(subtree.simplify(v, right = ALIGNED))
+
+
+ # Then the full elements, if any, in a P-tree (we can
+ # permute any two of them while keeping all the
+ # elements containing v on an interval
+
+ if n_FULL > 0:
+
+ new.append(new_P(set_FULL))
+
+ # We lock all of them in a Q-tree
+
+ self._children.append(new_Q(new))
+
+ return PARTIAL, True
+
+ # If there are 2 partial elements, we take care of both
+ # ends. We also know it will not be possible to align the
+ # interval of sets containing v to the right
+
+ else:
+
+ new = []
+
+ # The second partal element is aligned to the right
+ # while, as we want to put it at the end of the
+ # interval, it should be aligned to the left
+ set_PARTIAL_ALIGNED[1].reverse()
+
+ # 1/3
+ # Left partial subtree
+ subtree = set_PARTIAL_ALIGNED[0]
+ new.extend(subtree.simplify(v, right = ALIGNED))
+
+ # 2/3
+ # Center (Full elements, in a P-tree, as they can be
+ # permuted)
+
+ if n_FULL > 0:
+ new.append(new_P(set_FULL))
+
+ # 3/3
+ # Right partial subtree
+ subtree = set_PARTIAL_ALIGNED[1]
+ new.extend(subtree.simplify(v, left= ALIGNED))
+
+ # We add all of it, locked in a Q-Tree
+ self._children.append(new_Q(new))
+
+ return PARTIAL, False
+
+class Q(PQ):
+ r"""
+ A Q-Tree is a PQ-Tree whose children are
+ ordered up to reversal
+ """
+
+ def set_contiguous(self, v):
+ r"""
+ Updates ``self`` so that its sets containing ``v`` are
+ contiguous for any admissible permutation of its subtrees.
+
+ This function also ensures, whenever possible,
+ that all the sets containing ``v`` are located on an interval
+ on the right side of the ordering.
+
+ INPUT:
+
+ - ``v`` -- an element of the ground set
+
+ OUTPUT:
+
+ According to the cases :
+
+ * ``(EMPTY, ALIGNED)`` if no set of the tree contains
+ an occurrence of ``v``
+
+ * ``(FULL, ALIGNED)`` if all the sets of the tree contain
+ ``v``
+
+ * ``(PARTIAL, ALIGNED)`` if some (but not all) of the sets
+ contain ``v``, all of which are aligned
+ to the right of the ordering at the end when the function ends
+
+ * ``(PARTIAL, UNALIGNED)`` if some (but not all) of the
+ sets contain ``v``, though it is impossible to align them
+ all to the right
+
+ In any case, the sets containing ``v`` are contiguous when this
+ function ends. If there is no possibility of doing so, the function
+ raises a ``ValueError`` exception.
+
+ EXAMPLE:
+
+ Ensuring the sets containing ``0`` are continuous::
+
+ sage: from sage.graphs.pq_trees import P, Q
+ sage: q = Q([[2,3], Q([[3,0],[3,1]]), Q([[4,0],[4,5]])])
+ sage: q.set_contiguous(0)
+ (1, False)
+ sage: print q
+ ('Q', [{2, 3}, {1, 3}, {0, 3}, {0, 4}, {4, 5}])
+
+ Impossible situation::
+
+ sage: p = Q([[0,1], [1,2], [2,0]])
+ sage: p.set_contiguous(0)
+ Traceback (most recent call last):
+ ...
+ ValueError: Impossible
+
+
+ """
+
+
+ #################################################################
+ # Guidelines : #
+ # #
+ # As the tree is a Q-Tree, we can but reverse the order in #
+ # which the elements appear. It means that we can but check #
+ # the elements containing v are already contiguous (even #
+ # though we have to take special care of partial elements -- #
+ # the endpoints of the interval), and answer accordingly #
+ # (partial, full, empty, aligned..). We also want to align the #
+ # elements containing v to the right if possible. #
+ ################################################################
+
+
+ ###############################################################
+ # Defining Variables : #
+ # #
+ # Collecting the information of which children are FULL of v, #
+ # which ones are EMPTY, PARTIAL_ALIGNED and PARTIAL_UNALIGNED #
+ # #
+ # Defining variables for their cardinals, just to make the #
+ # code slightly more readable :-) #
+ ###############################################################
+
+ seq = [set_contiguous(x, v) for x in self]
+ self.flatten()
+ seq = [set_contiguous(x, v) for x in self]
+
+ f_seq = dict(zip(self, seq))
+
+ set_FULL = []
+ set_EMPTY = []
+ set_PARTIAL_ALIGNED = []
+ set_PARTIAL_UNALIGNED = []
+
+ sorting = {
+ (FULL, ALIGNED) : set_FULL,
+ (EMPTY, ALIGNED) : set_EMPTY,
+ (PARTIAL, ALIGNED) : set_PARTIAL_ALIGNED,
+ (PARTIAL, UNALIGNED) : set_PARTIAL_UNALIGNED
+ }
+
+ for i in self:
+ sorting[f_seq[i]].append(i)
+
+ n_FULL = len(set_FULL)
+ n_EMPTY = len(set_EMPTY)
+ n_PARTIAL_ALIGNED = len(set_PARTIAL_ALIGNED)
+ n_PARTIAL_UNALIGNED = len(set_PARTIAL_UNALIGNED)
+
+ counts = dict(map(lambda (x,y) : (x,len(y)), sorting.iteritems()))
+
+ ###################################################################
+ # #
+ # Picking the good ordering for the children : #
+ # #
+ # #
+ # There is a possibility of aligning to the right iif #
+ # the vector can assume the form (as a regular expression) : #
+ # #
+ # (EMPTY *) PARTIAL (FULL *) Of course, each of these three #
+ # members could be empty #
+ # #
+ # Hence, in the following case we reverse the vector : #
+ # #
+ # * if the last element is empty (as we checked the whole #
+ # vector is not empty #
+ # #
+ # * if the last element is partial, aligned, and all the #
+ # others are full #
+ ###################################################################
+
+ if (f_seq[self._children[-1]] == (EMPTY, ALIGNED) or
+ (f_seq[self._children[-1]] == (PARTIAL, ALIGNED) and n_FULL == self.cardinality() - 1)):
+
+ # We reverse the order of the elements in the SET only. Which means that they are still aligned to the right !
+ self._children.reverse()
+
+
+ #########################################################
+ # 1/2 #
+ # #
+ # Several easy cases where we can decide without paying #
+ # attention #
+ #########################################################
+
+
+ # Excludes the situation where there is no solution.
+ # read next comment for more explanations
+
+ if (n_PARTIAL_ALIGNED + n_PARTIAL_UNALIGNED > 2 or
+ (n_PARTIAL_UNALIGNED >= 1 and n_EMPTY != self.cardinality() -1)):
+
+ raise ValueError(impossible_msg)
+
+ # From now on, there are at most two pq-trees which are partially filled
+ # If there is one which is not aligned to the right, all the others are empty
+
+ # First trivial case, no checking neded
+ elif n_FULL == self.cardinality():
+ return FULL, True
+
+ # Second trivial case, no checking needed
+ elif n_EMPTY == self.cardinality():
+ return EMPTY, True
+
+ # Third trivial case, no checking needed
+ elif n_PARTIAL_UNALIGNED == 1:
+ return (PARTIAL, UNALIGNED)
+
+ # If there is just one partial element
+ # and all the others are empty, we just reorder
+ # the set to put it at the right end
+
+ elif (n_PARTIAL_ALIGNED == 1 and
+ n_EMPTY == self.cardinality()-1):
+
+ if set_PARTIAL_ALIGNED[0] == self._children[-1]:
+ return (PARTIAL, ALIGNED)
+
+ else:
+ return (PARTIAL, UNALIGNED)
+
+
+ ##############################################################
+ # 2/2 #
+ # #
+ # We iteratively consider all the children, and check #
+ # that the elements containing v are indeed #
+ # locate on an interval. #
+ # #
+ # We are also interested in knowing whether this interval is #
+ # aligned to the right #
+ # #
+ # Because of the previous tests, we can assume there are at #
+ # most two partial pq-trees and all of them are aligned to #
+ # their right #
+ ##############################################################
+
+ else:
+
+ new_children = []
+
+ # Two variables to remember where we are
+ # according to the interval
+
+ seen_nonempty = False
+ seen_right_end = False
+
+
+ for i in self:
+
+ type, aligned = f_seq[i]
+
+ # We met an empty element
+ if type == EMPTY:
+
+ # 2 possibilities :
+ #
+ # * we have NOT met a non-empty element before
+ # and it just means we are looking at the
+ # leading empty elements
+ #
+ # * we have met a non-empty element before and it
+ # means we will never met another non-empty
+ # element again => we have seen the right end
+ # of the interval
+
+ new_children.append(i)
+
+ if seen_nonempty:
+ seen_right_end = True
+
+ # We met a non-empty element
+ else:
+ if seen_right_end:
+ raise ValueError(impossible_msg)
+
+
+ if type == PARTIAL:
+
+ # if we see an ALIGNED partial tree after
+ # having seen a nonempty element then the
+ # partial tree must be aligned to the left and
+ # so we have seen the right end
+
+ if seen_nonempty and aligned:
+ i.reverse()
+ seen_right_end = True
+
+ # right partial subtree
+ subtree = i
+ new_children.extend(subtree.simplify(v, left = True))
+
+ # If we see an UNALIGNED partial element after
+ # having met a nonempty element, there is no
+ # solution to the alignment problem
+
+ elif seen_nonempty and not aligned:
+ raise ValueError(impossible_msg)
+
+ # If we see an unaligned element but no non-empty
+ # element since the beginning, we are witnessing both the
+ # left and right end
+
+ elif not seen_nonempty and not aligned:
+ raise ValueError("Bon, ben ca arrive O_o")
+ seen_right_end = True
+
+ elif not seen_nonempty and aligned:
+
+ # left partial subtree
+ subtree = i
+
+ new_children.extend(subtree.simplify(v, right = True))
+
+
+ else:
+ new_children.append(i)
+
+ seen_nonempty = True
+
+ # Setting the updated sequence of children
+ self._children = new_children
+
+
+ # Whether we achieved an alignment to the right is the
+ # complement of whether we have seen the right end
+
+ return (PARTIAL, not seen_right_end)
+