From 0e5ad3f8742617e2b497bbf0bc9e74c2df485a6b Mon Sep 17 00:00:00 2001 From: Cyril Bouvier Date: Tue, 26 Nov 2024 16:06:57 +0100 Subject: [PATCH 1/3] graphs: implementation of linear-time algorithm for modular decomposition --- src/sage/graphs/graph.py | 228 ++++-- .../modular_decomposition.hpp | 744 ++++++++++++++++++ .../modular_decomposition.pxd | 23 + ...mposition.py => modular_decomposition.pyx} | 603 +++++++------- 4 files changed, 1259 insertions(+), 339 deletions(-) create mode 100644 src/sage/graphs/graph_decompositions/modular_decomposition.hpp create mode 100644 src/sage/graphs/graph_decompositions/modular_decomposition.pxd rename src/sage/graphs/graph_decompositions/{modular_decomposition.py => modular_decomposition.pyx} (75%) diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index 4eced79fe9b..4c98901daad 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -88,6 +88,8 @@ - Jean-Florent Raymond (2019-04): is_redundant, is_dominating, private_neighbors +- Cyril Bouvier (2024-11): is_module + Graph Format ------------ @@ -7181,8 +7183,109 @@ def cores(self, k=None, with_labels=False): return core return list(core.values()) - @doc_index("Leftovers") - def modular_decomposition(self, algorithm=None, style='tuple'): + @doc_index("Modules") + def is_module(self, vertices): + r""" + Return whether ``vertices`` is a module of ``self``. + + A subset `M` of the vertices of a graph is a module if for every + vertex `v` outside of `M`, either all vertices of `M` are neighbors of + `v` or all vertices of `M` are not neighbors of `v`. + + INPUT: + + - ``vertices`` -- iterable; a subset of vertices of ``self`` + + EXAMPLES: + + The whole graph, the empty set and singletons are trivial modules:: + + sage: G = graphs.PetersenGraph() + sage: G.is_module([]) + True + sage: G.is_module([G.random_vertex()]) + True + sage: G.is_module(G) + True + + Prime graphs only have trivial modules:: + + sage: G = graphs.PathGraph(5) + sage: G.is_prime() + True + sage: all(not G.is_module(S) for S in subsets(G) + ....: if len(S) > 1 and len(S) < G.order()) + True + + For edgeless graphs and complete graphs, all subsets are modules:: + + sage: G = Graph(5) + sage: all(G.is_module(S) for S in subsets(G)) + True + sage: G = graphs.CompleteGraph(5) + sage: all(G.is_module(S) for S in subsets(G)) + True + + The modules of a graph and of its complements are the same:: + + sage: G = graphs.TuranGraph(10, 3) + sage: G.is_module([0,1,2]) + True + sage: G.complement().is_module([0,1,2]) + True + sage: G.is_module([3,4,5]) + True + sage: G.complement().is_module([3,4,5]) + True + sage: G.is_module([2,3,4]) + False + sage: G.complement().is_module([2,3,4]) + False + sage: G.is_module([3,4,5,6,7,8,9]) + True + sage: G.complement().is_module([3,4,5,6,7,8,9]) + True + + Elements of ``vertices`` must be in ``self``:: + + sage: G = graphs.PetersenGraph() + sage: G.is_module(['Terry']) + Traceback (most recent call last): + ... + LookupError: vertex (Terry) is not a vertex of the graph + sage: G.is_module([1, 'Graham']) + Traceback (most recent call last): + ... + LookupError: vertex (Graham) is not a vertex of the graph + """ + M = set(vertices) + + for v in M: + if v not in self: + raise LookupError(f"vertex ({v}) is not a vertex of the graph") + + if len(M) == 0 or len(M) == 1 or len(M) == self.order(): + return True + + N = None # will contains the neighborhood of M + for v in M: + if N is None: + # first iteration, the neighborhood N must be computed + N = { u for u in self.neighbor_iterator(v) if u not in M } + else: + # check that the neighborhood of v is N + n = 0 + for u in self.neighbor_iterator(v): + if u not in M: + n += 1 + if u not in N: + return False # u is a splitter + if n != len(N): + return False + return True + + @doc_index("Modules") + def modular_decomposition(self, algorithm=None, style="tuple"): r""" Return the modular decomposition of the current graph. @@ -7190,11 +7293,21 @@ def modular_decomposition(self, algorithm=None, style='tuple'): vertex outside the module is either connected to all members of the module or to none of them. Every graph that has a nontrivial module can be partitioned into modules, and the increasingly fine partitions into - modules form a tree. The ``modular_decomposition`` function returns - that tree, using an `O(n^3)` algorithm of [HM1979]_. + modules form a tree. The ``modular_decomposition`` method returns + that tree. INPUT: + - ``algorithm`` -- string (default: ``None``); the algorithm to use + among: + + - ``None`` or ``'corneil_habib_paul_tedder'`` -- will use the + Corneil-Habib-Paul-Tedder algorithm from [TCHP2008]_, its complexity + is linear in the number of vertices and edges. + + - ``'habib_maurer'`` -- will use the Habib-Maurer algorithm from + [HM1979]_, its complexity is cubic in the number of vertices. + - ``style`` -- string (default: ``'tuple'``); specifies the output format: @@ -7204,16 +7317,10 @@ def modular_decomposition(self, algorithm=None, style='tuple'): OUTPUT: - A pair of two values (recursively encoding the decomposition) : - - * The type of the current module : - - * ``'PARALLEL'`` - * ``'PRIME'`` - * ``'SERIES'`` - - * The list of submodules (as list of pairs ``(type, list)``, - recursively...) or the vertex's name if the module is a singleton. + The modular decomposition tree, either as nested tuples (if + ``style='tuple'``) or as an object of + :class:`~sage.combinat.rooted_tree.LabelledRootedTree` (if + ``style='tree'``) Crash course on modular decomposition: @@ -7266,7 +7373,19 @@ def modular_decomposition(self, algorithm=None, style='tuple'): The Petersen Graph too:: sage: graphs.PetersenGraph().modular_decomposition() - (PRIME, [1, 4, 5, 0, 2, 6, 3, 7, 8, 9]) + (PRIME, [1, 4, 5, 0, 6, 2, 3, 9, 7, 8]) + + Graph from the :wikipedia:`Modular_decomposition`:: + + sage: G = Graph('Jv\\zoKF@wN?', format='graph6') + sage: G.relabel([1..11]) + sage: G.modular_decomposition() + (PRIME, + [(SERIES, [4, (PARALLEL, [2, 3])]), + 1, + 5, + (PARALLEL, [6, 7]), + (SERIES, [(PARALLEL, [10, 11]), 9, 8])]) This a clique on 5 vertices with 2 pendant edges, though, has a more interesting decomposition:: @@ -7275,14 +7394,20 @@ def modular_decomposition(self, algorithm=None, style='tuple'): sage: g.add_edge(0,5) sage: g.add_edge(0,6) sage: g.modular_decomposition() - (SERIES, [(PARALLEL, [(SERIES, [1, 2, 3, 4]), 5, 6]), 0]) + (SERIES, [(PARALLEL, [(SERIES, [3, 4, 2, 1]), 5, 6]), 0]) + + TurĂ¡n graphs are co-graphs:: + + sage: graphs.TuranGraph(11, 3).modular_decomposition() + (SERIES, + [(PARALLEL, [7, 8, 9, 10]), (PARALLEL, [3, 4, 5, 6]), (PARALLEL, [0, 1, 2])]) We can choose output to be a :class:`~sage.combinat.rooted_tree.LabelledRootedTree`:: sage: g.modular_decomposition(style='tree') SERIES[0[], PARALLEL[5[], 6[], SERIES[1[], 2[], 3[], 4[]]]] - sage: ascii_art(g.modular_decomposition(style='tree')) + sage: ascii_art(g.modular_decomposition(algorithm="habib_maurer",style='tree')) __SERIES / / 0 ___PARALLEL @@ -7293,7 +7418,9 @@ def modular_decomposition(self, algorithm=None, style='tuple'): ALGORITHM: - This function uses the algorithm of M. Habib and M. Maurer [HM1979]_. + This function can use either the algorithm of D. Corneil, M. Habib, C. + Paul and M. Tedder [TCHP2008]_ or the algorithm of M. Habib and M. + Maurer [HM1979]_. .. SEEALSO:: @@ -7301,10 +7428,16 @@ def modular_decomposition(self, algorithm=None, style='tuple'): - :class:`~sage.combinat.rooted_tree.LabelledRootedTree`. + - :func:`~sage.graphs.graph_decompositions.modular_decomposition.corneil_habib_paul_tedder_algorithm` + + - :func:`~sage.graphs.graph_decompositions.modular_decomposition.habib_maurer_algorithm` + .. NOTE:: - A buggy implementation of linear time algorithm from [TCHP2008]_ was - removed in Sage 9.7, see :issue:`25872`. + A buggy implementation of the linear time algorithm from [TCHP2008]_ + was removed in Sage 9.7, see :issue:`25872`. A new implementation + was reintroduced in Sage 10.6 after some corrections to the original + algorithm, see :issue:`xxxxx`. TESTS: @@ -7343,41 +7476,33 @@ def modular_decomposition(self, algorithm=None, style='tuple'): sage: G2 = Graph('F@Nfg') sage: G1.is_isomorphic(G2) True - sage: G1.modular_decomposition() + sage: G1.modular_decomposition(algorithm="habib_maurer") (PRIME, [1, 2, 5, 6, 0, (PARALLEL, [3, 4])]) - sage: G2.modular_decomposition() + sage: G2.modular_decomposition(algorithm="habib_maurer") (PRIME, [5, 6, 3, 4, 2, (PARALLEL, [0, 1])]) + sage: G1.modular_decomposition(algorithm="corneil_habib_paul_tedder") + (PRIME, [6, 5, 1, 2, 0, (PARALLEL, [3, 4])]) + sage: G2.modular_decomposition(algorithm="corneil_habib_paul_tedder") + (PRIME, [6, 5, (PARALLEL, [0, 1]), 2, 3, 4]) Check that :issue:`37631` is fixed:: sage: G = Graph('GxJEE?') - sage: G.modular_decomposition(style='tree') + sage: G.modular_decomposition(algorithm="habib_maurer",style='tree') PRIME[2[], SERIES[0[], 1[]], PARALLEL[3[], 4[]], PARALLEL[5[], 6[], 7[]]] """ - from sage.graphs.graph_decompositions.modular_decomposition import (NodeType, - habib_maurer_algorithm, - create_prime_node, - create_normal_node) - - if algorithm is not None: - from sage.misc.superseded import deprecation - deprecation(25872, "algorithm=... parameter is obsolete and has no effect.") - self._scream_if_not_simple() + from sage.graphs.graph_decompositions.modular_decomposition import \ + modular_decomposition - if not self.order(): - D = None - elif self.order() == 1: - D = create_normal_node(next(self.vertex_iterator())) - else: - D = habib_maurer_algorithm(self) + D = modular_decomposition(self, algorithm=algorithm) if style == 'tuple': - if D is None: + if D.is_empty(): return tuple() def relabel(x): - if x.node_type == NodeType.NORMAL: + if x.is_leaf(): return x.children[0] return x.node_type, [relabel(y) for y in x.children] @@ -7385,11 +7510,11 @@ def relabel(x): elif style == 'tree': from sage.combinat.rooted_tree import LabelledRootedTree - if D is None: + if D.is_empty(): return LabelledRootedTree([]) def to_tree(x): - if x.node_type == NodeType.NORMAL: + if x.is_leaf(): return LabelledRootedTree([], label=x.children[0]) return LabelledRootedTree([to_tree(y) for y in x.children], label=x.node_type) @@ -7640,7 +7765,14 @@ def is_prime(self, algorithm=None): A graph is prime if all its modules are trivial (i.e. empty, all of the graph or singletons) -- see :meth:`modular_decomposition`. - Use the `O(n^3)` algorithm of [HM1979]_. + This method computes the modular decomposition tree using + :meth:`~sage.graphs.graph.Graph.modular_decomposition`. + + INPUT: + + - ``algorithm`` -- string (default: ``None``); the algorithm used to + compute the modular decomposition tree; the value is forwarded + directly to :meth:`~sage.graphs.graph.Graph.modular_decomposition`. EXAMPLES: @@ -7661,17 +7793,15 @@ def is_prime(self, algorithm=None): sage: graphs.EmptyGraph().is_prime() True """ - if algorithm is not None: - from sage.misc.superseded import deprecation - deprecation(25872, "algorithm=... parameter is obsolete and has no effect.") - from sage.graphs.graph_decompositions.modular_decomposition import NodeType + from sage.graphs.graph_decompositions.modular_decomposition import \ + modular_decomposition if self.order() <= 1: return True - D = self.modular_decomposition() + MD = modular_decomposition(self, algorithm=algorithm) - return D[0] == NodeType.PRIME and len(D[1]) == self.order() + return MD.is_prime() and len(MD.children) == self.order() def _gomory_hu_tree(self, vertices, algorithm=None): r""" diff --git a/src/sage/graphs/graph_decompositions/modular_decomposition.hpp b/src/sage/graphs/graph_decompositions/modular_decomposition.hpp new file mode 100644 index 00000000000..c5d822aea18 --- /dev/null +++ b/src/sage/graphs/graph_decompositions/modular_decomposition.hpp @@ -0,0 +1,744 @@ +/* + * Copyright (C) 2024 Cyril Bouvier + * + *This program is free software: you can redistribute it and/or modify + *it under the terms of the GNU General Public License as published by + *the Free Software Foundation, either version 2 of the License, or + *(at your option) any later version. + * https://www.gnu.org/licenses/ + */ + +/* + * This file contains inline implementations of utility structs, classes and + * functions used in the implementation of the modular decomposition method + * + * AUTHORS: + * + * - Cyril Bouvier (2024): code for second implementation of the linear time + * algorithm of D. Corneil, M. Habib, C. Paul and M. Tedder [TCHP2008]_ + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +enum class Label : uint8_t { + EMPTY = 0b00, + HOMOGENEOUS = 0b01, + BROKEN = 0b10, + DEAD = 0b11 +}; + +enum class Flag : uint8_t { + UNFLAGGED = 0b00, + FLAGGED = 0b01 +}; + +enum class Type_ : uint8_t { + PRIME = 0, + SERIES = 1, + PARALLEL = 2, + LEAF = 3 +}; + +struct SDData { + void set_from_data(size_t lex_label_offset_arg, + const int* sigma_arg, + const size_t *xslice_len_arg, + const std::vector *lex_label_arg) { + lex_label_offset = lex_label_offset_arg; + sigma = sigma_arg; + xslice_len = xslice_len_arg; + lex_label = lex_label_arg; + } + + void set_to_subslice(const SDData &sd, size_t offset) { + lex_label_offset = sd.lex_label[offset].size(); + sigma = sd.sigma + offset; + xslice_len = sd.xslice_len + offset; + lex_label = sd.lex_label + offset; + } + + size_t lex_label_size(size_t i) const { + if (lex_label[i].size() <= lex_label_offset) { + return 0; + } else { + return lex_label[i].size() - lex_label_offset; + } + } + + const int * lex_label_ptr(size_t i) const { + if (lex_label[i].size() <= lex_label_offset) { + return nullptr; + } else { + return lex_label[i].data() + lex_label_offset; + } + } + + size_t first_slice_index() const { + return 1; + } + + size_t next_slice_index(size_t idx) const { + return idx + xslice_len[idx]; + } + + size_t size() const { + return xslice_len[0]; + } + + bool is_pivot_isolated () const { + return lex_label[1].size() <= lex_label_offset; + } + + size_t lex_label_offset; + const int *sigma; + const size_t *xslice_len; + const std::vector *lex_label; +}; + +struct md_tree_node { + md_tree_node(Type_ type, Label label, Flag flag) + : parent(nullptr), vertex(INT_MAX), type(type), + label(label), flag(flag), + slice(SIZE_MAX), cc_tag(SIZE_MAX) { + } + + md_tree_node(int vertex) + : parent(nullptr), vertex(vertex), type(Type_::LEAF), + label(Label::EMPTY), flag(Flag::UNFLAGGED), + slice(SIZE_MAX), cc_tag(SIZE_MAX) { + } + + md_tree_node(Type_ type) + : md_tree_node(type, Label::EMPTY, Flag::UNFLAGGED) { + } + + bool is_leaf() const { + return type == Type_::LEAF; + } + + bool is_prime() const { + return type == Type_::PRIME; + } + + bool is_series() const { + return type == Type_::SERIES; + } + + bool is_parallel() const { + return type == Type_::PARALLEL; + } + + bool is_degenerate() const { + return type == Type_::SERIES || type == Type_::PARALLEL; + } + + bool is_empty() const { + return label == Label::EMPTY; + } + + bool is_homogeneous() const { + return label == Label::HOMOGENEOUS; + } + + bool is_homogeneous_or_empty() const { + return !(static_cast(label) >> 1U); + } + + bool is_broken() const { + return label == Label::BROKEN; + } + + bool is_dead() const { + return label == Label::DEAD; + } + + bool is_dead_or_broken() const { + return static_cast(label) >> 1U; + } + + void prepend_new_child(md_tree_node *c) { + c->parent = this; + if (children.empty()) { + vertex = c->vertex; + } + children.push_front(c); + } + + void append_new_child(md_tree_node *c) { + c->parent = this; + if (children.empty()) { + vertex = c->vertex; + } + children.push_back(c); + } + + void append_stolen_children_from(md_tree_node *n) { + if (!n->children.empty()) { + for (md_tree_node *c: n->children) { + c->parent = this; + } + if (children.empty()) { + vertex = n->children.front()->vertex; + } + children.splice(children.end(), n->children); + } + } + + void set_label_and_flag_recursively(Label l, Flag f) { + label = l; + flag = f; + for (md_tree_node *c: children) { + c->set_label_and_flag_recursively(l, f); + } + } + + md_tree_node *parent; + std::list children; + int vertex; + Type_ type; + Label label; + Flag flag; + size_t slice; + size_t cc_tag; +}; + +using md_forest = std::list; + +struct ScratchData { + struct MDSequences { + std::unordered_map leaves; + std::unordered_set Marked; + std::unordered_set Full; + std::deque Explore; + }; + + struct Clusters { + /* Assumes i < p < j where p is the index of the "cluster" {x}. */ + bool are_clusters_non_adjacent(size_t i, size_t j) const { + size_t sj = clusters[j].front()->slice; + auto e = std::make_pair(0, sj); + for (const md_tree_node *mi: clusters[i]) { + e.first = mi->vertex; + auto it = module_slice_adjacency.find(e); + if (it != module_slice_adjacency.end()) { + return false; + } + } + return true; + } + + struct pair_hash { + inline size_t operator()(const std::pair &v) const { + return ((size_t) v.first)*31+v.second; + } + }; + + std::vector> clusters; + /* adjacency list between a module (represented using the corresponding + * .vertex from the root node) and a slice. + */ + std::unordered_set, pair_hash> module_slice_adjacency; + std::vector Left; + std::vector Right; + std::unordered_map cluster_of_v; + }; + + md_tree_node *new_leaf(int vertex) { + return mdseq.leaves[vertex] = new md_tree_node(vertex); + } + + MDSequences mdseq; + Clusters clusters; +}; + + +void dealloc_md_tree_nodes_recursively(md_tree_node *n) { + for (md_tree_node *c: n->children) { + dealloc_md_tree_nodes_recursively(c); + } + delete n; +} + +void md_forest_preprocess(md_forest &MDi) { + Type_ one_cc_type = Type_::PARALLEL; /* only for first iteration */ + size_t s = 0; + for (md_tree_node *md: MDi) { + md->set_label_and_flag_recursively(Label::EMPTY, Flag::UNFLAGGED); + md->slice = s; + if (md->type == Type_::PRIME || md->type == one_cc_type) { + md->cc_tag = 0; + } else { + md->cc_tag = SIZE_MAX; + size_t i = 0; + for (md_tree_node *c: md->children) { + c->cc_tag = i; + i++; + } + } + one_cc_type = Type_::SERIES; + s += 1; + } +} + +void mark_partitive_forest_finish_inner_rec(md_tree_node *r) { + size_t nb = 0; /* number of HOMOGENEOUS or EMPTY children */ + + /* Do a postorder visit: so we first visit the children */ + for (md_tree_node *c: r->children) { + mark_partitive_forest_finish_inner_rec(c); + nb += c->is_homogeneous_or_empty(); + } + + if (r->is_dead_or_broken()) { + if (r->parent != nullptr && !(r->parent->is_dead())) { + /* if parent.label is not DEAD set it to BROKEN */ + r->parent->label = Label::BROKEN; + } + if (r->is_broken() && r->is_degenerate() && nb > 1) { + md_tree_node *newnode = new md_tree_node(r->type, Label::EMPTY, + Flag::UNFLAGGED); + + /* Iterate over the children to gather HOMOGENEOUS and EMPTY + * child under newnode + * */ + auto it = r->children.begin(); + while (it != r->children.end()) { + if ((*it)->is_homogeneous_or_empty()) { + newnode->append_new_child(*it); + it = r->children.erase(it); /* get iterator to next child */ + } else { + ++it; + } + + } + r->append_new_child(newnode); + } + } +} + +void md_forest_mark_partitive_forest(md_forest &MDi, const SDData &sd, + ScratchData::MDSequences &scratch) { + size_t i = sd.first_slice_index(); + i = sd.next_slice_index(i); /* skip first slice */ + scratch.Explore.clear(); + for (; i < sd.size(); i = sd.next_slice_index(i)) { + scratch.Marked.clear(); + scratch.Full.clear(); + + for (auto it = sd.lex_label[i].begin() + sd.lex_label_offset; + it != sd.lex_label[i].end() ; ++it) { + scratch.Explore.push_back(scratch.leaves.at(*it)); + } + + while (scratch.Explore.size() > 0) { + md_tree_node *n = scratch.Explore.front(); + scratch.Explore.pop_front(); + md_tree_node *p = n->parent; + scratch.Full.insert(n); + if (n->is_empty()) { + n->label = Label::HOMOGENEOUS; + } + if (p) { + scratch.Marked.insert(p); + /* if all children of p are Full, move p to Explore */ + bool b = true; + for (md_tree_node *c: p->children) { + if (scratch.Full.find(c) == scratch.Full.end()) { + b = false; + break; + } + } + if (b) { + scratch.Marked.erase(p); + scratch.Explore.push_back(p); + } + } + } + + for (md_tree_node *n: scratch.Marked) { + /* If n is SERIES or PARALLEL => gather children of n in Full below + * the same new node A (needed only if there are >= 2 such children) + * and the children of n not in Full below the same new node B + * (needed only if there is >= 2 such children). + */ + if (n->is_degenerate() && n->children.size() > 2) { + Type_ t = n->type; + md_tree_node *newnodes[2] = { + new md_tree_node(t, Label::HOMOGENEOUS, Flag::FLAGGED), + new md_tree_node(t, Label::EMPTY, Flag::UNFLAGGED) + }; + auto end = n->children.end(); + for (auto it = n->children.begin(); it != end; ++it){ + bool notFull = scratch.Full.find(*it) == scratch.Full.end(); + newnodes[notFull]->append_new_child(*it); + } + n->children.clear(); + for (size_t i = 0; i < 2; i++) { + if (newnodes[i]->children.size() == 1) { + n->append_new_child(newnodes[i]->children.front()); + } else { + n->append_new_child(newnodes[i]); + } + } + } + + if (n->label != Label::DEAD) { + n->label = Label::DEAD; + /* Set flag to * for children of n that are in Full */ + for (md_tree_node *c: n->children) { + if (scratch.Full.find(c) != scratch.Full.end()) { + c->flag = Flag::FLAGGED; + } + } + } + } + } + + for (md_tree_node *md: MDi) { + mark_partitive_forest_finish_inner_rec(md); + } +} + +void sort_broken_nodes_recursively(md_tree_node *n, + bool dead_and_broken_first) { + if (n->is_dead_or_broken()) { + /* If the label is not DEAD or BROKEN, no need to go deeper: they + * will not be any DEAD or BROKEN nodes. + */ + for (md_tree_node *c: n->children) { + sort_broken_nodes_recursively(c, dead_and_broken_first); + } + + if (n->is_broken()) { + /* if dead_and_broken_first is true + * => put DEAD and BROKEN children at the beginning + * if dead_and_broken_first is false + * => put EMPTY and HOMOGENEOUS children at the beginning + */ + auto b = n->children.begin(); + auto end = n->children.end(); + for (auto it = n->children.begin(); it != end; ++it) { + if (dead_and_broken_first == (*it)->is_dead_or_broken()) { + std::iter_swap(it, b); + ++b; + } + } + } + } +} + +void sort_dead_nodes_recursively(md_tree_node *n, bool flagged_first) { + if (n->is_dead_or_broken()) { + /* If the label is not DEAD or BROKEN, no need to go deeper: they + * will not be any DEAD or BROKEN nodes. + */ + for (md_tree_node *c: n->children) { + sort_dead_nodes_recursively(c, flagged_first); + } + + if (n->is_dead()) { + /* if flagged_first is true => put flagged children at the beginning + * if flagged_first is talse => put unflagged children at the beginning + */ + auto b = n->children.begin(); + auto end = n->children.end(); + for (auto it = n->children.begin(); it != end; ++it) { + if (flagged_first == ((*it)->flag == Flag::FLAGGED)) { + std::iter_swap(it, b); + ++b; + } + } + } + } +} + +void md_forest_extract_and_sort(md_forest &MDi) { + bool is_first_slice = true; + auto it = MDi.begin(); + while (it != MDi.end()) { + md_tree_node *md = *it; + /* sort children of DEAD nodes */ + sort_dead_nodes_recursively(md, is_first_slice); + /* sort children of BROKEN nodes */ + sort_broken_nodes_recursively(md, is_first_slice); + + /* remove DEAD and BROKEN nodes */ + auto it_delete = it; + ++it; + while (it_delete != it) { + md_tree_node *r = *it_delete; + if (r->is_dead_or_broken()) { + for (md_tree_node *c: r->children) { + /* propagate cc tag and slice, if set */ + c->cc_tag = r->cc_tag != SIZE_MAX ? r->cc_tag : c->cc_tag; + c->slice = r->slice != SIZE_MAX ? r->slice : c->slice; + c->parent = nullptr; + } + auto insert_it = MDi.erase(it_delete); + it_delete = r->children.begin(); + MDi.splice(insert_it, r->children); + delete r; + } else { + it_delete++; + } + } + + is_first_slice = false; + } +} + +void md_forest_clusters_computation(const md_forest &MDi, const SDData &sd, + ScratchData::Clusters &scratch) { + scratch.clusters.clear(); + scratch.cluster_of_v.clear(); + size_t prev_cc = SIZE_MAX, prev_slice = SIZE_MAX; + for (md_tree_node *n: MDi) { + size_t cc = n->cc_tag; + size_t slice = n->slice; + int v = n->vertex; /* a vertex belonging to the module */ + + if (cc == SIZE_MAX) { /* n is alone in the cluster */ + scratch.clusters.emplace_back(1, n); /* new cluster */ + } else { + if (cc != prev_cc || slice != prev_slice) { + scratch.clusters.emplace_back(); /* start new cluster */ + } + scratch.clusters.back().push_back(n); + } + + prev_cc = cc; + prev_slice = slice; + scratch.cluster_of_v[v] = scratch.clusters.size()-1; + } + + size_t p = scratch.cluster_of_v[sd.sigma[0]]; + size_t q = scratch.clusters.size(); + + /* Left and Right computation. + * Left(i) == i if i <= p + * Left(i) == Left(j) for p < i,j if clusters Ki and Kj in same slice + * Right(i) = p for 0 <= i <= p + * Also compute adjacency between modules and slices (except the first one). + */ + scratch.Left.clear(); + scratch.Right.clear(); + scratch.module_slice_adjacency.clear(); + scratch.Left.reserve(q); + scratch.Right.reserve(q); + for (size_t i = 0; i <= p; i++) { + scratch.Left.push_back(i); + } + scratch.Right.resize(p+1, p); /* Right[i] = p for 0 <= i <= p */ + for (size_t i = p+1; i < q; i++) { + scratch.Right.push_back(i); + } + for (size_t i = sd.first_slice_index(), s = 0, j = 0; i < sd.size(); + i = sd.next_slice_index(i), s++) { + size_t j0 = j; + /* Compute j, the highest index of a cluster of the current slice s */ + for (; j+1 < q && scratch.clusters[j+1].front()->slice == s; j++); + + if (s == 0) { /* nothing to do for the first slice */ + j += 1; /* skip "cluster" {x} */ + } else { + /* for Right and module_slice_adjacency: iterate over the + * lexicographic labels of the slice. + */ + for (auto it = sd.lex_label[i].begin() + sd.lex_label_offset; + it != sd.lex_label[i].end() ; ++it) { + auto c = scratch.cluster_of_v.find(*it); + if (c != scratch.cluster_of_v.end()) { + scratch.module_slice_adjacency.emplace(*it, s); + /* cluster j is adjacent to cluster containing c so Right of + * the cluster c is >= j + */ + scratch.Right[c->second] = j; + } + } + + /* for Left: find the cluster of the first non adjacent module */ + size_t lp; /* lp will be the Left for all clusters of the slice */ + for (lp = 0; lp < p; lp++) { + bool adj = true; + for (const md_tree_node *m: scratch.clusters[lp]) { + if (scratch.module_slice_adjacency.find(std::make_pair(m->vertex, s)) == scratch.module_slice_adjacency.end()) { + adj = false; + break; + } + } + if (!adj) { + break; + } + } + /* Left is lp for all clusters of the slice s */ + std::fill_n(std::back_inserter(scratch.Left), j-j0, lp); + } + } + +} + + +md_tree_node *md_forest_parse_and_assemble(md_tree_node *root, + size_t p, + const ScratchData::Clusters &scratch) { + size_t q = scratch.clusters.size(); + size_t l = p; + size_t r = p; + while (l > 0 || r+1 < q) { + Type_ t; + size_t i; + size_t lp, old_l = l; + size_t rp, old_r = r; + + if (r+1 == q || (l>0 && scratch.are_clusters_non_adjacent(l-1, r+1))) { + lp = l-1; + rp = r; + t = Type_::SERIES; + } else { + lp = l; + rp = r+1; + t = Type_::PARALLEL; + } + + while (lp < l || r < rp) { + if (lp < l) { + i = l = l-1; + } else { + i = r = r+1; + } + lp = std::min(lp, scratch.Left[i]); + rp = std::max(rp, scratch.Right[i]); + } + + t = (r-l)-(old_r-old_l) > 1 ? Type_::PRIME : t; + md_tree_node *old_root = root; + root = new md_tree_node(t); + + for (size_t i = l; i <= r; i++) { + if (i == old_l) { /* add the previous root */ + root->append_new_child(old_root); + i = old_r; + } else { + for (md_tree_node *m: scratch.clusters[i]) { + if (t != Type_::PRIME && m->type == t) { + root->append_stolen_children_from(m); + delete m; + } else { + root->append_new_child(m); + } + } + } + } + } + return root; +} + + +md_tree_node *corneil_habib_paul_tedder_inner_rec(const SDData &sd, + ScratchData &scratch) { + if (sd.size() == 0) { /* empty graph */ + return nullptr; + } + + SDData sub_sd; + std::list MDi; + int x = sd.sigma[0]; + + /* First create a new leaf for x */ + md_tree_node *root = scratch.new_leaf(x); + + if (sd.size() == 1) { /* graph with one vertex */ + return root; + } else if (sd.size() == 2) { /* graph with two vertices */ + int y = sd.sigma[1]; + /* root is SERIES if there is an edge between x and y, else PARALLEL */ + Type_ t = sd.is_pivot_isolated() ? Type_::PARALLEL : Type_::SERIES; + root = new md_tree_node(t); + root->append_new_child(scratch.mdseq.leaves[x]); + root->append_new_child(scratch.new_leaf(y)); + return root; + } + + /* Now it is known that the graph has more than two vertices */ + + /* Recursive calls on all the slices */ + size_t first_of_last_slice = 1; /* set to 1 to remove warning */ + for (size_t i = sd.first_slice_index(); i < sd.size(); + i = sd.next_slice_index(i)) { + first_of_last_slice = i; + sub_sd.set_to_subslice(sd, i); + md_tree_node *md = corneil_habib_paul_tedder_inner_rec(sub_sd, scratch); + MDi.push_back(md); + } + + if (sd.is_pivot_isolated()) { /* x is isolated (i.e., has no neighbor) */ + md_tree_node *md = MDi.front(); /* only one slice in this case */ + if (md->type == Type_::PARALLEL) { + root = md; + } else { + root = new md_tree_node(Type_::PARALLEL); + root->append_new_child(md); + } + root->prepend_new_child(scratch.mdseq.leaves[x]); + return root; + } + + /* Now, it is known that x has at least one neighbor, so the first slice + * contains the neighborhood of x. + * If G is not connected, the last slice is all the connected components + * that do not contains x, it should be treated separately: the tree + * corresponding to the last slice is removed from MDi and will be added + * back before the clusters computation. + */ + md_tree_node *last_md = nullptr; + if (sd.lex_label_size(first_of_last_slice) == 0) { /* not connected */ + last_md = MDi.back(); + last_md->slice = MDi.size()-1; /* remember the slice */ + MDi.pop_back(); + } + + /* Preprocessing of the sub md trees: + * - set the slice attribute + * - set the connected components tag (for clusters computation later) + * - set label to EMPTY and flag to UNFLAGGED on all nodes + */ + md_forest_preprocess(MDi); + + /* Add the pivot {x} in MDi after the first slice (= the neighbors of x) */ + MDi.insert(++MDi.begin(), root); + + /* Mark partitive forest */ + md_forest_mark_partitive_forest(MDi, sd, scratch.mdseq); + + /* Extract and sort */ + md_forest_extract_and_sort(MDi); + + /* Add back the last slice if graph is not connected */ + if (last_md != nullptr) { + MDi.push_back(last_md); + } + + /* Postprocessing for connected (co-)components of slices: compute the + * factoring x-m-cluster sequence + */ + md_forest_clusters_computation(MDi, sd, scratch.clusters); + + size_t p = scratch.clusters.cluster_of_v[x]; + + /* Parse and assemble */ + root = md_forest_parse_and_assemble(root, p, scratch.clusters); + + return root; +} + +md_tree_node *corneil_habib_paul_tedder_inner(const SDData &sd) { + ScratchData tmp; + return corneil_habib_paul_tedder_inner_rec(sd, tmp); +} diff --git a/src/sage/graphs/graph_decompositions/modular_decomposition.pxd b/src/sage/graphs/graph_decompositions/modular_decomposition.pxd new file mode 100644 index 00000000000..bc5e6c6c34d --- /dev/null +++ b/src/sage/graphs/graph_decompositions/modular_decomposition.pxd @@ -0,0 +1,23 @@ +from libcpp cimport bool +from libcpp.list cimport list as cpplist +from libcpp.vector cimport vector + +cdef extern from "modular_decomposition.hpp": + cdef cppclass SDData: + void set_from_data(size_t lex_label_offset, const int* sigma, + const size_t *xslice_len, + const vector[int] *lex_label) + + cdef cppclass md_tree_node: + bool is_leaf() const + bool is_prime() const + bool is_parallel() const + bool is_series() const + cpplist[md_tree_node *] children + # For a leaf, the corresponding vertex, for a internal node, any vertex + # corresponding to a any leaf below the node + int vertex + + void dealloc_md_tree_nodes_recursively(md_tree_node *) + + cdef md_tree_node * corneil_habib_paul_tedder_inner(const SDData &SD) diff --git a/src/sage/graphs/graph_decompositions/modular_decomposition.py b/src/sage/graphs/graph_decompositions/modular_decomposition.pyx similarity index 75% rename from src/sage/graphs/graph_decompositions/modular_decomposition.py rename to src/sage/graphs/graph_decompositions/modular_decomposition.pyx index 001f127d63d..571d44d489b 100644 --- a/src/sage/graphs/graph_decompositions/modular_decomposition.py +++ b/src/sage/graphs/graph_decompositions/modular_decomposition.pyx @@ -1,11 +1,25 @@ +# distutils: language = c++ +# distutils: extra_compile_args = -std=c++11 r""" Modular Decomposition This module implements the function for computing the modular decomposition of undirected graphs. + +AUTHORS: + +- Lokesh Jain (2017): first implementation of the linear time algorithm of + D. Corneil, M. Habib, C. Paul and M. Tedder [TCHP2008]_ + +- David Einstein (2018): added the algorithm of M. Habib and M. Maurer [HM1979]_ + +- Cyril Bouvier (2024): second implementation of the linear time algorithm + of D. Corneil, M. Habib, C. Paul and M. Tedder [TCHP2008]_ """ # **************************************************************************** # Copyright (C) 2017 Lokesh Jain +# 2018 David Einstein +# 2024 Cyril Bouvier # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU General Public License as published by @@ -13,15 +27,109 @@ # (at your option) any later version. # https://www.gnu.org/licenses/ # **************************************************************************** +from cython.operator cimport dereference as deref -from enum import Enum, IntEnum +from enum import IntEnum +from sage.graphs.base.c_graph cimport CGraph, CGraphBackend +from sage.graphs.graph_decompositions.slice_decomposition cimport \ + extended_lex_BFS +from sage.groups.perm_gps.permgroup_element import PermutationGroupElement from sage.misc.lazy_import import lazy_import from sage.misc.random_testing import random_testing -lazy_import('sage.groups.perm_gps.permgroup_element', 'PermutationGroupElement') +################################################################################ +# Corneil-Habib-Paul-Tedder algorithm # +################################################################################ +def corneil_habib_paul_tedder_algorithm(G): + r""" + Compute the modular decomposition by the algorithm of Corneil, Habib, Paul + and Tedder. + + INPUT: + + - ``G`` -- the graph for which modular decomposition tree needs to be + computed + + OUTPUT: an object of type Node representing the modular decomposition tree + of the graph G + + This function compute the modular decomposition of the given graph by the + algorithm of Corneil, Habib, Paul and Tedder [TCHP2008]_. It is a recursive, + linear-time algorithm that first computes the slice decomposition of the + graph (via the extended lexBFS algorithm) and then computes the modular + decomposition by calling itself recursively on the slices of the previously + computed slice decomposition. + + .. SEEALSO:: + + * :mod:`~sage.graphs.graph_decompositions.slice_decomposition` -- + compute a slice decomposition of the simple undirect graph + + This function should not be used directly, it should be called via the + ``modular_decomposition`` method of ``Graph`` with the parameter + ``algorithm='corneil_habib_paul_tedder'``. + + This functions assumes that ``graph`` is a object of the class ``Graph`` and + is a simple graph. + + TESTS:: + + sage: from sage.graphs.graph_decompositions.modular_decomposition import * + sage: recreate_decomposition(15, corneil_habib_paul_tedder_algorithm, + ....: 3, 4, 0.2) + sage: recreate_decomposition(10, corneil_habib_paul_tedder_algorithm, + ....: 4, 5, 0.2) + sage: recreate_decomposition(3, corneil_habib_paul_tedder_algorithm, + ....: 6, 5, 0.2) + """ + cdef CGraphBackend Gbackend = G._backend + cdef CGraph cg = Gbackend.cg() + + cdef vector[int] sigma + cdef vector[vector[int]] lex_label + cdef vector[size_t] xslice_len + + # Compute the slice decomposition using the extended lexBFS algorithm + extended_lex_BFS(cg, sigma, NULL, -1, NULL, &xslice_len, &lex_label) + + cdef SDData SD + SD.set_from_data(0, sigma.data(), xslice_len.data(), lex_label.data()) + MD = corneil_habib_paul_tedder_inner(SD) + + r = md_tree_node_to_md_tree(MD, Gbackend) + dealloc_md_tree_nodes_recursively(MD) + return r + + +cdef object _md_tree_node_to_md_tree_inner_rec(const md_tree_node *n, + CGraphBackend Gb): + cdef md_tree_node *c + if deref(n).is_leaf(): + return Node.create_leaf(Gb.vertex_label(deref(n).vertex)) + else: + if deref(n).is_series(): + node = Node(NodeType.SERIES) + elif deref(n).is_parallel(): + node = Node(NodeType.PARALLEL) + else: # is_prime + node = Node(NodeType.PRIME) + node.children.extend( + _md_tree_node_to_md_tree_inner_rec(c, Gb) + for c in deref(n).children) + return node + + +cdef object md_tree_node_to_md_tree(const md_tree_node *n, CGraphBackend Gb): + if n == NULL: + return Node(NodeType.EMPTY) + else: + return _md_tree_node_to_md_tree_inner_rec(n, Gb) + + +################################################################################ class NodeType(IntEnum): """ NodeType is an enumeration class used to define the various types of nodes @@ -35,7 +143,7 @@ class NodeType(IntEnum): - ``PRIME`` -- indicates the node is a prime module - - ``FOREST`` -- indicates a forest containing trees + - ``EMPTY`` -- indicates a empty tree - ``NORMAL`` -- indicates the node is normal containing a vertex """ @@ -43,101 +151,32 @@ class NodeType(IntEnum): SERIES = 1 PARALLEL = 2 NORMAL = 3 - FOREST = -1 + EMPTY = -1 def __repr__(self) -> str: r""" - String representation of this node type. + Return a string representation of a ``NodeType`` object. - EXAMPLES:: + TESTS:: sage: from sage.graphs.graph_decompositions.modular_decomposition import NodeType sage: repr(NodeType.PARALLEL) 'PARALLEL' + sage: str(NodeType.PRIME) + 'PRIME' """ return self.name - def __str__(self): - """ - String representation of this node type. - - EXAMPLES:: - - sage: from sage.graphs.graph_decompositions.modular_decomposition import NodeType - sage: str(NodeType.PARALLEL) - 'PARALLEL' - """ - return repr(self) - - -class NodeSplit(Enum): - """ - Enumeration class used to specify the split that has occurred at the node or - at any of its descendants. - - ``NodeSplit`` is defined for every node in modular decomposition tree and is - required during the refinement and promotion phase of modular decomposition - tree computation. Various node splits defined are - - - ``LEFT_SPLIT`` -- indicates a left split has occurred - - - ``RIGHT_SPLIT`` -- indicates a right split has occurred - - - ``BOTH_SPLIT`` -- indicates both left and right split have occurred - - - ``NO_SPLIT`` -- indicates no split has occurred - """ - LEFT_SPLIT = 1 - RIGHT_SPLIT = 2 - BOTH_SPLIT = 3 - NO_SPLIT = 0 - - -class VertexPosition(Enum): - """ - Enumeration class used to define position of a vertex w.r.t source in - modular decomposition. - - For computing modular decomposition of connected graphs a source vertex is - chosen. The position of vertex is w.r.t this source vertex. The various - positions defined are - - - ``LEFT_OF_SOURCE`` -- indicates vertex is to left of source and is a - neighbour of source vertex - - - ``RIGHT_OF_SOURCE`` -- indicates vertex is to right of source and is - connected to but not a neighbour of source vertex - - - ``SOURCE`` -- indicates vertex is source vertex - """ - LEFT_OF_SOURCE = -1 - RIGHT_OF_SOURCE = 1 - SOURCE = 0 - + __str__ = __repr__ class Node: """ - Node class stores information about the node type, node split and index of - the node in the parent tree. + Node class stores information about the node type. Node type can be ``PRIME``, ``SERIES``, ``PARALLEL``, ``NORMAL`` or - ``FOREST``. Node split can be ``NO_SPLIT``, ``LEFT_SPLIT``, ``RIGHT_SPLIT`` - or ``BOTH_SPLIT``. A node is split in the refinement phase and the split - used is propagated to the ancestors. + ``EMPTY``. - ``node_type`` -- is of type NodeType and specifies the type of node - - - ``node_split`` -- is of type NodeSplit and specifies the type of splits - which have occurred in the node and its descendants - - - ``index_in_root`` -- specifies the index of the node in the forest - obtained after promotion phase - - - ``comp_num`` -- specifies the number given to nodes in a (co)component - before refinement - - - ``is_separated`` -- specifies whether a split has occurred with the node - as the root """ def __init__(self, node_type): r""" @@ -152,83 +191,72 @@ def __init__(self, node_type): [] """ self.node_type = node_type - self.node_split = NodeSplit.NO_SPLIT - self.index_in_root = -1 - self.comp_num = -1 - self.is_separated = False self.children = [] - def set_node_split(self, node_split): - """ - Add node_split to the node split of ``self``. + def is_prime(self): + r""" + Return ``True`` if the node is a prime node, ``False`` otherwise. - ``LEFT_SPLIT`` and ``RIGHT_SPLIT`` can exist together in ``self`` as - ``BOTH_SPLIT``. + EXAMPLES:: - INPUT: + sage: from sage.graphs.graph_decompositions.modular_decomposition import * + sage: n = Node(NodeType.PRIME) + sage: n.children.append(Node.create_leaf(1)) + sage: n.children.append(Node.create_leaf(2)) + sage: n.is_prime() + True + sage: (n.children[0].is_prime(), n.children[1].is_prime()) + (False, False) + """ + return self.node_type == NodeType.PRIME - - ``node_split`` -- ``node_split`` to be added to ``self`` + def is_series(self): + r""" + Return ``True`` if the node is series, ``False`` otherwise. EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.LEFT_SPLIT) - sage: node.node_split == NodeSplit.LEFT_SPLIT - True - sage: node.set_node_split(NodeSplit.RIGHT_SPLIT) - sage: node.node_split == NodeSplit.BOTH_SPLIT + sage: n = Node(NodeType.SERIES) + sage: n.children.append(Node.create_leaf(1)) + sage: n.children.append(Node.create_leaf(2)) + sage: n.is_series() True - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.BOTH_SPLIT) - sage: node.node_split == NodeSplit.BOTH_SPLIT - True - """ - if self.node_split == NodeSplit.NO_SPLIT: - self.node_split = node_split - elif ((self.node_split == NodeSplit.LEFT_SPLIT and - node_split == NodeSplit.RIGHT_SPLIT) or - (self.node_split == NodeSplit.RIGHT_SPLIT and - node_split == NodeSplit.LEFT_SPLIT)): - self.node_split = NodeSplit.BOTH_SPLIT - - def has_left_split(self): + sage: (n.children[0].is_series(), n.children[1].is_series()) + (False, False) """ - Check whether ``self`` has ``LEFT_SPLIT``. + return self.node_type == NodeType.SERIES + + def is_empty(self): + r""" + Return ``True`` if the node is empty, ``False`` otherwise. EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.LEFT_SPLIT) - sage: node.has_left_split() - True - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.BOTH_SPLIT) - sage: node.has_left_split() + sage: Node(NodeType.EMPTY).is_empty() True + sage: Node.create_leaf(1).is_empty() + False """ - return (self.node_split == NodeSplit.LEFT_SPLIT or - self.node_split == NodeSplit.BOTH_SPLIT) + return self.node_type == NodeType.EMPTY - def has_right_split(self): - """ - Check whether ``self`` has ``RIGHT_SPLIT``. + def is_leaf(self): + r""" + Return ``True`` if the node is a leaf, ``False`` otherwise. EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.RIGHT_SPLIT) - sage: node.has_right_split() - True - sage: node = Node(NodeType.PRIME) - sage: node.set_node_split(NodeSplit.BOTH_SPLIT) - sage: node.has_right_split() + sage: n = Node(NodeType.PRIME) + sage: n.children.append(Node.create_leaf(1)) + sage: n.children.append(Node.create_leaf(2)) + sage: n.is_leaf() + False + sage: all(c.is_leaf() for c in n.children) True """ - return (self.node_split == NodeSplit.RIGHT_SPLIT or - self.node_split == NodeSplit.BOTH_SPLIT) + return self.node_type == NodeType.NORMAL def __repr__(self): r""" @@ -238,24 +266,12 @@ def __repr__(self): sage: from sage.graphs.graph_decompositions.modular_decomposition import * sage: n = Node(NodeType.PRIME) - sage: n.children.append(create_normal_node(1)) - sage: n.children.append(create_normal_node(2)) + sage: n.children.append(Node.create_leaf(1)) + sage: n.children.append(Node.create_leaf(2)) sage: str(n) 'PRIME [NORMAL [1], NORMAL [2]]' """ - if self.node_type == NodeType.SERIES: - s = "SERIES " - elif self.node_type == NodeType.PARALLEL: - s = "PARALLEL " - elif self.node_type == NodeType.PRIME: - s = "PRIME " - elif self.node_type == NodeType.FOREST: - s = "FOREST " - else: - s = "NORMAL " - - s += str(self.children) - return s + return f"{self.node_type} {self.children}" def __eq__(self, other): r""" @@ -273,82 +289,30 @@ def __eq__(self, other): False """ return (self.node_type == other.node_type and - self.node_split == other.node_split and - self.index_in_root == other.index_in_root and - self.comp_num == other.comp_num and - self.is_separated == other.is_separated and self.children == other.children) + @classmethod + def create_leaf(cls, v): + """ + Return Node object that is a leaf corresponding to the vertex ``v`` -def create_prime_node(): - """ - Return a prime node with no children. - - OUTPUT: a node object with ``node_type`` set as ``NodeType.PRIME`` - - EXAMPLES:: - - sage: from sage.graphs.graph_decompositions.modular_decomposition import create_prime_node - sage: node = create_prime_node() - sage: node - PRIME [] - """ - return Node(NodeType.PRIME) - - -def create_parallel_node(): - """ - Return a parallel node with no children. - - OUTPUT: a node object with ``node_type`` set as ``NodeType.PARALLEL`` - - EXAMPLES:: - - sage: from sage.graphs.graph_decompositions.modular_decomposition import create_parallel_node - sage: node = create_parallel_node() - sage: node - PARALLEL [] - """ - return Node(NodeType.PARALLEL) - - -def create_series_node(): - """ - Return a series node with no children. - - OUTPUT: a node object with ``node_type`` set as ``NodeType.SERIES`` - - EXAMPLES:: - - sage: from sage.graphs.graph_decompositions.modular_decomposition import create_series_node - sage: node = create_series_node() - sage: node - SERIES [] - """ - return Node(NodeType.SERIES) - - -def create_normal_node(vertex): - """ - Return a normal node with no children. - - INPUT: + INPUT: - - ``vertex`` -- vertex number + - ``vertex`` -- vertex number - OUTPUT: a node object representing the vertex with ``node_type`` set as - ``NodeType.NORMAL`` + OUTPUT: a node object representing the vertex with ``node_type`` set as + ``NodeType.NORMAL`` - EXAMPLES:: + EXAMPLES:: - sage: from sage.graphs.graph_decompositions.modular_decomposition import create_normal_node - sage: node = create_normal_node(2) - sage: node - NORMAL [2] - """ - node = Node(NodeType.NORMAL) - node.children.append(vertex) - return node + sage: from sage.graphs.graph_decompositions.modular_decomposition import Node + sage: node = Node.create_leaf(2) + sage: node + NORMAL [2] + """ + node = cls(NodeType.NORMAL) + node.children.append(v) + return node def print_md_tree(root): @@ -362,7 +326,7 @@ def print_md_tree(root): EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * - sage: print_md_tree(modular_decomposition(graphs.IcosahedralGraph())) + sage: print_md_tree(habib_maurer_algorithm(graphs.IcosahedralGraph())) PRIME 3 4 @@ -467,6 +431,13 @@ def habib_maurer_algorithm(graph, g_classes=None): classes for the current root and each of the submodules. See also [BM1983]_ for an equivalent algorithm described in greater detail. + This function should not be used directly, it should be called via the + ``modular_decomposition`` method of ``Graph`` with the parameter + ``algorithm='habib_maurer'``. + + This functions assumes that ``graph`` is a object of the class ``Graph``, is + a simple graph and has at least 1 vertex. + INPUT: - ``graph`` -- the graph for which modular decomposition tree needs to be @@ -546,16 +517,6 @@ def habib_maurer_algorithm(graph, g_classes=None): sage: test_modular_decomposition(habib_maurer_algorithm(g), g) True - Graph from the :wikipedia:`Modular_decomposition`:: - - sage: d2 = {1:[2,3,4], 2:[1,4,5,6,7], 3:[1,4,5,6,7], 4:[1,2,3,5,6,7], - ....: 5:[2,3,4,6,7], 6:[2,3,4,5,8,9,10,11], - ....: 7:[2,3,4,5,8,9,10,11], 8:[6,7,9,10,11], 9:[6,7,8,10,11], - ....: 10:[6,7,8,9], 11:[6,7,8,9]} - sage: g = Graph(d2) - sage: test_modular_decomposition(habib_maurer_algorithm(g), g) - True - Tetrahedral Graph is Series:: sage: print_md_tree(habib_maurer_algorithm(graphs.TetrahedralGraph())) @@ -581,39 +542,18 @@ def habib_maurer_algorithm(graph, g_classes=None): TESTS: - Bad Input:: - - sage: g = DiGraph() - sage: habib_maurer_algorithm(g) - Traceback (most recent call last): - ... - ValueError: Graph must be undirected - - Empty Graph is Prime:: - - sage: g = Graph() - sage: habib_maurer_algorithm(g) - PRIME [] - - Ensure that a random graph and an isomorphic graph have identical modular decompositions. :: sage: from sage.graphs.graph_decompositions.modular_decomposition import permute_decomposition sage: permute_decomposition(2, habib_maurer_algorithm, 20, 0.5) # needs sage.groups """ - if graph.is_directed(): - raise ValueError("Graph must be undirected") - - if not graph.order(): - return create_prime_node() - if graph.order() == 1: - root = create_normal_node(next(graph.vertex_iterator())) + root = Node.create_leaf(next(graph.vertex_iterator())) return root elif not graph.is_connected(): - root = create_parallel_node() + root = Node(NodeType.PARALLEL) root.children = [habib_maurer_algorithm(graph.subgraph(vertices=sg), g_classes) for sg in graph.connected_components(sort=False)] return root @@ -621,7 +561,7 @@ def habib_maurer_algorithm(graph, g_classes=None): g_comp = graph.complement() if g_comp.is_connected(): from collections import defaultdict - root = create_prime_node() + root = Node(NodeType.PRIME) if g_classes is None: g_classes = gamma_classes(graph) vertex_set = frozenset(graph) @@ -638,14 +578,72 @@ def habib_maurer_algorithm(graph, g_classes=None): for sg in d1.values()] return root - root = create_series_node() + root = Node(NodeType.SERIES) root.children = [habib_maurer_algorithm(graph.subgraph(vertices=sg), g_classes) for sg in g_comp.connected_components(sort=False)] return root +################################################################################ +# Exported modular_decomposition function # +################################################################################ +def modular_decomposition(G, algorithm=None): + r""" + Return the modular decomposition of the current graph. + + This function should not be used directly, it should be called via the + ``modular_decomposition`` method of ``Graph``. + + TESTS:: + + sage: from sage.graphs.graph_decompositions.modular_decomposition import * + + sage: modular_decomposition(Graph()) + EMPTY [] + + sage: modular_decomposition(Graph(1)) + NORMAL [0] + + sage: modular_decomposition(DiGraph()) + Traceback (most recent call last): + ... + TypeError: the input must be an undirected Sage graph + + sage: modular_decomposition(Graph(5, loops=True)) + Traceback (most recent call last): + ... + ValueError: This method is not known to work on graphs with loops... -modular_decomposition = habib_maurer_algorithm + sage: modular_decomposition(Graph(5, multiedges=True)) + Traceback (most recent call last): + ... + ValueError: This method is not known to work on graphs with multiedges... + sage: modular_decomposition(Graph(), algorithm='silly walk') + Traceback (most recent call last): + ... + ValueError: unknown algorithm "silly walk" + """ + from sage.graphs.graph import Graph + if not isinstance(G, Graph): + raise TypeError("the input must be an undirected Sage graph") + G._scream_if_not_simple() + + if algorithm is None: + algorithm = "corneil_habib_paul_tedder" + + if algorithm not in ("habib_maurer", "corneil_habib_paul_tedder"): + raise ValueError(f'unknown algorithm "{algorithm}"') + + if not G.order(): + return Node(NodeType.EMPTY) + elif G.order() == 1: + D = Node(NodeType.NORMAL) + D.children.append(next(G.vertex_iterator())) + return D + elif algorithm == "habib_maurer": + return habib_maurer_algorithm(G) + else: # algorithm == "corneil_habib_paul_tedder" + return corneil_habib_paul_tedder_algorithm(G) # ============================================================================ # Below functions are implemented to test the modular decomposition tree @@ -758,19 +756,19 @@ def get_vertices(component_root): EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * - sage: forest = Node(NodeType.FOREST) - sage: forest.children = [create_normal_node(2), - ....: create_normal_node(3), create_normal_node(1)] + sage: forest = Node(NodeType.PRIME) + sage: forest.children = [Node.create_leaf(2), Node.create_leaf(0), + ....: Node.create_leaf(3), Node.create_leaf(1)] sage: series_node = Node(NodeType.SERIES) - sage: series_node.children = [create_normal_node(4), - ....: create_normal_node(5)] + sage: series_node.children = [Node.create_leaf(4), + ....: Node.create_leaf(5)] sage: parallel_node = Node(NodeType.PARALLEL) - sage: parallel_node.children = [create_normal_node(6), - ....: create_normal_node(7)] + sage: parallel_node.children = [Node.create_leaf(6), + ....: Node.create_leaf(7)] sage: forest.children.insert(1, series_node) sage: forest.children.insert(3, parallel_node) sage: get_vertices(forest) - [2, 4, 5, 3, 6, 7, 1] + [2, 4, 5, 0, 6, 7, 3, 1] """ vertices = [] @@ -963,17 +961,17 @@ def children_node_type(module, node_type): sage: from sage.graphs.graph_decompositions.modular_decomposition import * sage: g = graphs.OctahedralGraph() sage: tree_root = modular_decomposition(g) - sage: print_md_tree(modular_decomposition(g)) + sage: print_md_tree(tree_root) SERIES PARALLEL - 0 - 5 + 2 + 3 PARALLEL 1 4 PARALLEL - 2 - 3 + 0 + 5 sage: children_node_type(tree_root, NodeType.SERIES) False sage: children_node_type(tree_root, NodeType.PARALLEL) @@ -1003,14 +1001,14 @@ def either_connected_or_not_connected(v, vertices_in_module, graph): sage: print_md_tree(modular_decomposition(g)) SERIES PARALLEL - 0 - 5 + 2 + 3 PARALLEL 1 4 PARALLEL - 2 - 3 + 0 + 5 sage: either_connected_or_not_connected(2, [1, 4], g) True sage: either_connected_or_not_connected(2, [3, 4], g) @@ -1043,7 +1041,7 @@ def tree_to_nested_tuple(root): sage: from sage.graphs.graph_decompositions.modular_decomposition import * sage: g = graphs.OctahedralGraph() sage: tree_to_nested_tuple(modular_decomposition(g)) - (SERIES, [(PARALLEL, [0, 5]), (PARALLEL, [1, 4]), (PARALLEL, [2, 3])]) + (SERIES, [(PARALLEL, [2, 3]), (PARALLEL, [1, 4]), (PARALLEL, [0, 5])]) """ if root.node_type == NodeType.NORMAL: return root.children[0] @@ -1075,7 +1073,7 @@ def nested_tuple_to_tree(nest): 4 """ if not isinstance(nest, tuple): - return create_normal_node(nest) + return Node.create_leaf(nest) root = Node(nest[0]) root.children = [nested_tuple_to_tree(n) for n in nest[1:]] @@ -1187,7 +1185,7 @@ def relabel_tree(root, perm): raise TypeError("type of perm is not supported for relabeling") if root.node_type == NodeType.NORMAL: - return create_normal_node(perm[root.children[0]]) + return Node.create_leaf(perm[root.children[0]]) else: new_root = Node(root.node_type) new_root.children = [relabel_tree(child, perm) for child in root.children] @@ -1304,7 +1302,7 @@ def rand_md_tree(max_depth, parent_type): is one less than its parent's. """ if random() < leaf_probability or max_depth == 1: - root = create_normal_node(current_leaf[0]) + root = Node.create_leaf(current_leaf[0]) current_leaf[0] += 1 return root if parent_type == NodeType.PRIME: @@ -1332,18 +1330,18 @@ def rand_md_tree(max_depth, parent_type): return root -def md_tree_to_graph(root): +def md_tree_to_graph(root, prime_node_generator=None): r""" Create a graph having the given MD tree. - For the prime nodes we use that every path of length 4 or more is prime. - - TODO: accept a function that generates prime graphs as a parameter and - use that in the prime nodes. + For the prime nodes, the parameter ``prime_node_generator`` is called with + the number of vertices as the only argument. If it is ``None``, the path + graph is used (it is prime when the length is 4 or more). EXAMPLES:: sage: from sage.graphs.graph_decompositions.modular_decomposition import * + sage: from sage.graphs.graph_generators import graphs sage: tup1 = (NodeType.PRIME, 1, (NodeType.SERIES, 2, 3), ....: (NodeType.PARALLEL, 4, 5), 6) sage: tree1 = nested_tuple_to_tree(tup1) @@ -1352,32 +1350,57 @@ def md_tree_to_graph(root): ....: 4: [2, 3, 6], 5: [2, 3, 6], 6: [4, 5]}) sage: g1.is_isomorphic(g2) True + + sage: G = md_tree_to_graph(Node(NodeType.EMPTY)) + sage: G.is_isomorphic(Graph()) + True + + sage: tree = Node(NodeType.SERIES) + sage: tree.children.extend(Node.create_leaf(i) for i in range(5)) + sage: G = md_tree_to_graph(tree) + sage: G.is_isomorphic(graphs.CompleteGraph(5)) + True + + sage: tree = Node(NodeType.PRIME) + sage: tree.children.extend(Node.create_leaf(i) for i in range(5)) + sage: png = lambda n: (graphs.PathGraph if n == 4 else graphs.CycleGraph)(n) + sage: G = md_tree_to_graph(tree, prime_node_generator=png) + sage: G.is_isomorphic(graphs.CycleGraph(5)) + True """ from itertools import product, combinations from sage.graphs.graph import Graph + if prime_node_generator is None: + from sage.graphs.graph_generators import graphs + prime_node_generator = graphs.PathGraph + def tree_to_vertices_and_edges(root): r""" Give the list of vertices and edges of the graph having the given md tree. """ - if root.node_type == NodeType.NORMAL: + if root.is_leaf(): return (root.children, []) children_ve = [tree_to_vertices_and_edges(child) for child in root.children] vertices = [v for vs, es in children_ve for v in vs] edges = [e for vs, es in children_ve for e in es] vertex_lists = [vs for vs, es in children_ve] - if root.node_type == NodeType.PRIME: - for vs1, vs2 in zip(vertex_lists, vertex_lists[1:]): - for v1, v2 in product(vs1, vs2): - edges.append((v1, v2)) - elif root.node_type == NodeType.SERIES: + if root.is_prime(): + G = prime_node_generator(len(vertex_lists)) + G.relabel(range(len(vertex_lists))) + for i1, i2 in G.edge_iterator(labels=False): + edges.extend(product(vertex_lists[i1], vertex_lists[i2])) + elif root.is_series(): for vs1, vs2 in combinations(vertex_lists, 2): - for v1, v2 in product(vs1, vs2): - edges.append((v1, v2)) + edges.extend(product(vs1, vs2)) + # else: no edge to be created for PARALLEL nodes return (vertices, edges) - vs, es = tree_to_vertices_and_edges(root) - return Graph([vs, es], format='vertices_and_edges') + if root.is_empty(): + return Graph() + else: + vs, es = tree_to_vertices_and_edges(root) + return Graph([vs, es], format='vertices_and_edges') @random_testing From cdda9d5ece2ae6faa8cb5656ae138435479a8d0b Mon Sep 17 00:00:00 2001 From: Cyril Bouvier Date: Tue, 26 Nov 2024 16:28:22 +0100 Subject: [PATCH 2/3] Update name of PR in comment --- src/sage/graphs/graph.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/graphs/graph.py b/src/sage/graphs/graph.py index 4c98901daad..05720b51951 100644 --- a/src/sage/graphs/graph.py +++ b/src/sage/graphs/graph.py @@ -7437,7 +7437,7 @@ def modular_decomposition(self, algorithm=None, style="tuple"): A buggy implementation of the linear time algorithm from [TCHP2008]_ was removed in Sage 9.7, see :issue:`25872`. A new implementation was reintroduced in Sage 10.6 after some corrections to the original - algorithm, see :issue:`xxxxx`. + algorithm, see :issue:`39038`. TESTS: From b018722593d49c6a6b5f4f0d98ef3ece0b438bbe Mon Sep 17 00:00:00 2001 From: Cyril Bouvier Date: Tue, 26 Nov 2024 18:49:15 +0100 Subject: [PATCH 3/3] graphs.modular_decomposition: fix linter --- .../graphs/graph_decompositions/modular_decomposition.pyx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/sage/graphs/graph_decompositions/modular_decomposition.pyx b/src/sage/graphs/graph_decompositions/modular_decomposition.pyx index 571d44d489b..051a7f068c6 100644 --- a/src/sage/graphs/graph_decompositions/modular_decomposition.pyx +++ b/src/sage/graphs/graph_decompositions/modular_decomposition.pyx @@ -169,6 +169,7 @@ class NodeType(IntEnum): __str__ = __repr__ + class Node: """ Node class stores information about the node type. @@ -583,6 +584,7 @@ def habib_maurer_algorithm(graph, g_classes=None): for sg in g_comp.connected_components(sort=False)] return root + ################################################################################ # Exported modular_decomposition function # ################################################################################ @@ -645,10 +647,10 @@ def modular_decomposition(G, algorithm=None): else: # algorithm == "corneil_habib_paul_tedder" return corneil_habib_paul_tedder_algorithm(G) + # ============================================================================ # Below functions are implemented to test the modular decomposition tree # ============================================================================ - # Function implemented for testing def test_modular_decomposition(tree_root, graph): """