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,2732 @@
|
|
1
|
+
# sage_setup: distribution = sagemath-graphs
|
2
|
+
# autopep8: off
|
3
|
+
r"""
|
4
|
+
Bipartite graphs
|
5
|
+
|
6
|
+
This module implements bipartite graphs.
|
7
|
+
|
8
|
+
AUTHORS:
|
9
|
+
|
10
|
+
- Robert L. Miller (2008-01-20): initial version
|
11
|
+
|
12
|
+
- Ryan W. Hinton (2010-03-04): overrides for adding and deleting vertices
|
13
|
+
and edges
|
14
|
+
|
15
|
+
- Enjeck M. Cleopatra (2022): fixes incorrect partite sets and adds graph
|
16
|
+
creation from graph6 string
|
17
|
+
|
18
|
+
TESTS::
|
19
|
+
|
20
|
+
sage: B = graphs.CompleteBipartiteGraph(7, 9)
|
21
|
+
sage: loads(dumps(B)) == B
|
22
|
+
True
|
23
|
+
|
24
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4))
|
25
|
+
sage: B == B.copy()
|
26
|
+
True
|
27
|
+
sage: type(B.copy())
|
28
|
+
<class 'sage.graphs.bipartite_graph.BipartiteGraph'>
|
29
|
+
"""
|
30
|
+
# ****************************************************************************
|
31
|
+
# Copyright (C) 2008 Robert L. Miller <rlmillster@gmail.com>
|
32
|
+
# 2018 Julian Rüth <julian.rueth@fsfe.org>
|
33
|
+
# 2022 Enjeck M. Cleopatra <enjeckc1e0@gmail.com>
|
34
|
+
#
|
35
|
+
# This program is free software: you can redistribute it and/or modify
|
36
|
+
# it under the terms of the GNU General Public License as published by
|
37
|
+
# the Free Software Foundation, either version 2 of the License, or
|
38
|
+
# (at your option) any later version.
|
39
|
+
# https://www.gnu.org/licenses/
|
40
|
+
# ****************************************************************************
|
41
|
+
from collections import defaultdict
|
42
|
+
from collections.abc import Iterable
|
43
|
+
import itertools
|
44
|
+
|
45
|
+
from .generic_graph import GenericGraph
|
46
|
+
from .graph import Graph
|
47
|
+
from sage.rings.integer import Integer
|
48
|
+
from sage.misc.cachefunc import cached_method
|
49
|
+
from sage.misc.lazy_import import lazy_import
|
50
|
+
|
51
|
+
lazy_import('networkx', ['MultiGraph', 'Graph'], as_=['networkx_MultiGraph', 'networkx_Graph'])
|
52
|
+
|
53
|
+
|
54
|
+
class BipartiteGraph(Graph):
|
55
|
+
r"""
|
56
|
+
Bipartite graph.
|
57
|
+
|
58
|
+
INPUT:
|
59
|
+
|
60
|
+
- ``data`` -- can be any of the following:
|
61
|
+
|
62
|
+
#. Empty or ``None`` (creates an empty graph).
|
63
|
+
|
64
|
+
#. An arbitrary graph.
|
65
|
+
|
66
|
+
#. A reduced adjacency matrix.
|
67
|
+
|
68
|
+
A reduced adjacency matrix contains only the non-redundant
|
69
|
+
portion of the full adjacency matrix for the bipartite graph.
|
70
|
+
Specifically, for zero matrices of the appropriate size, for
|
71
|
+
the reduced adjacency matrix ``H``, the full adjacency matrix
|
72
|
+
is ``[[0, H'], [H, 0]]``. The columns correspond to vertices
|
73
|
+
on the left, and the rows correspond to vertices on the
|
74
|
+
right.
|
75
|
+
|
76
|
+
#. A file in alist format.
|
77
|
+
|
78
|
+
The alist file format is described at
|
79
|
+
http://www.inference.phy.cam.ac.uk/mackay/codes/alist.html
|
80
|
+
|
81
|
+
#. A ``graph6`` string (see documentation of :meth:`~graph6_string`).
|
82
|
+
|
83
|
+
#. From a NetworkX bipartite graph.
|
84
|
+
|
85
|
+
- ``partition`` -- (default: ``None``) a tuple defining vertices of the left
|
86
|
+
and right partition of the graph. Partitions will be determined
|
87
|
+
automatically if ``partition`` is ``None``.
|
88
|
+
|
89
|
+
- ``check`` -- boolean (default: ``True``); if ``True``, an invalid input
|
90
|
+
partition raises an exception. In the other case offending edges simply
|
91
|
+
won't be included.
|
92
|
+
|
93
|
+
- ``loops`` -- ignored; bipartite graphs cannot have loops
|
94
|
+
|
95
|
+
- ``multiedges`` -- boolean (default: ``None``); whether to allow multiple
|
96
|
+
edges
|
97
|
+
|
98
|
+
- ``weighted`` -- boolean (default: ``None``); whether graph thinks of
|
99
|
+
itself as weighted or not. See ``self.weighted()``
|
100
|
+
|
101
|
+
- ``hash_labels`` -- boolean (default: ``None``); whether to include edge
|
102
|
+
labels during hashing. This parameter defaults to ``True`` if the graph is
|
103
|
+
weighted. This parameter is ignored if the graph is mutable.
|
104
|
+
Beware that trying to hash unhashable labels will raise an error.
|
105
|
+
|
106
|
+
.. NOTE::
|
107
|
+
|
108
|
+
All remaining arguments are passed to the ``Graph`` constructor
|
109
|
+
|
110
|
+
EXAMPLES:
|
111
|
+
|
112
|
+
#. No inputs or ``None`` for the input creates an empty graph::
|
113
|
+
|
114
|
+
sage: B = BipartiteGraph()
|
115
|
+
sage: type(B)
|
116
|
+
<class 'sage.graphs.bipartite_graph.BipartiteGraph'>
|
117
|
+
sage: B.order()
|
118
|
+
0
|
119
|
+
sage: B == BipartiteGraph(None)
|
120
|
+
True
|
121
|
+
|
122
|
+
#. From a graph: without any more information, finds a bipartition::
|
123
|
+
|
124
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4))
|
125
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(5))
|
126
|
+
Traceback (most recent call last):
|
127
|
+
...
|
128
|
+
ValueError: input graph is not bipartite
|
129
|
+
sage: G = Graph({0: [5, 6], 1: [4, 5], 2: [4, 6], 3: [4, 5, 6]})
|
130
|
+
sage: B = BipartiteGraph(G)
|
131
|
+
sage: B == G
|
132
|
+
True
|
133
|
+
sage: B.left
|
134
|
+
{0, 1, 2, 3}
|
135
|
+
sage: B.right
|
136
|
+
{4, 5, 6}
|
137
|
+
sage: B = BipartiteGraph({0: [5, 6], 1: [4, 5], 2: [4, 6], 3: [4, 5, 6]})
|
138
|
+
sage: B == G
|
139
|
+
True
|
140
|
+
sage: B.left
|
141
|
+
{0, 1, 2, 3}
|
142
|
+
sage: B.right
|
143
|
+
{4, 5, 6}
|
144
|
+
|
145
|
+
#. If a ``Graph`` or ``DiGraph`` is used as data, you can specify
|
146
|
+
a partition using ``partition`` argument. Note that if such
|
147
|
+
graph is not bipartite, then Sage will raise an error. However,
|
148
|
+
if one specifies ``check=False``, the offending edges are
|
149
|
+
simply deleted (along with those vertices not appearing in
|
150
|
+
either list). We also lump creating one bipartite graph from
|
151
|
+
another into this category::
|
152
|
+
|
153
|
+
sage: P = graphs.PetersenGraph()
|
154
|
+
sage: partition = [list(range(5)), list(range(5, 10))]
|
155
|
+
sage: B = BipartiteGraph(P, partition)
|
156
|
+
Traceback (most recent call last):
|
157
|
+
...
|
158
|
+
TypeError: input graph is not bipartite with respect to the given partition
|
159
|
+
|
160
|
+
sage: B = BipartiteGraph(P, partition, check=False)
|
161
|
+
sage: B.left
|
162
|
+
{0, 1, 2, 3, 4}
|
163
|
+
sage: B.show() # needs sage.plot
|
164
|
+
|
165
|
+
::
|
166
|
+
|
167
|
+
sage: G = Graph({0: [5, 6], 1: [4, 5], 2: [4, 6], 3: [4, 5, 6]})
|
168
|
+
sage: B = BipartiteGraph(G)
|
169
|
+
sage: B2 = BipartiteGraph(B)
|
170
|
+
sage: B == B2
|
171
|
+
True
|
172
|
+
sage: B3 = BipartiteGraph(G, [list(range(4)), list(range(4, 7))])
|
173
|
+
sage: B3
|
174
|
+
Bipartite graph on 7 vertices
|
175
|
+
sage: B3 == B2
|
176
|
+
True
|
177
|
+
|
178
|
+
::
|
179
|
+
|
180
|
+
sage: G = Graph({0: [], 1: [], 2: []})
|
181
|
+
sage: part = (list(range(2)), [2])
|
182
|
+
sage: B = BipartiteGraph(G, part)
|
183
|
+
sage: B2 = BipartiteGraph(B)
|
184
|
+
sage: B == B2
|
185
|
+
True
|
186
|
+
|
187
|
+
::
|
188
|
+
|
189
|
+
sage: d = DiGraph(6)
|
190
|
+
sage: d.add_edge(0, 1)
|
191
|
+
sage: part=[[1, 2, 3], [0, 4, 5]]
|
192
|
+
sage: b = BipartiteGraph(d, part)
|
193
|
+
sage: b.left
|
194
|
+
{1, 2, 3}
|
195
|
+
sage: b.right
|
196
|
+
{0, 4, 5}
|
197
|
+
|
198
|
+
#. From a reduced adjacency matrix::
|
199
|
+
|
200
|
+
sage: # needs sage.modules
|
201
|
+
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
|
202
|
+
....: (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
|
203
|
+
sage: M
|
204
|
+
[1 1 1 0 0 0 0]
|
205
|
+
[1 0 0 1 1 0 0]
|
206
|
+
[0 1 0 1 0 1 0]
|
207
|
+
[1 1 0 1 0 0 1]
|
208
|
+
sage: H = BipartiteGraph(M); H
|
209
|
+
Bipartite graph on 11 vertices
|
210
|
+
sage: H.edges(sort=True)
|
211
|
+
[(0, 7, None),
|
212
|
+
(0, 8, None),
|
213
|
+
(0, 10, None),
|
214
|
+
(1, 7, None),
|
215
|
+
(1, 9, None),
|
216
|
+
(1, 10, None),
|
217
|
+
(2, 7, None),
|
218
|
+
(3, 8, None),
|
219
|
+
(3, 9, None),
|
220
|
+
(3, 10, None),
|
221
|
+
(4, 8, None),
|
222
|
+
(5, 9, None),
|
223
|
+
(6, 10, None)]
|
224
|
+
|
225
|
+
::
|
226
|
+
|
227
|
+
sage: M = Matrix([(1, 1, 2, 0, 0), (0, 2, 1, 1, 1), (0, 1, 2, 1, 1)]) # needs sage.modules
|
228
|
+
sage: B = BipartiteGraph(M, multiedges=True, sparse=True) # needs sage.modules
|
229
|
+
sage: B.edges(sort=True) # needs sage.modules
|
230
|
+
[(0, 5, None),
|
231
|
+
(1, 5, None),
|
232
|
+
(1, 6, None),
|
233
|
+
(1, 6, None),
|
234
|
+
(1, 7, None),
|
235
|
+
(2, 5, None),
|
236
|
+
(2, 5, None),
|
237
|
+
(2, 6, None),
|
238
|
+
(2, 7, None),
|
239
|
+
(2, 7, None),
|
240
|
+
(3, 6, None),
|
241
|
+
(3, 7, None),
|
242
|
+
(4, 6, None),
|
243
|
+
(4, 7, None)]
|
244
|
+
|
245
|
+
::
|
246
|
+
|
247
|
+
sage: # needs sage.modules sage.rings.finite_rings
|
248
|
+
sage: F.<a> = GF(4)
|
249
|
+
sage: MS = MatrixSpace(F, 2, 3)
|
250
|
+
sage: M = MS.matrix([[0, 1, a + 1], [a, 1, 1]])
|
251
|
+
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
|
252
|
+
sage: B.edges(sort=True)
|
253
|
+
[(0, 4, a), (1, 3, 1), (1, 4, 1), (2, 3, a + 1), (2, 4, 1)]
|
254
|
+
sage: B.weighted()
|
255
|
+
True
|
256
|
+
|
257
|
+
#. From an alist file::
|
258
|
+
|
259
|
+
sage: import tempfile
|
260
|
+
sage: with tempfile.NamedTemporaryFile(mode='w+t') as f:
|
261
|
+
....: _ = f.write("7 4 \n 3 4 \n 3 3 1 3 1 1 1 \n\
|
262
|
+
....: 3 3 3 4 \n 1 2 4 \n 1 3 4 \n 1 0 0 \n\
|
263
|
+
....: 2 3 4 \n 2 0 0 \n 3 0 0 \n 4 0 0 \n\
|
264
|
+
....: 1 2 3 0 \n 1 4 5 0 \n 2 4 6 0 \n\
|
265
|
+
....: 1 2 4 7 \n")
|
266
|
+
....: f.flush()
|
267
|
+
....: B = BipartiteGraph(f.name)
|
268
|
+
sage: B.is_isomorphic(H) # needs sage.modules
|
269
|
+
True
|
270
|
+
|
271
|
+
#. From a ``graph6`` string::
|
272
|
+
|
273
|
+
sage: B = BipartiteGraph('Bo')
|
274
|
+
sage: B
|
275
|
+
Bipartite graph on 3 vertices
|
276
|
+
sage: B.left
|
277
|
+
{0}
|
278
|
+
sage: B.right
|
279
|
+
{1, 2}
|
280
|
+
|
281
|
+
::
|
282
|
+
|
283
|
+
sage: B = BipartiteGraph('F?^T_\n', format='graph6')
|
284
|
+
sage: B.vertices(sort=True)
|
285
|
+
[0, 1, 2, 3, 4, 5, 6]
|
286
|
+
sage: B.edges(sort=True)
|
287
|
+
[(0, 5, None), (0, 6, None), (1, 4, None), (1, 5, None), (2, 4, None),
|
288
|
+
(2, 6, None), (3, 4, None), (3, 5, None), (3, 6, None)]
|
289
|
+
sage: B.left
|
290
|
+
{0, 1, 2, 3}
|
291
|
+
sage: B.right
|
292
|
+
{4, 5, 6}
|
293
|
+
|
294
|
+
::
|
295
|
+
sage: B = BipartiteGraph('Bo', partition=[[0], [1, 2]])
|
296
|
+
sage: B.left
|
297
|
+
{0}
|
298
|
+
sage: B.right
|
299
|
+
{1, 2}
|
300
|
+
|
301
|
+
::
|
302
|
+
|
303
|
+
sage: B = BipartiteGraph('F?^T_\n', partition=[[0, 1, 2], [3, 4, 5, 6]])
|
304
|
+
Traceback (most recent call last):
|
305
|
+
...
|
306
|
+
TypeError: input graph is not bipartite with respect to the given partition
|
307
|
+
|
308
|
+
sage: B = BipartiteGraph('F?^T_\n', partition=[[0, 1, 2], [3, 4, 5, 6]], check=False)
|
309
|
+
sage: B.left
|
310
|
+
{0, 1, 2}
|
311
|
+
sage: B.show() # needs sage.plot
|
312
|
+
|
313
|
+
#. From a NetworkX bipartite graph::
|
314
|
+
|
315
|
+
sage: # needs networkx
|
316
|
+
sage: import networkx
|
317
|
+
sage: G = graphs.OctahedralGraph()
|
318
|
+
sage: N = networkx.make_clique_bipartite(G.networkx_graph())
|
319
|
+
sage: B = BipartiteGraph(N)
|
320
|
+
|
321
|
+
TESTS:
|
322
|
+
|
323
|
+
Make sure we can create a ``BipartiteGraph`` with keywords but no positional
|
324
|
+
arguments (:issue:`10958`)::
|
325
|
+
|
326
|
+
sage: B = BipartiteGraph(multiedges=True)
|
327
|
+
sage: B.allows_multiple_edges()
|
328
|
+
True
|
329
|
+
|
330
|
+
Ensure that we can construct a ``BipartiteGraph`` with isolated vertices via
|
331
|
+
the reduced adjacency matrix (:issue:`10356`)::
|
332
|
+
|
333
|
+
sage: # needs sage.modules
|
334
|
+
sage: a = BipartiteGraph(matrix(2, 2, [1, 0, 1, 0]))
|
335
|
+
sage: a
|
336
|
+
Bipartite graph on 4 vertices
|
337
|
+
sage: a.vertices(sort=True)
|
338
|
+
[0, 1, 2, 3]
|
339
|
+
sage: g = BipartiteGraph(matrix(4, 4, [1] * 4 + [0] * 12))
|
340
|
+
sage: g.vertices(sort=True)
|
341
|
+
[0, 1, 2, 3, 4, 5, 6, 7]
|
342
|
+
sage: sorted(g.left.union(g.right))
|
343
|
+
[0, 1, 2, 3, 4, 5, 6, 7]
|
344
|
+
|
345
|
+
Make sure that loops are not allowed (:issue:`23275`)::
|
346
|
+
|
347
|
+
sage: B = BipartiteGraph(loops=True)
|
348
|
+
Traceback (most recent call last):
|
349
|
+
...
|
350
|
+
ValueError: loops are not allowed in bipartite graphs
|
351
|
+
sage: B = BipartiteGraph(loops=None)
|
352
|
+
sage: B.allows_loops()
|
353
|
+
False
|
354
|
+
sage: B.add_edge(0, 0)
|
355
|
+
Traceback (most recent call last):
|
356
|
+
...
|
357
|
+
ValueError: cannot add edge from 0 to 0 in graph without loops
|
358
|
+
"""
|
359
|
+
|
360
|
+
def __init__(self, data=None, partition=None, check=True, hash_labels=None, *args, **kwds):
|
361
|
+
"""
|
362
|
+
Create a bipartite graph.
|
363
|
+
|
364
|
+
See documentation ``BipartiteGraph?`` for detailed information.
|
365
|
+
|
366
|
+
EXAMPLES::
|
367
|
+
|
368
|
+
sage: P = graphs.PetersenGraph()
|
369
|
+
sage: partition = [list(range(5)), list(range(5, 10))]
|
370
|
+
sage: B = BipartiteGraph(P, partition, check=False)
|
371
|
+
|
372
|
+
TESTS:
|
373
|
+
|
374
|
+
Check that :issue:`33249` is fixed::
|
375
|
+
|
376
|
+
sage: G = BipartiteGraph({2:[1], 3:[1], 4:[5]}, partition=([2,3,4],[1,5]))
|
377
|
+
sage: print(G.left, G.right)
|
378
|
+
{2, 3, 4} {1, 5}
|
379
|
+
sage: G = BipartiteGraph({2:[1], 3:[1]}, partition=([1,2],[3]), check=True)
|
380
|
+
Traceback (most recent call last):
|
381
|
+
...
|
382
|
+
TypeError: input graph is not bipartite with respect to the given partition
|
383
|
+
sage: G = BipartiteGraph({2:[1], 3:[1], 4:[5]}, partition=([2,3,4],[1]))
|
384
|
+
Traceback (most recent call last):
|
385
|
+
...
|
386
|
+
ValueError: not all vertices appear in partition
|
387
|
+
sage: G = BipartiteGraph({2:[1], 3:[1], 4:[5]}, partition=([2,3,4],[1, 2]))
|
388
|
+
Traceback (most recent call last):
|
389
|
+
...
|
390
|
+
ValueError: the parts are not disjoint
|
391
|
+
sage: G = BipartiteGraph({2:[1], 3:[1], 4:[5]}, partition=([2, 3, 4], [1, 7]))
|
392
|
+
Traceback (most recent call last):
|
393
|
+
...
|
394
|
+
LookupError: vertex (7) is not a vertex of the graph
|
395
|
+
|
396
|
+
Check that :issue:`39295` is fixed::
|
397
|
+
|
398
|
+
sage: # needs sage.modules
|
399
|
+
sage: B = BipartiteGraph(matrix([[1, 1], [1, 1]]), immutable=True)
|
400
|
+
sage: print(B.vertices(), B.edges())
|
401
|
+
[0, 1, 2, 3] [(0, 2, None), (0, 3, None), (1, 2, None), (1, 3, None)]
|
402
|
+
sage: B.add_vertices([4], left=True)
|
403
|
+
Traceback (most recent call last):
|
404
|
+
...
|
405
|
+
ValueError: graph is immutable; please change a copy instead (use function copy())
|
406
|
+
"""
|
407
|
+
if kwds is None:
|
408
|
+
kwds = {'loops': False}
|
409
|
+
else:
|
410
|
+
if 'loops' in kwds and kwds['loops']:
|
411
|
+
raise ValueError('loops are not allowed in bipartite graphs')
|
412
|
+
kwds['loops'] = False
|
413
|
+
|
414
|
+
if data is None:
|
415
|
+
if partition is not None and check:
|
416
|
+
if partition[0] or partition[1]:
|
417
|
+
raise ValueError("invalid partition")
|
418
|
+
Graph.__init__(self, **kwds)
|
419
|
+
self.left = set()
|
420
|
+
self.right = set()
|
421
|
+
self._hash_labels = hash_labels
|
422
|
+
return
|
423
|
+
|
424
|
+
# need to turn off partition checking for Graph.__init__() adding
|
425
|
+
# vertices and edges; methods are restored at the end of big "if"
|
426
|
+
# statement below
|
427
|
+
from types import MethodType
|
428
|
+
self.add_vertex = MethodType(Graph.add_vertex, self)
|
429
|
+
self.add_vertices = MethodType(Graph.add_vertices, self)
|
430
|
+
self.add_edge = MethodType(Graph.add_edge, self)
|
431
|
+
self.add_edges = MethodType(Graph.add_edges, self)
|
432
|
+
alist_file = True
|
433
|
+
|
434
|
+
from sage.structure.element import Matrix
|
435
|
+
if isinstance(data, BipartiteGraph):
|
436
|
+
Graph.__init__(self, data, *args, **kwds)
|
437
|
+
self.left = set(data.left)
|
438
|
+
self.right = set(data.right)
|
439
|
+
elif isinstance(data, str):
|
440
|
+
import os
|
441
|
+
alist_file = os.path.exists(data)
|
442
|
+
Graph.__init__(self, data=None if alist_file else data, *args, **kwds)
|
443
|
+
|
444
|
+
# methods; initialize left and right attributes
|
445
|
+
self.left = set()
|
446
|
+
self.right = set()
|
447
|
+
|
448
|
+
# determine partitions and populate self.left and self.right
|
449
|
+
if not alist_file:
|
450
|
+
if partition is not None:
|
451
|
+
left, right = set(partition[0]), set(partition[1])
|
452
|
+
|
453
|
+
# Some error checking.
|
454
|
+
if left & right:
|
455
|
+
raise ValueError("the parts are not disjoint")
|
456
|
+
if len(left) + len(right) != self.num_verts():
|
457
|
+
raise ValueError("not all vertices appear in partition")
|
458
|
+
|
459
|
+
if check:
|
460
|
+
if (any(left.intersection(self.neighbor_iterator(a)) for a in left) or
|
461
|
+
any(right.intersection(self.neighbor_iterator(a)) for a in right)):
|
462
|
+
raise TypeError("input graph is not bipartite with "
|
463
|
+
"respect to the given partition")
|
464
|
+
else:
|
465
|
+
for a in left:
|
466
|
+
a_nbrs = left.intersection(self.neighbor_iterator(a))
|
467
|
+
if a_nbrs:
|
468
|
+
self.delete_edges((a, b) for b in a_nbrs)
|
469
|
+
for a in right:
|
470
|
+
a_nbrs = right.intersection(self.neighbor_iterator(a))
|
471
|
+
if a_nbrs:
|
472
|
+
self.delete_edges((a, b) for b in a_nbrs)
|
473
|
+
self.left, self.right = left, right
|
474
|
+
else:
|
475
|
+
# Automatically get partitions if not provided
|
476
|
+
self._upgrade_from_graph()
|
477
|
+
elif isinstance(data, Matrix):
|
478
|
+
# sanity check for mutually exclusive keywords
|
479
|
+
if kwds.get("multiedges", False) and kwds.get("weighted", False):
|
480
|
+
raise TypeError("weighted multi-edge bipartite graphs from "
|
481
|
+
"reduced adjacency matrix not supported")
|
482
|
+
ncols = data.ncols()
|
483
|
+
nrows = data.nrows()
|
484
|
+
self.left = set(range(ncols))
|
485
|
+
self.right = set(range(ncols, nrows + ncols))
|
486
|
+
|
487
|
+
def edges():
|
488
|
+
if kwds.get("multiedges", False):
|
489
|
+
for ii in range(ncols):
|
490
|
+
for jj in range(nrows):
|
491
|
+
for _ in range(data[jj, ii]):
|
492
|
+
yield (ii, jj + ncols)
|
493
|
+
elif kwds.get("weighted", False):
|
494
|
+
for ii in range(ncols):
|
495
|
+
for jj in range(nrows):
|
496
|
+
if data[jj, ii]:
|
497
|
+
yield (ii, jj + ncols, data[jj, ii])
|
498
|
+
else:
|
499
|
+
for ii in range(ncols):
|
500
|
+
for jj in range(nrows):
|
501
|
+
if data[jj, ii]:
|
502
|
+
yield (ii, jj + ncols)
|
503
|
+
|
504
|
+
# ensure that construction works
|
505
|
+
# when immutable=True (issue #39295)
|
506
|
+
Graph.__init__(self, data=[range(nrows + ncols), edges()], format='vertices_and_edges', *args, **kwds)
|
507
|
+
else:
|
508
|
+
if partition is not None:
|
509
|
+
left, right = set(partition[0]), set(partition[1])
|
510
|
+
if isinstance(data, GenericGraph):
|
511
|
+
verts = left | right
|
512
|
+
if set(data) != verts:
|
513
|
+
data = data.subgraph(verts)
|
514
|
+
Graph.__init__(self, data, *args, **kwds)
|
515
|
+
if partition is not None:
|
516
|
+
# Some error checking.
|
517
|
+
if left & right:
|
518
|
+
raise ValueError("the parts are not disjoint")
|
519
|
+
if len(left) + len(right) != self.num_verts():
|
520
|
+
raise ValueError("not all vertices appear in partition")
|
521
|
+
|
522
|
+
if isinstance(data, (networkx_MultiGraph, networkx_Graph)):
|
523
|
+
if hasattr(data, "node_type"):
|
524
|
+
# Assume the graph is bipartite
|
525
|
+
self.left = set()
|
526
|
+
self.right = set()
|
527
|
+
for v in data.nodes_iter():
|
528
|
+
if data.node_type[v] == "Bottom":
|
529
|
+
self.left.add(v)
|
530
|
+
elif data.node_type[v] == "Top":
|
531
|
+
self.right.add(v)
|
532
|
+
else:
|
533
|
+
raise TypeError("NetworkX node_type defies bipartite "
|
534
|
+
"assumption (is not 'Top' or 'Bottom')")
|
535
|
+
elif partition:
|
536
|
+
if check:
|
537
|
+
if (any(left.intersection(self.neighbor_iterator(a)) for a in left) or
|
538
|
+
any(right.intersection(self.neighbor_iterator(a)) for a in right)):
|
539
|
+
raise TypeError("input graph is not bipartite with "
|
540
|
+
"respect to the given partition")
|
541
|
+
else:
|
542
|
+
for a in left:
|
543
|
+
a_nbrs = left.intersection(data.neighbor_iterator(a))
|
544
|
+
if a_nbrs:
|
545
|
+
self.delete_edges((a, b) for b in a_nbrs)
|
546
|
+
for a in right:
|
547
|
+
a_nbrs = right.intersection(data.neighbor_iterator(a))
|
548
|
+
if a_nbrs:
|
549
|
+
self.delete_edges((a, b) for b in a_nbrs)
|
550
|
+
self.left, self.right = left, right
|
551
|
+
|
552
|
+
# make sure we found a bipartition
|
553
|
+
if not (hasattr(self, "left") and hasattr(self, "right")):
|
554
|
+
self._upgrade_from_graph()
|
555
|
+
|
556
|
+
# restore vertex partition checking
|
557
|
+
del self.add_vertex
|
558
|
+
del self.add_vertices
|
559
|
+
del self.add_edge
|
560
|
+
del self.add_edges
|
561
|
+
|
562
|
+
# post-processing
|
563
|
+
if isinstance(data, str):
|
564
|
+
if alist_file:
|
565
|
+
self.load_afile(data)
|
566
|
+
|
567
|
+
if hash_labels is None and hasattr(data, '_hash_labels'):
|
568
|
+
hash_labels = data._hash_labels
|
569
|
+
self._hash_labels = hash_labels
|
570
|
+
|
571
|
+
return
|
572
|
+
|
573
|
+
@cached_method
|
574
|
+
def __hash__(self):
|
575
|
+
"""
|
576
|
+
Compute a hash for ``self``, if ``self`` is immutable.
|
577
|
+
|
578
|
+
EXAMPLES::
|
579
|
+
|
580
|
+
sage: A = BipartiteGraph([(1, 2, 1)], immutable=True)
|
581
|
+
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True)
|
582
|
+
sage: A.__hash__() == B.__hash__()
|
583
|
+
True
|
584
|
+
sage: A = BipartiteGraph([(1, 2, 1)], immutable=True, hash_labels=True)
|
585
|
+
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True, hash_labels=True)
|
586
|
+
sage: A.__hash__() == B.__hash__()
|
587
|
+
False
|
588
|
+
sage: A = BipartiteGraph([(1, 2, 1)], immutable=True, weighted=True)
|
589
|
+
sage: B = BipartiteGraph([(1, 2, 33)], immutable=True, weighted=True)
|
590
|
+
sage: A.__hash__() == B.__hash__()
|
591
|
+
False
|
592
|
+
|
593
|
+
TESTS::
|
594
|
+
|
595
|
+
sage: A = BipartiteGraph([(1, 2, 1)], immutable=False)
|
596
|
+
sage: A.__hash__()
|
597
|
+
Traceback (most recent call last):
|
598
|
+
...
|
599
|
+
TypeError: This graph is mutable, and thus not hashable. Create an immutable copy by `g.copy(immutable=True)`
|
600
|
+
sage: B = BipartiteGraph([(1, 2, {'length': 3})], immutable=True, hash_labels=True)
|
601
|
+
sage: B.__hash__()
|
602
|
+
Traceback (most recent call last):
|
603
|
+
...
|
604
|
+
TypeError: unhashable type: 'dict'
|
605
|
+
"""
|
606
|
+
if self.is_immutable():
|
607
|
+
# Determine whether to hash edge labels
|
608
|
+
use_labels = self._use_labels_for_hash()
|
609
|
+
edge_items = self.edge_iterator(labels=use_labels)
|
610
|
+
if self.allows_multiple_edges():
|
611
|
+
from collections import Counter
|
612
|
+
edge_items = Counter(edge_items).items()
|
613
|
+
return hash((frozenset(self.left), frozenset(self.right), frozenset(edge_items)))
|
614
|
+
|
615
|
+
raise TypeError("This graph is mutable, and thus not hashable. "
|
616
|
+
"Create an immutable copy by `g.copy(immutable=True)`")
|
617
|
+
|
618
|
+
def _upgrade_from_graph(self):
|
619
|
+
"""
|
620
|
+
Set the left and right sets of vertices from the input graph.
|
621
|
+
|
622
|
+
TESTS::
|
623
|
+
|
624
|
+
sage: B = BipartiteGraph(Graph(1))
|
625
|
+
sage: B.left, B.right
|
626
|
+
({0}, set())
|
627
|
+
sage: B = BipartiteGraph(Graph(2))
|
628
|
+
sage: B.left, B.right
|
629
|
+
({0, 1}, set())
|
630
|
+
sage: B = BipartiteGraph(graphs.PathGraph(2))
|
631
|
+
sage: B.left, B.right
|
632
|
+
({0}, {1})
|
633
|
+
sage: B = BipartiteGraph(graphs.PathGraph(3))
|
634
|
+
sage: B.left, B.right
|
635
|
+
({0, 2}, {1})
|
636
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(3))
|
637
|
+
Traceback (most recent call last):
|
638
|
+
...
|
639
|
+
ValueError: input graph is not bipartite
|
640
|
+
"""
|
641
|
+
ans, certif = GenericGraph.is_bipartite(self, certificate=True)
|
642
|
+
if not ans:
|
643
|
+
raise ValueError("input graph is not bipartite")
|
644
|
+
cols = defaultdict(set)
|
645
|
+
for k, v in certif.items():
|
646
|
+
cols[v].add(k)
|
647
|
+
self.left = cols[1]
|
648
|
+
self.right = cols[0]
|
649
|
+
|
650
|
+
def __repr__(self):
|
651
|
+
r"""
|
652
|
+
Return a short string representation of ``self``.
|
653
|
+
|
654
|
+
EXAMPLES::
|
655
|
+
|
656
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(16))
|
657
|
+
sage: B
|
658
|
+
Bipartite cycle graph: graph on 16 vertices
|
659
|
+
"""
|
660
|
+
s = Graph._repr_(self).lower()
|
661
|
+
if "bipartite" in s:
|
662
|
+
return s.capitalize()
|
663
|
+
return "".join(["Bipartite ", s])
|
664
|
+
|
665
|
+
def add_vertex(self, name=None, left=False, right=False):
|
666
|
+
"""
|
667
|
+
Create an isolated vertex. If the vertex already exists, then
|
668
|
+
nothing is done.
|
669
|
+
|
670
|
+
INPUT:
|
671
|
+
|
672
|
+
- ``name`` -- (default: ``None``) name of the new vertex. If no name is
|
673
|
+
specified, then the vertex will be represented by the least
|
674
|
+
nonnegative integer not already representing a vertex. Name must be
|
675
|
+
an immutable object and cannot be ``None``.
|
676
|
+
|
677
|
+
- ``left`` -- boolean (default: ``False``); if ``True``, puts the new
|
678
|
+
vertex in the left partition
|
679
|
+
|
680
|
+
- ``right`` -- boolean (default: ``False``); if ``True``, puts the new
|
681
|
+
vertex in the right partition
|
682
|
+
|
683
|
+
Obviously, ``left`` and ``right`` are mutually exclusive.
|
684
|
+
|
685
|
+
As it is implemented now, if a graph `G` has a large number of vertices
|
686
|
+
with numeric labels, then ``G.add_vertex()`` could potentially be slow,
|
687
|
+
if name is ``None``.
|
688
|
+
|
689
|
+
OUTPUT:
|
690
|
+
|
691
|
+
- If ``name`` is ``None``, the new vertex name is returned. ``None``
|
692
|
+
otherwise.
|
693
|
+
|
694
|
+
EXAMPLES::
|
695
|
+
|
696
|
+
sage: G = BipartiteGraph()
|
697
|
+
sage: G.add_vertex(left=True)
|
698
|
+
0
|
699
|
+
sage: G.add_vertex(right=True)
|
700
|
+
1
|
701
|
+
sage: G.vertices(sort=True)
|
702
|
+
[0, 1]
|
703
|
+
sage: G.left
|
704
|
+
{0}
|
705
|
+
sage: G.right
|
706
|
+
{1}
|
707
|
+
|
708
|
+
TESTS:
|
709
|
+
|
710
|
+
Exactly one of ``left`` and ``right`` must be true::
|
711
|
+
|
712
|
+
sage: G = BipartiteGraph()
|
713
|
+
sage: G.add_vertex()
|
714
|
+
Traceback (most recent call last):
|
715
|
+
...
|
716
|
+
RuntimeError: partition must be specified (e.g. left=True)
|
717
|
+
sage: G.add_vertex(left=True, right=True)
|
718
|
+
Traceback (most recent call last):
|
719
|
+
...
|
720
|
+
RuntimeError: only one partition may be specified
|
721
|
+
|
722
|
+
Adding the same vertex must specify the same partition::
|
723
|
+
|
724
|
+
sage: bg = BipartiteGraph()
|
725
|
+
sage: bg.add_vertex(0, right=True)
|
726
|
+
sage: bg.add_vertex(0, right=True)
|
727
|
+
sage: bg.vertices(sort=False)
|
728
|
+
[0]
|
729
|
+
sage: bg.add_vertex(0, left=True)
|
730
|
+
Traceback (most recent call last):
|
731
|
+
...
|
732
|
+
RuntimeError: cannot add duplicate vertex to other partition
|
733
|
+
"""
|
734
|
+
# sanity check on partition specifiers
|
735
|
+
if left and right:
|
736
|
+
raise RuntimeError("only one partition may be specified")
|
737
|
+
if not (left or right):
|
738
|
+
raise RuntimeError("partition must be specified (e.g. left=True)")
|
739
|
+
|
740
|
+
# do nothing if we already have this vertex (idempotent)
|
741
|
+
if name is not None and name in self:
|
742
|
+
if ((left and name in self.left) or
|
743
|
+
(right and name in self.right)):
|
744
|
+
return
|
745
|
+
raise RuntimeError("cannot add duplicate vertex to other partition")
|
746
|
+
|
747
|
+
# add the vertex
|
748
|
+
retval = Graph.add_vertex(self, name)
|
749
|
+
if retval is not None:
|
750
|
+
name = retval
|
751
|
+
|
752
|
+
# add to proper partition
|
753
|
+
if left:
|
754
|
+
self.left.add(name)
|
755
|
+
else:
|
756
|
+
self.right.add(name)
|
757
|
+
|
758
|
+
return retval
|
759
|
+
|
760
|
+
def add_vertices(self, vertices, left=False, right=False):
|
761
|
+
"""
|
762
|
+
Add vertices to the bipartite graph from an iterable container of
|
763
|
+
vertices.
|
764
|
+
|
765
|
+
Vertices that already exist in the graph will not be added again.
|
766
|
+
|
767
|
+
INPUT:
|
768
|
+
|
769
|
+
- ``vertices`` -- sequence of vertices to add
|
770
|
+
|
771
|
+
- ``left`` -- (default: ``False``) either ``True`` or sequence of same
|
772
|
+
length as ``vertices`` with boolean elements
|
773
|
+
|
774
|
+
- ``right`` -- (default: ``False``) either ``True`` or sequence of the
|
775
|
+
same length as ``vertices`` with boolean elements
|
776
|
+
|
777
|
+
Only one of ``left`` and ``right`` keywords should be provided. See
|
778
|
+
the examples below.
|
779
|
+
|
780
|
+
EXAMPLES::
|
781
|
+
|
782
|
+
sage: bg = BipartiteGraph()
|
783
|
+
sage: bg.add_vertices([0, 1, 2], left=True)
|
784
|
+
sage: bg.add_vertices([3, 4, 5], left=[True, False, True])
|
785
|
+
sage: bg.add_vertices([6, 7, 8], right=[True, False, True])
|
786
|
+
sage: bg.add_vertices([9, 10, 11], right=True)
|
787
|
+
sage: bg.left
|
788
|
+
{0, 1, 2, 3, 5, 7}
|
789
|
+
sage: bg.right
|
790
|
+
{4, 6, 8, 9, 10, 11}
|
791
|
+
|
792
|
+
TESTS::
|
793
|
+
|
794
|
+
sage: bg = BipartiteGraph()
|
795
|
+
sage: bg.add_vertices([0, 1, 2], left=True)
|
796
|
+
sage: bg.add_vertices([0, 1, 2], left=[True, True, True])
|
797
|
+
sage: bg.add_vertices([0, 1, 2], right=[False, False, False])
|
798
|
+
sage: bg.add_vertices([0, 1, 2], right=[False, False, False])
|
799
|
+
sage: bg.add_vertices([0, 1, 2])
|
800
|
+
Traceback (most recent call last):
|
801
|
+
...
|
802
|
+
RuntimeError: partition must be specified (e.g. left=True)
|
803
|
+
sage: bg.add_vertices([0,1,2], left=True, right=True)
|
804
|
+
Traceback (most recent call last):
|
805
|
+
...
|
806
|
+
RuntimeError: only one partition may be specified
|
807
|
+
sage: bg.add_vertices([0,1,2], right=True)
|
808
|
+
Traceback (most recent call last):
|
809
|
+
...
|
810
|
+
RuntimeError: cannot add duplicate vertex to other partition
|
811
|
+
sage: (bg.left, bg.right)
|
812
|
+
({0, 1, 2}, set())
|
813
|
+
"""
|
814
|
+
# sanity check on partition specifiers
|
815
|
+
if left and right: # also triggered if both lists are specified
|
816
|
+
raise RuntimeError("only one partition may be specified")
|
817
|
+
if not (left or right):
|
818
|
+
raise RuntimeError("partition must be specified (e.g. left=True)")
|
819
|
+
|
820
|
+
# handle partitions
|
821
|
+
if left and (not isinstance(left, Iterable)):
|
822
|
+
new_left = set(vertices)
|
823
|
+
new_right = set()
|
824
|
+
elif right and (not isinstance(right, Iterable)):
|
825
|
+
new_left = set()
|
826
|
+
new_right = set(vertices)
|
827
|
+
else:
|
828
|
+
# simplify to always work with left
|
829
|
+
if right:
|
830
|
+
left = [not tf for tf in right]
|
831
|
+
new_left = set()
|
832
|
+
new_right = set()
|
833
|
+
for tf, vv in zip(left, vertices):
|
834
|
+
if tf:
|
835
|
+
new_left.add(vv)
|
836
|
+
else:
|
837
|
+
new_right.add(vv)
|
838
|
+
|
839
|
+
# check that we're not trying to add vertices to the wrong sets
|
840
|
+
# or that a vertex is to be placed in both
|
841
|
+
if ((new_left & self.right) or
|
842
|
+
(new_right & self.left) or
|
843
|
+
(new_right & new_left)):
|
844
|
+
raise RuntimeError("cannot add duplicate vertex to other partition")
|
845
|
+
|
846
|
+
# add vertices
|
847
|
+
Graph.add_vertices(self, vertices)
|
848
|
+
self.left.update(new_left)
|
849
|
+
self.right.update(new_right)
|
850
|
+
|
851
|
+
def delete_vertex(self, vertex, in_order=False):
|
852
|
+
"""
|
853
|
+
Delete vertex, removing all incident edges.
|
854
|
+
|
855
|
+
Deleting a non-existent vertex will raise an exception.
|
856
|
+
|
857
|
+
INPUT:
|
858
|
+
|
859
|
+
- ``vertex`` -- a vertex to delete
|
860
|
+
|
861
|
+
- ``in_order`` -- boolean (default: ``False``); if ``True``, deletes the
|
862
|
+
`i`-th vertex in the sorted list of vertices,
|
863
|
+
i.e. ``G.vertices(sort=True)[i]``
|
864
|
+
|
865
|
+
EXAMPLES::
|
866
|
+
|
867
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4))
|
868
|
+
sage: B
|
869
|
+
Bipartite cycle graph: graph on 4 vertices
|
870
|
+
sage: B.delete_vertex(0)
|
871
|
+
sage: B
|
872
|
+
Bipartite cycle graph: graph on 3 vertices
|
873
|
+
sage: B.left
|
874
|
+
{2}
|
875
|
+
sage: B.edges(sort=True)
|
876
|
+
[(1, 2, None), (2, 3, None)]
|
877
|
+
sage: B.delete_vertex(3)
|
878
|
+
sage: B.right
|
879
|
+
{1}
|
880
|
+
sage: B.edges(sort=True)
|
881
|
+
[(1, 2, None)]
|
882
|
+
sage: B.delete_vertex(0)
|
883
|
+
Traceback (most recent call last):
|
884
|
+
...
|
885
|
+
ValueError: vertex (0) not in the graph
|
886
|
+
|
887
|
+
::
|
888
|
+
|
889
|
+
sage: g = Graph({'a': ['b'], 'c': ['b']})
|
890
|
+
sage: bg = BipartiteGraph(g) # finds bipartition
|
891
|
+
sage: bg.vertices(sort=True)
|
892
|
+
['a', 'b', 'c']
|
893
|
+
sage: bg.delete_vertex('a')
|
894
|
+
sage: bg.edges(sort=True)
|
895
|
+
[('b', 'c', None)]
|
896
|
+
sage: bg.vertices(sort=True)
|
897
|
+
['b', 'c']
|
898
|
+
sage: bg2 = BipartiteGraph(g)
|
899
|
+
sage: bg2.delete_vertex(0, in_order=True)
|
900
|
+
sage: bg2 == bg
|
901
|
+
True
|
902
|
+
"""
|
903
|
+
# cache vertex lookup if requested
|
904
|
+
if in_order:
|
905
|
+
vertex = self.vertices(sort=True)[vertex]
|
906
|
+
|
907
|
+
# delete from the graph
|
908
|
+
Graph.delete_vertex(self, vertex)
|
909
|
+
|
910
|
+
# now remove from partition (exception already thrown for non-existent
|
911
|
+
# vertex)
|
912
|
+
if vertex in self.left:
|
913
|
+
self.left.remove(vertex)
|
914
|
+
elif vertex in self.right:
|
915
|
+
self.right.remove(vertex)
|
916
|
+
else:
|
917
|
+
raise RuntimeError("vertex (%s) not found in partitions" % vertex)
|
918
|
+
|
919
|
+
def delete_vertices(self, vertices):
|
920
|
+
"""
|
921
|
+
Remove vertices from the bipartite graph taken from an iterable
|
922
|
+
sequence of vertices.
|
923
|
+
|
924
|
+
Deleting a non-existent vertex will raise an exception.
|
925
|
+
|
926
|
+
INPUT:
|
927
|
+
|
928
|
+
- ``vertices`` -- a sequence of vertices to remove
|
929
|
+
|
930
|
+
EXAMPLES::
|
931
|
+
|
932
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4))
|
933
|
+
sage: B
|
934
|
+
Bipartite cycle graph: graph on 4 vertices
|
935
|
+
sage: B.delete_vertices([0, 3])
|
936
|
+
sage: B
|
937
|
+
Bipartite cycle graph: graph on 2 vertices
|
938
|
+
sage: B.left
|
939
|
+
{2}
|
940
|
+
sage: B.right
|
941
|
+
{1}
|
942
|
+
sage: B.edges(sort=True)
|
943
|
+
[(1, 2, None)]
|
944
|
+
sage: B.delete_vertices([0])
|
945
|
+
Traceback (most recent call last):
|
946
|
+
...
|
947
|
+
ValueError: vertex (0) not in the graph
|
948
|
+
"""
|
949
|
+
# remove vertices from the graph
|
950
|
+
Graph.delete_vertices(self, vertices)
|
951
|
+
|
952
|
+
# now remove vertices from partition lists (exception already thrown
|
953
|
+
# for non-existent vertices)
|
954
|
+
for vertex in vertices:
|
955
|
+
if vertex in self.left:
|
956
|
+
self.left.remove(vertex)
|
957
|
+
elif vertex in self.right:
|
958
|
+
self.right.remove(vertex)
|
959
|
+
else:
|
960
|
+
raise RuntimeError("vertex (%s) not found in partitions" % vertex)
|
961
|
+
|
962
|
+
def _flip_vertices(self, vertices):
|
963
|
+
r"""
|
964
|
+
Helper method to flip the sides of a list of vertices.
|
965
|
+
|
966
|
+
INPUT:
|
967
|
+
|
968
|
+
- ``vertices`` -- an iterable container of vertices
|
969
|
+
|
970
|
+
TESTS::
|
971
|
+
|
972
|
+
sage: G = BipartiteGraph()
|
973
|
+
sage: G.add_vertices([0, 1, 2], left=[True, False, True])
|
974
|
+
sage: G.bipartition()
|
975
|
+
({0, 2}, {1})
|
976
|
+
sage: G._flip_vertices([0, 1])
|
977
|
+
sage: G.bipartition()
|
978
|
+
({1, 2}, {0})
|
979
|
+
sage: G._flip_vertices([7])
|
980
|
+
Traceback (most recent call last):
|
981
|
+
...
|
982
|
+
RuntimeError: vertex (7) is neither in left nor in right
|
983
|
+
"""
|
984
|
+
for vertex in vertices:
|
985
|
+
if vertex in self.left:
|
986
|
+
self.left.remove(vertex)
|
987
|
+
self.right.add(vertex)
|
988
|
+
elif vertex in self.right:
|
989
|
+
self.right.remove(vertex)
|
990
|
+
self.left.add(vertex)
|
991
|
+
else:
|
992
|
+
raise RuntimeError(f"vertex ({vertex}) is neither in left nor in right")
|
993
|
+
|
994
|
+
def add_edge(self, u, v=None, label=None):
|
995
|
+
r"""
|
996
|
+
Add an edge from `u` to `v`.
|
997
|
+
|
998
|
+
INPUT:
|
999
|
+
|
1000
|
+
- ``u`` -- the tail of an edge
|
1001
|
+
|
1002
|
+
- ``v`` -- (default: ``None``) the head of an edge. If ``v=None``, then
|
1003
|
+
attempt to understand ``u`` as a edge tuple
|
1004
|
+
|
1005
|
+
- ``label`` -- (default: ``None``) the label of the edge ``(u, v)``
|
1006
|
+
|
1007
|
+
The following forms are all accepted:
|
1008
|
+
|
1009
|
+
- ``G.add_edge(1, 2)``
|
1010
|
+
- ``G.add_edge((1, 2))``
|
1011
|
+
- ``G.add_edges([(1, 2)])``
|
1012
|
+
- ``G.add_edge(1, 2, 'label')``
|
1013
|
+
- ``G.add_edge((1, 2, 'label'))``
|
1014
|
+
- ``G.add_edges([(1, 2, 'label')])``
|
1015
|
+
|
1016
|
+
See :meth:`~sage.graphs.graph.Graph.add_edge` for more detail.
|
1017
|
+
|
1018
|
+
This method simply checks that the edge endpoints are in different
|
1019
|
+
partitions. If a new vertex is to be created, it will be added to the
|
1020
|
+
proper partition. If both vertices are created, the first one will be
|
1021
|
+
added to the left partition, the second to the right partition. If
|
1022
|
+
both vertices are in the same partition but different connected
|
1023
|
+
components, one of the components will be "flipped", i.e. each vertex
|
1024
|
+
will be put into whichever partition it's not currently in. This will
|
1025
|
+
allow for the graph to remain bipartite, without changing the edges or
|
1026
|
+
vertices.
|
1027
|
+
|
1028
|
+
TESTS::
|
1029
|
+
|
1030
|
+
sage: bg = BipartiteGraph()
|
1031
|
+
sage: bg.add_vertices([0, 1, 2], left=[True, False, True])
|
1032
|
+
sage: bg.add_edges([(0, 1), (2, 1)])
|
1033
|
+
sage: bg.add_edge(0, 2)
|
1034
|
+
Traceback (most recent call last):
|
1035
|
+
...
|
1036
|
+
RuntimeError: edge vertices must lie in different partitions
|
1037
|
+
sage: bg.add_edge(0, 3); list(bg.right)
|
1038
|
+
[1, 3]
|
1039
|
+
sage: bg.add_edge(5, 6); 5 in bg.left; 6 in bg.right
|
1040
|
+
True
|
1041
|
+
True
|
1042
|
+
sage: G = BipartiteGraph()
|
1043
|
+
sage: G.add_edges([(0, 1), (3, 2)])
|
1044
|
+
sage: G.bipartition()
|
1045
|
+
({0, 3}, {1, 2})
|
1046
|
+
sage: G.add_edge(1,2)
|
1047
|
+
sage: G.bipartition()
|
1048
|
+
({0, 2}, {1, 3})
|
1049
|
+
"""
|
1050
|
+
# logic for getting endpoints copied from generic_graph.py
|
1051
|
+
if label is None:
|
1052
|
+
if v is None:
|
1053
|
+
try:
|
1054
|
+
u, v, label = u
|
1055
|
+
except Exception:
|
1056
|
+
u, v = u
|
1057
|
+
label = None
|
1058
|
+
else:
|
1059
|
+
if v is None:
|
1060
|
+
u, v = u
|
1061
|
+
|
1062
|
+
# if endpoints are in the same partition
|
1063
|
+
if self.left.issuperset((u, v)) or self.right.issuperset((u, v)):
|
1064
|
+
|
1065
|
+
# get v's connected component
|
1066
|
+
v_connected_component = self.connected_component_containing_vertex(v, sort=False)
|
1067
|
+
|
1068
|
+
# if u is in it, then the edge still cannot exist
|
1069
|
+
if u in v_connected_component:
|
1070
|
+
raise RuntimeError("edge vertices must lie in different partitions")
|
1071
|
+
|
1072
|
+
# if not, we can "flip" the connected component
|
1073
|
+
# swapping which partition the vertices are in
|
1074
|
+
self._flip_vertices(v_connected_component)
|
1075
|
+
|
1076
|
+
# automatically decide partitions for the newly created vertices
|
1077
|
+
if u not in self:
|
1078
|
+
self.add_vertex(u, left=(v in self.right or v not in self), right=(v in self.left))
|
1079
|
+
if v not in self:
|
1080
|
+
self.add_vertex(v, left=(u in self.right), right=(u in self.left))
|
1081
|
+
|
1082
|
+
# add the edge
|
1083
|
+
Graph.add_edge(self, u, v, label)
|
1084
|
+
|
1085
|
+
def add_edges(self, edges, loops=True):
|
1086
|
+
"""
|
1087
|
+
Add edges from an iterable container.
|
1088
|
+
|
1089
|
+
INPUT:
|
1090
|
+
|
1091
|
+
- ``edges`` -- an iterable of edges, given either as ``(u, v)``
|
1092
|
+
or ``(u, v, label)``
|
1093
|
+
|
1094
|
+
- ``loops`` -- ignored
|
1095
|
+
|
1096
|
+
See :meth:`~sage.graphs.graph.Graph.add_edges` for more detail.
|
1097
|
+
|
1098
|
+
This method simply checks that the edge endpoints are in different
|
1099
|
+
partitions. If a new vertex is to be created, it will be added to the
|
1100
|
+
proper partition. If both vertices are created, the first one will be
|
1101
|
+
added to the left partition, the second to the right partition. If
|
1102
|
+
both vertices are in the same partition but different connected
|
1103
|
+
components, one of the components will be "flipped", i.e. each vertex
|
1104
|
+
will be put into whichever partition it's not currently in. This will
|
1105
|
+
allow for the graph to remain bipartite, without changing the edges or
|
1106
|
+
vertices.
|
1107
|
+
|
1108
|
+
EXAMPLES::
|
1109
|
+
|
1110
|
+
sage: bg = BipartiteGraph()
|
1111
|
+
sage: bg.add_vertices([0, 1, 2], left=[True, False, True])
|
1112
|
+
sage: bg.add_edges([(0, 1), (2, 1)])
|
1113
|
+
sage: bg.add_edges([[0, 2]])
|
1114
|
+
Traceback (most recent call last):
|
1115
|
+
...
|
1116
|
+
ValueError: the specified set of edges cannot be added
|
1117
|
+
while still preserving the bipartition property
|
1118
|
+
sage: G = BipartiteGraph()
|
1119
|
+
sage: G.add_edges([(0, 1), (3, 2), (1, 2)])
|
1120
|
+
sage: G.bipartition()
|
1121
|
+
({0, 2}, {1, 3})
|
1122
|
+
|
1123
|
+
|
1124
|
+
Loops will raise an error::
|
1125
|
+
|
1126
|
+
sage: bg.add_edges([[0, 3], [3, 3]])
|
1127
|
+
Traceback (most recent call last):
|
1128
|
+
...
|
1129
|
+
ValueError: the specified set of edges cannot be added
|
1130
|
+
while still preserving the bipartition property
|
1131
|
+
|
1132
|
+
Adding edges is fine as long as there exists a valid bipartition.
|
1133
|
+
Otherwise an error is raised without modifyiong the graph::
|
1134
|
+
|
1135
|
+
sage: G = BipartiteGraph()
|
1136
|
+
sage: G.add_edges([(0, 1), (2, 3)])
|
1137
|
+
sage: G.bipartition()
|
1138
|
+
({0, 2}, {1, 3})
|
1139
|
+
sage: G.add_edges([(0,2), (0,3)])
|
1140
|
+
Traceback (most recent call last):
|
1141
|
+
...
|
1142
|
+
ValueError: the specified set of edges cannot be added
|
1143
|
+
while still preserving the bipartition property
|
1144
|
+
sage: G.bipartition()
|
1145
|
+
({0, 2}, {1, 3})
|
1146
|
+
sage: G.edges(labels=False, sort=True)
|
1147
|
+
[(0, 1), (2, 3)]
|
1148
|
+
"""
|
1149
|
+
edges_to_add = []
|
1150
|
+
for edge in edges:
|
1151
|
+
try:
|
1152
|
+
if len(edge) == 3:
|
1153
|
+
u, v, label = edge
|
1154
|
+
else:
|
1155
|
+
u, v = edge
|
1156
|
+
label = None
|
1157
|
+
edges_to_add.append((u, v, label))
|
1158
|
+
except Exception:
|
1159
|
+
raise TypeError("cannot interpret {!r} as graph edge".format(edge))
|
1160
|
+
|
1161
|
+
# Check whether there exists a bipartition supporting the addition of
|
1162
|
+
# input edges to the current graph before adding any edge to the
|
1163
|
+
# graph. This way, if an error is raised, self is not modified
|
1164
|
+
vertex_in_left = self._check_bipartition_for_add_edges(edges_to_add)
|
1165
|
+
|
1166
|
+
if vertex_in_left is False:
|
1167
|
+
raise ValueError("the specified set of edges cannot be added while "
|
1168
|
+
"still preserving the bipartition property")
|
1169
|
+
|
1170
|
+
# If we get here, then we've found a valid bipartition.
|
1171
|
+
# We update the bipartition
|
1172
|
+
self.left.clear()
|
1173
|
+
self.right.clear()
|
1174
|
+
for v, left in vertex_in_left.items():
|
1175
|
+
if left:
|
1176
|
+
self.left.add(v)
|
1177
|
+
else:
|
1178
|
+
self.right.add(v)
|
1179
|
+
|
1180
|
+
# Each edge now has one endpoint in left and the other in right
|
1181
|
+
for u, v, label in edges_to_add:
|
1182
|
+
if u not in self:
|
1183
|
+
self.add_vertex(u, left=vertex_in_left[u], right=not vertex_in_left[u])
|
1184
|
+
if v not in self:
|
1185
|
+
self.add_vertex(v, left=vertex_in_left[v], right=not vertex_in_left[v])
|
1186
|
+
|
1187
|
+
self._backend.add_edge(u, v, label, self._directed)
|
1188
|
+
|
1189
|
+
def _check_bipartition_for_add_edges(self, edges):
|
1190
|
+
r"""
|
1191
|
+
Helper method for ``add_edges``.
|
1192
|
+
|
1193
|
+
This method checks whether the input list of edges can be added to the
|
1194
|
+
graph. More precisely, it checks whether there exists a bipartition of
|
1195
|
+
the vertices supporting the addition of input edges. If so it returns it
|
1196
|
+
as a mapping associating to each vertex a side of the bipartition.
|
1197
|
+
Otherwise, it returns ``False``.
|
1198
|
+
|
1199
|
+
INPUT:
|
1200
|
+
|
1201
|
+
- ``edges`` -- an iterable of edges, given either as ``(u, v)``
|
1202
|
+
or ``(u, v, label)``
|
1203
|
+
|
1204
|
+
TESTS::
|
1205
|
+
|
1206
|
+
sage: bg = BipartiteGraph()
|
1207
|
+
sage: bg.add_vertices([0, 1, 2, 3], left=[True, False, True, False])
|
1208
|
+
sage: b = bg._check_bipartition_for_add_edges([(0, 1), (3, 2), (1, 2)])
|
1209
|
+
sage: sorted(b.items())
|
1210
|
+
[(0, True), (1, False), (2, True), (3, False)]
|
1211
|
+
sage: b = bg._check_bipartition_for_add_edges([(0, 2)])
|
1212
|
+
sage: sorted(b.items())
|
1213
|
+
[(0, True), (1, False), (2, False), (3, False)]
|
1214
|
+
sage: bg.add_edges([(0, 1), (3, 2), (1, 2)])
|
1215
|
+
sage: bg._check_bipartition_for_add_edges([[0, 2]])
|
1216
|
+
False
|
1217
|
+
"""
|
1218
|
+
# Map each vertex of the graph to a side
|
1219
|
+
vertex_in_left = {v: True for v in self.left}
|
1220
|
+
for v in self.right:
|
1221
|
+
vertex_in_left[v] = False
|
1222
|
+
|
1223
|
+
# Map each vertex to the connected component it belongs to
|
1224
|
+
vertex_to_component = {v: comp for comp in self.connected_components(sort=False)
|
1225
|
+
for v in comp}
|
1226
|
+
|
1227
|
+
for e in edges:
|
1228
|
+
u, v = e[:2]
|
1229
|
+
# if we haven't encountered either/both vertices, we choose a side
|
1230
|
+
# and extend components
|
1231
|
+
if u not in vertex_in_left:
|
1232
|
+
if v in vertex_in_left:
|
1233
|
+
vertex_in_left[u] = not vertex_in_left[v]
|
1234
|
+
else:
|
1235
|
+
vertex_in_left[u] = True
|
1236
|
+
vertex_in_left[v] = False
|
1237
|
+
vertex_to_component[v] = [v]
|
1238
|
+
vertex_to_component[v].append(u)
|
1239
|
+
vertex_to_component[u] = vertex_to_component[v]
|
1240
|
+
|
1241
|
+
elif v not in vertex_in_left:
|
1242
|
+
vertex_in_left[v] = not vertex_in_left[u]
|
1243
|
+
vertex_to_component[u].append(v)
|
1244
|
+
vertex_to_component[v] = vertex_to_component[u]
|
1245
|
+
|
1246
|
+
elif vertex_in_left[u] == vertex_in_left[v]:
|
1247
|
+
if vertex_to_component[u] is vertex_to_component[v]:
|
1248
|
+
# Same side and same component. We can't add that edge
|
1249
|
+
return False
|
1250
|
+
|
1251
|
+
# Otherwise, we flip the bipartition in v's component
|
1252
|
+
for w in vertex_to_component[v]:
|
1253
|
+
vertex_in_left[w] = not vertex_in_left[w]
|
1254
|
+
|
1255
|
+
# and merge the components
|
1256
|
+
comp_u = vertex_to_component[u]
|
1257
|
+
comp_u.extend(vertex_to_component[v])
|
1258
|
+
for w in vertex_to_component[v]:
|
1259
|
+
vertex_to_component[w] = comp_u
|
1260
|
+
|
1261
|
+
# Return the bipartition
|
1262
|
+
return vertex_in_left
|
1263
|
+
|
1264
|
+
def allow_loops(self, new, check=True):
|
1265
|
+
"""
|
1266
|
+
Change whether loops are permitted in the (di)graph.
|
1267
|
+
|
1268
|
+
.. NOTE::
|
1269
|
+
|
1270
|
+
This method overwrite the
|
1271
|
+
:meth:`~sage.graphs.generic_graph.GenericGraph.allow_loops` method
|
1272
|
+
to ensure that loops are forbidden in :class:`~BipartiteGraph`.
|
1273
|
+
|
1274
|
+
INPUT:
|
1275
|
+
|
1276
|
+
- ``new`` -- boolean
|
1277
|
+
|
1278
|
+
EXAMPLES::
|
1279
|
+
|
1280
|
+
sage: B = BipartiteGraph()
|
1281
|
+
sage: B.allow_loops(True)
|
1282
|
+
Traceback (most recent call last):
|
1283
|
+
...
|
1284
|
+
ValueError: loops are not allowed in bipartite graphs
|
1285
|
+
"""
|
1286
|
+
if new is True:
|
1287
|
+
raise ValueError("loops are not allowed in bipartite graphs")
|
1288
|
+
|
1289
|
+
def is_bipartite(self, certificate=False):
|
1290
|
+
r"""
|
1291
|
+
Check whether the graph is bipartite.
|
1292
|
+
|
1293
|
+
This method always returns ``True`` as first value, plus a certificate
|
1294
|
+
when ``certificate == True``.
|
1295
|
+
|
1296
|
+
INPUT:
|
1297
|
+
|
1298
|
+
- ``certificate`` -- boolean (default: ``False``); whether to return a
|
1299
|
+
certificate. If set to ``True``, the certificate returned is a proper
|
1300
|
+
2-coloring of the vertices.
|
1301
|
+
|
1302
|
+
.. SEEALSO:: :meth:`~GenericGraph.is_bipartite`
|
1303
|
+
|
1304
|
+
EXAMPLES::
|
1305
|
+
|
1306
|
+
sage: g = BipartiteGraph(graphs.RandomBipartite(3, 3, .5)) # needs numpy
|
1307
|
+
sage: g.is_bipartite() # needs numpy
|
1308
|
+
True
|
1309
|
+
sage: g.is_bipartite(certificate=True) # random # needs numpy
|
1310
|
+
(True, {(0, 0): 0, (0, 1): 0, (0, 2): 0, (1, 0): 1, (1, 1): 1, (1, 2): 1})
|
1311
|
+
|
1312
|
+
TESTS::
|
1313
|
+
|
1314
|
+
sage: BipartiteGraph().is_bipartite()
|
1315
|
+
True
|
1316
|
+
sage: BipartiteGraph().is_bipartite(certificate=True)
|
1317
|
+
(True, {})
|
1318
|
+
"""
|
1319
|
+
if certificate:
|
1320
|
+
color = {u: 0 for u in self.left}
|
1321
|
+
color.update({u: 1 for u in self.right})
|
1322
|
+
return True, color
|
1323
|
+
return True
|
1324
|
+
|
1325
|
+
def complement(self):
|
1326
|
+
r"""
|
1327
|
+
Return a complement of this graph.
|
1328
|
+
|
1329
|
+
Given a simple :class:`~sage.graphs.bipartite_graph.BipartiteGraph`
|
1330
|
+
`G = (L, R, E)` with vertex set `L\cup R` and edge set `E`, this method
|
1331
|
+
returns a :class:`~sage.graphs.graph.Graph` `H = (V, F)`, where
|
1332
|
+
`V = L\cup R` and `F` is the set of edges of a complete graph of order
|
1333
|
+
`|V|` minus the edges in `E`.
|
1334
|
+
|
1335
|
+
.. WARNING::
|
1336
|
+
|
1337
|
+
This method returns the complement of a bipartite graph
|
1338
|
+
`G = (V = L \cup R, E)` with respect the complete graph of order
|
1339
|
+
`|V|`. If looking for the complement with respect the complete
|
1340
|
+
bipartite graph `K = (L, R, L\times R)`, use method
|
1341
|
+
:meth:`~sage.graphs.bipartite_graph.BipartiteGraph.complement_bipartite`.
|
1342
|
+
|
1343
|
+
.. SEEALSO::
|
1344
|
+
|
1345
|
+
:meth:`~sage.graphs.bipartite_graph.BipartiteGraph.complement_bipartite`
|
1346
|
+
|
1347
|
+
EXAMPLES::
|
1348
|
+
|
1349
|
+
sage: B = BipartiteGraph({1: [2, 4], 3: [4, 5]})
|
1350
|
+
sage: G = B.complement(); G
|
1351
|
+
Graph on 5 vertices
|
1352
|
+
sage: G.edges(sort=True, labels=False)
|
1353
|
+
[(1, 3), (1, 5), (2, 3), (2, 4), (2, 5), (4, 5)]
|
1354
|
+
sage: B.size() + G.size() == graphs.CompleteGraph(B.order()).size()
|
1355
|
+
True
|
1356
|
+
"""
|
1357
|
+
# This is needed because complement() of generic graph
|
1358
|
+
# would return a graph of class BipartiteGraph that is
|
1359
|
+
# not bipartite. See issue #12376.
|
1360
|
+
return Graph(self).complement()
|
1361
|
+
|
1362
|
+
def complement_bipartite(self):
|
1363
|
+
r"""
|
1364
|
+
Return the bipartite complement of this bipartite graph.
|
1365
|
+
|
1366
|
+
Given a simple :class:`~sage.graphs.bipartite_graph.BipartiteGraph`
|
1367
|
+
`G = (L, R, E)` with vertex set `L\cup R` and edge set `E`, this
|
1368
|
+
method returns a :class:`~sage.graphs.bipartite_graph.BipartiteGraph`
|
1369
|
+
`H = (L\cup R, F)`, where `F` is the set of edges of a complete
|
1370
|
+
bipartite graph between vertex sets `L` and `R` minus the edges in `E`.
|
1371
|
+
|
1372
|
+
.. SEEALSO::
|
1373
|
+
|
1374
|
+
:meth:`~sage.graphs.bipartite_graph.BipartiteGraph.complement`
|
1375
|
+
|
1376
|
+
EXAMPLES:
|
1377
|
+
|
1378
|
+
sage: B = BipartiteGraph({0: [1, 2, 3]})
|
1379
|
+
sage: C = B.complement_bipartite()
|
1380
|
+
sage: C
|
1381
|
+
Bipartite graph on 4 vertices
|
1382
|
+
sage: C.is_bipartite()
|
1383
|
+
True
|
1384
|
+
sage: B.left == C.left and B.right == C.right
|
1385
|
+
True
|
1386
|
+
sage: C.size() == len(B.left)*len(B.right) - B.size()
|
1387
|
+
True
|
1388
|
+
sage: G = B.complement()
|
1389
|
+
sage: G.is_bipartite()
|
1390
|
+
False
|
1391
|
+
"""
|
1392
|
+
self._scream_if_not_simple()
|
1393
|
+
|
1394
|
+
E = [e for e in itertools.product(self.left, self.right) if not self.has_edge(e)]
|
1395
|
+
H = BipartiteGraph([self, E], format='vertices_and_edges', partition=[self.left, self.right])
|
1396
|
+
|
1397
|
+
if self.name():
|
1398
|
+
H.name("complement-bipartite({})".format(self.name()))
|
1399
|
+
if self.is_immutable():
|
1400
|
+
return H.copy(immutable=True)
|
1401
|
+
return H
|
1402
|
+
|
1403
|
+
def to_undirected(self):
|
1404
|
+
"""
|
1405
|
+
Return an undirected Graph (without bipartite constraint) of the given
|
1406
|
+
object.
|
1407
|
+
|
1408
|
+
EXAMPLES::
|
1409
|
+
|
1410
|
+
sage: BipartiteGraph(graphs.CycleGraph(6)).to_undirected()
|
1411
|
+
Cycle graph: Graph on 6 vertices
|
1412
|
+
"""
|
1413
|
+
return Graph(self)
|
1414
|
+
|
1415
|
+
def bipartition(self):
|
1416
|
+
r"""
|
1417
|
+
Return the underlying bipartition of the bipartite graph.
|
1418
|
+
|
1419
|
+
EXAMPLES::
|
1420
|
+
|
1421
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4))
|
1422
|
+
sage: B.bipartition()
|
1423
|
+
({0, 2}, {1, 3})
|
1424
|
+
"""
|
1425
|
+
return (self.left, self.right)
|
1426
|
+
|
1427
|
+
def project_left(self):
|
1428
|
+
r"""
|
1429
|
+
Project ``self`` onto left vertices. Edges are 2-paths in the original.
|
1430
|
+
|
1431
|
+
EXAMPLES::
|
1432
|
+
|
1433
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(20))
|
1434
|
+
sage: G = B.project_left()
|
1435
|
+
sage: G.order(), G.size()
|
1436
|
+
(10, 10)
|
1437
|
+
"""
|
1438
|
+
G = Graph()
|
1439
|
+
G.add_vertices(self.left)
|
1440
|
+
for v in G:
|
1441
|
+
for u in self.neighbor_iterator(v):
|
1442
|
+
G.add_edges(((v, w) for w in self.neighbor_iterator(u)), loops=False)
|
1443
|
+
return G
|
1444
|
+
|
1445
|
+
def project_right(self):
|
1446
|
+
r"""
|
1447
|
+
Project ``self`` onto right vertices. Edges are 2-paths in the original.
|
1448
|
+
|
1449
|
+
EXAMPLES::
|
1450
|
+
|
1451
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(20))
|
1452
|
+
sage: G = B.project_right()
|
1453
|
+
sage: G.order(), G.size()
|
1454
|
+
(10, 10)
|
1455
|
+
|
1456
|
+
TESTS:
|
1457
|
+
|
1458
|
+
Issue :issue:`25985` is fixed::
|
1459
|
+
|
1460
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(6))
|
1461
|
+
sage: B.project_left().vertices(sort=True)
|
1462
|
+
[0, 2, 4]
|
1463
|
+
sage: B.project_right().vertices(sort=True)
|
1464
|
+
[1, 3, 5]
|
1465
|
+
"""
|
1466
|
+
G = Graph()
|
1467
|
+
G.add_vertices(self.right)
|
1468
|
+
for v in G:
|
1469
|
+
for u in self.neighbor_iterator(v):
|
1470
|
+
G.add_edges(((v, w) for w in self.neighbor_iterator(u)), loops=False)
|
1471
|
+
return G
|
1472
|
+
|
1473
|
+
def plot(self, *args, **kwds):
|
1474
|
+
r"""
|
1475
|
+
Override Graph's plot function, to illustrate the bipartite nature.
|
1476
|
+
|
1477
|
+
EXAMPLES::
|
1478
|
+
|
1479
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(20))
|
1480
|
+
sage: B.plot() # needs sage.plot
|
1481
|
+
Graphics object consisting of 41 graphics primitives
|
1482
|
+
"""
|
1483
|
+
if "pos" not in kwds:
|
1484
|
+
kwds["pos"] = None
|
1485
|
+
if kwds["pos"] is None:
|
1486
|
+
if self.left:
|
1487
|
+
y = 0 if len(self.left) == 1 else 1
|
1488
|
+
pos = self._line_embedding(sorted(self.left), first=(-1, y), last=(-1, -y), return_dict=True)
|
1489
|
+
else:
|
1490
|
+
pos = {}
|
1491
|
+
if self.right:
|
1492
|
+
y = 0 if len(self.right) == 1 else 1
|
1493
|
+
pos.update(self._line_embedding(sorted(self.right), first=(1, y), last=(1, -y), return_dict=True))
|
1494
|
+
kwds["pos"] = pos
|
1495
|
+
return Graph.plot(self, *args, **kwds)
|
1496
|
+
|
1497
|
+
def matching_polynomial(self, algorithm='Godsil', name=None):
|
1498
|
+
r"""
|
1499
|
+
Compute the matching polynomial.
|
1500
|
+
|
1501
|
+
The *matching polynomial* is defined as in [God1993]_, where `p(G, k)`
|
1502
|
+
denotes the number of `k`-matchings (matchings with `k` edges) in `G` :
|
1503
|
+
|
1504
|
+
.. MATH::
|
1505
|
+
|
1506
|
+
\mu(x)=\sum_{k \geq 0} (-1)^k p(G,k) x^{n-2k}
|
1507
|
+
|
1508
|
+
INPUT:
|
1509
|
+
|
1510
|
+
- ``algorithm`` -- string (default: ``'Godsil'``); either "Godsil" or
|
1511
|
+
"rook"; "rook" is usually faster for larger graphs
|
1512
|
+
|
1513
|
+
- ``name`` -- string (default: ``None``); name of the variable in the
|
1514
|
+
polynomial, set to `x` when ``name`` is ``None``
|
1515
|
+
|
1516
|
+
EXAMPLES::
|
1517
|
+
|
1518
|
+
sage: BipartiteGraph(graphs.CubeGraph(3)).matching_polynomial() # needs sage.libs.flint
|
1519
|
+
x^8 - 12*x^6 + 42*x^4 - 44*x^2 + 9
|
1520
|
+
|
1521
|
+
::
|
1522
|
+
|
1523
|
+
sage: x = polygen(QQ)
|
1524
|
+
sage: g = BipartiteGraph(graphs.CompleteBipartiteGraph(16, 16))
|
1525
|
+
sage: bool(factorial(16) * laguerre(16, x^2) # needs sage.symbolic
|
1526
|
+
....: == g.matching_polynomial(algorithm='rook'))
|
1527
|
+
True
|
1528
|
+
|
1529
|
+
Compute the matching polynomial of a line with `60` vertices::
|
1530
|
+
|
1531
|
+
sage: from sage.functions.orthogonal_polys import chebyshev_U # needs sage.symbolic
|
1532
|
+
sage: g = next(graphs.trees(60))
|
1533
|
+
sage: (chebyshev_U(60, x/2) # needs sage.symbolic
|
1534
|
+
....: == BipartiteGraph(g).matching_polynomial(algorithm='rook'))
|
1535
|
+
True
|
1536
|
+
|
1537
|
+
The matching polynomial of a tree is equal to its characteristic
|
1538
|
+
polynomial::
|
1539
|
+
|
1540
|
+
sage: g = graphs.RandomTree(20)
|
1541
|
+
sage: p = g.characteristic_polynomial() # needs sage.modules
|
1542
|
+
sage: p == BipartiteGraph(g).matching_polynomial(algorithm='rook') # needs sage.modules
|
1543
|
+
True
|
1544
|
+
|
1545
|
+
TESTS::
|
1546
|
+
|
1547
|
+
sage: # needs sage.modules
|
1548
|
+
sage: g = BipartiteGraph(matrix.ones(4, 3))
|
1549
|
+
sage: g.matching_polynomial() # needs sage.libs.flint
|
1550
|
+
x^7 - 12*x^5 + 36*x^3 - 24*x
|
1551
|
+
sage: g.matching_polynomial(algorithm='rook')
|
1552
|
+
x^7 - 12*x^5 + 36*x^3 - 24*x
|
1553
|
+
"""
|
1554
|
+
if algorithm == "Godsil":
|
1555
|
+
return Graph.matching_polynomial(self, complement=False, name=name)
|
1556
|
+
elif algorithm == "rook":
|
1557
|
+
from sage.rings.polynomial.polynomial_ring_constructor import PolynomialRing
|
1558
|
+
A = self.reduced_adjacency_matrix()
|
1559
|
+
a = A.rook_vector()
|
1560
|
+
m = A.nrows()
|
1561
|
+
n = A.ncols()
|
1562
|
+
b = [0] * (m + n + 1)
|
1563
|
+
for i in range(min(m, n) + 1):
|
1564
|
+
b[m + n - 2 * i] = a[i] * (-1)**i
|
1565
|
+
if name is None:
|
1566
|
+
name = 'x'
|
1567
|
+
K = PolynomialRing(A.base_ring(), name)
|
1568
|
+
p = K(b)
|
1569
|
+
return p
|
1570
|
+
raise ValueError('algorithm must be one of "Godsil" or "rook"')
|
1571
|
+
|
1572
|
+
def perfect_matchings(self, labels=False):
|
1573
|
+
r"""
|
1574
|
+
Return an iterator over all perfect matchings of the bipartite graph.
|
1575
|
+
|
1576
|
+
ALGORITHM:
|
1577
|
+
|
1578
|
+
Choose a vertex `v` in the right set of vertices, then recurse through
|
1579
|
+
all edges incident to `v`, removing one edge at a time whenever an edge
|
1580
|
+
is added to a matching.
|
1581
|
+
|
1582
|
+
INPUT:
|
1583
|
+
|
1584
|
+
- ``labels`` -- boolean (default: ``False``); when ``True``, the edges
|
1585
|
+
in each perfect matching are triples (containing the label as the
|
1586
|
+
third element), otherwise the edges are pairs.
|
1587
|
+
|
1588
|
+
.. SEEALSO::
|
1589
|
+
|
1590
|
+
:meth:`~sage.graphs.graph.Graph.perfect_matchings`
|
1591
|
+
:meth:`matching`
|
1592
|
+
|
1593
|
+
EXAMPLES::
|
1594
|
+
|
1595
|
+
sage: B = BipartiteGraph({0: [5, 7], 1: [4, 6, 7], 2: [4, 5, 8],
|
1596
|
+
....: 3: [4, 5, 6], 6: [9], 8: [9]})
|
1597
|
+
sage: len(list(B.perfect_matchings()))
|
1598
|
+
6
|
1599
|
+
sage: G = Graph(B.edges(sort=False))
|
1600
|
+
sage: len(list(G.perfect_matchings()))
|
1601
|
+
6
|
1602
|
+
|
1603
|
+
The algorithm ensures that for any edge of a perfect matching, the first
|
1604
|
+
vertex is on the left set of vertices and the second vertex in the right
|
1605
|
+
set::
|
1606
|
+
|
1607
|
+
sage: B = BipartiteGraph({0: [5, 7], 1: [4, 6, 7], 2: [4, 5, 8],
|
1608
|
+
....: 3: [4, 5, 6], 6: [9], 8: [9]})
|
1609
|
+
sage: m = next(B.perfect_matchings(labels=False))
|
1610
|
+
sage: B.left
|
1611
|
+
{0, 1, 2, 3, 9}
|
1612
|
+
sage: B.right
|
1613
|
+
{4, 5, 6, 7, 8}
|
1614
|
+
sage: sorted(m)
|
1615
|
+
[(0, 7), (1, 4), (2, 5), (3, 6), (9, 8)]
|
1616
|
+
sage: all((u in B.left and v in B.right) for u, v in m)
|
1617
|
+
True
|
1618
|
+
|
1619
|
+
Multiple edges are taken into account::
|
1620
|
+
|
1621
|
+
sage: B = BipartiteGraph({0: [5, 7], 1: [4, 6, 7], 2: [4, 5, 8],
|
1622
|
+
....: 3: [4, 5, 6], 6: [9], 8: [9]})
|
1623
|
+
sage: B.allow_multiple_edges(True)
|
1624
|
+
sage: B.add_edge(0, 7)
|
1625
|
+
sage: len(list(B.perfect_matchings()))
|
1626
|
+
10
|
1627
|
+
|
1628
|
+
|
1629
|
+
Empty graph::
|
1630
|
+
|
1631
|
+
sage: list(BipartiteGraph().perfect_matchings())
|
1632
|
+
[[]]
|
1633
|
+
|
1634
|
+
Bipartite graph without perfect matching::
|
1635
|
+
|
1636
|
+
sage: B = BipartiteGraph(graphs.CompleteBipartiteGraph(3, 4))
|
1637
|
+
sage: list(B.perfect_matchings())
|
1638
|
+
[]
|
1639
|
+
|
1640
|
+
Check that the number of perfect matchings of a complete bipartite graph
|
1641
|
+
is consistent with the matching polynomial::
|
1642
|
+
|
1643
|
+
sage: B = BipartiteGraph(graphs.CompleteBipartiteGraph(4, 4))
|
1644
|
+
sage: len(list(B.perfect_matchings()))
|
1645
|
+
24
|
1646
|
+
sage: B.matching_polynomial(algorithm='rook')(0) # needs sage.modules
|
1647
|
+
24
|
1648
|
+
|
1649
|
+
TESTS::
|
1650
|
+
|
1651
|
+
sage: B = BipartiteGraph(graphs.CompleteBipartiteGraph(3, 4))
|
1652
|
+
sage: B.left, B.right
|
1653
|
+
({0, 1, 2}, {3, 4, 5, 6})
|
1654
|
+
sage: B.add_vertex(left=True)
|
1655
|
+
7
|
1656
|
+
sage: B.left, B.right
|
1657
|
+
({0, 1, 2, 7}, {3, 4, 5, 6})
|
1658
|
+
sage: list(B.perfect_matchings())
|
1659
|
+
[]
|
1660
|
+
sage: B = BipartiteGraph(graphs.CompleteBipartiteGraph(3, 3))
|
1661
|
+
sage: B.add_vertex(left=True)
|
1662
|
+
6
|
1663
|
+
sage: B.add_vertex(right=True)
|
1664
|
+
7
|
1665
|
+
sage: list(B.perfect_matchings())
|
1666
|
+
[]
|
1667
|
+
sage: G = Graph(B)
|
1668
|
+
sage: list(G.perfect_matchings())
|
1669
|
+
[]
|
1670
|
+
"""
|
1671
|
+
if not self:
|
1672
|
+
yield []
|
1673
|
+
return
|
1674
|
+
if len(self.left) != len(self.right):
|
1675
|
+
return
|
1676
|
+
|
1677
|
+
def rec(G):
|
1678
|
+
"""
|
1679
|
+
Iterator over all perfect matchings of a simple bipartite graph `G`.
|
1680
|
+
"""
|
1681
|
+
if not G:
|
1682
|
+
yield []
|
1683
|
+
return
|
1684
|
+
# Take an element from the right set
|
1685
|
+
v = next(iter(G.right))
|
1686
|
+
Nv = G.neighbors(v)
|
1687
|
+
# We must have at least one vertex in Nv
|
1688
|
+
if not Nv:
|
1689
|
+
return
|
1690
|
+
G.delete_vertex(v)
|
1691
|
+
for u in Nv:
|
1692
|
+
Nu = G.neighbors(u)
|
1693
|
+
G.delete_vertex(u)
|
1694
|
+
for partial_matching in rec(G):
|
1695
|
+
partial_matching.append((u, v))
|
1696
|
+
yield partial_matching
|
1697
|
+
G.add_vertex(u, left=True)
|
1698
|
+
G.add_edges((u, nu) for nu in Nu)
|
1699
|
+
G.add_vertex(v, right=True)
|
1700
|
+
G.add_edges((nv, v) for nv in Nv)
|
1701
|
+
|
1702
|
+
# We create a mutable copy of self
|
1703
|
+
G = self.copy(immutable=False)
|
1704
|
+
|
1705
|
+
# We create a mapping from frozen unlabeled edges to (labeled) edges.
|
1706
|
+
# This ease for instance the manipulation of multiedges (if any)
|
1707
|
+
edges = {}
|
1708
|
+
for e in G.edges(sort=False, labels=labels):
|
1709
|
+
f = frozenset(e[:2])
|
1710
|
+
if e[0] not in G.left:
|
1711
|
+
e = (e[1], e[0], e[2]) if labels else (e[1], e[0])
|
1712
|
+
if f in edges:
|
1713
|
+
edges[f].append(e)
|
1714
|
+
else:
|
1715
|
+
edges[f] = [e]
|
1716
|
+
|
1717
|
+
# We now get rid of multiple edges, if any
|
1718
|
+
G.allow_multiple_edges(False)
|
1719
|
+
|
1720
|
+
# For each unlabeled matching, we yield all its possible labelings
|
1721
|
+
for m in rec(G):
|
1722
|
+
yield from itertools.product(*[edges[frozenset(e)] for e in m])
|
1723
|
+
|
1724
|
+
def load_afile(self, fname):
|
1725
|
+
r"""
|
1726
|
+
Load into the current object the bipartite graph specified in the given
|
1727
|
+
file name.
|
1728
|
+
|
1729
|
+
This file should follow David MacKay's alist format, see
|
1730
|
+
http://www.inference.phy.cam.ac.uk/mackay/codes/data.html for examples
|
1731
|
+
and definition of the format.
|
1732
|
+
|
1733
|
+
EXAMPLES::
|
1734
|
+
|
1735
|
+
sage: import tempfile
|
1736
|
+
sage: with tempfile.NamedTemporaryFile(mode='w+t') as f:
|
1737
|
+
....: _ = f.write("7 4 \n 3 4 \n 3 3 1 3 1 1 1 \n\
|
1738
|
+
....: 3 3 3 4 \n 1 2 4 \n 1 3 4 \n\
|
1739
|
+
....: 1 0 0 \n 2 3 4 \n 2 0 0 \n 3 0 0 \n\
|
1740
|
+
....: 4 0 0 \n 1 2 3 0 \n 1 4 5 0 \n\
|
1741
|
+
....: 2 4 6 0 \n 1 2 4 7 \n")
|
1742
|
+
....: f.flush()
|
1743
|
+
....: B = BipartiteGraph()
|
1744
|
+
....: B2 = BipartiteGraph(f.name)
|
1745
|
+
....: B.load_afile(f.name)
|
1746
|
+
Bipartite graph on 11 vertices
|
1747
|
+
sage: B.edges(sort=True)
|
1748
|
+
[(0, 7, None),
|
1749
|
+
(0, 8, None),
|
1750
|
+
(0, 10, None),
|
1751
|
+
(1, 7, None),
|
1752
|
+
(1, 9, None),
|
1753
|
+
(1, 10, None),
|
1754
|
+
(2, 7, None),
|
1755
|
+
(3, 8, None),
|
1756
|
+
(3, 9, None),
|
1757
|
+
(3, 10, None),
|
1758
|
+
(4, 8, None),
|
1759
|
+
(5, 9, None),
|
1760
|
+
(6, 10, None)]
|
1761
|
+
sage: B2 == B
|
1762
|
+
True
|
1763
|
+
"""
|
1764
|
+
# open the file
|
1765
|
+
try:
|
1766
|
+
fi = open(fname)
|
1767
|
+
except OSError:
|
1768
|
+
print("unable to open file <<" + fname + ">>")
|
1769
|
+
return None
|
1770
|
+
|
1771
|
+
# read header information
|
1772
|
+
num_cols, num_rows = (int(_) for _ in fi.readline().split())
|
1773
|
+
# next are max_col_degree, max_row_degree, not used
|
1774
|
+
_ = [int(_) for _ in fi.readline().split()]
|
1775
|
+
col_degrees = [int(_) for _ in fi.readline().split()]
|
1776
|
+
row_degrees = [int(_) for _ in fi.readline().split()]
|
1777
|
+
|
1778
|
+
# sanity checks on header info
|
1779
|
+
if len(col_degrees) != num_cols:
|
1780
|
+
print("invalid Alist format: ")
|
1781
|
+
print("number of column degree entries does not match number of columns")
|
1782
|
+
return None
|
1783
|
+
if len(row_degrees) != num_rows:
|
1784
|
+
print("invalid Alist format: ")
|
1785
|
+
print("number of row degree entries does not match number of rows")
|
1786
|
+
return None
|
1787
|
+
|
1788
|
+
# clear out self
|
1789
|
+
self.clear()
|
1790
|
+
self.add_vertices(range(num_cols), left=True)
|
1791
|
+
self.add_vertices(range(num_cols, num_cols + num_rows), right=True)
|
1792
|
+
|
1793
|
+
# read adjacency information
|
1794
|
+
for cidx in range(num_cols):
|
1795
|
+
for ridx in map(int, fi.readline().split()):
|
1796
|
+
# A-list uses 1-based indices with 0s as place-holders
|
1797
|
+
if ridx > 0:
|
1798
|
+
self.add_edge(cidx, num_cols + ridx - 1)
|
1799
|
+
|
1800
|
+
# NOTE:: we could read in the row adjacency information as well to
|
1801
|
+
# double-check....
|
1802
|
+
# NOTE:: we could check the actual node degrees against the reported
|
1803
|
+
# node degrees....
|
1804
|
+
|
1805
|
+
# now we have all the edges in our graph, just fill in the
|
1806
|
+
# bipartite partitioning
|
1807
|
+
self.left = set(range(num_cols))
|
1808
|
+
self.right = set(range(num_cols, num_cols + num_rows))
|
1809
|
+
|
1810
|
+
# return self for chaining calls if desired
|
1811
|
+
return self
|
1812
|
+
|
1813
|
+
def save_afile(self, fname):
|
1814
|
+
r"""
|
1815
|
+
Save the graph to file in alist format.
|
1816
|
+
|
1817
|
+
Saves this graph to file in David MacKay's alist format, see
|
1818
|
+
http://www.inference.phy.cam.ac.uk/mackay/codes/data.html
|
1819
|
+
for examples and definition of the format.
|
1820
|
+
|
1821
|
+
EXAMPLES::
|
1822
|
+
|
1823
|
+
sage: # needs sage.modules
|
1824
|
+
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
|
1825
|
+
....: (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
|
1826
|
+
sage: M
|
1827
|
+
[1 1 1 0 0 0 0]
|
1828
|
+
[1 0 0 1 1 0 0]
|
1829
|
+
[0 1 0 1 0 1 0]
|
1830
|
+
[1 1 0 1 0 0 1]
|
1831
|
+
sage: b = BipartiteGraph(M)
|
1832
|
+
sage: import tempfile
|
1833
|
+
sage: with tempfile.NamedTemporaryFile() as f:
|
1834
|
+
....: b.save_afile(f.name)
|
1835
|
+
....: b2 = BipartiteGraph(f.name)
|
1836
|
+
sage: b.is_isomorphic(b2)
|
1837
|
+
True
|
1838
|
+
|
1839
|
+
TESTS::
|
1840
|
+
|
1841
|
+
sage: import tempfile
|
1842
|
+
sage: f = tempfile.NamedTemporaryFile()
|
1843
|
+
sage: for order in range(3, 13, 3): # needs sage.combinat
|
1844
|
+
....: num_chks = int(order / 3)
|
1845
|
+
....: num_vars = order - num_chks
|
1846
|
+
....: partition = (list(range(num_vars)), list(range(num_vars, num_vars+num_chks)))
|
1847
|
+
....: for idx in range(100):
|
1848
|
+
....: g = graphs.RandomGNP(order, 0.5)
|
1849
|
+
....: try:
|
1850
|
+
....: b = BipartiteGraph(g, partition, check=False)
|
1851
|
+
....: b.save_afile(f.name)
|
1852
|
+
....: b2 = BipartiteGraph(f.name)
|
1853
|
+
....: if not b.is_isomorphic(b2):
|
1854
|
+
....: print("Load/save failed for code with edges:")
|
1855
|
+
....: print(b.edges(sort=True))
|
1856
|
+
....: break
|
1857
|
+
....: except Exception:
|
1858
|
+
....: print("Exception encountered for graph of order "+ str(order))
|
1859
|
+
....: print("with edges: ")
|
1860
|
+
....: g.edges(sort=True)
|
1861
|
+
....: raise
|
1862
|
+
sage: f.close() # this removes the file
|
1863
|
+
"""
|
1864
|
+
# open the file
|
1865
|
+
try:
|
1866
|
+
fi = open(fname, "w")
|
1867
|
+
except OSError:
|
1868
|
+
print("Unable to open file <<" + fname + ">>.")
|
1869
|
+
return
|
1870
|
+
|
1871
|
+
# The alist file format assumes that vertices are labeled in [0..n-1]
|
1872
|
+
# with:
|
1873
|
+
# - vertices in left labeled in [0..|left|-1]
|
1874
|
+
# - vertices in right labeled in [|left|..n-1]
|
1875
|
+
# Then, for vertex `vidx`, it stores in the file the index + 1 of a
|
1876
|
+
# neighbor in the list right. We use appropriate mappings.
|
1877
|
+
# See http://www.inference.org.uk/mackay/codes/alist.html
|
1878
|
+
|
1879
|
+
# prep: handy lists, functions for extracting adjacent nodes
|
1880
|
+
vnodes = list(self.left)
|
1881
|
+
cnodes = list(self.right)
|
1882
|
+
max_vdeg = max(self.degree(vnodes))
|
1883
|
+
max_cdeg = max(self.degree(cnodes))
|
1884
|
+
vnode_to_str = {v: str(i + 1) for i, v in enumerate(vnodes)}
|
1885
|
+
cnode_to_str = {v: str(i + 1) for i, v in enumerate(cnodes)}
|
1886
|
+
|
1887
|
+
def vnbr_str(idx):
|
1888
|
+
return cnode_to_str[idx]
|
1889
|
+
|
1890
|
+
def cnbr_str(idx):
|
1891
|
+
return vnode_to_str[idx]
|
1892
|
+
|
1893
|
+
# write header information
|
1894
|
+
fi.write("%d %d\n" % (len(vnodes), len(cnodes)))
|
1895
|
+
fi.write("%d %d\n" % (max_vdeg, max_cdeg))
|
1896
|
+
fi.write(" ".join(map(str, self.degree(vnodes))) + "\n")
|
1897
|
+
fi.write(" ".join(map(str, self.degree(cnodes))) + "\n")
|
1898
|
+
for vidx in vnodes:
|
1899
|
+
nbrs = self.neighbors(vidx)
|
1900
|
+
fi.write(" ".join(map(vnbr_str, nbrs)))
|
1901
|
+
fi.write(" 0" * (max_vdeg - len(nbrs)) + "\n")
|
1902
|
+
for cidx in cnodes:
|
1903
|
+
nbrs = self.neighbors(cidx)
|
1904
|
+
fi.write(" ".join(map(cnbr_str, nbrs)))
|
1905
|
+
fi.write(" 0" * (max_cdeg - len(nbrs)) + "\n")
|
1906
|
+
|
1907
|
+
# done
|
1908
|
+
fi.close()
|
1909
|
+
|
1910
|
+
# return self for chaining calls if desired
|
1911
|
+
return
|
1912
|
+
|
1913
|
+
def reduced_adjacency_matrix(self, sparse=True, *, base_ring=None, **kwds):
|
1914
|
+
r"""
|
1915
|
+
Return the reduced adjacency matrix for the given graph.
|
1916
|
+
|
1917
|
+
A reduced adjacency matrix contains only the non-redundant portion of
|
1918
|
+
the full adjacency matrix for the bipartite graph. Specifically, for
|
1919
|
+
zero matrices of the appropriate size, for the reduced adjacency
|
1920
|
+
matrix ``H``, the full adjacency matrix is ``[[0, H'], [H, 0]]``.
|
1921
|
+
|
1922
|
+
By default, the matrix returned is over the integers.
|
1923
|
+
|
1924
|
+
INPUT:
|
1925
|
+
|
1926
|
+
- ``sparse`` -- boolean (default: ``True``); whether to return a sparse
|
1927
|
+
matrix
|
1928
|
+
|
1929
|
+
- ``base_ring`` -- a ring (default: ``None``); the base ring of the
|
1930
|
+
matrix space to use. By default, the base ring is ``ZZ`` if the graph
|
1931
|
+
is not weighted and otherwise the same ring as the (first) weights.
|
1932
|
+
|
1933
|
+
- ``**kwds`` -- other keywords to pass to
|
1934
|
+
:func:`~sage.matrix.constructor.matrix`
|
1935
|
+
|
1936
|
+
EXAMPLES:
|
1937
|
+
|
1938
|
+
Bipartite graphs that are not weighted will return a matrix over ZZ,
|
1939
|
+
unless a base ring is specified::
|
1940
|
+
|
1941
|
+
sage: # needs sage.modules
|
1942
|
+
sage: M = Matrix([(1,1,1,0,0,0,0), (1,0,0,1,1,0,0),
|
1943
|
+
....: (0,1,0,1,0,1,0), (1,1,0,1,0,0,1)])
|
1944
|
+
sage: B = BipartiteGraph(M)
|
1945
|
+
sage: N = B.reduced_adjacency_matrix(); N
|
1946
|
+
[1 1 1 0 0 0 0]
|
1947
|
+
[1 0 0 1 1 0 0]
|
1948
|
+
[0 1 0 1 0 1 0]
|
1949
|
+
[1 1 0 1 0 0 1]
|
1950
|
+
sage: N == M
|
1951
|
+
True
|
1952
|
+
sage: N[0,0].parent()
|
1953
|
+
Integer Ring
|
1954
|
+
sage: N2 = B.reduced_adjacency_matrix(base_ring=RDF); N2
|
1955
|
+
[1.0 1.0 1.0 0.0 0.0 0.0 0.0]
|
1956
|
+
[1.0 0.0 0.0 1.0 1.0 0.0 0.0]
|
1957
|
+
[0.0 1.0 0.0 1.0 0.0 1.0 0.0]
|
1958
|
+
[1.0 1.0 0.0 1.0 0.0 0.0 1.0]
|
1959
|
+
sage: N2[0, 0].parent()
|
1960
|
+
Real Double Field
|
1961
|
+
|
1962
|
+
Multi-edge graphs also return a matrix over ZZ,
|
1963
|
+
unless a base ring is specified::
|
1964
|
+
|
1965
|
+
sage: # needs sage.modules
|
1966
|
+
sage: M = Matrix([(1,1,2,0,0), (0,2,1,1,1), (0,1,2,1,1)])
|
1967
|
+
sage: B = BipartiteGraph(M, multiedges=True, sparse=True)
|
1968
|
+
sage: N = B.reduced_adjacency_matrix()
|
1969
|
+
sage: N == M
|
1970
|
+
True
|
1971
|
+
sage: N[0,0].parent()
|
1972
|
+
Integer Ring
|
1973
|
+
sage: N2 = B.reduced_adjacency_matrix(base_ring=RDF)
|
1974
|
+
sage: N2[0, 0].parent()
|
1975
|
+
Real Double Field
|
1976
|
+
|
1977
|
+
Weighted graphs will return a matrix over the ring given by their
|
1978
|
+
(first) weights, unless a base ring is specified::
|
1979
|
+
|
1980
|
+
sage: # needs sage.modules sage.rings.finite_rings
|
1981
|
+
sage: F.<a> = GF(4)
|
1982
|
+
sage: MS = MatrixSpace(F, 2, 3)
|
1983
|
+
sage: M = MS.matrix([[0, 1, a+1], [a, 1, 1]])
|
1984
|
+
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
|
1985
|
+
sage: N = B.reduced_adjacency_matrix(sparse=False)
|
1986
|
+
sage: N == M
|
1987
|
+
True
|
1988
|
+
sage: N[0,0].parent()
|
1989
|
+
Finite Field in a of size 2^2
|
1990
|
+
sage: N2 = B.reduced_adjacency_matrix(base_ring=F)
|
1991
|
+
sage: N2[0, 0].parent()
|
1992
|
+
Finite Field in a of size 2^2
|
1993
|
+
|
1994
|
+
TESTS::
|
1995
|
+
|
1996
|
+
sage: B = BipartiteGraph()
|
1997
|
+
sage: B.reduced_adjacency_matrix() # needs sage.modules
|
1998
|
+
[]
|
1999
|
+
sage: M = Matrix([[0,0], [0,0]]) # needs sage.modules
|
2000
|
+
sage: BipartiteGraph(M).reduced_adjacency_matrix() == M # needs sage.modules
|
2001
|
+
True
|
2002
|
+
sage: M = Matrix([[10,2/3], [0,0]]) # needs sage.modules
|
2003
|
+
sage: B = BipartiteGraph(M, weighted=True, sparse=True) # needs sage.modules
|
2004
|
+
sage: M == B.reduced_adjacency_matrix() # needs sage.modules
|
2005
|
+
True
|
2006
|
+
|
2007
|
+
An error is raised if the specified base ring is not compatible with the
|
2008
|
+
type of the weights of the bipartite graph::
|
2009
|
+
|
2010
|
+
sage: # needs sage.modules sage.rings.finite_rings
|
2011
|
+
sage: F.<a> = GF(4)
|
2012
|
+
sage: MS = MatrixSpace(F, 2, 3)
|
2013
|
+
sage: M = MS.matrix([[0, 1, a+1], [a, 1, 1]])
|
2014
|
+
sage: B = BipartiteGraph(M, weighted=True, sparse=True)
|
2015
|
+
sage: B.reduced_adjacency_matrix(base_ring=RDF)
|
2016
|
+
Traceback (most recent call last):
|
2017
|
+
...
|
2018
|
+
TypeError: float() argument must be a string or a ...number, not 'sage.rings.finite_rings.element_givaro.FiniteField_givaroElement'
|
2019
|
+
"""
|
2020
|
+
if self.multiple_edges() and self.weighted():
|
2021
|
+
raise NotImplementedError(
|
2022
|
+
"don't know how to represent weights for a multigraph")
|
2023
|
+
if self.is_directed():
|
2024
|
+
raise NotImplementedError(
|
2025
|
+
"reduced adjacency matrix does not exist for directed graphs")
|
2026
|
+
|
2027
|
+
# Create mappings of left and right vertices to integers.
|
2028
|
+
# These mappings are used to translate an edge to its reduced adjacency
|
2029
|
+
# matrix position, that is: (row index, column index)
|
2030
|
+
left = {v: i for i, v in enumerate(sorted(self.left))}
|
2031
|
+
right = {v: i for i, v in enumerate(sorted(self.right))}
|
2032
|
+
|
2033
|
+
# create dictionary of edges, values are weights for weighted graph,
|
2034
|
+
# otherwise the number of edges (always 1 for simple graphs)
|
2035
|
+
D = {}
|
2036
|
+
if self.weighted():
|
2037
|
+
for v1, v2, weight in self.edge_iterator():
|
2038
|
+
if v1 in left:
|
2039
|
+
D[right[v2], left[v1]] = weight
|
2040
|
+
else:
|
2041
|
+
D[right[v1], left[v2]] = weight
|
2042
|
+
else:
|
2043
|
+
# if we're normal or multi-edge, just create the matrix over ZZ
|
2044
|
+
for v1, v2, name in self.edge_iterator():
|
2045
|
+
idx = (right[v2], left[v1]) if v1 in left else (right[v1], left[v2])
|
2046
|
+
if idx in D:
|
2047
|
+
D[idx] += 1
|
2048
|
+
else:
|
2049
|
+
D[idx] = 1
|
2050
|
+
|
2051
|
+
# now construct and return the matrix from the dictionary we created
|
2052
|
+
from sage.matrix.constructor import matrix
|
2053
|
+
if base_ring is None:
|
2054
|
+
return matrix(len(self.right), len(self.left), D, sparse=sparse, **kwds)
|
2055
|
+
return matrix(base_ring, len(self.right), len(self.left), D, sparse=sparse, **kwds)
|
2056
|
+
|
2057
|
+
def matching(self, value_only=False, algorithm=None,
|
2058
|
+
use_edge_labels=False, solver=None, verbose=0,
|
2059
|
+
*, integrality_tolerance=1e-3):
|
2060
|
+
r"""
|
2061
|
+
Return a maximum matching of the graph represented by the list of its
|
2062
|
+
edges.
|
2063
|
+
|
2064
|
+
Given a graph `G` such that each edge `e` has a weight `w_e`, a maximum
|
2065
|
+
matching is a subset `S` of the edges of `G` of maximum weight such that
|
2066
|
+
no two edges of `S` are incident with each other.
|
2067
|
+
|
2068
|
+
INPUT:
|
2069
|
+
|
2070
|
+
- ``value_only`` -- boolean (default: ``False``); when set to ``True``,
|
2071
|
+
only the cardinal (or the weight) of the matching is returned
|
2072
|
+
|
2073
|
+
- ``algorithm`` -- string (default: ``'Hopcroft-Karp'`` if
|
2074
|
+
``use_edge_labels==False``, otherwise ``'Edmonds'``); algorithm to use
|
2075
|
+
among:
|
2076
|
+
|
2077
|
+
- ``'Hopcroft-Karp'`` selects the default bipartite graph algorithm as
|
2078
|
+
implemented in NetworkX
|
2079
|
+
|
2080
|
+
- ``'Eppstein'`` selects Eppstein's algorithm as implemented in
|
2081
|
+
NetworkX
|
2082
|
+
|
2083
|
+
- ``'Edmonds'`` selects Edmonds' algorithm as implemented in NetworkX
|
2084
|
+
|
2085
|
+
- ``'LP'`` uses a Linear Program formulation of the matching problem
|
2086
|
+
|
2087
|
+
- ``use_edge_labels`` -- boolean (default: ``False``)
|
2088
|
+
|
2089
|
+
- when set to ``True``, computes a weighted matching where each edge
|
2090
|
+
is weighted by its label (if an edge has no label, `1` is assumed);
|
2091
|
+
only if ``algorithm`` is ``'Edmonds'``, ``'LP'``
|
2092
|
+
|
2093
|
+
- when set to ``False``, each edge has weight `1`
|
2094
|
+
|
2095
|
+
- ``solver`` -- string (default: ``None``); specifies a Mixed
|
2096
|
+
Integer Linear Programming (MILP) solver to be used. If set
|
2097
|
+
to ``None``, the default one is used. For more information
|
2098
|
+
on MILP solvers and which default solver is used, see the
|
2099
|
+
method :meth:`solve
|
2100
|
+
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the
|
2101
|
+
class :class:`MixedIntegerLinearProgram
|
2102
|
+
<sage.numerical.mip.MixedIntegerLinearProgram>`.
|
2103
|
+
|
2104
|
+
- ``verbose`` -- integer (default: 0); sets the level of
|
2105
|
+
verbosity. Set to 0 by default, which means quiet.
|
2106
|
+
|
2107
|
+
- ``integrality_tolerance`` -- float; parameter for use with
|
2108
|
+
MILP solvers over an inexact base ring; see
|
2109
|
+
:meth:`MixedIntegerLinearProgram.get_values`.
|
2110
|
+
|
2111
|
+
.. SEEALSO::
|
2112
|
+
|
2113
|
+
- :wikipedia:`Matching_(graph_theory)`
|
2114
|
+
- :meth:`~Graph.matching`
|
2115
|
+
|
2116
|
+
EXAMPLES:
|
2117
|
+
|
2118
|
+
Maximum matching in a cycle graph::
|
2119
|
+
|
2120
|
+
sage: G = BipartiteGraph(graphs.CycleGraph(10))
|
2121
|
+
sage: G.matching() # needs networkx
|
2122
|
+
[(0, 1, None), (2, 3, None), (4, 5, None), (6, 7, None), (8, 9, None)]
|
2123
|
+
|
2124
|
+
The size of a maximum matching in a complete bipartite graph using
|
2125
|
+
Eppstein::
|
2126
|
+
|
2127
|
+
sage: G = BipartiteGraph(graphs.CompleteBipartiteGraph(4,5))
|
2128
|
+
sage: G.matching(algorithm='Eppstein', value_only=True) # needs networkx
|
2129
|
+
4
|
2130
|
+
|
2131
|
+
TESTS:
|
2132
|
+
|
2133
|
+
If ``algorithm`` is not set to one of the supported algorithms, an
|
2134
|
+
exception is raised::
|
2135
|
+
|
2136
|
+
sage: G = BipartiteGraph(graphs.CompleteBipartiteGraph(4,5))
|
2137
|
+
sage: G.matching(algorithm='somethingdifferent')
|
2138
|
+
Traceback (most recent call last):
|
2139
|
+
...
|
2140
|
+
ValueError: algorithm must be "Hopcroft-Karp", "Eppstein", "Edmonds" or "LP"
|
2141
|
+
|
2142
|
+
Maximum matching in a weighted bipartite graph::
|
2143
|
+
|
2144
|
+
sage: G = graphs.CycleGraph(4)
|
2145
|
+
sage: B = BipartiteGraph([(u,v,2) for u,v in G.edges(sort=True, labels=0)])
|
2146
|
+
sage: sorted(B.matching(use_edge_labels=True)) # needs networkx
|
2147
|
+
[(0, 3, 2), (1, 2, 2)]
|
2148
|
+
sage: B.matching(use_edge_labels=True, value_only=True) # needs networkx
|
2149
|
+
4
|
2150
|
+
sage: B.matching(use_edge_labels=True, value_only=True, algorithm='Edmonds') # needs networkx
|
2151
|
+
4
|
2152
|
+
sage: B.matching(use_edge_labels=True, value_only=True, algorithm='LP') # needs sage.numerical.mip
|
2153
|
+
4
|
2154
|
+
sage: B.matching(use_edge_labels=True, value_only=True, algorithm='Eppstein')
|
2155
|
+
Traceback (most recent call last):
|
2156
|
+
...
|
2157
|
+
ValueError: use_edge_labels cannot be used with "Hopcroft-Karp" or "Eppstein"
|
2158
|
+
sage: B.matching(use_edge_labels=True, value_only=True, algorithm='Hopcroft-Karp')
|
2159
|
+
Traceback (most recent call last):
|
2160
|
+
...
|
2161
|
+
ValueError: use_edge_labels cannot be used with "Hopcroft-Karp" or "Eppstein"
|
2162
|
+
sage: B.matching(use_edge_labels=False, value_only=True, # needs networkx
|
2163
|
+
....: algorithm='Hopcroft-Karp')
|
2164
|
+
2
|
2165
|
+
sage: B.matching(use_edge_labels=False, value_only=True, # needs networkx
|
2166
|
+
....: algorithm='Eppstein')
|
2167
|
+
2
|
2168
|
+
sage: B.matching(use_edge_labels=False, value_only=True, algorithm='Edmonds') # needs networkx
|
2169
|
+
2
|
2170
|
+
sage: B.matching(use_edge_labels=False, value_only=True, algorithm='LP') # needs sage.numerical.mip
|
2171
|
+
2
|
2172
|
+
|
2173
|
+
With multiedges enabled::
|
2174
|
+
|
2175
|
+
sage: G = BipartiteGraph(graphs.CubeGraph(3))
|
2176
|
+
sage: for e in G.edges(sort=True):
|
2177
|
+
....: G.set_edge_label(e[0], e[1], int(e[0]) + int(e[1]))
|
2178
|
+
sage: G.allow_multiple_edges(True)
|
2179
|
+
sage: G.matching(use_edge_labels=True, value_only=True) # needs networkx
|
2180
|
+
444
|
2181
|
+
|
2182
|
+
Empty bipartite graph and bipartite graphs without edges::
|
2183
|
+
|
2184
|
+
sage: B = BipartiteGraph()
|
2185
|
+
sage: algorithms = ["Hopcroft-Karp", "Eppstein", "Edmonds", "LP"]
|
2186
|
+
sage: not any(B.matching(algorithm=algo) for algo in algorithms) # needs networkx
|
2187
|
+
True
|
2188
|
+
sage: all(B.matching(algorithm=algo, value_only=True) == 0 for algo in algorithms) # needs networkx
|
2189
|
+
True
|
2190
|
+
sage: B.add_vertex(1, left=True)
|
2191
|
+
sage: B.add_vertex(2, left=True)
|
2192
|
+
sage: B.add_vertex(3, right=True)
|
2193
|
+
sage: not any(B.matching(algorithm=algo) for algo in algorithms) # needs networkx
|
2194
|
+
True
|
2195
|
+
sage: all(B.matching(algorithm=algo, value_only=True) == 0 for algo in algorithms) # needs networkx
|
2196
|
+
True
|
2197
|
+
"""
|
2198
|
+
if algorithm is None:
|
2199
|
+
algorithm = "Edmonds" if use_edge_labels else "Hopcroft-Karp"
|
2200
|
+
|
2201
|
+
if algorithm == "Hopcroft-Karp" or algorithm == "Eppstein":
|
2202
|
+
if use_edge_labels:
|
2203
|
+
raise ValueError('use_edge_labels cannot be used with '
|
2204
|
+
'"Hopcroft-Karp" or "Eppstein"')
|
2205
|
+
d = []
|
2206
|
+
if self.size():
|
2207
|
+
import networkx
|
2208
|
+
# NetworkX matching algorithms for bipartite graphs may fail
|
2209
|
+
# when the graph is not connected
|
2210
|
+
if not self.is_connected():
|
2211
|
+
CC = [g for g in self.connected_components_subgraphs() if g.size()]
|
2212
|
+
else:
|
2213
|
+
CC = [self]
|
2214
|
+
v2int = {v: i for i, v in enumerate(self)}
|
2215
|
+
for g in CC:
|
2216
|
+
h = g.networkx_graph()
|
2217
|
+
if algorithm == "Hopcroft-Karp":
|
2218
|
+
m = networkx.bipartite.hopcroft_karp_matching(h)
|
2219
|
+
else:
|
2220
|
+
m = networkx.bipartite.eppstein_matching(h)
|
2221
|
+
d.extend((u, v, g.edge_label(u, v)) for u, v in m.items()
|
2222
|
+
if v2int[u] < v2int[v])
|
2223
|
+
|
2224
|
+
if value_only:
|
2225
|
+
return Integer(len(d))
|
2226
|
+
return d
|
2227
|
+
|
2228
|
+
elif algorithm == "Edmonds" or algorithm == "LP":
|
2229
|
+
return Graph.matching(self, value_only=value_only,
|
2230
|
+
algorithm=algorithm,
|
2231
|
+
use_edge_labels=use_edge_labels,
|
2232
|
+
solver=solver, verbose=verbose,
|
2233
|
+
integrality_tolerance=integrality_tolerance)
|
2234
|
+
raise ValueError('algorithm must be "Hopcroft-Karp", '
|
2235
|
+
'"Eppstein", "Edmonds" or "LP"')
|
2236
|
+
|
2237
|
+
def vertex_cover(self, algorithm='Konig', value_only=False,
|
2238
|
+
reduction_rules=True, solver=None, verbose=0,
|
2239
|
+
*, integrality_tolerance=1e-3):
|
2240
|
+
r"""
|
2241
|
+
Return a minimum vertex cover of ``self`` represented by a set of vertices.
|
2242
|
+
|
2243
|
+
A minimum vertex cover of a graph is a set `S` of vertices such that
|
2244
|
+
each edge is incident to at least one element of `S`, and such that `S`
|
2245
|
+
is of minimum cardinality. For more information, see
|
2246
|
+
:wikipedia:`Vertex_cover`.
|
2247
|
+
|
2248
|
+
Equivalently, a vertex cover is defined as the complement of an
|
2249
|
+
independent set.
|
2250
|
+
|
2251
|
+
As an optimization problem, it can be expressed as follows:
|
2252
|
+
|
2253
|
+
.. MATH::
|
2254
|
+
|
2255
|
+
\mbox{Minimize : }&\sum_{v\in G} b_v\\
|
2256
|
+
\mbox{Such that : }&\forall (u,v) \in G.edges(sort=True), b_u+b_v\geq 1\\
|
2257
|
+
&\forall x\in G, b_x\mbox{ is a binary variable}
|
2258
|
+
|
2259
|
+
INPUT:
|
2260
|
+
|
2261
|
+
- ``algorithm`` -- string (default: ``'Konig'``); algorithm to use
|
2262
|
+
among:
|
2263
|
+
|
2264
|
+
- ``'Konig'`` will compute a minimum vertex cover using Konig's
|
2265
|
+
algorithm (:wikipedia:`Kőnig%27s_theorem_(graph_theory)`)
|
2266
|
+
|
2267
|
+
- ``'Cliquer'`` will compute a minimum vertex cover
|
2268
|
+
using the Cliquer package
|
2269
|
+
|
2270
|
+
- ``'MILP'`` will compute a minimum vertex cover through a mixed
|
2271
|
+
integer linear program
|
2272
|
+
|
2273
|
+
- ``'mcqd'`` will use the MCQD solver
|
2274
|
+
(`<http://www.sicmm.org/~konc/maxclique/>`_), and the MCQD
|
2275
|
+
package must be installed
|
2276
|
+
|
2277
|
+
- ``value_only`` -- boolean (default: ``False``); if set to ``True``,
|
2278
|
+
only the size of a minimum vertex cover is returned. Otherwise,
|
2279
|
+
a minimum vertex cover is returned as a list of vertices.
|
2280
|
+
|
2281
|
+
- ``reduction_rules`` -- (default: ``True``) specify if the reductions
|
2282
|
+
rules from kernelization must be applied as pre-processing or not.
|
2283
|
+
See [ACFLSS04]_ for more details. Note that depending on the instance,
|
2284
|
+
it might be faster to disable reduction rules. This parameter is
|
2285
|
+
currently ignored when ``algorithm == "Konig"``.
|
2286
|
+
|
2287
|
+
- ``solver`` -- string (default: ``None``); specifies a Mixed Integer
|
2288
|
+
Linear Programming (MILP) solver to be used. If set to ``None``, the
|
2289
|
+
default one is used. For more information on MILP solvers and which
|
2290
|
+
default solver is used, see the method :meth:`solve
|
2291
|
+
<sage.numerical.mip.MixedIntegerLinearProgram.solve>` of the class
|
2292
|
+
:class:`MixedIntegerLinearProgram
|
2293
|
+
<sage.numerical.mip.MixedIntegerLinearProgram>`.
|
2294
|
+
|
2295
|
+
- ``verbose`` -- integer (default: 0); sets the level of
|
2296
|
+
verbosity. Set to 0 by default, which means quiet.
|
2297
|
+
|
2298
|
+
- ``integrality_tolerance`` -- float; parameter for use with MILP
|
2299
|
+
solvers over an inexact base ring; see
|
2300
|
+
:meth:`MixedIntegerLinearProgram.get_values`.
|
2301
|
+
|
2302
|
+
EXAMPLES:
|
2303
|
+
|
2304
|
+
On the Cycle Graph::
|
2305
|
+
|
2306
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(6))
|
2307
|
+
sage: len(B.vertex_cover()) # needs networkx
|
2308
|
+
3
|
2309
|
+
sage: B.vertex_cover(value_only=True) # needs networkx
|
2310
|
+
3
|
2311
|
+
|
2312
|
+
The two algorithms should return the same result::
|
2313
|
+
|
2314
|
+
sage: # needs networkx numpy
|
2315
|
+
sage: g = BipartiteGraph(graphs.RandomBipartite(10, 10, .5))
|
2316
|
+
sage: vc1 = g.vertex_cover(algorithm='Konig')
|
2317
|
+
sage: vc2 = g.vertex_cover(algorithm='Cliquer') # needs cliquer
|
2318
|
+
sage: len(vc1) == len(vc2) # needs cliquer
|
2319
|
+
True
|
2320
|
+
|
2321
|
+
TESTS:
|
2322
|
+
|
2323
|
+
Giving a non connected bipartite graph::
|
2324
|
+
|
2325
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(4) * 2)
|
2326
|
+
sage: len(B.vertex_cover()) # needs networkx
|
2327
|
+
4
|
2328
|
+
|
2329
|
+
Empty bipartite graph and bipartite graphs without edges::
|
2330
|
+
|
2331
|
+
sage: B = BipartiteGraph()
|
2332
|
+
sage: algorithms = ["Konig", "MILP"]
|
2333
|
+
sage: import sage.graphs.cliquer; algorithms += ["Cliquer"] # needs cliquer
|
2334
|
+
sage: all(B.vertex_cover(algorithm=algo) == [] for algo in algorithms)
|
2335
|
+
True
|
2336
|
+
sage: all(B.vertex_cover(algorithm=algo, value_only=True) == 0 for algo in algorithms)
|
2337
|
+
True
|
2338
|
+
sage: B.add_vertex(1, left=True)
|
2339
|
+
sage: B.add_vertex(2, left=True)
|
2340
|
+
sage: B.add_vertex(3, right=True)
|
2341
|
+
sage: all(B.vertex_cover(algorithm=algo) == [] for algo in algorithms)
|
2342
|
+
True
|
2343
|
+
sage: all(B.vertex_cover(algorithm=algo, value_only=True) == 0 for algo in algorithms)
|
2344
|
+
True
|
2345
|
+
"""
|
2346
|
+
if algorithm != "Konig":
|
2347
|
+
return Graph.vertex_cover(self, algorithm=algorithm,
|
2348
|
+
value_only=value_only,
|
2349
|
+
reduction_rules=reduction_rules,
|
2350
|
+
solver=solver,
|
2351
|
+
verbose=verbose,
|
2352
|
+
integrality_tolerance=integrality_tolerance)
|
2353
|
+
|
2354
|
+
if not self.is_connected():
|
2355
|
+
VC = []
|
2356
|
+
for b in self.connected_components_subgraphs():
|
2357
|
+
if b.size():
|
2358
|
+
VC.extend(b.vertex_cover(algorithm='Konig'))
|
2359
|
+
if value_only:
|
2360
|
+
return sum(VC)
|
2361
|
+
return VC
|
2362
|
+
|
2363
|
+
M = Graph(self.matching())
|
2364
|
+
left = set(self.left)
|
2365
|
+
right = set(self.right)
|
2366
|
+
|
2367
|
+
# Initialize Z with vertices in left that are not involved in the
|
2368
|
+
# matching
|
2369
|
+
Z = left.difference(M.vertex_iterator())
|
2370
|
+
|
2371
|
+
# Alternate: extend Z with all vertices reachable by alternate paths
|
2372
|
+
# (match / non-match edges).
|
2373
|
+
X = set(Z)
|
2374
|
+
while X:
|
2375
|
+
# Follow non matched edges
|
2376
|
+
Y = set()
|
2377
|
+
for u in X:
|
2378
|
+
for v in self.neighbors(u):
|
2379
|
+
if v not in Z and not M.has_edge(u, v):
|
2380
|
+
Y.add(v)
|
2381
|
+
Z.update(Y)
|
2382
|
+
|
2383
|
+
# Follow matched edges
|
2384
|
+
X = set()
|
2385
|
+
for u in Y:
|
2386
|
+
for v in M.neighbor_iterator(u):
|
2387
|
+
if v not in Z:
|
2388
|
+
X.add(v)
|
2389
|
+
Z.update(X)
|
2390
|
+
|
2391
|
+
# The solution is (left \ Z) + (right \cap Z)
|
2392
|
+
VC = list((left.difference(Z)).union(right.intersection(Z)))
|
2393
|
+
|
2394
|
+
if value_only:
|
2395
|
+
return len(VC)
|
2396
|
+
return VC
|
2397
|
+
|
2398
|
+
def _subgraph_by_adding(self, vertices=None, edges=None, edge_property=None, immutable=None):
|
2399
|
+
r"""
|
2400
|
+
Return the subgraph containing the given vertices and edges.
|
2401
|
+
|
2402
|
+
The edges also satisfy the edge_property, if it is not None. The
|
2403
|
+
subgraph is created by creating a new empty graph and adding the
|
2404
|
+
necessary vertices, edges, and other properties.
|
2405
|
+
|
2406
|
+
INPUT:
|
2407
|
+
|
2408
|
+
- ``vertices`` -- list (default: ``None``); list of vertices
|
2409
|
+
|
2410
|
+
- ``edges`` -- (default: ``None``) either a single edge or an iterable
|
2411
|
+
container of edges (e.g., a list, set, file, numeric array, etc.). If
|
2412
|
+
no edges are specified, then all edges are assumed and the returned
|
2413
|
+
graph is an induced subgraph. In the case of multiple edges,
|
2414
|
+
specifying an edge as `(u, v)` means to keep all edges `(u, v)`,
|
2415
|
+
regardless of the label.
|
2416
|
+
|
2417
|
+
- ``edge_property`` -- (default: ``None``) if specified, this is
|
2418
|
+
expected to be a function on edges, which is intersected with the
|
2419
|
+
edges specified, if any are
|
2420
|
+
|
2421
|
+
- ``immutable`` -- boolean (default: ``None``); currently ignored for
|
2422
|
+
``BipartiteGraph``
|
2423
|
+
|
2424
|
+
EXAMPLES::
|
2425
|
+
|
2426
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(6))
|
2427
|
+
sage: H = B._subgraph_by_adding(vertices=B.left)
|
2428
|
+
sage: H.order(), H.size()
|
2429
|
+
(3, 0)
|
2430
|
+
sage: H = B._subgraph_by_adding(vertices=[0, 1])
|
2431
|
+
sage: H.order(), H.size()
|
2432
|
+
(2, 1)
|
2433
|
+
sage: H = B._subgraph_by_adding(vertices=[0, 1, 2], edges=[(0, 1)])
|
2434
|
+
sage: H.order(), H.size()
|
2435
|
+
(3, 1)
|
2436
|
+
|
2437
|
+
Using the property arguments::
|
2438
|
+
|
2439
|
+
sage: B = BipartiteGraph([(0, 1, 1), (0, 2, 0), (0, 3, 0), (3, 4, 1)])
|
2440
|
+
sage: H = B._subgraph_by_adding(vertices=B.vertices(sort=False), edge_property=(lambda e: e[2] == 1))
|
2441
|
+
sage: H.order(), H.size()
|
2442
|
+
(5, 2)
|
2443
|
+
"""
|
2444
|
+
B = self.__class__(weighted=self._weighted, loops=self.allows_loops(),
|
2445
|
+
multiedges=self.allows_multiple_edges())
|
2446
|
+
B.name("Subgraph of ({})".format(self.name()))
|
2447
|
+
for u in vertices:
|
2448
|
+
if u in self.left:
|
2449
|
+
B.add_vertex(u, left=True)
|
2450
|
+
elif u in self.right:
|
2451
|
+
B.add_vertex(u, right=True)
|
2452
|
+
if edges is None:
|
2453
|
+
edges_to_keep = self.edge_boundary(B.left, B.right, sort=False)
|
2454
|
+
else:
|
2455
|
+
edges_to_keep_labeled = set(e for e in edges if len(e) == 3)
|
2456
|
+
edges_to_keep_unlabeled = set(e for e in edges if len(e) == 2)
|
2457
|
+
edges_to_keep = []
|
2458
|
+
for u, v, l in self.edge_boundary(B.left, B.right, sort=False):
|
2459
|
+
if ((u, v, l) in edges_to_keep_labeled
|
2460
|
+
or (v, u, l) in edges_to_keep_labeled
|
2461
|
+
or (u, v) in edges_to_keep_unlabeled
|
2462
|
+
or (v, u) in edges_to_keep_unlabeled):
|
2463
|
+
edges_to_keep.append((u, v, l))
|
2464
|
+
|
2465
|
+
if edge_property is not None:
|
2466
|
+
edges_to_keep = [e for e in edges_to_keep if edge_property(e)]
|
2467
|
+
|
2468
|
+
B.add_edges(edges_to_keep)
|
2469
|
+
|
2470
|
+
B._copy_attribute_from(self, '_pos')
|
2471
|
+
B._copy_attribute_from(self, '_assoc')
|
2472
|
+
|
2473
|
+
return B
|
2474
|
+
|
2475
|
+
def _subgraph_by_deleting(self, vertices=None, edges=None, inplace=False,
|
2476
|
+
edge_property=None, immutable=None):
|
2477
|
+
r"""
|
2478
|
+
Return the subgraph containing the given vertices and edges.
|
2479
|
+
|
2480
|
+
The edges also satisfy the edge_property, if it is not None. The
|
2481
|
+
subgraph is created by creating deleting things that are not needed.
|
2482
|
+
|
2483
|
+
INPUT:
|
2484
|
+
|
2485
|
+
- ``vertices`` -- list (default: ``None``); list of vertices
|
2486
|
+
|
2487
|
+
- ``edges`` -- (default: ``None``) either a single edge or an iterable
|
2488
|
+
container of edges (e.g., a list, set, file, numeric array, etc.). If
|
2489
|
+
no edges are specified, then all edges are assumed and the returned
|
2490
|
+
graph is an induced subgraph. In the case of multiple edges,
|
2491
|
+
specifying an edge as `(u, v)` means to keep all edges `(u, v)`,
|
2492
|
+
regardless of the label.
|
2493
|
+
|
2494
|
+
- ``inplace`` -- boolean (default: ``False``); when ``True``, the
|
2495
|
+
current graph is modified in place by deleting the extra vertices and
|
2496
|
+
edges. Otherwise a modified copy of the graph is returned
|
2497
|
+
|
2498
|
+
- ``edge_property`` -- (default: ``None``) if specified, this is
|
2499
|
+
expected to be a function on edges, which is intersected with the
|
2500
|
+
edges specified, if any are
|
2501
|
+
|
2502
|
+
- ``immutable`` -- boolean (default: ``None``); currently ignored for
|
2503
|
+
``BipartiteGraph``
|
2504
|
+
|
2505
|
+
EXAMPLES::
|
2506
|
+
|
2507
|
+
sage: B = BipartiteGraph(graphs.CycleGraph(6))
|
2508
|
+
sage: H = B._subgraph_by_deleting(vertices=B.left)
|
2509
|
+
sage: H.order(), H.size()
|
2510
|
+
(3, 0)
|
2511
|
+
sage: H = B._subgraph_by_deleting(vertices=[0, 1])
|
2512
|
+
sage: H.order(), H.size()
|
2513
|
+
(2, 1)
|
2514
|
+
sage: H = B._subgraph_by_deleting(vertices=[0, 1, 2], edges=[(0, 1)])
|
2515
|
+
sage: H.order(), H.size()
|
2516
|
+
(3, 1)
|
2517
|
+
|
2518
|
+
Using the property arguments::
|
2519
|
+
|
2520
|
+
sage: B = BipartiteGraph([(0, 1, 1), (0, 2, 0), (0, 3, 0), (3, 4, 1)])
|
2521
|
+
sage: H = B._subgraph_by_deleting(vertices=B.vertices(sort=False), edge_property=(lambda e: e[2] == 1))
|
2522
|
+
sage: H.order(), H.size()
|
2523
|
+
(5, 2)
|
2524
|
+
"""
|
2525
|
+
if inplace:
|
2526
|
+
B = self
|
2527
|
+
else:
|
2528
|
+
# We make a copy of the graph
|
2529
|
+
B = BipartiteGraph(data=self.edges(sort=True), partition=[self.left, self.right])
|
2530
|
+
B._copy_attribute_from(self, '_pos')
|
2531
|
+
B._copy_attribute_from(self, '_assoc')
|
2532
|
+
B.name("Subgraph of ({})".format(self.name()))
|
2533
|
+
|
2534
|
+
vertices = set(vertices)
|
2535
|
+
B.delete_vertices([v for v in B.vertex_iterator() if v not in vertices])
|
2536
|
+
|
2537
|
+
edges_to_delete = []
|
2538
|
+
if edges is not None:
|
2539
|
+
edges_to_keep_labeled = set(e for e in edges if len(e) == 3)
|
2540
|
+
edges_to_keep_unlabeled = set(e for e in edges if len(e) == 2)
|
2541
|
+
for u, v, l in B.edge_iterator():
|
2542
|
+
if ((u, v, l) not in edges_to_keep_labeled
|
2543
|
+
and (v, u, l) not in edges_to_keep_labeled
|
2544
|
+
and (u, v) not in edges_to_keep_unlabeled
|
2545
|
+
and (v, u) not in edges_to_keep_unlabeled):
|
2546
|
+
edges_to_delete.append((u, v, l))
|
2547
|
+
if edge_property is not None:
|
2548
|
+
# We might get duplicate edges, but this does handle the case of
|
2549
|
+
# multiple edges.
|
2550
|
+
edges_to_delete.extend(e for e in B.edge_iterator() if not edge_property(e))
|
2551
|
+
|
2552
|
+
B.delete_edges(edges_to_delete)
|
2553
|
+
return B
|
2554
|
+
|
2555
|
+
def canonical_label(self, partition=None, certificate=False,
|
2556
|
+
edge_labels=False, algorithm=None, return_graph=True,
|
2557
|
+
immutable=None):
|
2558
|
+
r"""
|
2559
|
+
Return the canonical graph.
|
2560
|
+
|
2561
|
+
A canonical graph is the representative graph of an isomorphism
|
2562
|
+
class by some canonization function `c`. If `G` and `H` are graphs,
|
2563
|
+
then `G \cong c(G)`, and `c(G) == c(H)` if and only if `G \cong H`.
|
2564
|
+
|
2565
|
+
See the :wikipedia:`Graph_canonization` for more information.
|
2566
|
+
|
2567
|
+
INPUT:
|
2568
|
+
|
2569
|
+
- ``partition`` -- if given, the canonical label with respect
|
2570
|
+
to this set partition will be computed. The default is the unit
|
2571
|
+
set partition.
|
2572
|
+
|
2573
|
+
- ``certificate`` -- boolean (default: ``False``); when set to
|
2574
|
+
``True``, a dictionary mapping from the vertices of the (di)graph
|
2575
|
+
to its canonical label will also be returned
|
2576
|
+
|
2577
|
+
- ``edge_labels`` -- boolean (default: ``False``); when set to
|
2578
|
+
``True``, allows only permutations respecting edge labels
|
2579
|
+
|
2580
|
+
- ``algorithm`` -- string (default: ``None``); the algorithm to use.
|
2581
|
+
Currently available:
|
2582
|
+
|
2583
|
+
* ``'bliss'``: use the optional package bliss
|
2584
|
+
(http://www.tcs.tkk.fi/Software/bliss/index.html);
|
2585
|
+
* ``'sage'``: always use Sage's implementation.
|
2586
|
+
* ``None`` (default): use bliss when available and possible
|
2587
|
+
|
2588
|
+
.. NOTE::
|
2589
|
+
|
2590
|
+
Make sure you always compare canonical forms obtained by the
|
2591
|
+
same algorithm.
|
2592
|
+
|
2593
|
+
- ``return_graph`` -- boolean (default: ``True``); when set to
|
2594
|
+
``False``, returns the list of edges of the canonical graph
|
2595
|
+
instead of the canonical graph. Only available when ``'bliss'``
|
2596
|
+
is explicitly set as algorithm.
|
2597
|
+
|
2598
|
+
- ``immutable`` -- boolean (default: ``None``); whether to create a
|
2599
|
+
mutable/immutable (di)graph. ``immutable=None`` (default) means that
|
2600
|
+
the (di)graph and its canonical (di)graph will behave the same way.
|
2601
|
+
|
2602
|
+
EXAMPLES::
|
2603
|
+
|
2604
|
+
sage: B = BipartiteGraph( [(0, 4), (0, 5), (0, 6), (0, 8), (1, 5),
|
2605
|
+
....: (1, 7), (1, 8), (2, 6), (2, 7), (2, 8),
|
2606
|
+
....: (3, 4), (3, 7), (3, 8), (4, 9), (5, 9),
|
2607
|
+
....: (6, 9), (7, 9)] )
|
2608
|
+
sage: C = B.canonical_label(partition=(B.left,B.right), algorithm='sage')
|
2609
|
+
sage: C
|
2610
|
+
Bipartite graph on 10 vertices
|
2611
|
+
sage: C.left
|
2612
|
+
{0, 1, 2, 3, 4}
|
2613
|
+
sage: C.right
|
2614
|
+
{5, 6, 7, 8, 9}
|
2615
|
+
|
2616
|
+
::
|
2617
|
+
|
2618
|
+
sage: B = BipartiteGraph( [(0, 4), (0, 5), (0, 6), (0, 8), (1, 5),
|
2619
|
+
....: (1, 7), (1, 8), (2, 6), (2, 7), (2, 8),
|
2620
|
+
....: (3, 4), (3, 7), (3, 8), (4, 9), (5, 9),
|
2621
|
+
....: (6, 9), (7, 9)] )
|
2622
|
+
sage: C, cert = B.canonical_label(partition=(B.left, B.right),
|
2623
|
+
....: certificate=True, algorithm='sage')
|
2624
|
+
sage: C
|
2625
|
+
Bipartite graph on 10 vertices
|
2626
|
+
sage: C.left
|
2627
|
+
{0, 1, 2, 3, 4}
|
2628
|
+
sage: C.right
|
2629
|
+
{5, 6, 7, 8, 9}
|
2630
|
+
sage: cert == {0: 3, 1: 0, 2: 1, 3: 2, 4: 5, 5: 7, 6: 6, 7: 8, 8: 9, 9: 4}
|
2631
|
+
True
|
2632
|
+
|
2633
|
+
::
|
2634
|
+
|
2635
|
+
sage: G = Graph({0: [5, 6], 1: [4, 5], 2: [4, 6], 3: [4, 5, 6]})
|
2636
|
+
sage: B = BipartiteGraph(G)
|
2637
|
+
sage: C = B.canonical_label(partition=(B.left, B.right),
|
2638
|
+
....: edge_labels=True, algorithm='sage')
|
2639
|
+
sage: C.left
|
2640
|
+
{0, 1, 2, 3}
|
2641
|
+
sage: C.right
|
2642
|
+
{4, 5, 6}
|
2643
|
+
|
2644
|
+
TESTS:
|
2645
|
+
|
2646
|
+
Check that :issue:`38832` is fixed::
|
2647
|
+
|
2648
|
+
sage: # needs sage.modules
|
2649
|
+
sage: B = BipartiteGraph(matrix([[1, 1], [1, 1]]))
|
2650
|
+
sage: B.canonical_label()
|
2651
|
+
Bipartite graph on 4 vertices
|
2652
|
+
sage: B.canonical_label(certificate=True)[0]
|
2653
|
+
Bipartite graph on 4 vertices
|
2654
|
+
sage: B.canonical_label(edge_labels=True)
|
2655
|
+
Bipartite graph on 4 vertices
|
2656
|
+
sage: B.allow_multiple_edges(True)
|
2657
|
+
sage: B.add_edges(B.edges())
|
2658
|
+
sage: B.canonical_label()
|
2659
|
+
Bipartite multi-graph on 4 vertices
|
2660
|
+
|
2661
|
+
Check the behavior for immutable graphs::
|
2662
|
+
|
2663
|
+
sage: G = BipartiteGraph(graphs.CycleGraph(4))
|
2664
|
+
sage: G.canonical_label().is_immutable()
|
2665
|
+
False
|
2666
|
+
sage: G.canonical_label(immutable=True).is_immutable()
|
2667
|
+
True
|
2668
|
+
sage: G = BipartiteGraph(graphs.CycleGraph(4), immutable=True)
|
2669
|
+
sage: G.canonical_label().is_immutable()
|
2670
|
+
True
|
2671
|
+
sage: G.canonical_label(immutable=False).is_immutable()
|
2672
|
+
False
|
2673
|
+
|
2674
|
+
.. SEEALSO::
|
2675
|
+
|
2676
|
+
:meth:`~sage.graphs.generic_graph.GenericGraph.canonical_label()`
|
2677
|
+
"""
|
2678
|
+
if certificate:
|
2679
|
+
C, cert = GenericGraph.canonical_label(self, partition=partition,
|
2680
|
+
certificate=certificate,
|
2681
|
+
edge_labels=edge_labels,
|
2682
|
+
algorithm=algorithm,
|
2683
|
+
return_graph=return_graph,
|
2684
|
+
immutable=immutable)
|
2685
|
+
|
2686
|
+
else:
|
2687
|
+
from sage.groups.perm_gps.partn_ref.refinement_graphs import search_tree
|
2688
|
+
from sage.graphs.graph import Graph
|
2689
|
+
from sage.graphs.generic_graph import graph_isom_equivalent_non_edge_labeled_graph
|
2690
|
+
from itertools import chain
|
2691
|
+
|
2692
|
+
cert = {}
|
2693
|
+
|
2694
|
+
if edge_labels or self.has_multiple_edges():
|
2695
|
+
G, partition, relabeling = graph_isom_equivalent_non_edge_labeled_graph(self, partition, return_relabeling=True)
|
2696
|
+
G_vertices = list(chain(*partition))
|
2697
|
+
G_to = {u: i for i, u in enumerate(G_vertices)}
|
2698
|
+
H = Graph(len(G_vertices))
|
2699
|
+
HB = H._backend
|
2700
|
+
for u, v in G.edge_iterator(labels=False):
|
2701
|
+
HB.add_edge(G_to[u], G_to[v], None, False)
|
2702
|
+
GC = HB.c_graph()[0]
|
2703
|
+
partition = [[G_to[vv] for vv in cell] for cell in partition]
|
2704
|
+
a, b, c = search_tree(GC, partition, certificate=True, dig=False)
|
2705
|
+
# c is a permutation to the canonical label of G,
|
2706
|
+
# which depends only on isomorphism class of self.
|
2707
|
+
cert = {v: c[G_to[relabeling[v]]] for v in self}
|
2708
|
+
|
2709
|
+
else:
|
2710
|
+
if partition is None:
|
2711
|
+
partition = self.bipartition()
|
2712
|
+
G_vertices = list(chain(*partition))
|
2713
|
+
G_to = {u: i for i, u in enumerate(G_vertices)}
|
2714
|
+
H = Graph(len(G_vertices))
|
2715
|
+
HB = H._backend
|
2716
|
+
for u, v in self.edge_iterator(labels=False):
|
2717
|
+
HB.add_edge(G_to[u], G_to[v], None, False)
|
2718
|
+
GC = HB.c_graph()[0]
|
2719
|
+
partition = [[G_to[vv] for vv in cell] for cell in partition]
|
2720
|
+
a, b, c = search_tree(GC, partition, certificate=True, dig=False)
|
2721
|
+
cert = {v: c[G_to[v]] for v in G_to}
|
2722
|
+
|
2723
|
+
if immutable is None:
|
2724
|
+
immutable = self.is_immutable()
|
2725
|
+
C = self.relabel(perm=cert, inplace=False, immutable=immutable)
|
2726
|
+
|
2727
|
+
C.left = {cert[v] for v in self.left}
|
2728
|
+
C.right = {cert[v] for v in self.right}
|
2729
|
+
|
2730
|
+
if certificate:
|
2731
|
+
return C, cert
|
2732
|
+
return C
|