passagemath-graphs 10.6.1rc1__cp310-cp310-musllinux_1_2_aarch64.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- passagemath_graphs-10.6.1rc1.dist-info/METADATA +292 -0
- passagemath_graphs-10.6.1rc1.dist-info/RECORD +260 -0
- passagemath_graphs-10.6.1rc1.dist-info/WHEEL +5 -0
- passagemath_graphs-10.6.1rc1.dist-info/top_level.txt +2 -0
- passagemath_graphs.libs/libgcc_s-69c45f16.so.1 +0 -0
- passagemath_graphs.libs/libgmp-8e78bd9b.so.10.5.0 +0 -0
- passagemath_graphs.libs/libstdc++-1f1a71be.so.6.0.33 +0 -0
- sage/all__sagemath_graphs.py +39 -0
- sage/combinat/abstract_tree.py +2723 -0
- sage/combinat/all__sagemath_graphs.py +34 -0
- sage/combinat/binary_tree.py +5306 -0
- sage/combinat/cluster_algebra_quiver/all.py +22 -0
- sage/combinat/cluster_algebra_quiver/cluster_seed.py +5208 -0
- sage/combinat/cluster_algebra_quiver/interact.py +124 -0
- sage/combinat/cluster_algebra_quiver/mutation_class.py +625 -0
- sage/combinat/cluster_algebra_quiver/mutation_type.py +1555 -0
- sage/combinat/cluster_algebra_quiver/quiver.py +2290 -0
- sage/combinat/cluster_algebra_quiver/quiver_mutation_type.py +2468 -0
- sage/combinat/designs/MOLS_handbook_data.py +570 -0
- sage/combinat/designs/all.py +58 -0
- sage/combinat/designs/bibd.py +1655 -0
- sage/combinat/designs/block_design.py +1071 -0
- sage/combinat/designs/covering_array.py +269 -0
- sage/combinat/designs/covering_design.py +530 -0
- sage/combinat/designs/database.py +5615 -0
- sage/combinat/designs/design_catalog.py +122 -0
- sage/combinat/designs/designs_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/designs_pyx.pxd +21 -0
- sage/combinat/designs/designs_pyx.pyx +993 -0
- sage/combinat/designs/difference_family.py +3951 -0
- sage/combinat/designs/difference_matrices.py +279 -0
- sage/combinat/designs/evenly_distributed_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/evenly_distributed_sets.pyx +661 -0
- sage/combinat/designs/ext_rep.py +1064 -0
- sage/combinat/designs/gen_quadrangles_with_spread.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/gen_quadrangles_with_spread.pyx +339 -0
- sage/combinat/designs/group_divisible_designs.py +361 -0
- sage/combinat/designs/incidence_structures.py +2357 -0
- sage/combinat/designs/latin_squares.py +581 -0
- sage/combinat/designs/orthogonal_arrays.py +2244 -0
- sage/combinat/designs/orthogonal_arrays_build_recursive.py +1780 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/orthogonal_arrays_find_recursive.pyx +967 -0
- sage/combinat/designs/resolvable_bibd.py +815 -0
- sage/combinat/designs/steiner_quadruple_systems.py +1306 -0
- sage/combinat/designs/subhypergraph_search.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/designs/subhypergraph_search.pyx +530 -0
- sage/combinat/designs/twographs.py +306 -0
- sage/combinat/finite_state_machine.py +14874 -0
- sage/combinat/finite_state_machine_generators.py +2006 -0
- sage/combinat/graph_path.py +448 -0
- sage/combinat/interval_posets.py +3908 -0
- sage/combinat/nu_tamari_lattice.py +269 -0
- sage/combinat/ordered_tree.py +1446 -0
- sage/combinat/posets/all.py +46 -0
- sage/combinat/posets/bubble_shuffle.py +247 -0
- sage/combinat/posets/cartesian_product.py +493 -0
- sage/combinat/posets/d_complete.py +182 -0
- sage/combinat/posets/elements.py +273 -0
- sage/combinat/posets/forest.py +30 -0
- sage/combinat/posets/hasse_cython.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/hasse_cython.pyx +174 -0
- sage/combinat/posets/hasse_diagram.py +3672 -0
- sage/combinat/posets/hochschild_lattice.py +158 -0
- sage/combinat/posets/incidence_algebras.py +794 -0
- sage/combinat/posets/lattices.py +5117 -0
- sage/combinat/posets/linear_extension_iterator.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/combinat/posets/linear_extension_iterator.pyx +292 -0
- sage/combinat/posets/linear_extensions.py +1037 -0
- sage/combinat/posets/mobile.py +275 -0
- sage/combinat/posets/moebius_algebra.py +776 -0
- sage/combinat/posets/poset_examples.py +2178 -0
- sage/combinat/posets/posets.py +9360 -0
- sage/combinat/rooted_tree.py +1070 -0
- sage/combinat/shard_order.py +239 -0
- sage/combinat/tamari_lattices.py +384 -0
- sage/combinat/yang_baxter_graph.py +923 -0
- sage/databases/all__sagemath_graphs.py +1 -0
- sage/databases/knotinfo_db.py +1231 -0
- sage/ext_data/all__sagemath_graphs.py +1 -0
- sage/ext_data/graphs/graph_plot_js.html +330 -0
- sage/ext_data/kenzo/CP2.txt +45 -0
- sage/ext_data/kenzo/CP3.txt +349 -0
- sage/ext_data/kenzo/CP4.txt +4774 -0
- sage/ext_data/kenzo/README.txt +49 -0
- sage/ext_data/kenzo/S4.txt +20 -0
- sage/graphs/all.py +42 -0
- sage/graphs/asteroidal_triples.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/asteroidal_triples.pyx +320 -0
- sage/graphs/base/all.py +1 -0
- sage/graphs/base/boost_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/boost_graph.pxd +106 -0
- sage/graphs/base/boost_graph.pyx +3045 -0
- sage/graphs/base/c_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/c_graph.pxd +106 -0
- sage/graphs/base/c_graph.pyx +5096 -0
- sage/graphs/base/dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/dense_graph.pxd +28 -0
- sage/graphs/base/dense_graph.pyx +801 -0
- sage/graphs/base/graph_backends.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/graph_backends.pxd +5 -0
- sage/graphs/base/graph_backends.pyx +797 -0
- sage/graphs/base/overview.py +85 -0
- sage/graphs/base/sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/sparse_graph.pxd +90 -0
- sage/graphs/base/sparse_graph.pyx +1653 -0
- sage/graphs/base/static_dense_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_dense_graph.pxd +5 -0
- sage/graphs/base/static_dense_graph.pyx +1032 -0
- sage/graphs/base/static_sparse_backend.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_backend.pxd +27 -0
- sage/graphs/base/static_sparse_backend.pyx +1583 -0
- sage/graphs/base/static_sparse_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/base/static_sparse_graph.pxd +37 -0
- sage/graphs/base/static_sparse_graph.pyx +1375 -0
- sage/graphs/bipartite_graph.py +2732 -0
- sage/graphs/centrality.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/centrality.pyx +1038 -0
- sage/graphs/cographs.py +519 -0
- sage/graphs/comparability.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/comparability.pyx +851 -0
- sage/graphs/connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/connectivity.pxd +157 -0
- sage/graphs/connectivity.pyx +4813 -0
- sage/graphs/convexity_properties.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/convexity_properties.pxd +16 -0
- sage/graphs/convexity_properties.pyx +870 -0
- sage/graphs/digraph.py +4754 -0
- sage/graphs/digraph_generators.py +1993 -0
- sage/graphs/distances_all_pairs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/distances_all_pairs.pxd +12 -0
- sage/graphs/distances_all_pairs.pyx +2938 -0
- sage/graphs/domination.py +1363 -0
- sage/graphs/dot2tex_utils.py +100 -0
- sage/graphs/edge_connectivity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/edge_connectivity.pyx +1215 -0
- sage/graphs/generators/all.py +1 -0
- sage/graphs/generators/basic.py +1769 -0
- sage/graphs/generators/chessboard.py +538 -0
- sage/graphs/generators/classical_geometries.py +1611 -0
- sage/graphs/generators/degree_sequence.py +235 -0
- sage/graphs/generators/distance_regular.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generators/distance_regular.pyx +2846 -0
- sage/graphs/generators/families.py +4759 -0
- sage/graphs/generators/intersection.py +565 -0
- sage/graphs/generators/platonic_solids.py +262 -0
- sage/graphs/generators/random.py +2623 -0
- sage/graphs/generators/smallgraphs.py +5741 -0
- sage/graphs/generators/world_map.py +724 -0
- sage/graphs/generic_graph.py +26867 -0
- sage/graphs/generic_graph_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/generic_graph_pyx.pxd +34 -0
- sage/graphs/generic_graph_pyx.pyx +1673 -0
- sage/graphs/genus.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/genus.pyx +622 -0
- sage/graphs/graph.py +9645 -0
- sage/graphs/graph_coloring.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_coloring.pyx +2284 -0
- sage/graphs/graph_database.py +1177 -0
- sage/graphs/graph_decompositions/all.py +1 -0
- sage/graphs/graph_decompositions/bandwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/bandwidth.pyx +428 -0
- sage/graphs/graph_decompositions/clique_separators.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/clique_separators.pyx +616 -0
- sage/graphs/graph_decompositions/cutwidth.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/cutwidth.pyx +753 -0
- sage/graphs/graph_decompositions/fast_digraph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/fast_digraph.pxd +13 -0
- sage/graphs/graph_decompositions/fast_digraph.pyx +212 -0
- sage/graphs/graph_decompositions/graph_products.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/graph_products.pyx +508 -0
- sage/graphs/graph_decompositions/modular_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/modular_decomposition.pxd +27 -0
- sage/graphs/graph_decompositions/modular_decomposition.pyx +1536 -0
- sage/graphs/graph_decompositions/slice_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/slice_decomposition.pxd +18 -0
- sage/graphs/graph_decompositions/slice_decomposition.pyx +1106 -0
- sage/graphs/graph_decompositions/tree_decomposition.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/tree_decomposition.pxd +17 -0
- sage/graphs/graph_decompositions/tree_decomposition.pyx +1996 -0
- sage/graphs/graph_decompositions/vertex_separation.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_decompositions/vertex_separation.pxd +5 -0
- sage/graphs/graph_decompositions/vertex_separation.pyx +1963 -0
- sage/graphs/graph_editor.py +82 -0
- sage/graphs/graph_generators.py +3314 -0
- sage/graphs/graph_generators_pyx.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/graph_generators_pyx.pyx +95 -0
- sage/graphs/graph_input.py +812 -0
- sage/graphs/graph_latex.py +2064 -0
- sage/graphs/graph_list.py +410 -0
- sage/graphs/graph_plot.py +1756 -0
- sage/graphs/graph_plot_js.py +338 -0
- sage/graphs/hyperbolicity.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/hyperbolicity.pyx +1704 -0
- sage/graphs/hypergraph_generators.py +364 -0
- sage/graphs/independent_sets.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/independent_sets.pxd +13 -0
- sage/graphs/independent_sets.pyx +402 -0
- sage/graphs/isgci.py +1033 -0
- sage/graphs/isoperimetric_inequalities.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/isoperimetric_inequalities.pyx +489 -0
- sage/graphs/line_graph.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/line_graph.pyx +743 -0
- sage/graphs/lovasz_theta.py +77 -0
- sage/graphs/matching.py +1633 -0
- sage/graphs/matching_covered_graph.py +3590 -0
- sage/graphs/orientations.py +1489 -0
- sage/graphs/partial_cube.py +459 -0
- sage/graphs/path_enumeration.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/path_enumeration.pyx +2040 -0
- sage/graphs/pq_trees.py +1129 -0
- sage/graphs/print_graphs.py +201 -0
- sage/graphs/schnyder.py +865 -0
- sage/graphs/spanning_tree.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/spanning_tree.pyx +1457 -0
- sage/graphs/strongly_regular_db.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/strongly_regular_db.pyx +3340 -0
- sage/graphs/traversals.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/traversals.pxd +9 -0
- sage/graphs/traversals.pyx +1872 -0
- sage/graphs/trees.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/trees.pxd +15 -0
- sage/graphs/trees.pyx +310 -0
- sage/graphs/tutte_polynomial.py +713 -0
- sage/graphs/views.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/views.pyx +794 -0
- sage/graphs/weakly_chordal.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/graphs/weakly_chordal.pyx +604 -0
- sage/groups/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/all__sagemath_graphs.py +1 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.cpython-310-aarch64-linux-gnu.so +0 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pxd +38 -0
- sage/groups/perm_gps/partn_ref/refinement_graphs.pyx +1666 -0
- sage/knots/all.py +6 -0
- sage/knots/free_knotinfo_monoid.py +507 -0
- sage/knots/gauss_code.py +291 -0
- sage/knots/knot.py +682 -0
- sage/knots/knot_table.py +284 -0
- sage/knots/knotinfo.py +2900 -0
- sage/knots/link.py +4715 -0
- sage/sandpiles/all.py +13 -0
- sage/sandpiles/examples.py +225 -0
- sage/sandpiles/sandpile.py +6365 -0
- sage/topology/all.py +22 -0
- sage/topology/cell_complex.py +1214 -0
- sage/topology/cubical_complex.py +1976 -0
- sage/topology/delta_complex.py +1806 -0
- sage/topology/filtered_simplicial_complex.py +744 -0
- sage/topology/moment_angle_complex.py +823 -0
- sage/topology/simplicial_complex.py +5160 -0
- sage/topology/simplicial_complex_catalog.py +92 -0
- sage/topology/simplicial_complex_examples.py +1680 -0
- sage/topology/simplicial_complex_homset.py +205 -0
- sage/topology/simplicial_complex_morphism.py +836 -0
- sage/topology/simplicial_set.py +4102 -0
- sage/topology/simplicial_set_catalog.py +55 -0
- sage/topology/simplicial_set_constructions.py +2954 -0
- sage/topology/simplicial_set_examples.py +865 -0
- sage/topology/simplicial_set_morphism.py +1464 -0
@@ -0,0 +1,1996 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-graphs
|
2
|
+
# cython: binding=True
|
3
|
+
r"""
|
4
|
+
Tree decompositions
|
5
|
+
|
6
|
+
This module implements tree-decomposition methods.
|
7
|
+
|
8
|
+
A tree-decomposition of a graph `G = (V, E)` is a pair `(X, T)`, where `X=\{X_1,
|
9
|
+
X_2, \ldots, X_t\}` is a family of subsets of `V`, usually called *bags*, and
|
10
|
+
`T` is a tree of order `t` whose nodes are the subsets `X_i` satisfying the
|
11
|
+
following properties:
|
12
|
+
|
13
|
+
- The union of all sets `X_i` equals `V`. That is, each vertex of the graph `G`
|
14
|
+
is associated with at least one tree node.
|
15
|
+
|
16
|
+
- For every edge `(v, w)` in the graph, there is a subset `X_i` that contains
|
17
|
+
both `v` and `w`. That is, each edge of the graph `G` appears in a tree node.
|
18
|
+
|
19
|
+
- The nodes associated with vertex `v \in V` form a connected subtree of
|
20
|
+
`T`. That is, if `X_i` and `X_j` both contain a vertex `v \in V`, then all
|
21
|
+
nodes `X_k` of the tree in the (unique) path between `X_i` and `X_j` contain
|
22
|
+
`v` as well, and we have `X_i \cap X_j \subseteq X_k`.
|
23
|
+
|
24
|
+
The *width* of a tree decomposition is the size of the largest set `X_i` minus
|
25
|
+
one, i.e., `\max_{X_i \in X} |X_i| - 1`, and the *treewidth* `tw(G)` of a graph
|
26
|
+
`G` is the minimum width among all possible tree decompositions of `G`. Observe
|
27
|
+
that, the size of the largest set is diminished by one in order to make the
|
28
|
+
treewidth of a tree equal to one.
|
29
|
+
|
30
|
+
The *length* of a tree decomposition, as proposed in [DG2006]_, is the maximum
|
31
|
+
*diameter* in `G` of its bags, where the diameter of a bag `X_i` is the largest
|
32
|
+
distance in `G` between the vertices in `X_i` (i.e., `\max_{u, v \in X_i}
|
33
|
+
\dist_G(u, v)`). The *treelength* `tl(G)` of a graph `G` is the minimum length
|
34
|
+
among all possible tree decompositions of `G`.
|
35
|
+
|
36
|
+
While deciding whether a graph has treelength 1 can be done in linear time
|
37
|
+
(equivalent to deciding if the graph is chordal), deciding if it has treelength
|
38
|
+
at most `k` for any fixed constant `k \leq 2` is NP-complete [Lokshtanov2009]_.
|
39
|
+
|
40
|
+
Treewidth and treelength are different measures of tree-likeness. In particular,
|
41
|
+
trees have treewidth and treelength 1::
|
42
|
+
|
43
|
+
sage: T = graphs.RandomTree(20)
|
44
|
+
sage: T.treewidth() # needs cliquer
|
45
|
+
1
|
46
|
+
sage: T.treelength()
|
47
|
+
1
|
48
|
+
|
49
|
+
The treewidth of a cycle is 2 and its treelength is `\lceil n/3 \rceil`::
|
50
|
+
|
51
|
+
sage: [graphs.CycleGraph(n).treewidth() for n in range(3, 11)] # needs cliquer
|
52
|
+
[2, 2, 2, 2, 2, 2, 2, 2]
|
53
|
+
sage: [graphs.CycleGraph(n).treelength() for n in range(3, 11)] # needs cliquer
|
54
|
+
[1, 2, 2, 2, 3, 3, 3, 4]
|
55
|
+
|
56
|
+
The treewidth of a clique is `n-1` and its treelength is 1::
|
57
|
+
|
58
|
+
sage: [graphs.CompleteGraph(n).treewidth() for n in range(3, 11)] # needs cliquer
|
59
|
+
[2, 3, 4, 5, 6, 7, 8, 9]
|
60
|
+
sage: [graphs.CompleteGraph(n).treelength() for n in range(3, 11)] # needs cliquer
|
61
|
+
[1, 1, 1, 1, 1, 1, 1, 1]
|
62
|
+
|
63
|
+
|
64
|
+
.. SEEALSO::
|
65
|
+
|
66
|
+
- :wikipedia:`Tree_decomposition`
|
67
|
+
- :wikipedia:`Treewidth`
|
68
|
+
|
69
|
+
|
70
|
+
**This module contains the following methods**
|
71
|
+
|
72
|
+
.. csv-table::
|
73
|
+
:class: contentstable
|
74
|
+
:widths: 30, 70
|
75
|
+
:delim: |
|
76
|
+
|
77
|
+
:meth:`treewidth` | Compute the treewidth of `G` (and provide a decomposition).
|
78
|
+
:meth:`treelength` | Compute the treelength of `G` (and provide a decomposition).
|
79
|
+
:meth:`make_nice_tree_decomposition` | Return a *nice* tree decomposition (TD) of the TD ``tree_decomp``.
|
80
|
+
:meth:`label_nice_tree_decomposition` | Return a nice tree decomposition with nodes labelled accordingly.
|
81
|
+
:meth:`is_valid_tree_decomposition` | Check whether `T` is a valid tree-decomposition for `G`.
|
82
|
+
:meth:`reduced_tree_decomposition` | Return a reduced tree-decomposition of `T`.
|
83
|
+
:meth:`width_of_tree_decomposition` | Return the width of the tree decomposition `T` of `G`.
|
84
|
+
:meth:`length_of_tree_decomposition` | Return the length of the tree decomposition `T` of `G`.
|
85
|
+
|
86
|
+
|
87
|
+
.. TODO:
|
88
|
+
|
89
|
+
- Approximation of treelength based on :meth:`~sage.graphs.graph.Graph.lex_M`
|
90
|
+
- Approximation of treelength based on BFS Layering
|
91
|
+
|
92
|
+
|
93
|
+
Methods
|
94
|
+
-------
|
95
|
+
"""
|
96
|
+
# ****************************************************************************
|
97
|
+
# Copyright (C) 2020 David Coudert <david.coudert@inria.fr>
|
98
|
+
#
|
99
|
+
# This program is free software: you can redistribute it and/or modify
|
100
|
+
# it under the terms of the GNU General Public License as published by
|
101
|
+
# the Free Software Foundation, either version 2 of the License, or
|
102
|
+
# (at your option) any later version.
|
103
|
+
# https://www.gnu.org/licenses/
|
104
|
+
# ****************************************************************************
|
105
|
+
|
106
|
+
from sage.sets.set import Set
|
107
|
+
from sage.misc.cachefunc import cached_function
|
108
|
+
from sage.sets.disjoint_set import DisjointSet
|
109
|
+
from sage.rings.infinity import Infinity
|
110
|
+
from sage.graphs.distances_all_pairs cimport c_distances_all_pairs
|
111
|
+
from cysignals.memory cimport sig_calloc, sig_free
|
112
|
+
|
113
|
+
from itertools import chain
|
114
|
+
from collections.abc import Iterable
|
115
|
+
|
116
|
+
|
117
|
+
def is_valid_tree_decomposition(G, T):
|
118
|
+
r"""
|
119
|
+
Check whether `T` is a valid tree-decomposition for `G`.
|
120
|
+
|
121
|
+
INPUT:
|
122
|
+
|
123
|
+
- ``G`` -- a sage Graph
|
124
|
+
|
125
|
+
- ``T`` -- a tree decomposition, i.e., a tree whose vertices are the bags
|
126
|
+
(subsets of vertices) of the decomposition
|
127
|
+
|
128
|
+
EXAMPLES::
|
129
|
+
|
130
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import is_valid_tree_decomposition
|
131
|
+
sage: K = graphs.CompleteGraph(4)
|
132
|
+
sage: T = Graph()
|
133
|
+
sage: T.add_vertex(Set(K))
|
134
|
+
sage: is_valid_tree_decomposition(K, T)
|
135
|
+
True
|
136
|
+
|
137
|
+
sage: G = graphs.RandomGNP(10, .2)
|
138
|
+
sage: T = G.treewidth(certificate=True) # needs cliquer
|
139
|
+
sage: is_valid_tree_decomposition(G, T) # needs cliquer
|
140
|
+
True
|
141
|
+
|
142
|
+
The union of the bags is the set of vertices of `G`::
|
143
|
+
|
144
|
+
sage: G = graphs.PathGraph(4)
|
145
|
+
sage: T = G.treewidth(certificate=True) # needs cliquer
|
146
|
+
sage: _ = G.add_vertex()
|
147
|
+
sage: is_valid_tree_decomposition(G, T) # needs cliquer
|
148
|
+
False
|
149
|
+
|
150
|
+
Each edge of `G` is contained in a bag::
|
151
|
+
|
152
|
+
sage: G = graphs.PathGraph(4)
|
153
|
+
sage: T = G.treewidth(certificate=True) # needs cliquer
|
154
|
+
sage: G.add_edge(0, 3)
|
155
|
+
sage: is_valid_tree_decomposition(G, T) # needs cliquer
|
156
|
+
False
|
157
|
+
|
158
|
+
The bags containing a vertex `v` form a subtree of `T`::
|
159
|
+
|
160
|
+
sage: G = graphs.PathGraph(4)
|
161
|
+
sage: X1, X2, X3 = Set([0, 1]), Set([1, 2]), Set([2, 3])
|
162
|
+
sage: T = Graph([(X1, X3), (X3, X2)])
|
163
|
+
sage: is_valid_tree_decomposition(G, T)
|
164
|
+
False
|
165
|
+
|
166
|
+
TESTS:
|
167
|
+
|
168
|
+
Check that both parameters are sage graphs::
|
169
|
+
|
170
|
+
sage: is_valid_tree_decomposition("foo", Graph())
|
171
|
+
Traceback (most recent call last):
|
172
|
+
...
|
173
|
+
ValueError: the first parameter must be a sage Graph
|
174
|
+
sage: is_valid_tree_decomposition(Graph(), "foo")
|
175
|
+
Traceback (most recent call last):
|
176
|
+
...
|
177
|
+
ValueError: the second parameter must be a sage Graph
|
178
|
+
|
179
|
+
Check that `T` is a tree::
|
180
|
+
|
181
|
+
sage: is_valid_tree_decomposition(Graph(), Graph(2))
|
182
|
+
Traceback (most recent call last):
|
183
|
+
...
|
184
|
+
ValueError: the second parameter must be a tree
|
185
|
+
|
186
|
+
The vertices of `T` must be iterables::
|
187
|
+
|
188
|
+
sage: is_valid_tree_decomposition(Graph(1), Graph(1))
|
189
|
+
Traceback (most recent call last):
|
190
|
+
...
|
191
|
+
ValueError: the vertices of T must be iterables
|
192
|
+
|
193
|
+
Small cases::
|
194
|
+
|
195
|
+
sage: is_valid_tree_decomposition(Graph(), Graph())
|
196
|
+
True
|
197
|
+
sage: is_valid_tree_decomposition(Graph(1), Graph())
|
198
|
+
False
|
199
|
+
"""
|
200
|
+
from sage.graphs.graph import Graph
|
201
|
+
if not isinstance(G, Graph):
|
202
|
+
raise ValueError("the first parameter must be a sage Graph")
|
203
|
+
if not isinstance(T, Graph):
|
204
|
+
raise ValueError("the second parameter must be a sage Graph")
|
205
|
+
if not T:
|
206
|
+
return not G
|
207
|
+
elif not T.is_tree():
|
208
|
+
raise ValueError("the second parameter must be a tree")
|
209
|
+
|
210
|
+
for X in T:
|
211
|
+
if not isinstance(X, Iterable):
|
212
|
+
raise ValueError("the vertices of T must be iterables")
|
213
|
+
|
214
|
+
# 1. The union of the bags equals V
|
215
|
+
if set(G) != set(chain(*T)):
|
216
|
+
return False
|
217
|
+
|
218
|
+
# 2. Each edge of G is contained in a bag
|
219
|
+
vertex_to_bags = {u: set() for u in G}
|
220
|
+
for Xi in T:
|
221
|
+
for u in Xi:
|
222
|
+
vertex_to_bags[u].add(Xi)
|
223
|
+
|
224
|
+
for u, v in G.edge_iterator(labels=False):
|
225
|
+
if all(v not in Xi for Xi in vertex_to_bags[u]):
|
226
|
+
return False
|
227
|
+
|
228
|
+
# 3. The bags containing a vertex v form a connected subset of T
|
229
|
+
for X in vertex_to_bags.values():
|
230
|
+
D = DisjointSet(X)
|
231
|
+
for Xi in X:
|
232
|
+
for Xj in T.neighbor_iterator(Xi):
|
233
|
+
if Xj in X:
|
234
|
+
D.union(Xi, Xj)
|
235
|
+
if D.number_of_subsets() > 1:
|
236
|
+
return False
|
237
|
+
|
238
|
+
return True
|
239
|
+
|
240
|
+
|
241
|
+
def reduced_tree_decomposition(T):
|
242
|
+
r"""
|
243
|
+
Return a reduced tree-decomposition of `T`.
|
244
|
+
|
245
|
+
We merge all edges between two sets `S` and `S'` where `S` is a subset of
|
246
|
+
`S'`. To do so, we use a simple union-find data structure to record merge
|
247
|
+
operations and the good sets.
|
248
|
+
|
249
|
+
.. WARNING::
|
250
|
+
|
251
|
+
This method assumes that the vertices of the input tree `T` are hashable
|
252
|
+
and have attribute ``issuperset``, e.g., ``frozenset`` or
|
253
|
+
:class:`~sage.sets.set.Set_object_enumerated`.
|
254
|
+
|
255
|
+
INPUT:
|
256
|
+
|
257
|
+
- ``T`` -- a tree-decomposition
|
258
|
+
|
259
|
+
EXAMPLES::
|
260
|
+
|
261
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import reduced_tree_decomposition
|
262
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import is_valid_tree_decomposition
|
263
|
+
sage: G = graphs.PathGraph(3)
|
264
|
+
sage: T = Graph()
|
265
|
+
sage: T.add_path([Set([0]), Set([0, 1]), Set([1]), Set([1, 2]), Set([2])])
|
266
|
+
sage: T.order()
|
267
|
+
5
|
268
|
+
sage: is_valid_tree_decomposition(G, T)
|
269
|
+
True
|
270
|
+
sage: T2 = reduced_tree_decomposition(T)
|
271
|
+
sage: is_valid_tree_decomposition(G, T2)
|
272
|
+
True
|
273
|
+
sage: T2.order()
|
274
|
+
2
|
275
|
+
|
276
|
+
TESTS::
|
277
|
+
|
278
|
+
sage: # needs cliquer
|
279
|
+
sage: G = graphs.PathGraph(3)
|
280
|
+
sage: T = G.treewidth(certificate=True)
|
281
|
+
sage: is_valid_tree_decomposition(G, T)
|
282
|
+
True
|
283
|
+
sage: T == reduced_tree_decomposition(T)
|
284
|
+
True
|
285
|
+
sage: G = Graph(1)
|
286
|
+
sage: T = G.treewidth(certificate=True)
|
287
|
+
sage: T.order()
|
288
|
+
1
|
289
|
+
sage: T == reduced_tree_decomposition(T)
|
290
|
+
True
|
291
|
+
"""
|
292
|
+
if T.order() < 2:
|
293
|
+
return T
|
294
|
+
|
295
|
+
def get_ancestor(ancestor, u):
|
296
|
+
if ancestor[u] == u:
|
297
|
+
return u
|
298
|
+
ancestor[u] = get_ancestor(ancestor, ancestor[u])
|
299
|
+
return ancestor[u]
|
300
|
+
|
301
|
+
ancestor = {u: u for u in T}
|
302
|
+
for u, v in T.edge_iterator(labels=False):
|
303
|
+
u = get_ancestor(ancestor, u)
|
304
|
+
v = get_ancestor(ancestor, v)
|
305
|
+
if u == v:
|
306
|
+
continue
|
307
|
+
elif u.issuperset(v):
|
308
|
+
ancestor[v] = u
|
309
|
+
elif v.issuperset(u):
|
310
|
+
ancestor[u] = v
|
311
|
+
|
312
|
+
from sage.graphs.graph import Graph
|
313
|
+
H = Graph(multiedges=False, name="Reduced tree-decomposition of {}".format(T.name()))
|
314
|
+
for u, v in T.edge_iterator(labels=False):
|
315
|
+
u = get_ancestor(ancestor, u)
|
316
|
+
v = get_ancestor(ancestor, v)
|
317
|
+
if u != v:
|
318
|
+
H.add_edge(u, v)
|
319
|
+
return H
|
320
|
+
|
321
|
+
|
322
|
+
def width_of_tree_decomposition(G, T, check=True):
|
323
|
+
r"""
|
324
|
+
Return the width of the tree decomposition `T` of `G`.
|
325
|
+
|
326
|
+
The width of a tree-decomposition is the size of the largest bag minus
|
327
|
+
1. The empty graph and a graph of order 1 have treewidth 0.
|
328
|
+
|
329
|
+
INPUT:
|
330
|
+
|
331
|
+
- ``G`` -- a sage Graph
|
332
|
+
|
333
|
+
- ``T`` -- a tree-decomposition for `G`
|
334
|
+
|
335
|
+
- ``check`` -- boolean (default: ``True``); whether to check that the
|
336
|
+
tree-decomposition `T` is valid for `G`
|
337
|
+
|
338
|
+
EXAMPLES::
|
339
|
+
|
340
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import width_of_tree_decomposition
|
341
|
+
sage: G = graphs.PathGraph(3)
|
342
|
+
sage: T = G.treewidth(certificate=True) # needs cliquer
|
343
|
+
sage: width_of_tree_decomposition(G, T, check=True) # needs cliquer
|
344
|
+
1
|
345
|
+
|
346
|
+
TESTS::
|
347
|
+
|
348
|
+
sage: G = Graph()
|
349
|
+
sage: T = G.treewidth(certificate=True) # needs cliquer
|
350
|
+
sage: width_of_tree_decomposition(G, T, check=True) # needs cliquer
|
351
|
+
0
|
352
|
+
sage: width_of_tree_decomposition(Graph(1), T, check=True) # needs cliquer
|
353
|
+
Traceback (most recent call last):
|
354
|
+
...
|
355
|
+
ValueError: the tree-decomposition is not valid for this graph
|
356
|
+
"""
|
357
|
+
if check and not is_valid_tree_decomposition(G, T):
|
358
|
+
raise ValueError("the tree-decomposition is not valid for this graph")
|
359
|
+
|
360
|
+
if T:
|
361
|
+
return max(0, max(len(u) for u in T) - 1)
|
362
|
+
return 0
|
363
|
+
|
364
|
+
|
365
|
+
def _from_tree_decompositions_of_atoms_to_tree_decomposition(T_atoms, cliques):
|
366
|
+
r"""
|
367
|
+
Return a tree-decomposition formed by the tree-decompositions of the atoms.
|
368
|
+
|
369
|
+
This is a helper method to avoid duplicated code.
|
370
|
+
|
371
|
+
This method builds the tree decomposition of the graph by connecting the
|
372
|
+
tree decompositions of its atoms. This is done in an order that is
|
373
|
+
consistent with the order of the atoms and cliques returned by method
|
374
|
+
:meth:`~Graph.atoms_and_clique_separators`. More precisely, the first clique
|
375
|
+
separates the first atom from the rest of the graph (call G1 this part of
|
376
|
+
the graph), the second clique separates (in G1) the second atom from the
|
377
|
+
rest of the graph G1, etc. So we merge the tree decompositions in the
|
378
|
+
reverse order of the atoms.
|
379
|
+
|
380
|
+
INPUT:
|
381
|
+
|
382
|
+
- ``T_atoms`` -- list of tree-decompositions
|
383
|
+
|
384
|
+
- ``cliques`` -- list of clique separators
|
385
|
+
|
386
|
+
EXAMPLES:
|
387
|
+
|
388
|
+
Indirect doctest::
|
389
|
+
|
390
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import is_valid_tree_decomposition
|
391
|
+
sage: G = graphs.Grid2dGraph(2, 3)
|
392
|
+
sage: T = G.treewidth(algorithm='sage', certificate=True) # needs cliquer
|
393
|
+
sage: is_valid_tree_decomposition(G, T) # needs cliquer
|
394
|
+
True
|
395
|
+
|
396
|
+
TESTS::
|
397
|
+
|
398
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import _from_tree_decompositions_of_atoms_to_tree_decomposition
|
399
|
+
sage: _from_tree_decompositions_of_atoms_to_tree_decomposition([], [Set(range(2))])
|
400
|
+
Traceback (most recent call last):
|
401
|
+
...
|
402
|
+
ValueError: the number of cliques must be one less than the number of tree-decompositions of atoms
|
403
|
+
"""
|
404
|
+
if len(T_atoms) != len(cliques) + 1:
|
405
|
+
raise ValueError("the number of cliques must be one less than the "
|
406
|
+
"number of tree-decompositions of atoms")
|
407
|
+
|
408
|
+
T = T_atoms[-1]
|
409
|
+
for i in range(len(cliques) - 1, -1, -1):
|
410
|
+
A = T_atoms[i]
|
411
|
+
C = cliques[i]
|
412
|
+
|
413
|
+
# We search for a vertex in A and T containing clique C
|
414
|
+
ua, ut = None, None
|
415
|
+
for u in A:
|
416
|
+
if u.issuperset(C):
|
417
|
+
ua = u
|
418
|
+
break
|
419
|
+
for u in T:
|
420
|
+
if u.issuperset(C):
|
421
|
+
ut = u
|
422
|
+
break
|
423
|
+
if ua and ut:
|
424
|
+
# We merge T and A
|
425
|
+
T.add_vertices(A)
|
426
|
+
T.add_edges(A.edges(sort=False))
|
427
|
+
T.add_edge(ua, ut)
|
428
|
+
else:
|
429
|
+
# This should never happen
|
430
|
+
raise RuntimeError("something goes wrong. Please report the issue "
|
431
|
+
"to sage-devel@googlegroups.com")
|
432
|
+
|
433
|
+
return T
|
434
|
+
|
435
|
+
|
436
|
+
def treewidth(g, k=None, kmin=None, certificate=False, algorithm=None, nice=False):
|
437
|
+
r"""
|
438
|
+
Compute the treewidth of `g` (and provide a decomposition).
|
439
|
+
|
440
|
+
INPUT:
|
441
|
+
|
442
|
+
- ``g`` -- a Sage Graph
|
443
|
+
|
444
|
+
- ``k`` -- integer (default: ``None``); indicates the width to be
|
445
|
+
considered. When ``k`` is an integer, the method checks that the graph has
|
446
|
+
treewidth `\leq k`. If ``k`` is ``None`` (default), the method computes
|
447
|
+
the optimal treewidth.
|
448
|
+
|
449
|
+
- ``kmin`` -- integer (default: ``None``); when specified, search for a
|
450
|
+
tree-decomposition of width at least ``kmin``. This parameter is useful
|
451
|
+
when the graph can be decomposed into atoms. This parameter is ignored
|
452
|
+
when ``k`` is not ``None`` or when ``algorithm == 'tdlib'``.
|
453
|
+
|
454
|
+
- ``certificate`` -- boolean (default: ``False``); whether to return the
|
455
|
+
tree-decomposition itself
|
456
|
+
|
457
|
+
- ``algorithm`` -- whether to use ``'sage'`` or ``'tdlib'`` (requires the
|
458
|
+
installation of the :ref:`spkg_sagemath_tdlib` package). The default behaviour is to use
|
459
|
+
'tdlib' if it is available, and Sage's own algorithm when it is not.
|
460
|
+
|
461
|
+
- ``nice`` -- boolean (default: ``False``); whether or not to return the
|
462
|
+
nice tree decomposition, provided ``certificate`` is ``True``
|
463
|
+
|
464
|
+
OUTPUT:
|
465
|
+
|
466
|
+
``g.treewidth()`` returns treewidth of the graph ``g``.
|
467
|
+
|
468
|
+
When ``k`` is specified, it returns ``False`` if there is no tree
|
469
|
+
decomposition of width `\leq k`, and ``True`` otherwise.
|
470
|
+
|
471
|
+
When ``certificate=True``, the tree decomposition is returned.
|
472
|
+
|
473
|
+
When ``nice=True``, the nice tree decomposition is returned.
|
474
|
+
|
475
|
+
ALGORITHM:
|
476
|
+
|
477
|
+
This function virtually explores the graph of all pairs ``(vertex_cut, cc)``,
|
478
|
+
where ``vertex_cut`` is a vertex cut of the graph of cardinality `\leq k+1`,
|
479
|
+
and ``connected_component`` is a connected component of the graph induced by
|
480
|
+
``G-vertex_cut``.
|
481
|
+
|
482
|
+
We deduce that the pair ``(vertex_cut, cc)`` is feasible with treewidth `k`
|
483
|
+
if ``cc`` is empty, or if a vertex ``v`` from ``vertex_cut`` can be replaced
|
484
|
+
with a vertex from ``cc``, such that the pair ``(vertex_cut+v,cc-v)`` is
|
485
|
+
feasible.
|
486
|
+
|
487
|
+
.. NOTE::
|
488
|
+
|
489
|
+
The implementation would be much faster if ``cc``, the argument of the
|
490
|
+
recursive function, was a bitset. It would also be very nice to not copy
|
491
|
+
the graph in order to compute connected components, for this is really a
|
492
|
+
waste of time.
|
493
|
+
|
494
|
+
.. SEEALSO::
|
495
|
+
|
496
|
+
:meth:`~sage.graphs.graph_decompositions.vertex_separation.path_decomposition`
|
497
|
+
computes the pathwidth of a graph. See also the
|
498
|
+
:mod:`~sage.graphs.graph_decompositions.vertex_separation` module.
|
499
|
+
|
500
|
+
EXAMPLES:
|
501
|
+
|
502
|
+
The PetersenGraph has treewidth 4::
|
503
|
+
|
504
|
+
sage: petersen = graphs.PetersenGraph()
|
505
|
+
sage: petersen.treewidth(algorithm='sage') # needs cliquer
|
506
|
+
4
|
507
|
+
sage: petersen.treewidth(algorithm='sage', certificate=True) # needs cliquer
|
508
|
+
Tree decomposition: Graph on 6 vertices
|
509
|
+
|
510
|
+
The PetersenGraph has treewidth 4 (with ``tdlib``)::
|
511
|
+
|
512
|
+
sage: petersen = graphs.PetersenGraph()
|
513
|
+
sage: petersen.treewidth(algorithm='tdlib') # optional - tdlib
|
514
|
+
4
|
515
|
+
sage: petersen.treewidth(algorithm='tdlib', certificate=True) # optional - tdlib
|
516
|
+
Tree decomposition: Graph on 6 vertices
|
517
|
+
|
518
|
+
Nice tree decomposition of the PetersenGraph has 28 nodes::
|
519
|
+
|
520
|
+
sage: petersen = graphs.PetersenGraph()
|
521
|
+
sage: petersen.treewidth(algorithm='sage', certificate=True, nice=True) # needs cliquer
|
522
|
+
Nice tree decomposition of Tree decomposition: Graph on 28 vertices
|
523
|
+
sage: petersen.treewidth(algorithm='tdlib', certificate=True, nice=True) # optional - tdlib
|
524
|
+
Nice tree decomposition of Tree decomposition: Graph on 28 vertices
|
525
|
+
|
526
|
+
The treewidth of a 2-dimensional grid is its smallest side::
|
527
|
+
|
528
|
+
sage: graphs.Grid2dGraph(2,5).treewidth() # needs cliquer
|
529
|
+
2
|
530
|
+
sage: graphs.Grid2dGraph(3,5).treewidth() # needs cliquer
|
531
|
+
3
|
532
|
+
|
533
|
+
When parameter ``kmin`` is specified, the method searches for a
|
534
|
+
tree-decomposition of width at least ``kmin``::
|
535
|
+
|
536
|
+
sage: g = graphs.PetersenGraph()
|
537
|
+
sage: g.treewidth() # needs cliquer
|
538
|
+
4
|
539
|
+
sage: g.treewidth(kmin=2, algorithm='sage') # needs cliquer
|
540
|
+
4
|
541
|
+
sage: g.treewidth(kmin=g.order(), certificate=True, algorithm='sage') # needs cliquer
|
542
|
+
Tree decomposition: Graph on 1 vertex
|
543
|
+
|
544
|
+
TESTS::
|
545
|
+
|
546
|
+
sage: # needs cliquer
|
547
|
+
sage: g = graphs.PathGraph(3)
|
548
|
+
sage: g.treewidth()
|
549
|
+
1
|
550
|
+
sage: g = 2*graphs.PathGraph(3)
|
551
|
+
sage: g.treewidth()
|
552
|
+
1
|
553
|
+
sage: g.treewidth(certificate=True)
|
554
|
+
Tree decomposition: Graph on 4 vertices
|
555
|
+
sage: g.treewidth(2)
|
556
|
+
True
|
557
|
+
sage: g.treewidth(1)
|
558
|
+
True
|
559
|
+
sage: Graph(1).treewidth()
|
560
|
+
0
|
561
|
+
sage: Graph(0).treewidth()
|
562
|
+
-1
|
563
|
+
sage: g = graphs.PetersenGraph()
|
564
|
+
sage: g.treewidth(k=2)
|
565
|
+
False
|
566
|
+
sage: g.treewidth(k=6)
|
567
|
+
True
|
568
|
+
sage: g.treewidth(k=6, kmin=2)
|
569
|
+
True
|
570
|
+
sage: g.treewidth(certificate=True).is_tree()
|
571
|
+
True
|
572
|
+
sage: g.treewidth(k=3, certificate=True)
|
573
|
+
False
|
574
|
+
sage: T = g.treewidth(k=4, certificate=True)
|
575
|
+
sage: T
|
576
|
+
Tree decomposition: Graph on 6 vertices
|
577
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import is_valid_tree_decomposition
|
578
|
+
sage: is_valid_tree_decomposition(g, T)
|
579
|
+
True
|
580
|
+
|
581
|
+
All edges do appear (:issue:`17893`)::
|
582
|
+
|
583
|
+
sage: # needs cliquer
|
584
|
+
sage: from itertools import combinations
|
585
|
+
sage: g = graphs.PathGraph(10)
|
586
|
+
sage: td = g.treewidth(certificate=True)
|
587
|
+
sage: for bag in td:
|
588
|
+
....: g.delete_edges(list(combinations(bag,2)))
|
589
|
+
sage: g.size()
|
590
|
+
0
|
591
|
+
|
592
|
+
:issue:`19358`::
|
593
|
+
|
594
|
+
sage: g = Graph()
|
595
|
+
sage: for i in range(3):
|
596
|
+
....: for j in range(2):
|
597
|
+
....: g.add_path([i,(i,j),(i+1)%3])
|
598
|
+
sage: g.treewidth() # needs cliquer
|
599
|
+
2
|
600
|
+
|
601
|
+
The decomposition is a tree (:issue:`23546`)::
|
602
|
+
|
603
|
+
sage: # needs cliquer
|
604
|
+
sage: g = Graph({0:[1,2], 3:[4,5]})
|
605
|
+
sage: t = g.treewidth(certificate=True)
|
606
|
+
sage: t.is_tree()
|
607
|
+
True
|
608
|
+
sage: vertices = set()
|
609
|
+
sage: for s in t:
|
610
|
+
....: vertices = vertices.union(s)
|
611
|
+
sage: vertices == set(g)
|
612
|
+
True
|
613
|
+
|
614
|
+
Check that the use of atoms and clique separators is correct
|
615
|
+
(:issue:`30993`)::
|
616
|
+
|
617
|
+
sage: # needs cliquer
|
618
|
+
sage: g = 2 * graphs.Grid2dGraph(2, 3)
|
619
|
+
sage: g.treewidth(algorithm='sage')
|
620
|
+
2
|
621
|
+
sage: g.treewidth(algorithm='sage', certificate=True)
|
622
|
+
Tree decomposition: Graph on 8 vertices
|
623
|
+
sage: g.treewidth(algorithm='sage', certificate=True, kmin=4)
|
624
|
+
Tree decomposition: Graph on 4 vertices
|
625
|
+
|
626
|
+
Check that :issue:`38159` is fixed ::
|
627
|
+
|
628
|
+
sage: G = Graph('I~~}vPlr_')
|
629
|
+
sage: G.treewidth(algorithm='sage') == G.treewidth(algorithm='tdlib') # optional - tdlib, needs cliquer
|
630
|
+
True
|
631
|
+
|
632
|
+
Trivially true::
|
633
|
+
|
634
|
+
sage: graphs.PetersenGraph().treewidth(k=35) # needs cliquer
|
635
|
+
True
|
636
|
+
sage: graphs.PetersenGraph().treewidth(k=35, certificate=True) # needs cliquer
|
637
|
+
Tree decomposition: Graph on 1 vertex
|
638
|
+
|
639
|
+
Bad input::
|
640
|
+
|
641
|
+
sage: graphs.PetersenGraph().treewidth(k=-3) # needs cliquer
|
642
|
+
Traceback (most recent call last):
|
643
|
+
...
|
644
|
+
ValueError: k(=-3) must be a nonnegative integer
|
645
|
+
"""
|
646
|
+
# Check Input
|
647
|
+
if algorithm not in [None, 'tdlib', 'sage']:
|
648
|
+
raise ValueError("'algorithm' must be equal to 'tdlib', 'sage', or None")
|
649
|
+
|
650
|
+
try:
|
651
|
+
import sage.graphs.graph_decompositions.tdlib as tdlib
|
652
|
+
tdlib_found = True
|
653
|
+
except ImportError:
|
654
|
+
tdlib_found = False
|
655
|
+
|
656
|
+
if algorithm is None:
|
657
|
+
algorithm = 'tdlib' if tdlib_found else 'sage'
|
658
|
+
|
659
|
+
if (k is not None) and k < 0:
|
660
|
+
raise ValueError(f"k(={k}) must be a nonnegative integer")
|
661
|
+
|
662
|
+
# Silly cases
|
663
|
+
from sage.graphs.graph import Graph
|
664
|
+
if not g.order():
|
665
|
+
if certificate:
|
666
|
+
return Graph()
|
667
|
+
return -1 if k is None else True
|
668
|
+
|
669
|
+
if k is not None and k >= g.order() - 1:
|
670
|
+
if certificate:
|
671
|
+
return Graph({Set(g): []}, name="Tree decomposition")
|
672
|
+
return True
|
673
|
+
|
674
|
+
kmin = 0 if kmin is None else kmin
|
675
|
+
if k is None and kmin >= g.order() - 1:
|
676
|
+
if certificate:
|
677
|
+
return Graph({Set(g): []}, name="Tree decomposition")
|
678
|
+
return kmin
|
679
|
+
|
680
|
+
# TDLIB
|
681
|
+
if algorithm == 'tdlib':
|
682
|
+
if not tdlib_found:
|
683
|
+
from sage.features import FeatureNotPresentError
|
684
|
+
from sage.features.tdlib import Tdlib
|
685
|
+
raise FeatureNotPresentError(Tdlib())
|
686
|
+
|
687
|
+
tree_decomp = tdlib.treedecomposition_exact(g, -1 if k is None else k)
|
688
|
+
width = tdlib.get_width(tree_decomp)
|
689
|
+
|
690
|
+
if certificate:
|
691
|
+
if k is None or width <= k:
|
692
|
+
return make_nice_tree_decomposition(g, tree_decomp) if nice else tree_decomp
|
693
|
+
return False
|
694
|
+
return width if k is None else width <= k
|
695
|
+
|
696
|
+
# The treewidth of a graph is the maximum over its atoms. So, we decompose
|
697
|
+
# the graph by clique minimal separators, compute the treewidth of each of
|
698
|
+
# its atoms, and combine the results.
|
699
|
+
# This decomposition also deals with disconnected cases.
|
700
|
+
atoms, cliques = g.atoms_and_clique_separators()
|
701
|
+
if cliques:
|
702
|
+
# If we do not need the tree decomposition
|
703
|
+
if not certificate:
|
704
|
+
if k is None:
|
705
|
+
for a in atoms:
|
706
|
+
kmin = max(kmin, g.subgraph(a).treewidth(algorithm=algorithm, kmin=kmin))
|
707
|
+
return kmin
|
708
|
+
elif max(len(c) for c in cliques) - 1 > k:
|
709
|
+
return False
|
710
|
+
else:
|
711
|
+
return all(g.subgraph(a).treewidth(algorithm=algorithm, k=k) for a in atoms)
|
712
|
+
|
713
|
+
# Otherwise, compute the tree decomposition of each atom
|
714
|
+
T = []
|
715
|
+
for a in atoms:
|
716
|
+
ga = g.subgraph(a)
|
717
|
+
Ta = ga.treewidth(algorithm=algorithm, certificate=True, kmin=kmin)
|
718
|
+
kmin = max(kmin, width_of_tree_decomposition(ga, Ta, check=False))
|
719
|
+
T.append(Ta)
|
720
|
+
|
721
|
+
# Merge the resulting trees
|
722
|
+
tree_decomp = _from_tree_decompositions_of_atoms_to_tree_decomposition(T, cliques)
|
723
|
+
|
724
|
+
return make_nice_tree_decomposition(g, tree_decomp) if nice else tree_decomp
|
725
|
+
|
726
|
+
# Forcing k to be defined
|
727
|
+
if k is None:
|
728
|
+
for i in range(max(kmin, g.clique_number() - 1, min(g.degree())), g.order()):
|
729
|
+
ans = g.treewidth(algorithm=algorithm, k=i, certificate=certificate, nice=nice)
|
730
|
+
if ans:
|
731
|
+
return ans if certificate else i
|
732
|
+
|
733
|
+
# This is the recursion described in the method's documentation. All
|
734
|
+
# computations are cached, and depends on the pair ``cut,
|
735
|
+
# connected_component`` only.
|
736
|
+
#
|
737
|
+
# It returns either a boolean or the corresponding tree-decomposition, as a
|
738
|
+
# list of edges between vertex cuts (as it is done for the complete
|
739
|
+
# tree-decomposition at the end of the main function.
|
740
|
+
@cached_function
|
741
|
+
def rec(cut, cc):
|
742
|
+
# Easy cases
|
743
|
+
if len(cut) > k:
|
744
|
+
return False
|
745
|
+
if len(cc) + len(cut) <= k + 1:
|
746
|
+
return [(cut, cut.union(cc))] if certificate else True
|
747
|
+
|
748
|
+
# We explore all possible extensions of the cut
|
749
|
+
for v in cc:
|
750
|
+
|
751
|
+
# New cuts and connected components, with v respectively added
|
752
|
+
# and removed
|
753
|
+
cutv = cut.union([v])
|
754
|
+
ccv = cc.difference([v])
|
755
|
+
|
756
|
+
# The values returned by the recursive calls.
|
757
|
+
sons = []
|
758
|
+
|
759
|
+
# Removing v may have disconnected cc. We iterate on its
|
760
|
+
# connected components
|
761
|
+
for cci in g.subgraph(ccv).connected_components(sort=False):
|
762
|
+
|
763
|
+
# The recursive subcalls. We remove on-the-fly the vertices
|
764
|
+
# from the cut which play no role in separating the
|
765
|
+
# connected component from the rest of the graph.
|
766
|
+
reduced_cut = frozenset([x for x in cutv
|
767
|
+
if any(xx in cci for xx in g.neighbor_iterator(x))])
|
768
|
+
son = rec(reduced_cut, frozenset(cci))
|
769
|
+
if not son:
|
770
|
+
break
|
771
|
+
|
772
|
+
if certificate:
|
773
|
+
sons.extend(son)
|
774
|
+
sons.append((cut, cutv))
|
775
|
+
sons.append((cutv, reduced_cut))
|
776
|
+
|
777
|
+
# Weird Python syntax which is useful once in a lifetime : if break
|
778
|
+
# was never called in the loop above, we return "sons".
|
779
|
+
else:
|
780
|
+
return sons if certificate else True
|
781
|
+
|
782
|
+
return False
|
783
|
+
|
784
|
+
# Main call to rec function, i.e. rec({v}, V-{v})
|
785
|
+
V = list(g)
|
786
|
+
v = frozenset([V.pop()])
|
787
|
+
TD = rec(v, frozenset(V))
|
788
|
+
|
789
|
+
if TD is False:
|
790
|
+
return False
|
791
|
+
|
792
|
+
if not certificate:
|
793
|
+
return True
|
794
|
+
|
795
|
+
# Building the tree-decomposition graph. Its vertices are cuts of the
|
796
|
+
# decomposition, and there is an edge from a cut C1 to a cut C2 if C2 is an
|
797
|
+
# immediate subcall of C1
|
798
|
+
tree_decomp = Graph()
|
799
|
+
tree_decomp.add_edges(((Set(x), Set(y)) for x, y in TD), loops=False)
|
800
|
+
|
801
|
+
# The tree-decomposition may contain a lot of useless nodes.
|
802
|
+
# We merge all edges between two sets S, S' where S is a subset of S'
|
803
|
+
tree_decomp = reduced_tree_decomposition(tree_decomp)
|
804
|
+
|
805
|
+
tree_decomp.name("Tree decomposition")
|
806
|
+
if nice:
|
807
|
+
tree_decomp = make_nice_tree_decomposition(g, tree_decomp)
|
808
|
+
|
809
|
+
return tree_decomp
|
810
|
+
|
811
|
+
|
812
|
+
def make_nice_tree_decomposition(graph, tree_decomp):
|
813
|
+
r"""
|
814
|
+
Return a *nice* tree decomposition (TD) of the TD ``tree_decomp``.
|
815
|
+
|
816
|
+
See page 161 of [CFKLMPPS15]_ for a description of the nice tree decomposition.
|
817
|
+
|
818
|
+
A *nice* TD `NT` is a rooted tree with four types of nodes:
|
819
|
+
|
820
|
+
- *Leaf* nodes have no children and bag size 1;
|
821
|
+
- *Introduce* nodes have one child: If `v \in NT` is an introduce node and
|
822
|
+
`w \in NT` its child, then `Bag(v) = Bag(w) \cup \{ x \}`, where `x` is the
|
823
|
+
introduced node;
|
824
|
+
- *Forget* nodes have one child: If `v \in NT` is a forget node and
|
825
|
+
`w \in NT` its child, then `Bag(v) = Bag(w) \setminus \{ x \}`, where `x` is the
|
826
|
+
forgotten node;
|
827
|
+
- *Join* nodes have two children, both identical to the parent.
|
828
|
+
|
829
|
+
INPUT:
|
830
|
+
|
831
|
+
- ``graph`` -- a Sage graph
|
832
|
+
|
833
|
+
- ``tree_decomp`` -- a tree decomposition
|
834
|
+
|
835
|
+
OUTPUT: a nice tree decomposition
|
836
|
+
|
837
|
+
.. WARNING::
|
838
|
+
|
839
|
+
This method assumes that the vertices of the input tree ``tree_decomp``
|
840
|
+
are hashable and have attribute ``issuperset``, e.g., ``frozenset`` or
|
841
|
+
:class:`~sage.sets.set.Set_object_enumerated_with_category`.
|
842
|
+
|
843
|
+
EXAMPLES::
|
844
|
+
|
845
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
846
|
+
sage: petersen = graphs.PetersenGraph()
|
847
|
+
sage: petersen_TD = petersen.treewidth(certificate=True) # needs cliquer
|
848
|
+
sage: make_nice_tree_decomposition(petersen, petersen_TD) # needs cliquer
|
849
|
+
Nice tree decomposition of Tree decomposition: Graph on 28 vertices
|
850
|
+
|
851
|
+
::
|
852
|
+
|
853
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
854
|
+
sage: cherry = graphs.CompleteBipartiteGraph(1, 2)
|
855
|
+
sage: cherry_TD = cherry.treewidth(certificate=True) # needs cliquer
|
856
|
+
sage: make_nice_tree_decomposition(cherry, cherry_TD) # needs cliquer
|
857
|
+
Nice tree decomposition of Tree decomposition: Graph on 7 vertices
|
858
|
+
|
859
|
+
::
|
860
|
+
|
861
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
862
|
+
sage: bip_one_four = graphs.CompleteBipartiteGraph(1, 4)
|
863
|
+
sage: bip_one_four_TD = bip_one_four.treewidth(certificate=True) # needs cliquer
|
864
|
+
sage: make_nice_tree_decomposition(bip_one_four, bip_one_four_TD) # needs cliquer
|
865
|
+
Nice tree decomposition of Tree decomposition: Graph on 15 vertices
|
866
|
+
|
867
|
+
Check that :issue:`36843` is fixed::
|
868
|
+
|
869
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
870
|
+
sage: triangle = graphs.CompleteGraph(3)
|
871
|
+
sage: triangle_TD = triangle.treewidth(certificate=True) # needs cliquer
|
872
|
+
sage: make_nice_tree_decomposition(triangle, triangle_TD) # needs cliquer
|
873
|
+
Nice tree decomposition of Tree decomposition: Graph on 7 vertices
|
874
|
+
|
875
|
+
::
|
876
|
+
|
877
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
878
|
+
sage: graph = graphs.CompleteBipartiteGraph(2, 5)
|
879
|
+
sage: graph_TD = graph.treewidth(certificate=True) # needs cliquer
|
880
|
+
sage: make_nice_tree_decomposition(graph, graph_TD) # needs cliquer
|
881
|
+
Nice tree decomposition of Tree decomposition: Graph on 25 vertices
|
882
|
+
|
883
|
+
::
|
884
|
+
|
885
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
886
|
+
sage: empty_graph = graphs.EmptyGraph()
|
887
|
+
sage: tree_decomp = empty_graph.treewidth(certificate=True) # needs cliquer
|
888
|
+
sage: len(make_nice_tree_decomposition(empty_graph, tree_decomp)) # needs cliquer
|
889
|
+
0
|
890
|
+
|
891
|
+
::
|
892
|
+
|
893
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
894
|
+
sage: singleton = graphs.CompleteGraph(1)
|
895
|
+
sage: tree_decomp = singleton.treewidth(certificate=True) # needs cliquer
|
896
|
+
sage: make_nice_tree_decomposition(singleton, tree_decomp) # needs cliquer
|
897
|
+
Nice tree decomposition of Tree decomposition: Graph on 3 vertices
|
898
|
+
|
899
|
+
::
|
900
|
+
|
901
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition
|
902
|
+
sage: an_edge = graphs.CompleteGraph(2)
|
903
|
+
sage: tree_decomp = an_edge.treewidth(certificate=True) # needs cliquer
|
904
|
+
sage: make_nice_tree_decomposition(an_edge, tree_decomp) # needs cliquer
|
905
|
+
Nice tree decomposition of Tree decomposition: Graph on 5 vertices
|
906
|
+
"""
|
907
|
+
if not is_valid_tree_decomposition(graph, tree_decomp):
|
908
|
+
raise ValueError("input must be a valid tree decomposition for this graph")
|
909
|
+
|
910
|
+
name = f"Nice tree decomposition of {tree_decomp.name()}"
|
911
|
+
from sage.graphs.graph import Graph
|
912
|
+
if not tree_decomp:
|
913
|
+
return Graph(name=name)
|
914
|
+
|
915
|
+
# Step 1: Ensure the tree is directed and has a root
|
916
|
+
# Choose a root and orient the edges from root-to-leaves direction
|
917
|
+
#
|
918
|
+
# Testing <= 1 for the special case when one bag containing all vertices
|
919
|
+
leaves = [u for u in tree_decomp if tree_decomp.degree(u) <= 1]
|
920
|
+
|
921
|
+
from sage.graphs.digraph import DiGraph
|
922
|
+
if len(leaves) == 1:
|
923
|
+
root = leaves[0]
|
924
|
+
directed_tree = DiGraph(tree_decomp)
|
925
|
+
else:
|
926
|
+
root = leaves.pop()
|
927
|
+
|
928
|
+
directed_tree = DiGraph(tree_decomp.breadth_first_search(start=root, edges=True),
|
929
|
+
format='list_of_edges')
|
930
|
+
|
931
|
+
# Relabel the graph in range (0, |tree_decomp| - 1)
|
932
|
+
bags_to_int = directed_tree.relabel(inplace=True, return_map=True)
|
933
|
+
# Get the new name of the root node
|
934
|
+
root = bags_to_int[root]
|
935
|
+
# Force bags to be of type Set to simplify code
|
936
|
+
bag = {ui: Set(u) for u, ui in bags_to_int.items()}
|
937
|
+
|
938
|
+
# Step 2: Add the root node and the leaf nodes, with empty bags
|
939
|
+
# To each leaf node of `directed_tree`, we add a child with empty bag.
|
940
|
+
# We also add a new root with empty bag.
|
941
|
+
root, old_root = directed_tree.add_vertex(), root
|
942
|
+
directed_tree.add_edge(root, old_root)
|
943
|
+
bag[root] = Set()
|
944
|
+
for vi, u in enumerate(leaves, start=root + 1):
|
945
|
+
directed_tree.add_edge(bags_to_int[u], vi)
|
946
|
+
bag[vi] = Set()
|
947
|
+
|
948
|
+
# Step 3: Ensure that each node of directed_tree has at most 2 children.
|
949
|
+
# If a node has more than 2 children, introduce new nodes to
|
950
|
+
# make sure each node has at most 2 children:
|
951
|
+
#
|
952
|
+
# If v has k > 2 children (w_1, w_2, ..., w_k), we disconnect (w_1, ..., w_{k-1})
|
953
|
+
# from v, and introduce k - 2 new nodes (u_1, u_2, ..., u_{k-2}).
|
954
|
+
# We then let w_i be the children of u_i for 1 <= i <= k - 2.
|
955
|
+
# We also let w_{k-1} be the second child of u_{k-2}, and
|
956
|
+
# u_i the second child of u_{i-1}.
|
957
|
+
# Finally, we let u_1 the second child of u.
|
958
|
+
# Each node u_i has the same bag as u.
|
959
|
+
|
960
|
+
# We need to call list(...) since we modify directed_tree
|
961
|
+
for ui in list(directed_tree):
|
962
|
+
if directed_tree.out_degree(ui) > 2:
|
963
|
+
children = directed_tree.neighbors_out(ui)
|
964
|
+
children.pop() # one vertex remains a child of ui
|
965
|
+
|
966
|
+
directed_tree.delete_edges((ui, vi) for vi in children)
|
967
|
+
|
968
|
+
new_nodes = [directed_tree.add_vertex() for _ in range(len(children) - 1)]
|
969
|
+
|
970
|
+
directed_tree.add_edge(ui, new_nodes[0])
|
971
|
+
directed_tree.add_path(new_nodes)
|
972
|
+
directed_tree.add_edges(zip(new_nodes, children))
|
973
|
+
directed_tree.add_edge(new_nodes[-1], children[-1])
|
974
|
+
|
975
|
+
bag.update((vi, bag[ui]) for vi in new_nodes)
|
976
|
+
|
977
|
+
# Step 4: If current vertex v has two children w1 and w2,
|
978
|
+
# then bag[v] == bag[w1] == bag[w2]
|
979
|
+
for current_node in list(directed_tree):
|
980
|
+
if directed_tree.out_degree(current_node) < 2:
|
981
|
+
continue
|
982
|
+
for neighbor in directed_tree.neighbor_out_iterator(current_node):
|
983
|
+
if bag[current_node] != bag[neighbor]:
|
984
|
+
directed_tree.delete_edge(current_node, neighbor)
|
985
|
+
new_node = directed_tree.add_vertex()
|
986
|
+
directed_tree.add_path([current_node, new_node, neighbor])
|
987
|
+
bag[new_node] = bag[current_node]
|
988
|
+
|
989
|
+
# Step 5: If the node v has only one child, then it is either an introduce
|
990
|
+
# node or a forget node.
|
991
|
+
def add_path_of_intro_nodes(u, v):
|
992
|
+
"""
|
993
|
+
Replace the arc (u, v) by a path of introduce nodes.
|
994
|
+
"""
|
995
|
+
if len(bag[u]) + 1 == len(bag[v]):
|
996
|
+
return
|
997
|
+
|
998
|
+
diff = list(bag[v] - bag[u])
|
999
|
+
diff.pop()
|
1000
|
+
|
1001
|
+
last_node = u
|
1002
|
+
for w in diff:
|
1003
|
+
new_node = directed_tree.add_vertex()
|
1004
|
+
bag[new_node] = bag[last_node].union(Set((w,)))
|
1005
|
+
directed_tree.add_edge(last_node, new_node)
|
1006
|
+
last_node = new_node
|
1007
|
+
|
1008
|
+
directed_tree.add_edge(last_node, v)
|
1009
|
+
directed_tree.delete_edge(u, v)
|
1010
|
+
|
1011
|
+
def add_path_of_forget_nodes(u, v):
|
1012
|
+
"""
|
1013
|
+
Replace the arc (u, v) by a path of forget nodes.
|
1014
|
+
"""
|
1015
|
+
if len(bag[v]) + 1 == len(bag[u]):
|
1016
|
+
return
|
1017
|
+
|
1018
|
+
diff = list(bag[u] - bag[v])
|
1019
|
+
diff.pop()
|
1020
|
+
|
1021
|
+
last_node = u
|
1022
|
+
for w in diff:
|
1023
|
+
new_node = directed_tree.add_vertex()
|
1024
|
+
bag[new_node] = bag[last_node] - {w}
|
1025
|
+
directed_tree.add_edge(last_node, new_node)
|
1026
|
+
last_node = new_node
|
1027
|
+
|
1028
|
+
directed_tree.add_edge(last_node, v)
|
1029
|
+
directed_tree.delete_edge(u, v)
|
1030
|
+
|
1031
|
+
for ui in list(directed_tree):
|
1032
|
+
if directed_tree.out_degree(ui) != 1:
|
1033
|
+
continue
|
1034
|
+
|
1035
|
+
vi = next(directed_tree.neighbor_out_iterator(ui))
|
1036
|
+
bag_ui, bag_vi = bag[ui], bag[vi]
|
1037
|
+
|
1038
|
+
# Merge the nodes if the two bags are the same
|
1039
|
+
if bag_ui == bag_vi:
|
1040
|
+
if directed_tree.in_degree(ui) == 1:
|
1041
|
+
parent = next(directed_tree.neighbor_in_iterator(ui))
|
1042
|
+
directed_tree.add_edge(parent, vi)
|
1043
|
+
else:
|
1044
|
+
root = vi
|
1045
|
+
directed_tree.delete_vertex(ui)
|
1046
|
+
|
1047
|
+
# Add paths of intro / forget nodes accordingly
|
1048
|
+
|
1049
|
+
elif bag_ui.issubset(bag_vi):
|
1050
|
+
add_path_of_intro_nodes(ui, vi)
|
1051
|
+
|
1052
|
+
elif bag_vi.issubset(bag_ui):
|
1053
|
+
add_path_of_forget_nodes(ui, vi)
|
1054
|
+
|
1055
|
+
# Handle the case when the two nodes are not related in any way above
|
1056
|
+
else:
|
1057
|
+
wi = directed_tree.add_vertex()
|
1058
|
+
bag[wi] = bag[ui] & bag[vi]
|
1059
|
+
directed_tree.add_path([ui, wi, vi])
|
1060
|
+
directed_tree.delete_edge(ui, vi)
|
1061
|
+
add_path_of_forget_nodes(ui, wi)
|
1062
|
+
add_path_of_intro_nodes(wi, vi)
|
1063
|
+
|
1064
|
+
# Return the nice tree decomposition after the processing
|
1065
|
+
nice_tree_decomp = Graph(directed_tree, name=name)
|
1066
|
+
|
1067
|
+
bfs_ordering = nice_tree_decomp.breadth_first_search(start=root)
|
1068
|
+
relabeling = {u: (i, bag[u]) for i, u in enumerate(bfs_ordering)}
|
1069
|
+
nice_tree_decomp.relabel(inplace=True, perm=relabeling)
|
1070
|
+
|
1071
|
+
return nice_tree_decomp
|
1072
|
+
|
1073
|
+
|
1074
|
+
def label_nice_tree_decomposition(nice_TD, root, directed=False):
|
1075
|
+
r"""
|
1076
|
+
Return a nice tree decomposition with nodes labelled accordingly.
|
1077
|
+
|
1078
|
+
INPUT:
|
1079
|
+
|
1080
|
+
- ``nice_TD`` -- a nice tree decomposition
|
1081
|
+
|
1082
|
+
- ``root`` -- the root of the nice tree decomposition
|
1083
|
+
|
1084
|
+
- ``directed`` -- boolean (default: ``False``); whether to return the nice
|
1085
|
+
tree decomposition as a directed graph rooted at vertex ``root`` or as an
|
1086
|
+
undirected graph
|
1087
|
+
|
1088
|
+
OUTPUT: a nice tree decomposition with nodes labelled
|
1089
|
+
|
1090
|
+
EXAMPLES::
|
1091
|
+
|
1092
|
+
sage: # needs cliquer
|
1093
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import make_nice_tree_decomposition, label_nice_tree_decomposition
|
1094
|
+
sage: claw = graphs.CompleteBipartiteGraph(1, 3)
|
1095
|
+
sage: claw_TD = claw.treewidth(certificate=True)
|
1096
|
+
sage: nice_TD = make_nice_tree_decomposition(claw, claw_TD)
|
1097
|
+
sage: root = sorted(nice_TD.vertices())[0]
|
1098
|
+
sage: label_TD = label_nice_tree_decomposition(nice_TD, root, directed=True)
|
1099
|
+
sage: label_TD.name()
|
1100
|
+
'Labelled Nice tree decomposition of Tree decomposition'
|
1101
|
+
sage: for node in sorted(label_TD): # random
|
1102
|
+
....: print(node, label_TD.get_vertex(node))
|
1103
|
+
(0, {}) forget
|
1104
|
+
(1, {0}) forget
|
1105
|
+
(2, {0, 1}) intro
|
1106
|
+
(3, {0}) forget
|
1107
|
+
(4, {0, 3}) intro
|
1108
|
+
(5, {0}) forget
|
1109
|
+
(6, {0, 2}) intro
|
1110
|
+
(7, {2}) intro
|
1111
|
+
(8, {}) leaf
|
1112
|
+
"""
|
1113
|
+
from sage.graphs.digraph import DiGraph
|
1114
|
+
from sage.graphs.graph import Graph
|
1115
|
+
|
1116
|
+
directed_TD = DiGraph(nice_TD.breadth_first_search(start=root, edges=True),
|
1117
|
+
format='list_of_edges',
|
1118
|
+
name='Labelled {}'.format(nice_TD.name()))
|
1119
|
+
|
1120
|
+
# The loop starts from the root node
|
1121
|
+
# We assume the tree decomposition is valid and nice,
|
1122
|
+
# hence saving time on checking.
|
1123
|
+
for node in directed_TD:
|
1124
|
+
out_deg = directed_TD.out_degree(node)
|
1125
|
+
|
1126
|
+
if out_deg == 2:
|
1127
|
+
directed_TD.set_vertex(node, 'join')
|
1128
|
+
elif out_deg == 1:
|
1129
|
+
current_bag = node[1]
|
1130
|
+
child_bag = directed_TD.neighbors_out(node)[0][1]
|
1131
|
+
|
1132
|
+
if len(current_bag) == len(child_bag) + 1:
|
1133
|
+
directed_TD.set_vertex(node, 'intro')
|
1134
|
+
else:
|
1135
|
+
directed_TD.set_vertex(node, 'forget')
|
1136
|
+
else:
|
1137
|
+
directed_TD.set_vertex(node, 'leaf')
|
1138
|
+
|
1139
|
+
if directed:
|
1140
|
+
return directed_TD
|
1141
|
+
return Graph(directed_TD, name=nice_TD.name())
|
1142
|
+
|
1143
|
+
|
1144
|
+
#
|
1145
|
+
# Treelength
|
1146
|
+
#
|
1147
|
+
|
1148
|
+
def length_of_tree_decomposition(G, T, check=True):
|
1149
|
+
r"""
|
1150
|
+
Return the length of the tree decomposition `T` of `G`.
|
1151
|
+
|
1152
|
+
The *length* of a tree decomposition, as proposed in [DG2006]_, is the
|
1153
|
+
maximum *diameter* in `G` of its bags, where the diameter of a bag `X_i` is
|
1154
|
+
the largest distance in `G` between the vertices in `X_i` (i.e., `\max_{u, v
|
1155
|
+
\in X_i} \dist_G(u, v)`). See the documentation of the
|
1156
|
+
:mod:`~sage.graphs.graph_decompositions.tree_decomposition` module for more
|
1157
|
+
details.
|
1158
|
+
|
1159
|
+
INPUT:
|
1160
|
+
|
1161
|
+
- ``G`` -- a graph
|
1162
|
+
|
1163
|
+
- ``T`` -- a tree-decomposition for `G`
|
1164
|
+
|
1165
|
+
- ``check`` -- boolean (default: ``True``); whether to check that the
|
1166
|
+
tree-decomposition `T` is valid for `G`
|
1167
|
+
|
1168
|
+
EXAMPLES:
|
1169
|
+
|
1170
|
+
Trees and cliques have treelength 1::
|
1171
|
+
|
1172
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import length_of_tree_decomposition
|
1173
|
+
sage: G = graphs.CompleteGraph(5)
|
1174
|
+
sage: tl, T = G.treelength(certificate=True)
|
1175
|
+
sage: tl
|
1176
|
+
1
|
1177
|
+
sage: length_of_tree_decomposition(G, T, check=True)
|
1178
|
+
1
|
1179
|
+
sage: G = graphs.RandomTree(20)
|
1180
|
+
sage: tl, T = G.treelength(certificate=True)
|
1181
|
+
sage: tl
|
1182
|
+
1
|
1183
|
+
sage: length_of_tree_decomposition(G, T, check=True)
|
1184
|
+
1
|
1185
|
+
|
1186
|
+
The Petersen graph has treelength 2::
|
1187
|
+
|
1188
|
+
sage: G = graphs.PetersenGraph()
|
1189
|
+
sage: tl, T = G.treelength(certificate=True)
|
1190
|
+
sage: tl
|
1191
|
+
2
|
1192
|
+
sage: length_of_tree_decomposition(G, T)
|
1193
|
+
2
|
1194
|
+
|
1195
|
+
When a tree-decomposition has a single bag containing all vertices of a
|
1196
|
+
graph, the length of this tree-decomposition is the diameter of the graph::
|
1197
|
+
|
1198
|
+
sage: G = graphs.Grid2dGraph(2, 5)
|
1199
|
+
sage: G.treelength()
|
1200
|
+
2
|
1201
|
+
sage: G.diameter()
|
1202
|
+
5
|
1203
|
+
sage: T = Graph({Set(G): []})
|
1204
|
+
sage: length_of_tree_decomposition(G, T)
|
1205
|
+
5
|
1206
|
+
|
1207
|
+
TESTS::
|
1208
|
+
|
1209
|
+
sage: G = Graph()
|
1210
|
+
sage: _, T = G.treelength(certificate=True)
|
1211
|
+
sage: length_of_tree_decomposition(G, T, check=True)
|
1212
|
+
0
|
1213
|
+
sage: length_of_tree_decomposition(Graph(1), T, check=True)
|
1214
|
+
Traceback (most recent call last):
|
1215
|
+
...
|
1216
|
+
ValueError: the tree-decomposition is not valid for this graph
|
1217
|
+
"""
|
1218
|
+
if check and not is_valid_tree_decomposition(G, T):
|
1219
|
+
raise ValueError("the tree-decomposition is not valid for this graph")
|
1220
|
+
|
1221
|
+
cdef unsigned int n = G.order()
|
1222
|
+
|
1223
|
+
if n < 2:
|
1224
|
+
return 0
|
1225
|
+
if any(len(bag) == n for bag in T):
|
1226
|
+
return G.diameter()
|
1227
|
+
|
1228
|
+
cdef unsigned int i, j
|
1229
|
+
|
1230
|
+
# We map vertices to integers in range 0..n-1
|
1231
|
+
cdef list int_to_vertex = list(G)
|
1232
|
+
cdef dict vertex_to_int = {u: i for i, u in enumerate(int_to_vertex)}
|
1233
|
+
|
1234
|
+
# We compute the distance matrix.
|
1235
|
+
cdef unsigned short * c_distances = c_distances_all_pairs(G, vertex_list=int_to_vertex)
|
1236
|
+
cdef unsigned short ** distances = <unsigned short **>sig_calloc(n, sizeof(unsigned short *))
|
1237
|
+
for i in range(n):
|
1238
|
+
distances[i] = c_distances + i * n
|
1239
|
+
|
1240
|
+
# We now compute the maximum lengths of the bags
|
1241
|
+
from itertools import combinations
|
1242
|
+
cdef list bag_int
|
1243
|
+
cdef unsigned short dij
|
1244
|
+
cdef unsigned short length = 0
|
1245
|
+
for bag in T:
|
1246
|
+
bag_int = [vertex_to_int[u] for u in bag]
|
1247
|
+
for i, j in combinations(bag_int, 2):
|
1248
|
+
dij = distances[i][j]
|
1249
|
+
if dij > length:
|
1250
|
+
length = dij
|
1251
|
+
|
1252
|
+
sig_free(c_distances)
|
1253
|
+
sig_free(distances)
|
1254
|
+
|
1255
|
+
return length
|
1256
|
+
|
1257
|
+
|
1258
|
+
def treelength_lowerbound(G):
|
1259
|
+
r"""
|
1260
|
+
Return a lower bound on the treelength of `G`.
|
1261
|
+
|
1262
|
+
See [DG2006]_ for more details.
|
1263
|
+
|
1264
|
+
INPUT:
|
1265
|
+
|
1266
|
+
- ``G`` -- a sage Graph
|
1267
|
+
|
1268
|
+
EXAMPLES::
|
1269
|
+
|
1270
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import treelength_lowerbound
|
1271
|
+
sage: G = graphs.PetersenGraph()
|
1272
|
+
sage: treelength_lowerbound(G)
|
1273
|
+
1
|
1274
|
+
sage: G.treelength()
|
1275
|
+
2
|
1276
|
+
sage: G = graphs.CycleGraph(5)
|
1277
|
+
sage: treelength_lowerbound(G)
|
1278
|
+
2
|
1279
|
+
sage: G.treelength()
|
1280
|
+
2
|
1281
|
+
|
1282
|
+
TESTS::
|
1283
|
+
|
1284
|
+
sage: treelength_lowerbound(Graph())
|
1285
|
+
0
|
1286
|
+
"""
|
1287
|
+
if G.is_cycle():
|
1288
|
+
from sage.arith.misc import integer_ceil as ceil
|
1289
|
+
return int(ceil(G.order() / 3))
|
1290
|
+
|
1291
|
+
lowerbound = 0
|
1292
|
+
girth = G.girth()
|
1293
|
+
if girth is not Infinity:
|
1294
|
+
lowerbound = max(lowerbound, girth / 3)
|
1295
|
+
|
1296
|
+
return int(lowerbound)
|
1297
|
+
|
1298
|
+
|
1299
|
+
cdef class TreelengthConnected:
|
1300
|
+
r"""
|
1301
|
+
Compute the treelength of a connected graph (and provide a decomposition).
|
1302
|
+
|
1303
|
+
This class implements an algorithm for computing the treelength of a
|
1304
|
+
connected graph that virtually explores the graph of all pairs
|
1305
|
+
``(vertex_cut, connected_component)``, where ``vertex_cut`` is a vertex cut
|
1306
|
+
of the graph of length `\leq k`, and ``connected_component`` is a connected
|
1307
|
+
component of the graph induced by ``G - vertex_cut``.
|
1308
|
+
|
1309
|
+
We deduce that the pair ``(vertex_cut, connected_component)`` is feasible
|
1310
|
+
with treelength `k` if ``connected_component`` is empty, or if a vertex
|
1311
|
+
``v`` from ``vertex_cut`` can be replaced with a vertex from
|
1312
|
+
``connected_component``, such that the pair ``(vertex_cut + v,
|
1313
|
+
connected_component - v)`` is feasible.
|
1314
|
+
|
1315
|
+
INPUT:
|
1316
|
+
|
1317
|
+
- ``G`` -- a sage Graph
|
1318
|
+
|
1319
|
+
- ``k`` -- integer (default: ``None``); indicates the length to be
|
1320
|
+
considered. When `k` is an integer, the method checks that the graph has
|
1321
|
+
treelength `\leq k`. If `k` is ``None`` (default), the method computes the
|
1322
|
+
optimal treelength.
|
1323
|
+
|
1324
|
+
- ``certificate`` -- boolean (default: ``False``); whether to also compute
|
1325
|
+
the tree-decomposition itself
|
1326
|
+
|
1327
|
+
OUTPUT:
|
1328
|
+
|
1329
|
+
``TreelengthConnected(G)`` returns the treelength of `G`. When `k` is
|
1330
|
+
specified, it returns ``False`` when no tree-decomposition of length
|
1331
|
+
`\leq k` exists or ``True`` otherwise. When ``certificate=True``, the
|
1332
|
+
tree-decomposition is also returned.
|
1333
|
+
|
1334
|
+
EXAMPLES:
|
1335
|
+
|
1336
|
+
A clique has treelength 1::
|
1337
|
+
|
1338
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1339
|
+
sage: TreelengthConnected(graphs.CompleteGraph(3)).get_length()
|
1340
|
+
1
|
1341
|
+
sage: TC = TreelengthConnected(graphs.CompleteGraph(4), certificate=True)
|
1342
|
+
sage: TC.get_length()
|
1343
|
+
1
|
1344
|
+
sage: TC.get_tree_decomposition()
|
1345
|
+
Tree decomposition of Complete graph: Graph on 1 vertex
|
1346
|
+
|
1347
|
+
A cycle has treelength `\lceil n/3 \rceil`::
|
1348
|
+
|
1349
|
+
sage: TreelengthConnected(graphs.CycleGraph(6)).get_length()
|
1350
|
+
2
|
1351
|
+
sage: TreelengthConnected(graphs.CycleGraph(7)).get_length()
|
1352
|
+
3
|
1353
|
+
sage: TreelengthConnected(graphs.CycleGraph(7), k=3).is_less_than_k()
|
1354
|
+
True
|
1355
|
+
sage: TreelengthConnected(graphs.CycleGraph(7), k=2).is_less_than_k()
|
1356
|
+
False
|
1357
|
+
|
1358
|
+
TESTS:
|
1359
|
+
|
1360
|
+
The input graph must be connected::
|
1361
|
+
|
1362
|
+
sage: TreelengthConnected(Graph(2))
|
1363
|
+
Traceback (most recent call last):
|
1364
|
+
...
|
1365
|
+
ValueError: the graph is not connected
|
1366
|
+
|
1367
|
+
The parameter `k` must be nonnegative::
|
1368
|
+
|
1369
|
+
sage: TreelengthConnected(Graph(1), k=-1)
|
1370
|
+
Traceback (most recent call last):
|
1371
|
+
...
|
1372
|
+
ValueError: k (= -1) must be a nonnegative integer
|
1373
|
+
|
1374
|
+
Parameter ``certificate`` must be ``True`` to get a tree decomposition::
|
1375
|
+
|
1376
|
+
sage: TreelengthConnected(Graph(1), certificate=False).get_tree_decomposition()
|
1377
|
+
Traceback (most recent call last):
|
1378
|
+
...
|
1379
|
+
ValueError: parameter 'certificate' has not been set to True
|
1380
|
+
|
1381
|
+
When parameter `k` is specified and ``certificate`` is ``True``, the
|
1382
|
+
computed tree decomposition is any valid tree decomposition with length at
|
1383
|
+
most `k`. However, this tree decomposition exists only if the treelength of
|
1384
|
+
`G` is at most `k` (i.e., `tl(G) \leq k`)::
|
1385
|
+
|
1386
|
+
sage: G = graphs.Grid2dGraph(2, 3)
|
1387
|
+
sage: TC = TreelengthConnected(G, k=2, certificate=True)
|
1388
|
+
sage: TC.is_less_than_k()
|
1389
|
+
True
|
1390
|
+
sage: TC.get_tree_decomposition()
|
1391
|
+
Tree decomposition of 2D Grid Graph for [2, 3]: Graph on 3 vertices
|
1392
|
+
sage: TC = TreelengthConnected(G, k=1, certificate=True)
|
1393
|
+
sage: TC.is_less_than_k()
|
1394
|
+
False
|
1395
|
+
sage: TC.get_tree_decomposition()
|
1396
|
+
Traceback (most recent call last):
|
1397
|
+
...
|
1398
|
+
ValueError: no tree decomposition with length <= 1 was found
|
1399
|
+
"""
|
1400
|
+
|
1401
|
+
def __init__(self, G, k=None, certificate=False):
|
1402
|
+
r"""
|
1403
|
+
Initialize this object and compute the treelength of `G`.
|
1404
|
+
|
1405
|
+
INPUT:
|
1406
|
+
|
1407
|
+
- ``G`` -- a sage Graph
|
1408
|
+
|
1409
|
+
- ``k`` -- integer (default: ``None``); indicates the length to be
|
1410
|
+
considered. When `k` is an integer, the method checks that the graph
|
1411
|
+
has treelength `\leq k`. If `k` is ``None`` (default), the method
|
1412
|
+
computes the optimal treelength.
|
1413
|
+
|
1414
|
+
- ``certificate`` -- boolean (default: ``False``); whether to compute
|
1415
|
+
the tree-decomposition itself
|
1416
|
+
|
1417
|
+
TESTS::
|
1418
|
+
|
1419
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1420
|
+
sage: G = graphs.CycleGraph(4)
|
1421
|
+
sage: TreelengthConnected(G).get_length()
|
1422
|
+
2
|
1423
|
+
"""
|
1424
|
+
if k is not None and k < 0:
|
1425
|
+
raise ValueError("k (= {}) must be a nonnegative integer".format(k))
|
1426
|
+
G._scream_if_not_simple()
|
1427
|
+
if not G.is_connected():
|
1428
|
+
raise ValueError("the graph is not connected")
|
1429
|
+
|
1430
|
+
self.certificate = certificate
|
1431
|
+
self.k_is_defined = k is not None
|
1432
|
+
self.k = k if self.k_is_defined else 0
|
1433
|
+
|
1434
|
+
if certificate:
|
1435
|
+
from sage.graphs.graph import Graph
|
1436
|
+
self.name = "Tree decomposition of {}".format(G.name())
|
1437
|
+
|
1438
|
+
self.n = G.order()
|
1439
|
+
self.distances = NULL # used in the destructor
|
1440
|
+
|
1441
|
+
# Trivial cases
|
1442
|
+
if self.n <= 1 or (self.k_is_defined and self.n <= k):
|
1443
|
+
if certificate:
|
1444
|
+
if self.n:
|
1445
|
+
self.tree = Graph({Set(G): []}, format='dict_of_lists', name=self.name)
|
1446
|
+
else:
|
1447
|
+
self.tree = Graph(name=self.name)
|
1448
|
+
self.length = 0 if self.n <= 1 else G.diameter(algorithm='DHV')
|
1449
|
+
self.leq_k = True # We know that k is nonnegative
|
1450
|
+
return
|
1451
|
+
|
1452
|
+
if self.k_is_defined and not k:
|
1453
|
+
# We have at least 2 vertices and 1 edges, so tl >= 1
|
1454
|
+
self.leq_k = False
|
1455
|
+
return
|
1456
|
+
|
1457
|
+
if G.is_clique():
|
1458
|
+
if certificate:
|
1459
|
+
self.tree = Graph({Set(G): []}, format='dict_of_lists', name=self.name)
|
1460
|
+
self.length = 1
|
1461
|
+
self.leq_k = True
|
1462
|
+
return
|
1463
|
+
|
1464
|
+
cdef unsigned int i, j
|
1465
|
+
|
1466
|
+
# If the vertices are not labeled 0..n-1, we relabel the graph. This
|
1467
|
+
# way, the labeling of the vertices matches the rows and columns of the
|
1468
|
+
# distance matrix
|
1469
|
+
if set(G) == set(range(self.n)):
|
1470
|
+
graph = G
|
1471
|
+
self.perm_inv = dict()
|
1472
|
+
else:
|
1473
|
+
graph, perm = G.relabel(inplace=False, return_map=True)
|
1474
|
+
self.perm_inv = {i: u for u, i in perm.items()}
|
1475
|
+
|
1476
|
+
# We compute the distance matrix.
|
1477
|
+
self.c_distances = c_distances_all_pairs(graph, vertex_list=list(range(self.n)))
|
1478
|
+
self.distances = <unsigned short **>sig_calloc(self.n, sizeof(unsigned short *))
|
1479
|
+
for i in range(self.n):
|
1480
|
+
self.distances[i] = self.c_distances + i * self.n
|
1481
|
+
|
1482
|
+
# and the diameter of the graph
|
1483
|
+
self.diameter = 0
|
1484
|
+
for i in range(self.n):
|
1485
|
+
for j in range(i, self.n):
|
1486
|
+
self.diameter = max(self.diameter, self.distances[i][j])
|
1487
|
+
|
1488
|
+
if self.k_is_defined and k >= self.diameter:
|
1489
|
+
# All vertices fit in one bag
|
1490
|
+
if certificate:
|
1491
|
+
self.tree = Graph({Set(G): []}, format='dict_of_lists', name=self.name)
|
1492
|
+
self.length = self.diameter
|
1493
|
+
self.leq_k = True
|
1494
|
+
return
|
1495
|
+
|
1496
|
+
# Forcing k to be defined
|
1497
|
+
if not self.k_is_defined:
|
1498
|
+
for i in range(treelength_lowerbound(graph), self.diameter + 1):
|
1499
|
+
ans = self._treelength(graph, i)
|
1500
|
+
if ans:
|
1501
|
+
self.length = i
|
1502
|
+
return
|
1503
|
+
|
1504
|
+
# If k is defined
|
1505
|
+
ans = self._treelength(graph, k)
|
1506
|
+
if ans:
|
1507
|
+
self.length = k
|
1508
|
+
self.leq_k = True
|
1509
|
+
else:
|
1510
|
+
self.leq_k = False
|
1511
|
+
|
1512
|
+
def __dealloc__(self):
|
1513
|
+
r"""
|
1514
|
+
Destroy the object.
|
1515
|
+
|
1516
|
+
TESTS::
|
1517
|
+
|
1518
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1519
|
+
sage: G = graphs.CycleGraph(4)
|
1520
|
+
sage: TreelengthConnected(G).get_length()
|
1521
|
+
2
|
1522
|
+
"""
|
1523
|
+
if self.distances:
|
1524
|
+
sig_free(self.c_distances)
|
1525
|
+
sig_free(self.distances)
|
1526
|
+
|
1527
|
+
cdef bint _treelength(self, g, k) noexcept:
|
1528
|
+
r"""
|
1529
|
+
Check whether the treelength of `g` is at most `k`.
|
1530
|
+
|
1531
|
+
INPUT:
|
1532
|
+
|
1533
|
+
- ``g`` -- a sage Graph
|
1534
|
+
|
1535
|
+
- ``k`` -- integer; indicates the length to be considered
|
1536
|
+
|
1537
|
+
TESTS::
|
1538
|
+
|
1539
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1540
|
+
sage: G = graphs.CycleGraph(4)
|
1541
|
+
sage: TreelengthConnected(G, k=2).is_less_than_k()
|
1542
|
+
True
|
1543
|
+
"""
|
1544
|
+
|
1545
|
+
# This is the recursion described in the method's documentation. All
|
1546
|
+
# computations are cached, and depends on the pair ``cut,
|
1547
|
+
# connected_component`` only.
|
1548
|
+
#
|
1549
|
+
# It returns either a boolean or the corresponding tree-decomposition,
|
1550
|
+
# as a list of edges between vertex cuts (used to build the complete
|
1551
|
+
# tree-decomposition at the end of the _treelength method).
|
1552
|
+
@cached_function
|
1553
|
+
def rec(cut, cc):
|
1554
|
+
cdef int v
|
1555
|
+
cdef frozenset reduced_cut
|
1556
|
+
|
1557
|
+
if len(cc) == 1:
|
1558
|
+
[v] = cc
|
1559
|
+
# We identify the neighbors of v in cut
|
1560
|
+
reduced_cut = cut.intersection(g.neighbor_iterator(v))
|
1561
|
+
# We can form a new bag with its closed neighborhood, and this
|
1562
|
+
# bag has diameter at most 2. Furthermore, if k == 1, we know
|
1563
|
+
# that the bag cut has diameter <= 1, and so the new bag has
|
1564
|
+
# diameter 1
|
1565
|
+
if self.certificate:
|
1566
|
+
if cut == reduced_cut:
|
1567
|
+
return [(cut, cut.union(cc))]
|
1568
|
+
# We need to forget some vertices
|
1569
|
+
return [(cut, reduced_cut), (reduced_cut, reduced_cut.union(cc))]
|
1570
|
+
|
1571
|
+
return True
|
1572
|
+
|
1573
|
+
# We explore all possible extensions of the cut
|
1574
|
+
cdef frozenset cutv
|
1575
|
+
cdef frozenset ccv
|
1576
|
+
cdef frozenset cci
|
1577
|
+
cdef frozenset reduced_cuti
|
1578
|
+
cdef list sons
|
1579
|
+
cdef int x
|
1580
|
+
|
1581
|
+
for v in cc:
|
1582
|
+
|
1583
|
+
# We know that the cut has diameter <= k. So we check is adding
|
1584
|
+
# v to the cut does not make its diameter > k
|
1585
|
+
if any(self.distances[v][x] > k for x in cut):
|
1586
|
+
continue
|
1587
|
+
# We add v to the cut and remove it from cc
|
1588
|
+
cutv = cut.union([v])
|
1589
|
+
ccv = cc.difference([v])
|
1590
|
+
|
1591
|
+
# The values returned by the recursive calls.
|
1592
|
+
sons = []
|
1593
|
+
|
1594
|
+
# Removing v may have disconnected cc. We iterate on its
|
1595
|
+
# connected components
|
1596
|
+
for _cci in g.subgraph(ccv).connected_components(sort=False):
|
1597
|
+
cci = frozenset(_cci)
|
1598
|
+
|
1599
|
+
# The recursive subcalls. We remove on-the-fly the vertices
|
1600
|
+
# from the cut which play no role in separating the
|
1601
|
+
# connected component from the rest of the graph. That is,
|
1602
|
+
# we identify the vertices of cutv with a neighbor in cci
|
1603
|
+
reduced_cuti = frozenset([x for x in cutv
|
1604
|
+
if any(xx in cci for xx in g.neighbor_iterator(x))])
|
1605
|
+
if not reduced_cuti:
|
1606
|
+
# This should not happen
|
1607
|
+
break
|
1608
|
+
|
1609
|
+
# and we do a recursive call
|
1610
|
+
son = rec(reduced_cuti, cci)
|
1611
|
+
if not son:
|
1612
|
+
break
|
1613
|
+
|
1614
|
+
# We get a valid decomposition of cci
|
1615
|
+
if self.certificate:
|
1616
|
+
# We connect cut, cutv, reduced_cci and son
|
1617
|
+
sons.append((cut, cutv))
|
1618
|
+
if v in reduced_cuti:
|
1619
|
+
sons.append((cutv, reduced_cuti))
|
1620
|
+
else:
|
1621
|
+
reduced_cutv = reduced_cuti.union([v])
|
1622
|
+
sons.append((cutv, reduced_cutv))
|
1623
|
+
sons.append((reduced_cutv, reduced_cuti))
|
1624
|
+
sons.extend(son)
|
1625
|
+
|
1626
|
+
# Weird Python syntax which is useful once in a lifetime : if break
|
1627
|
+
# was never called in the loop above, we return "sons".
|
1628
|
+
else:
|
1629
|
+
return sons if self.certificate else True
|
1630
|
+
|
1631
|
+
return False
|
1632
|
+
|
1633
|
+
# Main call to rec function, i.e. rec({v}, V-{v})
|
1634
|
+
cdef list V = list(g)
|
1635
|
+
cdef frozenset v = frozenset([V.pop()])
|
1636
|
+
TD = rec(v, frozenset(V))
|
1637
|
+
|
1638
|
+
if TD is False:
|
1639
|
+
return False
|
1640
|
+
|
1641
|
+
if not self.certificate:
|
1642
|
+
return True
|
1643
|
+
|
1644
|
+
# Building the tree-decomposition graph. Its vertices are cuts of the
|
1645
|
+
# decomposition, and there is an edge from a cut C1 to a cut C2 if C2 is an
|
1646
|
+
# immediate subcall of C1. If needed, the vertices are relabeled.
|
1647
|
+
if self.perm_inv:
|
1648
|
+
def good_label(x):
|
1649
|
+
return Set([self.perm_inv[i] for i in x])
|
1650
|
+
else:
|
1651
|
+
def good_label(x):
|
1652
|
+
return Set(x)
|
1653
|
+
|
1654
|
+
from sage.graphs.graph import Graph
|
1655
|
+
T = Graph([(good_label(x), good_label(y)) for x, y in TD if x != y],
|
1656
|
+
format='list_of_edges')
|
1657
|
+
self.tree = reduced_tree_decomposition(T)
|
1658
|
+
self.tree.name(self.name)
|
1659
|
+
return True
|
1660
|
+
|
1661
|
+
def get_tree_decomposition(self):
|
1662
|
+
"""
|
1663
|
+
Return the tree-decomposition.
|
1664
|
+
|
1665
|
+
EXAMPLES::
|
1666
|
+
|
1667
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1668
|
+
sage: G = graphs.CycleGraph(4)
|
1669
|
+
sage: TreelengthConnected(G, certificate=True).get_tree_decomposition()
|
1670
|
+
Tree decomposition of Cycle graph: Graph on 2 vertices
|
1671
|
+
sage: G.diameter()
|
1672
|
+
2
|
1673
|
+
sage: TreelengthConnected(G, k=2, certificate=True).get_tree_decomposition()
|
1674
|
+
Tree decomposition of Cycle graph: Graph on 1 vertex
|
1675
|
+
sage: TreelengthConnected(G, k=1, certificate=True).get_tree_decomposition()
|
1676
|
+
Traceback (most recent call last):
|
1677
|
+
...
|
1678
|
+
ValueError: no tree decomposition with length <= 1 was found
|
1679
|
+
|
1680
|
+
TESTS::
|
1681
|
+
|
1682
|
+
sage: G = graphs.CycleGraph(4)
|
1683
|
+
sage: TreelengthConnected(G, certificate=False).get_tree_decomposition()
|
1684
|
+
Traceback (most recent call last):
|
1685
|
+
...
|
1686
|
+
ValueError: parameter 'certificate' has not been set to True
|
1687
|
+
"""
|
1688
|
+
if self.certificate:
|
1689
|
+
if self.k_is_defined and not self.leq_k:
|
1690
|
+
raise ValueError("no tree decomposition with length <= {} was found".format(self.k))
|
1691
|
+
return self.tree
|
1692
|
+
else:
|
1693
|
+
raise ValueError("parameter 'certificate' has not been set to True")
|
1694
|
+
|
1695
|
+
def get_length(self):
|
1696
|
+
"""
|
1697
|
+
Return the length of the tree decomposition.
|
1698
|
+
|
1699
|
+
EXAMPLES::
|
1700
|
+
|
1701
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1702
|
+
sage: G = graphs.CycleGraph(4)
|
1703
|
+
sage: TreelengthConnected(G).get_length()
|
1704
|
+
2
|
1705
|
+
sage: TreelengthConnected(G, k=2).get_length()
|
1706
|
+
2
|
1707
|
+
sage: TreelengthConnected(G, k=1).get_length()
|
1708
|
+
Traceback (most recent call last):
|
1709
|
+
...
|
1710
|
+
ValueError: no tree decomposition with length <= 1 was found
|
1711
|
+
|
1712
|
+
TESTS::
|
1713
|
+
|
1714
|
+
sage: TreelengthConnected(Graph()).get_length()
|
1715
|
+
0
|
1716
|
+
"""
|
1717
|
+
if self.k_is_defined and not self.leq_k:
|
1718
|
+
raise ValueError("no tree decomposition with length <= {} was found".format(self.k))
|
1719
|
+
return self.length
|
1720
|
+
|
1721
|
+
def is_less_than_k(self):
|
1722
|
+
"""
|
1723
|
+
Return whether a tree decomposition with length at most `k` was found.
|
1724
|
+
|
1725
|
+
EXAMPLES::
|
1726
|
+
|
1727
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1728
|
+
sage: G = graphs.CycleGraph(4)
|
1729
|
+
sage: TreelengthConnected(G, k=1).is_less_than_k()
|
1730
|
+
False
|
1731
|
+
sage: TreelengthConnected(G, k=2).is_less_than_k()
|
1732
|
+
True
|
1733
|
+
sage: TreelengthConnected(G).is_less_than_k()
|
1734
|
+
Traceback (most recent call last):
|
1735
|
+
...
|
1736
|
+
ValueError: parameter 'k' has not been specified
|
1737
|
+
|
1738
|
+
TESTS::
|
1739
|
+
|
1740
|
+
sage: TreelengthConnected(Graph(), k=1).is_less_than_k()
|
1741
|
+
True
|
1742
|
+
"""
|
1743
|
+
if self.k_is_defined:
|
1744
|
+
return self.leq_k
|
1745
|
+
raise ValueError("parameter 'k' has not been specified")
|
1746
|
+
|
1747
|
+
|
1748
|
+
def treelength(G, k=None, certificate=False):
|
1749
|
+
r"""
|
1750
|
+
Compute the treelength of `G` (and provide a decomposition).
|
1751
|
+
|
1752
|
+
The *length* of a tree decomposition, as proposed in [DG2006]_, is the
|
1753
|
+
maximum *diameter* in `G` of its bags, where the diameter of a bag `X_i` is
|
1754
|
+
the largest distance in `G` between the vertices in `X_i` (i.e., `\max_{u, v
|
1755
|
+
\in X_i} \dist_G(u, v)`). The *treelength* `tl(G)` of a graph `G` is the
|
1756
|
+
minimum length among all possible tree decompositions of `G`.
|
1757
|
+
See the documentation of the
|
1758
|
+
:mod:`~sage.graphs.graph_decompositions.tree_decomposition` module for more
|
1759
|
+
details.
|
1760
|
+
|
1761
|
+
INPUT:
|
1762
|
+
|
1763
|
+
- ``G`` -- a sage Graph
|
1764
|
+
|
1765
|
+
- ``k`` -- integer (default: ``None``); indicates the length to be
|
1766
|
+
considered. When `k` is an integer, the method checks that the graph has
|
1767
|
+
treelength `\leq k`. If `k` is ``None`` (default), the method computes the
|
1768
|
+
optimal treelength.
|
1769
|
+
|
1770
|
+
- ``certificate`` -- boolean (default: ``False``); whether to also return
|
1771
|
+
the tree-decomposition itself
|
1772
|
+
|
1773
|
+
OUTPUT:
|
1774
|
+
|
1775
|
+
``G.treelength()`` returns the treelength of `G`. When `k` is specified, it
|
1776
|
+
returns ``False`` when no tree-decomposition of length `\leq k` exists or
|
1777
|
+
``True`` otherwise. When ``certificate=True``, the tree-decomposition is
|
1778
|
+
also returned.
|
1779
|
+
|
1780
|
+
ALGORITHM:
|
1781
|
+
|
1782
|
+
This method virtually explores the graph of all pairs ``(vertex_cut,
|
1783
|
+
connected_component)``, where ``vertex_cut`` is a vertex cut of the graph of
|
1784
|
+
length `\leq k`, and ``connected_component`` is a connected component of the
|
1785
|
+
graph induced by ``G - vertex_cut``.
|
1786
|
+
|
1787
|
+
We deduce that the pair ``(vertex_cut, connected_component)`` is feasible
|
1788
|
+
with treelength `k` if ``connected_component`` is empty, or if a vertex
|
1789
|
+
``v`` from ``vertex_cut`` can be replaced with a vertex from
|
1790
|
+
``connected_component``, such that the pair ``(vertex_cut + v,
|
1791
|
+
connected_component - v)`` is feasible.
|
1792
|
+
|
1793
|
+
In practice, this method decomposes the graph by its clique minimal
|
1794
|
+
separators into atoms, computes the treelength of each of atom and returns
|
1795
|
+
the maximum value over all the atoms. Indeed, we have that `tl(G) = \max_{X
|
1796
|
+
\in A} tl(G[X])` where `A` is the set of atoms of the decomposition by
|
1797
|
+
clique separators of `G`. When ``certificate == True``, the
|
1798
|
+
tree-decompositions of the atoms are connected to each others by adding
|
1799
|
+
edges with respect to the clique separators.
|
1800
|
+
|
1801
|
+
.. SEEALSO::
|
1802
|
+
|
1803
|
+
- :meth:`treewidth` computes the treewidth of a graph.
|
1804
|
+
- :meth:`~sage.graphs.graph_decompositions.vertex_separation.path_decomposition`
|
1805
|
+
computes the pathwidth of a graph.
|
1806
|
+
- module :mod:`~sage.graphs.graph_decompositions.vertex_separation`.
|
1807
|
+
- :meth:`~sage.graphs.graph_decompositions.clique_separators.atoms_and_clique_separators`
|
1808
|
+
|
1809
|
+
EXAMPLES:
|
1810
|
+
|
1811
|
+
The PetersenGraph has treelength 2::
|
1812
|
+
|
1813
|
+
sage: G = graphs.PetersenGraph()
|
1814
|
+
sage: G.treelength()
|
1815
|
+
2
|
1816
|
+
|
1817
|
+
Disconnected graphs have infinite treelength::
|
1818
|
+
|
1819
|
+
sage: G = Graph(2)
|
1820
|
+
sage: G.treelength()
|
1821
|
+
+Infinity
|
1822
|
+
sage: G.treelength(k=+Infinity)
|
1823
|
+
True
|
1824
|
+
sage: G.treelength(k=2)
|
1825
|
+
False
|
1826
|
+
sage: G.treelength(certificate=True)
|
1827
|
+
Traceback (most recent call last):
|
1828
|
+
...
|
1829
|
+
ValueError: the tree decomposition of a disconnected graph is not defined
|
1830
|
+
|
1831
|
+
Chordal graphs have treelength 1::
|
1832
|
+
|
1833
|
+
sage: G = graphs.RandomChordalGraph(30)
|
1834
|
+
sage: while not G.is_connected():
|
1835
|
+
....: G = graphs.RandomChordalGraph(30)
|
1836
|
+
sage: G.treelength()
|
1837
|
+
1
|
1838
|
+
|
1839
|
+
Cycles have treelength `\lceil n/3 \rceil`::
|
1840
|
+
|
1841
|
+
sage: [graphs.CycleGraph(n).treelength() for n in range(3, 11)]
|
1842
|
+
[1, 2, 2, 2, 3, 3, 3, 4]
|
1843
|
+
|
1844
|
+
TESTS:
|
1845
|
+
|
1846
|
+
Check that the decomposition by clique separators is valid::
|
1847
|
+
|
1848
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import TreelengthConnected
|
1849
|
+
sage: from sage.graphs.graph_decompositions.tree_decomposition import is_valid_tree_decomposition
|
1850
|
+
sage: G = graphs.StarGraph(3)
|
1851
|
+
sage: G.subdivide_edges(G.edges(sort=False), 2)
|
1852
|
+
sage: G = G.cartesian_product(graphs.CycleGraph(3))
|
1853
|
+
sage: tl, T = G.treelength(certificate=True)
|
1854
|
+
sage: tl == TreelengthConnected(G).get_length()
|
1855
|
+
True
|
1856
|
+
sage: is_valid_tree_decomposition(G, T)
|
1857
|
+
True
|
1858
|
+
|
1859
|
+
Corner cases::
|
1860
|
+
|
1861
|
+
sage: Graph().treelength()
|
1862
|
+
0
|
1863
|
+
sage: Graph().treelength(certificate=True)
|
1864
|
+
(0, Tree decomposition: Graph on 0 vertices)
|
1865
|
+
sage: Graph(1).treelength()
|
1866
|
+
0
|
1867
|
+
sage: Graph(1).treelength(k=0)
|
1868
|
+
True
|
1869
|
+
sage: Graph(1).treelength(certificate=True)
|
1870
|
+
(0, Tree decomposition: Graph on 1 vertex)
|
1871
|
+
sage: Graph(1).treelength(k=0, certificate=True)
|
1872
|
+
(True, Tree decomposition: Graph on 1 vertex)
|
1873
|
+
sage: G = graphs.PathGraph(2)
|
1874
|
+
sage: G.treelength()
|
1875
|
+
1
|
1876
|
+
sage: G.treelength(k=0)
|
1877
|
+
False
|
1878
|
+
sage: G.treelength(certificate=True)
|
1879
|
+
(1, Tree decomposition of Path graph: Graph on 1 vertex)
|
1880
|
+
sage: G.treelength(certificate=True, k=0)
|
1881
|
+
(False, None)
|
1882
|
+
sage: G.treelength(certificate=True, k=1)
|
1883
|
+
(True, Tree decomposition of Path graph: Graph on 1 vertex)
|
1884
|
+
sage: G.treelength(certificate=True, k=0)
|
1885
|
+
(False, None)
|
1886
|
+
sage: G.treelength(k=-1)
|
1887
|
+
Traceback (most recent call last):
|
1888
|
+
...
|
1889
|
+
ValueError: k(=-1) must be a nonnegative integer
|
1890
|
+
"""
|
1891
|
+
if G.is_directed():
|
1892
|
+
raise ValueError("this method is defined for undirected graphs only")
|
1893
|
+
if k is not None and k < 0:
|
1894
|
+
raise ValueError("k(={}) must be a nonnegative integer".format(k))
|
1895
|
+
|
1896
|
+
cdef str name = "Tree decomposition"
|
1897
|
+
if G.name():
|
1898
|
+
name += " of {}".format(G.name())
|
1899
|
+
|
1900
|
+
# Corner cases
|
1901
|
+
from sage.graphs.graph import Graph
|
1902
|
+
if G.order() <= 1:
|
1903
|
+
answer = 0 if k is None else True
|
1904
|
+
if certificate:
|
1905
|
+
if G:
|
1906
|
+
answer = answer, Graph({Set(G): []}, format='dict_of_lists', name=name)
|
1907
|
+
else:
|
1908
|
+
answer = answer, Graph(name=name)
|
1909
|
+
return answer
|
1910
|
+
if not G.is_connected():
|
1911
|
+
if certificate:
|
1912
|
+
raise ValueError("the tree decomposition of a disconnected graph is not defined")
|
1913
|
+
elif k is None:
|
1914
|
+
return +Infinity
|
1915
|
+
else:
|
1916
|
+
return k is Infinity
|
1917
|
+
if k == 0:
|
1918
|
+
return (False, None) if certificate else False
|
1919
|
+
if not certificate and G.is_chordal():
|
1920
|
+
return 1 if k is None else True
|
1921
|
+
|
1922
|
+
# We decompose the graph by clique minimal separators into atoms and solve
|
1923
|
+
# the problem on each of them
|
1924
|
+
atoms, cliques = G.atoms_and_clique_separators()
|
1925
|
+
|
1926
|
+
if not cliques:
|
1927
|
+
# We have a single atom
|
1928
|
+
TC = TreelengthConnected(G, k=k, certificate=certificate)
|
1929
|
+
if certificate:
|
1930
|
+
if k is None:
|
1931
|
+
return TC.get_length(), TC.get_tree_decomposition()
|
1932
|
+
elif TC.is_less_than_k():
|
1933
|
+
return True, TC.get_tree_decomposition()
|
1934
|
+
else:
|
1935
|
+
return False, None
|
1936
|
+
if k is None:
|
1937
|
+
return TC.get_length()
|
1938
|
+
return TC.is_less_than_k()
|
1939
|
+
|
1940
|
+
# As some atoms might be isomorphic, we use a dictionary keyed by immutable
|
1941
|
+
# copies of canonical graphs to store intermediate results.
|
1942
|
+
cdef dict data = dict()
|
1943
|
+
cdef list result = []
|
1944
|
+
cdef int tl = 1 # The graph is connected and of order at least 2
|
1945
|
+
cdef dict certif_inv
|
1946
|
+
cdef dict perm
|
1947
|
+
|
1948
|
+
for atom in atoms:
|
1949
|
+
|
1950
|
+
ga = G.subgraph(atom)
|
1951
|
+
if ga.is_clique():
|
1952
|
+
if certificate:
|
1953
|
+
result.append(Graph({Set(atom): []}, format='dict_of_lists'))
|
1954
|
+
continue
|
1955
|
+
|
1956
|
+
gc, certif = ga.canonical_label(certificate=True)
|
1957
|
+
gci = gc.copy(immutable=True)
|
1958
|
+
|
1959
|
+
if gci in data:
|
1960
|
+
# We already solved the problem for an isomorphic atom.
|
1961
|
+
if certificate:
|
1962
|
+
# We deduce the solution for this atom
|
1963
|
+
certif_inv = {i: u for u, i in certif.items()}
|
1964
|
+
perm = {u: Set([certif_inv[i] for i in u]) for u in data[gci]}
|
1965
|
+
result.append(data[gci].relabel(perm=perm, inplace=False, immutable=False))
|
1966
|
+
continue
|
1967
|
+
|
1968
|
+
# We solve the problem for this atom and store the result
|
1969
|
+
TC = TreelengthConnected(gci, k=k, certificate=certificate)
|
1970
|
+
if certificate:
|
1971
|
+
T = TC.get_tree_decomposition()
|
1972
|
+
data[gci] = T
|
1973
|
+
certif_inv = {i: u for u, i in certif.items()}
|
1974
|
+
perm = {u: Set([certif_inv[i] for i in u]) for u in T}
|
1975
|
+
result.append(T.relabel(perm=perm, inplace=False, immutable=False))
|
1976
|
+
if k is None:
|
1977
|
+
tl = max(tl, TC.get_length())
|
1978
|
+
elif not TC.is_less_than_k():
|
1979
|
+
return False if not certificate else (False, None)
|
1980
|
+
|
1981
|
+
if not certificate:
|
1982
|
+
if k is None:
|
1983
|
+
return tl
|
1984
|
+
return True
|
1985
|
+
|
1986
|
+
# We now build the tree decomposition of the graph by connecting the tree
|
1987
|
+
# decompositions of its atoms.
|
1988
|
+
T = _from_tree_decompositions_of_atoms_to_tree_decomposition(result, cliques)
|
1989
|
+
|
1990
|
+
# The tree-decomposition may contain a lot of useless nodes.
|
1991
|
+
# We merge all edges between two sets S,S' where S is a subset of S'
|
1992
|
+
T = reduced_tree_decomposition(T)
|
1993
|
+
T.name(name)
|
1994
|
+
if k is None:
|
1995
|
+
return tl, T
|
1996
|
+
return True, T
|