Skip to content

Commit 0e5ad3f

Browse files
committed
graphs: implementation of linear-time algorithm for modular decomposition
1 parent 209ae4c commit 0e5ad3f

File tree

4 files changed

+1259
-339
lines changed

4 files changed

+1259
-339
lines changed

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+
Return 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) == 0 or 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:`xxxxx`.
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
def _gomory_hu_tree(self, vertices, algorithm=None):
76777807
r"""

0 commit comments

Comments
 (0)