Skip to content

Commit 3efa0aa

Browse files
author
Release Manager
committed
gh-39038: graphs: implementation of linear-time algorithm for modular decomposition <!-- ^ Please provide a concise and informative title. --> <!-- ^ Don't put issue numbers in the title, do this in the PR description below. --> <!-- ^ For example, instead of "Fixes #12345" use "Introduce new method to calculate 1 + 2". --> <!-- v Describe your changes below in detail. --> <!-- v Why is this change required? What problem does it solve? --> <!-- v If this PR resolves an open issue, please link to it here. For example, "Fixes #12345". --> This PR implements the recursive, linear-time algorithm to compute the modular decomposition of simple undirected graphs from [TCHP2008](https://arxiv.org/pdf/0710.3901). A previous implementation was introduced in version 9.7 but was removed after some bugs were discovered (see #25872 for exemple). This implementation is based on an updated version of the article and is intensively tested. This PR contains: - a implementation in C++ of the algorithm (in the file `graph_decompositions/modular_decomposition.hpp`) and the corresponding interface file `graph_decompositions/modular_decomposition.pxd` - a small reorganization of the method `modular_decomposition` of the class Graph: this method now accept a parameter `algorithm` (that can be 'habib_maurer' for the previous algorithm, or 'corneil_habib_paul_tedder' for the new one, which is the default value), it then call the function `modular_decomposition` from `graph_decompositions/modular_decomposition.pyx` which dispatch the call to the correct function (`habib_maurer_algorithm` or ` corneil_habib_paul_tedder_algorithm`) - a new parameter `algorithm` for the `is_prime` method (which is passed directly to the `modular_decomposition` method) - a new `is_module` method for the Graph class - a small clean up of the file `graph_decompositions/modular_decomposition.pyx` to remove unused field of the `Node` class and useless functions. - a new `prime_node_generator` parameter for the `md_tree_to_graph` (used for generating modular decomposition tree for tests) In this PR, I did not change the output of the `modular_decomposition` method to keep the changes to a minimum. If this PR is accepted, I intend to do it in another PR. The goal would be to return a object that contains all the information of the modular decomposition tree: currently the two formats for the output have no information for the prime graph corresponding to prime nodes. It can be computed from the output of the new algorithm but this information is lost during the conversion to one of the output format of the method `modular_decomposition`. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - #12345: short description why this is a dependency --> <!-- - #34567: ... --> URL: #39038 Reported by: cyrilbouvier Reviewer(s): cyrilbouvier, David Coudert
2 parents 2df4906 + 5d65672 commit 3efa0aa

File tree

6 files changed

+1476
-338
lines changed

6 files changed

+1476
-338
lines changed

src/doc/en/reference/references/index.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -277,6 +277,10 @@ REFERENCES:
277277
.. [Ap1997] \T. Apostol, Modular functions and Dirichlet series in
278278
number theory, Springer, 1997 (2nd ed), section 3.7--3.9.
279279
280+
.. [AP2024] William Atherton, Dmitrii V. Pasechnik, *Decline and Fall of the
281+
ICALP 2008 Modular Decomposition algorithm*, 2024.
282+
:arxiv:`2404.14049`.
283+
280284
.. [APR2001] George E. Andrews, Peter Paule, Axel Riese,
281285
*MacMahon's partition analysis: the Omega package*,
282286
European J. Combin. 22 (2001), no. 7, 887--904.

src/sage/graphs/graph.py

Lines changed: 179 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@
8888
- Jean-Florent Raymond (2019-04): is_redundant, is_dominating,
8989
private_neighbors
9090
91+
- Cyril Bouvier (2024-11): is_module
92+
9193
Graph Format
9294
------------
9395
@@ -7181,20 +7183,131 @@ def cores(self, k=None, with_labels=False):
71817183
return core
71827184
return list(core.values())
71837185

7184-
@doc_index("Leftovers")
7185-
def modular_decomposition(self, algorithm=None, style='tuple'):
7186+
@doc_index("Modules")
7187+
def is_module(self, vertices):
7188+
r"""
7189+
Check whether ``vertices`` is a module of ``self``.
7190+
7191+
A subset `M` of the vertices of a graph is a module if for every
7192+
vertex `v` outside of `M`, either all vertices of `M` are neighbors of
7193+
`v` or all vertices of `M` are not neighbors of `v`.
7194+
7195+
INPUT:
7196+
7197+
- ``vertices`` -- iterable; a subset of vertices of ``self``
7198+
7199+
EXAMPLES:
7200+
7201+
The whole graph, the empty set and singletons are trivial modules::
7202+
7203+
sage: G = graphs.PetersenGraph()
7204+
sage: G.is_module([])
7205+
True
7206+
sage: G.is_module([G.random_vertex()])
7207+
True
7208+
sage: G.is_module(G)
7209+
True
7210+
7211+
Prime graphs only have trivial modules::
7212+
7213+
sage: G = graphs.PathGraph(5)
7214+
sage: G.is_prime()
7215+
True
7216+
sage: all(not G.is_module(S) for S in subsets(G)
7217+
....: if len(S) > 1 and len(S) < G.order())
7218+
True
7219+
7220+
For edgeless graphs and complete graphs, all subsets are modules::
7221+
7222+
sage: G = Graph(5)
7223+
sage: all(G.is_module(S) for S in subsets(G))
7224+
True
7225+
sage: G = graphs.CompleteGraph(5)
7226+
sage: all(G.is_module(S) for S in subsets(G))
7227+
True
7228+
7229+
The modules of a graph and of its complements are the same::
7230+
7231+
sage: G = graphs.TuranGraph(10, 3)
7232+
sage: G.is_module([0,1,2])
7233+
True
7234+
sage: G.complement().is_module([0,1,2])
7235+
True
7236+
sage: G.is_module([3,4,5])
7237+
True
7238+
sage: G.complement().is_module([3,4,5])
7239+
True
7240+
sage: G.is_module([2,3,4])
7241+
False
7242+
sage: G.complement().is_module([2,3,4])
7243+
False
7244+
sage: G.is_module([3,4,5,6,7,8,9])
7245+
True
7246+
sage: G.complement().is_module([3,4,5,6,7,8,9])
7247+
True
7248+
7249+
Elements of ``vertices`` must be in ``self``::
7250+
7251+
sage: G = graphs.PetersenGraph()
7252+
sage: G.is_module(['Terry'])
7253+
Traceback (most recent call last):
7254+
...
7255+
LookupError: vertex (Terry) is not a vertex of the graph
7256+
sage: G.is_module([1, 'Graham'])
7257+
Traceback (most recent call last):
7258+
...
7259+
LookupError: vertex (Graham) is not a vertex of the graph
7260+
"""
7261+
M = set(vertices)
7262+
7263+
for v in M:
7264+
if v not in self:
7265+
raise LookupError(f"vertex ({v}) is not a vertex of the graph")
7266+
7267+
if len(M) <= 1 or len(M) == self.order():
7268+
return True
7269+
7270+
N = None # will contains the neighborhood of M
7271+
for v in M:
7272+
if N is None:
7273+
# first iteration, the neighborhood N must be computed
7274+
N = {u for u in self.neighbor_iterator(v) if u not in M}
7275+
else:
7276+
# check that the neighborhood of v is N
7277+
n = 0
7278+
for u in self.neighbor_iterator(v):
7279+
if u not in M:
7280+
n += 1
7281+
if u not in N:
7282+
return False # u is a splitter
7283+
if n != len(N):
7284+
return False
7285+
return True
7286+
7287+
@doc_index("Modules")
7288+
def modular_decomposition(self, algorithm=None, style="tuple"):
71867289
r"""
71877290
Return the modular decomposition of the current graph.
71887291
71897292
A module of an undirected graph is a subset of vertices such that every
71907293
vertex outside the module is either connected to all members of the
71917294
module or to none of them. Every graph that has a nontrivial module can
71927295
be partitioned into modules, and the increasingly fine partitions into
7193-
modules form a tree. The ``modular_decomposition`` function returns
7194-
that tree, using an `O(n^3)` algorithm of [HM1979]_.
7296+
modules form a tree. The ``modular_decomposition`` method returns
7297+
that tree.
71957298
71967299
INPUT:
71977300
7301+
- ``algorithm`` -- string (default: ``None``); the algorithm to use
7302+
among:
7303+
7304+
- ``None`` or ``'corneil_habib_paul_tedder'`` -- will use the
7305+
Corneil-Habib-Paul-Tedder algorithm from [TCHP2008]_, its complexity
7306+
is linear in the number of vertices and edges.
7307+
7308+
- ``'habib_maurer'`` -- will use the Habib-Maurer algorithm from
7309+
[HM1979]_, its complexity is cubic in the number of vertices.
7310+
71987311
- ``style`` -- string (default: ``'tuple'``); specifies the output
71997312
format:
72007313
@@ -7204,16 +7317,10 @@ def modular_decomposition(self, algorithm=None, style='tuple'):
72047317
72057318
OUTPUT:
72067319
7207-
A pair of two values (recursively encoding the decomposition) :
7208-
7209-
* The type of the current module :
7210-
7211-
* ``'PARALLEL'``
7212-
* ``'PRIME'``
7213-
* ``'SERIES'``
7214-
7215-
* The list of submodules (as list of pairs ``(type, list)``,
7216-
recursively...) or the vertex's name if the module is a singleton.
7320+
The modular decomposition tree, either as nested tuples (if
7321+
``style='tuple'``) or as an object of
7322+
:class:`~sage.combinat.rooted_tree.LabelledRootedTree` (if
7323+
``style='tree'``)
72177324
72187325
Crash course on modular decomposition:
72197326
@@ -7266,7 +7373,19 @@ def modular_decomposition(self, algorithm=None, style='tuple'):
72667373
The Petersen Graph too::
72677374
72687375
sage: graphs.PetersenGraph().modular_decomposition()
7269-
(PRIME, [1, 4, 5, 0, 2, 6, 3, 7, 8, 9])
7376+
(PRIME, [1, 4, 5, 0, 6, 2, 3, 9, 7, 8])
7377+
7378+
Graph from the :wikipedia:`Modular_decomposition`::
7379+
7380+
sage: G = Graph('Jv\\zoKF@wN?', format='graph6')
7381+
sage: G.relabel([1..11])
7382+
sage: G.modular_decomposition()
7383+
(PRIME,
7384+
[(SERIES, [4, (PARALLEL, [2, 3])]),
7385+
1,
7386+
5,
7387+
(PARALLEL, [6, 7]),
7388+
(SERIES, [(PARALLEL, [10, 11]), 9, 8])])
72707389
72717390
This a clique on 5 vertices with 2 pendant edges, though, has a more
72727391
interesting decomposition::
@@ -7275,14 +7394,20 @@ def modular_decomposition(self, algorithm=None, style='tuple'):
72757394
sage: g.add_edge(0,5)
72767395
sage: g.add_edge(0,6)
72777396
sage: g.modular_decomposition()
7278-
(SERIES, [(PARALLEL, [(SERIES, [1, 2, 3, 4]), 5, 6]), 0])
7397+
(SERIES, [(PARALLEL, [(SERIES, [3, 4, 2, 1]), 5, 6]), 0])
7398+
7399+
Turán graphs are co-graphs::
7400+
7401+
sage: graphs.TuranGraph(11, 3).modular_decomposition()
7402+
(SERIES,
7403+
[(PARALLEL, [7, 8, 9, 10]), (PARALLEL, [3, 4, 5, 6]), (PARALLEL, [0, 1, 2])])
72797404
72807405
We can choose output to be a
72817406
:class:`~sage.combinat.rooted_tree.LabelledRootedTree`::
72827407
72837408
sage: g.modular_decomposition(style='tree')
72847409
SERIES[0[], PARALLEL[5[], 6[], SERIES[1[], 2[], 3[], 4[]]]]
7285-
sage: ascii_art(g.modular_decomposition(style='tree'))
7410+
sage: ascii_art(g.modular_decomposition(algorithm="habib_maurer",style='tree'))
72867411
__SERIES
72877412
/ /
72887413
0 ___PARALLEL
@@ -7293,18 +7418,26 @@ def modular_decomposition(self, algorithm=None, style='tuple'):
72937418
72947419
ALGORITHM:
72957420
7296-
This function uses the algorithm of M. Habib and M. Maurer [HM1979]_.
7421+
This function can use either the algorithm of D. Corneil, M. Habib, C.
7422+
Paul and M. Tedder [TCHP2008]_ or the algorithm of M. Habib and M.
7423+
Maurer [HM1979]_.
72977424
72987425
.. SEEALSO::
72997426
73007427
- :meth:`is_prime` -- tests whether a graph is prime
73017428
73027429
- :class:`~sage.combinat.rooted_tree.LabelledRootedTree`.
73037430
7431+
- :func:`~sage.graphs.graph_decompositions.modular_decomposition.corneil_habib_paul_tedder_algorithm`
7432+
7433+
- :func:`~sage.graphs.graph_decompositions.modular_decomposition.habib_maurer_algorithm`
7434+
73047435
.. NOTE::
73057436
7306-
A buggy implementation of linear time algorithm from [TCHP2008]_ was
7307-
removed in Sage 9.7, see :issue:`25872`.
7437+
A buggy implementation of the linear time algorithm from [TCHP2008]_
7438+
was removed in Sage 9.7, see :issue:`25872`. A new implementation
7439+
was reintroduced in Sage 10.6 after some corrections to the original
7440+
algorithm, see :issue:`39038`.
73087441
73097442
TESTS:
73107443
@@ -7343,53 +7476,45 @@ def modular_decomposition(self, algorithm=None, style='tuple'):
73437476
sage: G2 = Graph('F@Nfg')
73447477
sage: G1.is_isomorphic(G2)
73457478
True
7346-
sage: G1.modular_decomposition()
7479+
sage: G1.modular_decomposition(algorithm="habib_maurer")
73477480
(PRIME, [1, 2, 5, 6, 0, (PARALLEL, [3, 4])])
7348-
sage: G2.modular_decomposition()
7481+
sage: G2.modular_decomposition(algorithm="habib_maurer")
73497482
(PRIME, [5, 6, 3, 4, 2, (PARALLEL, [0, 1])])
7483+
sage: G1.modular_decomposition(algorithm="corneil_habib_paul_tedder")
7484+
(PRIME, [6, 5, 1, 2, 0, (PARALLEL, [3, 4])])
7485+
sage: G2.modular_decomposition(algorithm="corneil_habib_paul_tedder")
7486+
(PRIME, [6, 5, (PARALLEL, [0, 1]), 2, 3, 4])
73507487
73517488
Check that :issue:`37631` is fixed::
73527489
73537490
sage: G = Graph('GxJEE?')
7354-
sage: G.modular_decomposition(style='tree')
7491+
sage: G.modular_decomposition(algorithm="habib_maurer",style='tree')
73557492
PRIME[2[], SERIES[0[], 1[]], PARALLEL[3[], 4[]],
73567493
PARALLEL[5[], 6[], 7[]]]
73577494
"""
7358-
from sage.graphs.graph_decompositions.modular_decomposition import (NodeType,
7359-
habib_maurer_algorithm,
7360-
create_prime_node,
7361-
create_normal_node)
7362-
7363-
if algorithm is not None:
7364-
from sage.misc.superseded import deprecation
7365-
deprecation(25872, "algorithm=... parameter is obsolete and has no effect.")
7366-
self._scream_if_not_simple()
7495+
from sage.graphs.graph_decompositions.modular_decomposition import \
7496+
modular_decomposition
73677497

7368-
if not self.order():
7369-
D = None
7370-
elif self.order() == 1:
7371-
D = create_normal_node(next(self.vertex_iterator()))
7372-
else:
7373-
D = habib_maurer_algorithm(self)
7498+
D = modular_decomposition(self, algorithm=algorithm)
73747499

73757500
if style == 'tuple':
7376-
if D is None:
7501+
if D.is_empty():
73777502
return tuple()
73787503

73797504
def relabel(x):
7380-
if x.node_type == NodeType.NORMAL:
7505+
if x.is_leaf():
73817506
return x.children[0]
73827507
return x.node_type, [relabel(y) for y in x.children]
73837508

73847509
return relabel(D)
73857510

73867511
elif style == 'tree':
73877512
from sage.combinat.rooted_tree import LabelledRootedTree
7388-
if D is None:
7513+
if D.is_empty():
73897514
return LabelledRootedTree([])
73907515

73917516
def to_tree(x):
7392-
if x.node_type == NodeType.NORMAL:
7517+
if x.is_leaf():
73937518
return LabelledRootedTree([], label=x.children[0])
73947519
return LabelledRootedTree([to_tree(y) for y in x.children],
73957520
label=x.node_type)
@@ -7640,7 +7765,14 @@ def is_prime(self, algorithm=None):
76407765
76417766
A graph is prime if all its modules are trivial (i.e. empty, all of the
76427767
graph or singletons) -- see :meth:`modular_decomposition`.
7643-
Use the `O(n^3)` algorithm of [HM1979]_.
7768+
This method computes the modular decomposition tree using
7769+
:meth:`~sage.graphs.graph.Graph.modular_decomposition`.
7770+
7771+
INPUT:
7772+
7773+
- ``algorithm`` -- string (default: ``None``); the algorithm used to
7774+
compute the modular decomposition tree; the value is forwarded
7775+
directly to :meth:`~sage.graphs.graph.Graph.modular_decomposition`.
76447776
76457777
EXAMPLES:
76467778
@@ -7661,17 +7793,15 @@ def is_prime(self, algorithm=None):
76617793
sage: graphs.EmptyGraph().is_prime()
76627794
True
76637795
"""
7664-
if algorithm is not None:
7665-
from sage.misc.superseded import deprecation
7666-
deprecation(25872, "algorithm=... parameter is obsolete and has no effect.")
7667-
from sage.graphs.graph_decompositions.modular_decomposition import NodeType
7796+
from sage.graphs.graph_decompositions.modular_decomposition import \
7797+
modular_decomposition
76687798

76697799
if self.order() <= 1:
76707800
return True
76717801

7672-
D = self.modular_decomposition()
7802+
MD = modular_decomposition(self, algorithm=algorithm)
76737803

7674-
return D[0] == NodeType.PRIME and len(D[1]) == self.order()
7804+
return MD.is_prime() and len(MD.children) == self.order()
76757805

76767806
@doc_index("Connectivity, orientations, trees")
76777807
def gomory_hu_tree(self, algorithm=None, solver=None, verbose=0,

src/sage/graphs/graph_decompositions/meson.build

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ py.install_sources(
1111
'all.py',
1212
'all__sagemath_tdlib.py',
1313
'fast_digraph.pxd',
14-
'modular_decomposition.py',
14+
'modular_decomposition.pxd',
1515
'rankwidth.pxd',
1616
'slice_decomposition.pxd',
1717
'tree_decomposition.pxd',
@@ -43,6 +43,7 @@ endforeach
4343
extension_data_cpp = {
4444
'clique_separators': files('clique_separators.pyx'),
4545
'slice_decomposition' : files('slice_decomposition.pyx'),
46+
'modular_decomposition' : files('modular_decomposition.pyx'),
4647
}
4748

4849
foreach name, pyx : extension_data_cpp

0 commit comments

Comments
 (0)