# Ticket #7563: trac_7563.patch

File trac_7563.patch, 41.3 KB (added by ncohen, 11 years ago)
• ## doc/en/reference/graphs.rst

# HG changeset patch
# User Nathann Cohen <nathann.cohen@gmail.com>
# 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 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
• ## sage/graphs/generic_graph.py

diff -r 9c2c2ec3f1fb -r 0117059ea902 sage/graphs/generic_graph.py
 a 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
• ## new file sage/graphs/pq_trees.py

diff -r 9c2c2ec3f1fb -r 0117059ea902 sage/graphs/pq_trees.py
 - 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)