passagemath-combinat 10.6.42__cp314-cp314-musllinux_1_2_x86_64.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_combinat/__init__.py +3 -0
- passagemath_combinat-10.6.42.dist-info/METADATA +160 -0
- passagemath_combinat-10.6.42.dist-info/RECORD +400 -0
- passagemath_combinat-10.6.42.dist-info/WHEEL +5 -0
- passagemath_combinat-10.6.42.dist-info/top_level.txt +3 -0
- passagemath_combinat.libs/libgmp-0e7fc84e.so.10.5.0 +0 -0
- passagemath_combinat.libs/libsymmetrica-81fe8739.so.3.0.0 +0 -0
- sage/algebras/affine_nil_temperley_lieb.py +263 -0
- sage/algebras/all.py +24 -0
- sage/algebras/all__sagemath_combinat.py +35 -0
- sage/algebras/askey_wilson.py +935 -0
- sage/algebras/associated_graded.py +345 -0
- sage/algebras/cellular_basis.py +350 -0
- sage/algebras/cluster_algebra.py +2766 -0
- sage/algebras/down_up_algebra.py +860 -0
- sage/algebras/free_algebra.py +1698 -0
- sage/algebras/free_algebra_element.py +345 -0
- sage/algebras/free_algebra_quotient.py +405 -0
- sage/algebras/free_algebra_quotient_element.py +295 -0
- sage/algebras/free_zinbiel_algebra.py +885 -0
- sage/algebras/hall_algebra.py +783 -0
- sage/algebras/hecke_algebras/all.py +4 -0
- sage/algebras/hecke_algebras/ariki_koike_algebra.py +1796 -0
- sage/algebras/hecke_algebras/ariki_koike_specht_modules.py +475 -0
- sage/algebras/hecke_algebras/cubic_hecke_algebra.py +3520 -0
- sage/algebras/hecke_algebras/cubic_hecke_base_ring.py +1473 -0
- sage/algebras/hecke_algebras/cubic_hecke_matrix_rep.py +1079 -0
- sage/algebras/iwahori_hecke_algebra.py +3095 -0
- sage/algebras/jordan_algebra.py +1773 -0
- sage/algebras/lie_conformal_algebras/abelian_lie_conformal_algebra.py +113 -0
- sage/algebras/lie_conformal_algebras/affine_lie_conformal_algebra.py +156 -0
- sage/algebras/lie_conformal_algebras/all.py +18 -0
- sage/algebras/lie_conformal_algebras/bosonic_ghosts_lie_conformal_algebra.py +134 -0
- sage/algebras/lie_conformal_algebras/examples.py +43 -0
- sage/algebras/lie_conformal_algebras/fermionic_ghosts_lie_conformal_algebra.py +131 -0
- sage/algebras/lie_conformal_algebras/finitely_freely_generated_lca.py +139 -0
- sage/algebras/lie_conformal_algebras/free_bosons_lie_conformal_algebra.py +174 -0
- sage/algebras/lie_conformal_algebras/free_fermions_lie_conformal_algebra.py +167 -0
- sage/algebras/lie_conformal_algebras/freely_generated_lie_conformal_algebra.py +107 -0
- sage/algebras/lie_conformal_algebras/graded_lie_conformal_algebra.py +135 -0
- sage/algebras/lie_conformal_algebras/lie_conformal_algebra.py +353 -0
- sage/algebras/lie_conformal_algebras/lie_conformal_algebra_element.py +236 -0
- sage/algebras/lie_conformal_algebras/lie_conformal_algebra_with_basis.py +78 -0
- sage/algebras/lie_conformal_algebras/lie_conformal_algebra_with_structure_coefs.py +328 -0
- sage/algebras/lie_conformal_algebras/n2_lie_conformal_algebra.py +117 -0
- sage/algebras/lie_conformal_algebras/neveu_schwarz_lie_conformal_algebra.py +86 -0
- sage/algebras/lie_conformal_algebras/virasoro_lie_conformal_algebra.py +82 -0
- sage/algebras/lie_conformal_algebras/weyl_lie_conformal_algebra.py +205 -0
- sage/algebras/nil_coxeter_algebra.py +191 -0
- sage/algebras/q_commuting_polynomials.py +673 -0
- sage/algebras/q_system.py +608 -0
- sage/algebras/quantum_clifford.py +959 -0
- sage/algebras/quantum_groups/ace_quantum_onsager.py +693 -0
- sage/algebras/quantum_groups/all.py +9 -0
- sage/algebras/quantum_groups/fock_space.py +2219 -0
- sage/algebras/quantum_groups/q_numbers.py +207 -0
- sage/algebras/quantum_groups/quantum_group_gap.py +2695 -0
- sage/algebras/quantum_groups/representations.py +591 -0
- sage/algebras/quantum_matrix_coordinate_algebra.py +1006 -0
- sage/algebras/quantum_oscillator.py +623 -0
- sage/algebras/quaternion_algebra.py +20 -0
- sage/algebras/quaternion_algebra_element.py +55 -0
- sage/algebras/rational_cherednik_algebra.py +525 -0
- sage/algebras/schur_algebra.py +670 -0
- sage/algebras/shuffle_algebra.py +1011 -0
- sage/algebras/splitting_algebra.py +779 -0
- sage/algebras/tensor_algebra.py +709 -0
- sage/algebras/yangian.py +1082 -0
- sage/algebras/yokonuma_hecke_algebra.py +1018 -0
- sage/all__sagemath_combinat.py +35 -0
- sage/combinat/SJT.py +255 -0
- sage/combinat/affine_permutation.py +2405 -0
- sage/combinat/algebraic_combinatorics.py +55 -0
- sage/combinat/all.py +53 -0
- sage/combinat/all__sagemath_combinat.py +195 -0
- sage/combinat/alternating_sign_matrix.py +2063 -0
- sage/combinat/baxter_permutations.py +346 -0
- sage/combinat/bijectionist.py +3220 -0
- sage/combinat/binary_recurrence_sequences.py +1180 -0
- sage/combinat/blob_algebra.py +685 -0
- sage/combinat/catalog_partitions.py +27 -0
- sage/combinat/chas/all.py +23 -0
- sage/combinat/chas/fsym.py +1180 -0
- sage/combinat/chas/wqsym.py +2601 -0
- sage/combinat/cluster_complex.py +326 -0
- sage/combinat/colored_permutations.py +2039 -0
- sage/combinat/colored_permutations_representations.py +964 -0
- sage/combinat/composition_signed.py +142 -0
- sage/combinat/composition_tableau.py +855 -0
- sage/combinat/constellation.py +1729 -0
- sage/combinat/core.py +751 -0
- sage/combinat/counting.py +12 -0
- sage/combinat/crystals/affine.py +742 -0
- sage/combinat/crystals/affine_factorization.py +518 -0
- sage/combinat/crystals/affinization.py +331 -0
- sage/combinat/crystals/alcove_path.py +2013 -0
- sage/combinat/crystals/all.py +22 -0
- sage/combinat/crystals/bkk_crystals.py +141 -0
- sage/combinat/crystals/catalog.py +115 -0
- sage/combinat/crystals/catalog_elementary_crystals.py +18 -0
- sage/combinat/crystals/catalog_infinity_crystals.py +33 -0
- sage/combinat/crystals/catalog_kirillov_reshetikhin.py +18 -0
- sage/combinat/crystals/crystals.py +257 -0
- sage/combinat/crystals/direct_sum.py +260 -0
- sage/combinat/crystals/elementary_crystals.py +1251 -0
- sage/combinat/crystals/fast_crystals.py +441 -0
- sage/combinat/crystals/fully_commutative_stable_grothendieck.py +1205 -0
- sage/combinat/crystals/generalized_young_walls.py +1076 -0
- sage/combinat/crystals/highest_weight_crystals.py +436 -0
- sage/combinat/crystals/induced_structure.py +695 -0
- sage/combinat/crystals/infinity_crystals.py +730 -0
- sage/combinat/crystals/kac_modules.py +863 -0
- sage/combinat/crystals/kirillov_reshetikhin.py +4196 -0
- sage/combinat/crystals/kyoto_path_model.py +497 -0
- sage/combinat/crystals/letters.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/crystals/letters.pxd +79 -0
- sage/combinat/crystals/letters.pyx +3056 -0
- sage/combinat/crystals/littelmann_path.py +1518 -0
- sage/combinat/crystals/monomial_crystals.py +1262 -0
- sage/combinat/crystals/multisegments.py +462 -0
- sage/combinat/crystals/mv_polytopes.py +467 -0
- sage/combinat/crystals/pbw_crystal.py +511 -0
- sage/combinat/crystals/pbw_datum.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/crystals/pbw_datum.pxd +4 -0
- sage/combinat/crystals/pbw_datum.pyx +487 -0
- sage/combinat/crystals/polyhedral_realization.py +372 -0
- sage/combinat/crystals/spins.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/crystals/spins.pxd +21 -0
- sage/combinat/crystals/spins.pyx +756 -0
- sage/combinat/crystals/star_crystal.py +290 -0
- sage/combinat/crystals/subcrystal.py +464 -0
- sage/combinat/crystals/tensor_product.py +1177 -0
- sage/combinat/crystals/tensor_product_element.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/crystals/tensor_product_element.pxd +35 -0
- sage/combinat/crystals/tensor_product_element.pyx +1870 -0
- sage/combinat/crystals/virtual_crystal.py +420 -0
- sage/combinat/cyclic_sieving_phenomenon.py +204 -0
- sage/combinat/debruijn_sequence.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/debruijn_sequence.pyx +355 -0
- sage/combinat/decorated_permutation.py +270 -0
- sage/combinat/degree_sequences.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/degree_sequences.pyx +588 -0
- sage/combinat/derangements.py +527 -0
- sage/combinat/descent_algebra.py +1008 -0
- sage/combinat/diagram.py +1551 -0
- sage/combinat/diagram_algebras.py +5886 -0
- sage/combinat/dyck_word.py +4349 -0
- sage/combinat/e_one_star.py +1623 -0
- sage/combinat/enumerated_sets.py +123 -0
- sage/combinat/expnums.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/expnums.pyx +148 -0
- sage/combinat/fast_vector_partitions.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/fast_vector_partitions.pyx +346 -0
- sage/combinat/fqsym.py +1977 -0
- sage/combinat/free_dendriform_algebra.py +954 -0
- sage/combinat/free_prelie_algebra.py +1141 -0
- sage/combinat/fully_commutative_elements.py +1077 -0
- sage/combinat/fully_packed_loop.py +1523 -0
- sage/combinat/gelfand_tsetlin_patterns.py +1409 -0
- sage/combinat/gray_codes.py +311 -0
- sage/combinat/grossman_larson_algebras.py +667 -0
- sage/combinat/growth.py +4352 -0
- sage/combinat/hall_polynomial.py +188 -0
- sage/combinat/hillman_grassl.py +866 -0
- sage/combinat/integer_matrices.py +329 -0
- sage/combinat/integer_vectors_mod_permgroup.py +1238 -0
- sage/combinat/k_tableau.py +4564 -0
- sage/combinat/kazhdan_lusztig.py +215 -0
- sage/combinat/key_polynomial.py +885 -0
- sage/combinat/knutson_tao_puzzles.py +2286 -0
- sage/combinat/lr_tableau.py +311 -0
- sage/combinat/matrices/all.py +24 -0
- sage/combinat/matrices/hadamard_matrix.py +3790 -0
- sage/combinat/matrices/latin.py +2912 -0
- sage/combinat/misc.py +401 -0
- sage/combinat/multiset_partition_into_sets_ordered.py +3541 -0
- sage/combinat/ncsf_qsym/all.py +21 -0
- sage/combinat/ncsf_qsym/combinatorics.py +317 -0
- sage/combinat/ncsf_qsym/generic_basis_code.py +1427 -0
- sage/combinat/ncsf_qsym/ncsf.py +5637 -0
- sage/combinat/ncsf_qsym/qsym.py +4053 -0
- sage/combinat/ncsf_qsym/tutorial.py +447 -0
- sage/combinat/ncsym/all.py +21 -0
- sage/combinat/ncsym/bases.py +855 -0
- sage/combinat/ncsym/dual.py +593 -0
- sage/combinat/ncsym/ncsym.py +2076 -0
- sage/combinat/necklace.py +551 -0
- sage/combinat/non_decreasing_parking_function.py +634 -0
- sage/combinat/nu_dyck_word.py +1474 -0
- sage/combinat/output.py +861 -0
- sage/combinat/parallelogram_polyomino.py +4326 -0
- sage/combinat/parking_functions.py +1602 -0
- sage/combinat/partition_algebra.py +1998 -0
- sage/combinat/partition_kleshchev.py +1982 -0
- sage/combinat/partition_shifting_algebras.py +584 -0
- sage/combinat/partition_tuple.py +3114 -0
- sage/combinat/path_tableaux/all.py +13 -0
- sage/combinat/path_tableaux/catalog.py +29 -0
- sage/combinat/path_tableaux/dyck_path.py +380 -0
- sage/combinat/path_tableaux/frieze.py +476 -0
- sage/combinat/path_tableaux/path_tableau.py +728 -0
- sage/combinat/path_tableaux/semistandard.py +510 -0
- sage/combinat/perfect_matching.py +779 -0
- sage/combinat/plane_partition.py +3300 -0
- sage/combinat/q_bernoulli.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/q_bernoulli.pyx +128 -0
- sage/combinat/quickref.py +81 -0
- sage/combinat/recognizable_series.py +2051 -0
- sage/combinat/regular_sequence.py +4316 -0
- sage/combinat/regular_sequence_bounded.py +543 -0
- sage/combinat/restricted_growth.py +81 -0
- sage/combinat/ribbon.py +20 -0
- sage/combinat/ribbon_shaped_tableau.py +489 -0
- sage/combinat/ribbon_tableau.py +1180 -0
- sage/combinat/rigged_configurations/all.py +46 -0
- sage/combinat/rigged_configurations/bij_abstract_class.py +548 -0
- sage/combinat/rigged_configurations/bij_infinity.py +370 -0
- sage/combinat/rigged_configurations/bij_type_A.py +163 -0
- sage/combinat/rigged_configurations/bij_type_A2_dual.py +338 -0
- sage/combinat/rigged_configurations/bij_type_A2_even.py +218 -0
- sage/combinat/rigged_configurations/bij_type_A2_odd.py +199 -0
- sage/combinat/rigged_configurations/bij_type_B.py +900 -0
- sage/combinat/rigged_configurations/bij_type_C.py +267 -0
- sage/combinat/rigged_configurations/bij_type_D.py +771 -0
- sage/combinat/rigged_configurations/bij_type_D_tri.py +392 -0
- sage/combinat/rigged_configurations/bij_type_D_twisted.py +576 -0
- sage/combinat/rigged_configurations/bij_type_E67.py +402 -0
- sage/combinat/rigged_configurations/bijection.py +143 -0
- sage/combinat/rigged_configurations/kleber_tree.py +1475 -0
- sage/combinat/rigged_configurations/kr_tableaux.py +1898 -0
- sage/combinat/rigged_configurations/rc_crystal.py +461 -0
- sage/combinat/rigged_configurations/rc_infinity.py +540 -0
- sage/combinat/rigged_configurations/rigged_configuration_element.py +2403 -0
- sage/combinat/rigged_configurations/rigged_configurations.py +1918 -0
- sage/combinat/rigged_configurations/rigged_partition.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/rigged_configurations/rigged_partition.pxd +15 -0
- sage/combinat/rigged_configurations/rigged_partition.pyx +680 -0
- sage/combinat/rigged_configurations/tensor_product_kr_tableaux.py +499 -0
- sage/combinat/rigged_configurations/tensor_product_kr_tableaux_element.py +428 -0
- sage/combinat/rsk.py +3438 -0
- sage/combinat/schubert_polynomial.py +508 -0
- sage/combinat/set_partition.py +3318 -0
- sage/combinat/set_partition_iterator.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/set_partition_iterator.pyx +136 -0
- sage/combinat/set_partition_ordered.py +1590 -0
- sage/combinat/sf/abreu_nigro.py +346 -0
- sage/combinat/sf/all.py +52 -0
- sage/combinat/sf/character.py +576 -0
- sage/combinat/sf/classical.py +319 -0
- sage/combinat/sf/dual.py +996 -0
- sage/combinat/sf/elementary.py +549 -0
- sage/combinat/sf/hall_littlewood.py +1028 -0
- sage/combinat/sf/hecke.py +336 -0
- sage/combinat/sf/homogeneous.py +464 -0
- sage/combinat/sf/jack.py +1428 -0
- sage/combinat/sf/k_dual.py +1458 -0
- sage/combinat/sf/kfpoly.py +447 -0
- sage/combinat/sf/llt.py +789 -0
- sage/combinat/sf/macdonald.py +2019 -0
- sage/combinat/sf/monomial.py +525 -0
- sage/combinat/sf/multiplicative.py +113 -0
- sage/combinat/sf/new_kschur.py +1786 -0
- sage/combinat/sf/ns_macdonald.py +964 -0
- sage/combinat/sf/orthogonal.py +246 -0
- sage/combinat/sf/orthotriang.py +355 -0
- sage/combinat/sf/powersum.py +963 -0
- sage/combinat/sf/schur.py +880 -0
- sage/combinat/sf/sf.py +1653 -0
- sage/combinat/sf/sfa.py +7053 -0
- sage/combinat/sf/symplectic.py +253 -0
- sage/combinat/sf/witt.py +721 -0
- sage/combinat/shifted_primed_tableau.py +2735 -0
- sage/combinat/shuffle.py +830 -0
- sage/combinat/sidon_sets.py +146 -0
- sage/combinat/similarity_class_type.py +1721 -0
- sage/combinat/sine_gordon.py +618 -0
- sage/combinat/six_vertex_model.py +784 -0
- sage/combinat/skew_partition.py +2053 -0
- sage/combinat/skew_tableau.py +2989 -0
- sage/combinat/sloane_functions.py +8935 -0
- sage/combinat/specht_module.py +1403 -0
- sage/combinat/species/all.py +48 -0
- sage/combinat/species/characteristic_species.py +321 -0
- sage/combinat/species/composition_species.py +273 -0
- sage/combinat/species/cycle_species.py +284 -0
- sage/combinat/species/empty_species.py +155 -0
- sage/combinat/species/functorial_composition_species.py +148 -0
- sage/combinat/species/generating_series.py +673 -0
- sage/combinat/species/library.py +148 -0
- sage/combinat/species/linear_order_species.py +169 -0
- sage/combinat/species/misc.py +83 -0
- sage/combinat/species/partition_species.py +290 -0
- sage/combinat/species/permutation_species.py +268 -0
- sage/combinat/species/product_species.py +423 -0
- sage/combinat/species/recursive_species.py +476 -0
- sage/combinat/species/set_species.py +192 -0
- sage/combinat/species/species.py +820 -0
- sage/combinat/species/structure.py +539 -0
- sage/combinat/species/subset_species.py +243 -0
- sage/combinat/species/sum_species.py +225 -0
- sage/combinat/subword.py +564 -0
- sage/combinat/subword_complex.py +2122 -0
- sage/combinat/subword_complex_c.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/subword_complex_c.pyx +119 -0
- sage/combinat/super_tableau.py +821 -0
- sage/combinat/superpartition.py +1154 -0
- sage/combinat/symmetric_group_algebra.py +3774 -0
- sage/combinat/symmetric_group_representations.py +1830 -0
- sage/combinat/t_sequences.py +877 -0
- sage/combinat/tableau.py +9506 -0
- sage/combinat/tableau_residues.py +860 -0
- sage/combinat/tableau_tuple.py +5353 -0
- sage/combinat/tiling.py +2432 -0
- sage/combinat/triangles_FHM.py +777 -0
- sage/combinat/tutorial.py +1857 -0
- sage/combinat/vector_partition.py +337 -0
- sage/combinat/words/abstract_word.py +1722 -0
- sage/combinat/words/all.py +59 -0
- sage/combinat/words/alphabet.py +268 -0
- sage/combinat/words/finite_word.py +7201 -0
- sage/combinat/words/infinite_word.py +113 -0
- sage/combinat/words/lyndon_word.py +652 -0
- sage/combinat/words/morphic.py +351 -0
- sage/combinat/words/morphism.py +3878 -0
- sage/combinat/words/paths.py +2932 -0
- sage/combinat/words/shuffle_product.py +278 -0
- sage/combinat/words/suffix_trees.py +1873 -0
- sage/combinat/words/word.py +769 -0
- sage/combinat/words/word_char.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/words/word_char.pyx +847 -0
- sage/combinat/words/word_datatypes.cpython-314-x86_64-linux-musl.so +0 -0
- sage/combinat/words/word_datatypes.pxd +4 -0
- sage/combinat/words/word_datatypes.pyx +1067 -0
- sage/combinat/words/word_generators.py +2026 -0
- sage/combinat/words/word_infinite_datatypes.py +1218 -0
- sage/combinat/words/word_options.py +99 -0
- sage/combinat/words/words.py +2396 -0
- sage/data_structures/all__sagemath_combinat.py +1 -0
- sage/databases/all__sagemath_combinat.py +13 -0
- sage/databases/findstat.py +4897 -0
- sage/databases/oeis.py +2058 -0
- sage/databases/sloane.py +393 -0
- sage/dynamics/all__sagemath_combinat.py +14 -0
- sage/dynamics/cellular_automata/all.py +7 -0
- sage/dynamics/cellular_automata/catalog.py +34 -0
- sage/dynamics/cellular_automata/elementary.py +612 -0
- sage/dynamics/cellular_automata/glca.py +477 -0
- sage/dynamics/cellular_automata/solitons.py +1463 -0
- sage/dynamics/finite_dynamical_system.py +1249 -0
- sage/dynamics/finite_dynamical_system_catalog.py +382 -0
- sage/games/all.py +7 -0
- sage/games/hexad.py +704 -0
- sage/games/quantumino.py +591 -0
- sage/games/sudoku.py +889 -0
- sage/games/sudoku_backtrack.cpython-314-x86_64-linux-musl.so +0 -0
- sage/games/sudoku_backtrack.pyx +189 -0
- sage/groups/all__sagemath_combinat.py +1 -0
- sage/groups/indexed_free_group.py +489 -0
- sage/libs/all__sagemath_combinat.py +6 -0
- sage/libs/lrcalc/__init__.py +1 -0
- sage/libs/lrcalc/lrcalc.py +525 -0
- sage/libs/symmetrica/__init__.py +7 -0
- sage/libs/symmetrica/all.py +101 -0
- sage/libs/symmetrica/kostka.pxi +168 -0
- sage/libs/symmetrica/part.pxi +193 -0
- sage/libs/symmetrica/plet.pxi +42 -0
- sage/libs/symmetrica/sab.pxi +196 -0
- sage/libs/symmetrica/sb.pxi +332 -0
- sage/libs/symmetrica/sc.pxi +192 -0
- sage/libs/symmetrica/schur.pxi +956 -0
- sage/libs/symmetrica/symmetrica.cpython-314-x86_64-linux-musl.so +0 -0
- sage/libs/symmetrica/symmetrica.pxi +1172 -0
- sage/libs/symmetrica/symmetrica.pyx +39 -0
- sage/monoids/all.py +13 -0
- sage/monoids/automatic_semigroup.py +1054 -0
- sage/monoids/free_abelian_monoid.py +315 -0
- sage/monoids/free_abelian_monoid_element.cpython-314-x86_64-linux-musl.so +0 -0
- sage/monoids/free_abelian_monoid_element.pxd +16 -0
- sage/monoids/free_abelian_monoid_element.pyx +397 -0
- sage/monoids/free_monoid.py +335 -0
- sage/monoids/free_monoid_element.py +431 -0
- sage/monoids/hecke_monoid.py +65 -0
- sage/monoids/string_monoid.py +817 -0
- sage/monoids/string_monoid_element.py +547 -0
- sage/monoids/string_ops.py +143 -0
- sage/monoids/trace_monoid.py +972 -0
- sage/rings/all__sagemath_combinat.py +2 -0
- sage/sat/all.py +4 -0
- sage/sat/boolean_polynomials.py +405 -0
- sage/sat/converters/__init__.py +6 -0
- sage/sat/converters/anf2cnf.py +14 -0
- sage/sat/converters/polybori.py +611 -0
- sage/sat/solvers/__init__.py +5 -0
- sage/sat/solvers/cryptominisat.py +287 -0
- sage/sat/solvers/dimacs.py +783 -0
- sage/sat/solvers/picosat.py +228 -0
- sage/sat/solvers/sat_lp.py +156 -0
- sage/sat/solvers/satsolver.cpython-314-x86_64-linux-musl.so +0 -0
- sage/sat/solvers/satsolver.pxd +3 -0
- sage/sat/solvers/satsolver.pyx +405 -0
|
@@ -0,0 +1,3220 @@
|
|
|
1
|
+
# sage_setup: distribution = sagemath-combinat
|
|
2
|
+
# sage.doctest: needs sage.numerical.mip
|
|
3
|
+
r"""
|
|
4
|
+
A bijectionist's toolkit
|
|
5
|
+
|
|
6
|
+
AUTHORS:
|
|
7
|
+
|
|
8
|
+
- Alexander Grosz, Tobias Kietreiber, Stephan Pfannerer and Martin
|
|
9
|
+
Rubey (2020-2022): Initial version
|
|
10
|
+
|
|
11
|
+
Quick reference
|
|
12
|
+
===============
|
|
13
|
+
|
|
14
|
+
.. csv-table::
|
|
15
|
+
:class: contentstable
|
|
16
|
+
:widths: 30, 70
|
|
17
|
+
:delim: |
|
|
18
|
+
|
|
19
|
+
:meth:`~Bijectionist.set_statistics` | Declare statistics that are preserved by the bijection.
|
|
20
|
+
:meth:`~Bijectionist.set_value_restrictions` | Restrict the values of the statistic on an element.
|
|
21
|
+
:meth:`~Bijectionist.set_constant_blocks` | Declare that the statistic is constant on some sets.
|
|
22
|
+
:meth:`~Bijectionist.set_distributions` | Restrict the distribution of values of the statistic on some elements.
|
|
23
|
+
:meth:`~Bijectionist.set_intertwining_relations` | Declare that the statistic intertwines with other maps.
|
|
24
|
+
:meth:`~Bijectionist.set_quadratic_relation` | Declare that the statistic satisfies a certain relation.
|
|
25
|
+
:meth:`~Bijectionist.set_homomesic` | Declare that the statistic is homomesic with respect to a given set partition.
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
:meth:`~Bijectionist.statistics_table` | Print a table collecting information on the given statistics.
|
|
29
|
+
:meth:`~Bijectionist.statistics_fibers` | Collect elements with the same statistics.
|
|
30
|
+
|
|
31
|
+
:meth:`~Bijectionist.constant_blocks` | Return the blocks on which the statistic is constant.
|
|
32
|
+
:meth:`~Bijectionist.solutions_iterator` | Iterate over all possible solutions.
|
|
33
|
+
:meth:`~Bijectionist.possible_values` | Return all possible values for a given element.
|
|
34
|
+
:meth:`~Bijectionist.minimal_subdistributions_iterator` | Iterate over the minimal subdistributions.
|
|
35
|
+
:meth:`~Bijectionist.minimal_subdistributions_blocks_iterator` | Iterate over the minimal subdistributions.
|
|
36
|
+
|
|
37
|
+
A guided tour
|
|
38
|
+
=============
|
|
39
|
+
|
|
40
|
+
Consider the following combinatorial statistics on a permutation:
|
|
41
|
+
|
|
42
|
+
* `wex`, the number of weak excedences,
|
|
43
|
+
* `fix`, the number of fixed points,
|
|
44
|
+
* `des`, the number of descents (after appending `0`),
|
|
45
|
+
* `adj`, the number of adjacencies (after appending `0`), and
|
|
46
|
+
* `llis`, the length of a longest increasing subsequence.
|
|
47
|
+
|
|
48
|
+
Moreover, let `rot` be action of rotation on a permutation, i.e., the
|
|
49
|
+
conjugation with the long cycle.
|
|
50
|
+
|
|
51
|
+
It is known that there must exist a statistic `s` on permutations,
|
|
52
|
+
which is equidistributed with `llis` but additionally invariant under
|
|
53
|
+
`rot`. However, at least very small cases do not contradict the
|
|
54
|
+
possibility that one can even find a statistic `s`, invariant under
|
|
55
|
+
`rot` and such that `(s, wex, fix) \sim (llis, des, adj)`. Let us
|
|
56
|
+
check this for permutations of size at most `3`::
|
|
57
|
+
|
|
58
|
+
sage: N = 3
|
|
59
|
+
sage: A = B = [pi for n in range(N+1) for pi in Permutations(n)]
|
|
60
|
+
sage: def alpha1(p): return len(p.weak_excedences())
|
|
61
|
+
sage: def alpha2(p): return len(p.fixed_points())
|
|
62
|
+
sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
|
|
63
|
+
sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
|
|
64
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
65
|
+
sage: def rotate_permutation(p):
|
|
66
|
+
....: cycle = Permutation(tuple(range(1, len(p)+1)))
|
|
67
|
+
....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)])
|
|
68
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
69
|
+
sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2))
|
|
70
|
+
sage: a, b = bij.statistics_table()
|
|
71
|
+
sage: table(a, header_row=True, frame=True)
|
|
72
|
+
┌───────────┬────────┬────────┬────────┐
|
|
73
|
+
│ a │ α_1(a) │ α_2(a) │ α_3(a) │
|
|
74
|
+
╞═══════════╪════════╪════════╪════════╡
|
|
75
|
+
│ [] │ 0 │ 0 │ 0 │
|
|
76
|
+
├───────────┼────────┼────────┼────────┤
|
|
77
|
+
│ [1] │ 1 │ 1 │ 1 │
|
|
78
|
+
├───────────┼────────┼────────┼────────┤
|
|
79
|
+
│ [1, 2] │ 2 │ 2 │ 2 │
|
|
80
|
+
├───────────┼────────┼────────┼────────┤
|
|
81
|
+
│ [2, 1] │ 2 │ 1 │ 0 │
|
|
82
|
+
├───────────┼────────┼────────┼────────┤
|
|
83
|
+
│ [1, 2, 3] │ 3 │ 3 │ 3 │
|
|
84
|
+
├───────────┼────────┼────────┼────────┤
|
|
85
|
+
│ [1, 3, 2] │ 3 │ 2 │ 1 │
|
|
86
|
+
├───────────┼────────┼────────┼────────┤
|
|
87
|
+
│ [2, 1, 3] │ 3 │ 2 │ 1 │
|
|
88
|
+
├───────────┼────────┼────────┼────────┤
|
|
89
|
+
│ [2, 3, 1] │ 3 │ 2 │ 0 │
|
|
90
|
+
├───────────┼────────┼────────┼────────┤
|
|
91
|
+
│ [3, 1, 2] │ 3 │ 1 │ 0 │
|
|
92
|
+
├───────────┼────────┼────────┼────────┤
|
|
93
|
+
│ [3, 2, 1] │ 3 │ 2 │ 1 │
|
|
94
|
+
└───────────┴────────┴────────┴────────┘
|
|
95
|
+
|
|
96
|
+
sage: table(b, header_row=True, frame=True)
|
|
97
|
+
┌───────────┬───┬────────┬────────┬────────┐
|
|
98
|
+
│ b │ τ │ β_1(b) │ β_2(b) │ β_3(b) │
|
|
99
|
+
╞═══════════╪═══╪════════╪════════╪════════╡
|
|
100
|
+
│ [] │ 0 │ 0 │ 0 │ 0 │
|
|
101
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
102
|
+
│ [1] │ 1 │ 1 │ 1 │ 1 │
|
|
103
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
104
|
+
│ [1, 2] │ 2 │ 2 │ 1 │ 0 │
|
|
105
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
106
|
+
│ [2, 1] │ 1 │ 2 │ 2 │ 2 │
|
|
107
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
108
|
+
│ [1, 2, 3] │ 3 │ 3 │ 1 │ 0 │
|
|
109
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
110
|
+
│ [1, 3, 2] │ 2 │ 3 │ 2 │ 1 │
|
|
111
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
112
|
+
│ [2, 1, 3] │ 2 │ 3 │ 2 │ 1 │
|
|
113
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
114
|
+
│ [2, 3, 1] │ 2 │ 3 │ 2 │ 1 │
|
|
115
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
116
|
+
│ [3, 1, 2] │ 2 │ 3 │ 2 │ 0 │
|
|
117
|
+
├───────────┼───┼────────┼────────┼────────┤
|
|
118
|
+
│ [3, 2, 1] │ 1 │ 3 │ 3 │ 3 │
|
|
119
|
+
└───────────┴───┴────────┴────────┴────────┘
|
|
120
|
+
|
|
121
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
122
|
+
sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
|
|
123
|
+
sage: bij.constant_blocks()
|
|
124
|
+
{{[1, 3, 2], [2, 1, 3], [3, 2, 1]}}
|
|
125
|
+
sage: next(bij.solutions_iterator())
|
|
126
|
+
{[]: 0,
|
|
127
|
+
[1]: 1,
|
|
128
|
+
[1, 2]: 1,
|
|
129
|
+
[1, 2, 3]: 1,
|
|
130
|
+
[1, 3, 2]: 2,
|
|
131
|
+
[2, 1]: 2,
|
|
132
|
+
[2, 1, 3]: 2,
|
|
133
|
+
[2, 3, 1]: 2,
|
|
134
|
+
[3, 1, 2]: 3,
|
|
135
|
+
[3, 2, 1]: 2}
|
|
136
|
+
|
|
137
|
+
On the other hand, we can check that there is no rotation invariant
|
|
138
|
+
statistic on non-crossing set partitions which is equidistributed
|
|
139
|
+
with the Strahler number on ordered trees::
|
|
140
|
+
|
|
141
|
+
sage: N = 8
|
|
142
|
+
sage: A = [SetPartition(d.to_noncrossing_partition()) for n in range(N) for d in DyckWords(n)]
|
|
143
|
+
sage: B = [t for n in range(1, N+1) for t in OrderedTrees(n)]
|
|
144
|
+
sage: def theta(m): return SetPartition([[i % m.size() + 1 for i in b] for b in m])
|
|
145
|
+
|
|
146
|
+
Code for the Strahler number can be obtained from FindStat. The
|
|
147
|
+
following code is equivalent to ``tau = findstat(397)``::
|
|
148
|
+
|
|
149
|
+
sage: def tau(T):
|
|
150
|
+
....: if len(T) == 0:
|
|
151
|
+
....: return 1
|
|
152
|
+
....: else:
|
|
153
|
+
....: l = [tau(S) for S in T]
|
|
154
|
+
....: m = max(l)
|
|
155
|
+
....: if l.count(m) == 1:
|
|
156
|
+
....: return m
|
|
157
|
+
....: else:
|
|
158
|
+
....: return m+1
|
|
159
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
160
|
+
sage: bij.set_statistics((lambda a: a.size(), lambda b: b.node_number()-1))
|
|
161
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
162
|
+
sage: bij.set_constant_blocks(orbit_decomposition(A, theta))
|
|
163
|
+
sage: list(bij.solutions_iterator())
|
|
164
|
+
[]
|
|
165
|
+
|
|
166
|
+
Next we demonstrate how to search for a bijection, instead An example
|
|
167
|
+
identifying `s` and `S`::
|
|
168
|
+
|
|
169
|
+
sage: N = 4
|
|
170
|
+
sage: A = [dyck_word for n in range(1, N) for dyck_word in DyckWords(n)]
|
|
171
|
+
sage: B = [binary_tree for n in range(1, N) for binary_tree in BinaryTrees(n)]
|
|
172
|
+
sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2))
|
|
173
|
+
sage: concat_tree = lambda B1, B2: concat_path(B1.to_dyck_word(),
|
|
174
|
+
....: B2.to_dyck_word()).to_binary_tree()
|
|
175
|
+
sage: bij = Bijectionist(A, B)
|
|
176
|
+
sage: bij.set_intertwining_relations((2, concat_path, concat_tree))
|
|
177
|
+
sage: bij.set_statistics((lambda d: d.semilength(), lambda t: t.node_number()))
|
|
178
|
+
sage: for D in sorted(bij.minimal_subdistributions_iterator(), key=lambda x: (len(x[0][0]), x)):
|
|
179
|
+
....: ascii_art(D)
|
|
180
|
+
( [ /\ ], [ o ] )
|
|
181
|
+
( [ o ] )
|
|
182
|
+
( [ \ ] )
|
|
183
|
+
( [ /\/\ ], [ o ] )
|
|
184
|
+
( [ o ] )
|
|
185
|
+
( [ /\ ] [ / ] )
|
|
186
|
+
( [ / \ ], [ o ] )
|
|
187
|
+
( [ o ] )
|
|
188
|
+
( [ \ ] )
|
|
189
|
+
( [ o ] )
|
|
190
|
+
( [ \ ] )
|
|
191
|
+
( [ /\/\/\ ], [ o ] )
|
|
192
|
+
( [ o ] )
|
|
193
|
+
( [ \ ] )
|
|
194
|
+
( [ o ] )
|
|
195
|
+
( [ /\ ] [ / ] )
|
|
196
|
+
( [ /\/ \ ], [ o ] )
|
|
197
|
+
( [ o ] )
|
|
198
|
+
( [ /\ ] [ / \ ] )
|
|
199
|
+
( [ / \/\ ], [ o o ] )
|
|
200
|
+
( [ o, o ] )
|
|
201
|
+
( [ / / ] )
|
|
202
|
+
( [ /\ ] [ o o ] )
|
|
203
|
+
( [ /\/\ / \ ] [ \ / ] )
|
|
204
|
+
( [ / \, / \ ], [ o o ] )
|
|
205
|
+
|
|
206
|
+
The output is in a form suitable for FindStat::
|
|
207
|
+
|
|
208
|
+
sage: findmap(list(bij.minimal_subdistributions_iterator())) # optional -- internet
|
|
209
|
+
0: Mp00034 (quality [100])
|
|
210
|
+
1: Mp00061oMp00023 (quality [100])
|
|
211
|
+
2: Mp00018oMp00140 (quality [100])
|
|
212
|
+
|
|
213
|
+
TESTS::
|
|
214
|
+
|
|
215
|
+
sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
|
|
216
|
+
sage: def theta(pi): return Permutation([x+1 if x != len(pi) else 1 for x in pi[-1:]+pi[:-1]])
|
|
217
|
+
sage: def tau(pi):
|
|
218
|
+
....: n = len(pi)
|
|
219
|
+
....: return sum([1 for i in range(1, n+1) for j in range(1, n+1)
|
|
220
|
+
....: if i<j <= pi(i)<pi(j) or pi(i)<pi(j)<i<j])
|
|
221
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
222
|
+
sage: bij.set_statistics((len, len))
|
|
223
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
224
|
+
sage: bij.set_constant_blocks(orbit_decomposition(A, theta))
|
|
225
|
+
sage: for solution in sorted(bij.solutions_iterator(), key=lambda d: sorted(d.items())):
|
|
226
|
+
....: print(solution)
|
|
227
|
+
{[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 0, [3, 1, 2]: 1}
|
|
228
|
+
{[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 1, [3, 1, 2]: 0}
|
|
229
|
+
{[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 0, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [3, 2, 1]: 0, [2, 3, 1]: 0, [3, 1, 2]: 0}
|
|
230
|
+
|
|
231
|
+
A test including intertwining relations::
|
|
232
|
+
|
|
233
|
+
sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
|
|
234
|
+
sage: def alpha(D): return (D.area(), D.bounce())
|
|
235
|
+
sage: def beta(D): return (D.bounce(), D.area())
|
|
236
|
+
sage: def tau(D): return D.number_of_touch_points()
|
|
237
|
+
|
|
238
|
+
The following looks correct::
|
|
239
|
+
|
|
240
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
241
|
+
sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
|
|
242
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
243
|
+
....: print(solution)
|
|
244
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
|
|
245
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
|
|
246
|
+
|
|
247
|
+
The following looks correct, because `alpha = beta \circ S` forces
|
|
248
|
+
`S([1,0,1,0]) = [1,1,0,0]` and `s = tau \circ S` forces therefore `s([1,0,1,0])
|
|
249
|
+
= \tau(S([1,0,1,0])) = \tau([1,1,0,0]) = 1`::
|
|
250
|
+
|
|
251
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
252
|
+
sage: bij.set_statistics((alpha, beta), (lambda d: d.semilength(), lambda d: d.semilength()))
|
|
253
|
+
sage: for solution in bij.solutions_iterator():
|
|
254
|
+
....: print(solution)
|
|
255
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
|
|
256
|
+
|
|
257
|
+
Now we introduce a intertwining relation::
|
|
258
|
+
|
|
259
|
+
sage: concat_path = lambda D1, D2: DyckWord(list(D1) + list(D2))
|
|
260
|
+
sage: pi_rho = (2, concat_path, lambda x, y: x+y)
|
|
261
|
+
|
|
262
|
+
Without `\alpha` and `\beta` but with `\pi` and `\rho` the other values are
|
|
263
|
+
forced because `s([1,0,1,0]) = s(\pi([1,0], [1,0])) = \rho(s([1,0]), s([1,0]))
|
|
264
|
+
= 2`::
|
|
265
|
+
|
|
266
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
267
|
+
sage: bij.set_intertwining_relations(pi_rho)
|
|
268
|
+
sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
|
|
269
|
+
sage: for solution in bij.solutions_iterator():
|
|
270
|
+
....: print(solution)
|
|
271
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
|
|
272
|
+
|
|
273
|
+
Thus the combination of both constraints should be infeasible::
|
|
274
|
+
|
|
275
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
276
|
+
sage: bij.set_statistics((alpha, beta), (lambda d: d.semilength(), lambda d: d.semilength()))
|
|
277
|
+
sage: bij.set_intertwining_relations(pi_rho)
|
|
278
|
+
sage: list(bij.solutions_iterator())
|
|
279
|
+
[]
|
|
280
|
+
|
|
281
|
+
Repeating some tests, but using the constructor instead of set_XXX() methods:
|
|
282
|
+
|
|
283
|
+
sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
|
|
284
|
+
sage: def alpha(D): return (D.area(), D.bounce())
|
|
285
|
+
sage: def beta(D): return (D.bounce(), D.area())
|
|
286
|
+
sage: def tau(D): return D.number_of_touch_points()
|
|
287
|
+
|
|
288
|
+
sage: bij = Bijectionist(A, B, tau, alpha_beta=((lambda d: d.semilength(), lambda d: d.semilength()),))
|
|
289
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
290
|
+
....: print(solution)
|
|
291
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
|
|
292
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
|
|
293
|
+
|
|
294
|
+
Constant blocks::
|
|
295
|
+
|
|
296
|
+
sage: A = B = 'abcd'
|
|
297
|
+
sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
|
|
298
|
+
sage: def rho(s1, s2): return (s1 + s2) % 2
|
|
299
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, P=[['a', 'c']], pi_rho=((2, pi, rho),))
|
|
300
|
+
sage: list(bij.solutions_iterator())
|
|
301
|
+
[{'a': 0, 'b': 1, 'c': 0, 'd': 1}]
|
|
302
|
+
sage: bij.constant_blocks()
|
|
303
|
+
{{'a', 'c'}, {'b', 'd'}}
|
|
304
|
+
|
|
305
|
+
Distributions::
|
|
306
|
+
|
|
307
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
308
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
309
|
+
sage: bij = Bijectionist(A, B, tau, alpha_beta=((len, len),), elements_distributions=(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]),))
|
|
310
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
311
|
+
....: print(sol)
|
|
312
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
313
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
314
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
315
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
316
|
+
|
|
317
|
+
Intertwining relations::
|
|
318
|
+
|
|
319
|
+
sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
|
|
320
|
+
|
|
321
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
322
|
+
sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points, alpha_beta=((len, len),), pi_rho=((2, concat, lambda x, y: x + y),))
|
|
323
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
324
|
+
....: print(solution)
|
|
325
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
326
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 0}
|
|
327
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 0}
|
|
328
|
+
|
|
329
|
+
Statistics::
|
|
330
|
+
|
|
331
|
+
sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
|
|
332
|
+
sage: def wex(p): return len(p.weak_excedences())
|
|
333
|
+
sage: def fix(p): return len(p.fixed_points())
|
|
334
|
+
sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
|
|
335
|
+
sage: bij = Bijectionist(A, B, fix, alpha_beta=((wex, des), (len, len)))
|
|
336
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
337
|
+
....: print(solution)
|
|
338
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
339
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
340
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
341
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
342
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
343
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
344
|
+
|
|
345
|
+
Value restrictions::
|
|
346
|
+
|
|
347
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
348
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
349
|
+
sage: alpha_beta = [(len, len)]
|
|
350
|
+
sage: value_restrictions = [(Permutation([1, 2]), [1]), (Permutation([3, 2, 1]), [2, 3, 4])]
|
|
351
|
+
sage: bij = Bijectionist(A, B, tau, alpha_beta=alpha_beta, value_restrictions=value_restrictions)
|
|
352
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
353
|
+
....: print(sol)
|
|
354
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3}
|
|
355
|
+
...
|
|
356
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
357
|
+
|
|
358
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
359
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
360
|
+
sage: bij = Bijectionist(A, B, tau, value_restrictions=((Permutation([1, 2]), [4, 5]),))
|
|
361
|
+
sage: next(bij.solutions_iterator())
|
|
362
|
+
Traceback (most recent call last):
|
|
363
|
+
...
|
|
364
|
+
ValueError: no possible values found for singleton block [[1, 2]]
|
|
365
|
+
"""
|
|
366
|
+
# ****************************************************************************
|
|
367
|
+
# Copyright (C) 2020 Martin Rubey <martin.rubey at tuwien.ac.at>
|
|
368
|
+
# Stephan Pfannerer
|
|
369
|
+
# Tobias Kietreiber
|
|
370
|
+
# Alexander Grosz
|
|
371
|
+
#
|
|
372
|
+
# Distributed under the terms of the GNU General Public License (GPL)
|
|
373
|
+
#
|
|
374
|
+
# This code is distributed in the hope that it will be useful,
|
|
375
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
376
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
|
|
377
|
+
# General Public License for more details.
|
|
378
|
+
#
|
|
379
|
+
# The full text of the GPL is available at:
|
|
380
|
+
#
|
|
381
|
+
# https://www.gnu.org/licenses/
|
|
382
|
+
# ***************************************************************************
|
|
383
|
+
import itertools
|
|
384
|
+
from collections import namedtuple, defaultdict
|
|
385
|
+
from sage.numerical.mip import MixedIntegerLinearProgram, MIPSolverException
|
|
386
|
+
from sage.rings.integer_ring import ZZ
|
|
387
|
+
from sage.combinat.set_partition import SetPartition, SetPartitions
|
|
388
|
+
from sage.sets.disjoint_set import DisjointSet
|
|
389
|
+
from sage.structure.sage_object import SageObject
|
|
390
|
+
from copy import copy
|
|
391
|
+
from sage.misc.verbose import get_verbose
|
|
392
|
+
|
|
393
|
+
|
|
394
|
+
class Bijectionist(SageObject):
|
|
395
|
+
r"""
|
|
396
|
+
A toolbox to list all possible bijections between two finite sets
|
|
397
|
+
under various constraints.
|
|
398
|
+
|
|
399
|
+
INPUT:
|
|
400
|
+
|
|
401
|
+
- ``A``, ``B`` -- sets of equal size, given as a list
|
|
402
|
+
|
|
403
|
+
- ``tau`` -- (optional) a function from ``B`` to ``Z``, in case of
|
|
404
|
+
``None``, the identity map ``lambda x: x`` is used
|
|
405
|
+
|
|
406
|
+
- ``alpha_beta`` -- (optional) a list of pairs of statistics ``alpha`` from
|
|
407
|
+
``A`` to ``W`` and ``beta`` from ``B`` to ``W``
|
|
408
|
+
|
|
409
|
+
- ``P`` -- (optional) a partition of ``A``
|
|
410
|
+
|
|
411
|
+
- ``pi_rho`` -- (optional) a list of triples ``(k, pi, rho)``, where
|
|
412
|
+
|
|
413
|
+
* ``pi`` -- a ``k``-ary operation composing objects in ``A`` and
|
|
414
|
+
* ``rho`` -- a ``k``-ary function composing statistic values in ``Z``
|
|
415
|
+
|
|
416
|
+
- ``elements_distributions`` -- (optional) a list of pairs ``(tA, tZ)``,
|
|
417
|
+
specifying the distributions of ``tA``
|
|
418
|
+
|
|
419
|
+
- ``value_restrictions`` -- (optional) a list of pairs ``(a, tZ)``,
|
|
420
|
+
restricting the possible values of ``a``
|
|
421
|
+
|
|
422
|
+
- ``solver`` -- (optional) the backend used to solve the mixed integer
|
|
423
|
+
linear programs
|
|
424
|
+
|
|
425
|
+
``W`` and ``Z`` can be arbitrary sets. As a natural example we may think
|
|
426
|
+
of the natural numbers or tuples of integers.
|
|
427
|
+
|
|
428
|
+
We are looking for a statistic `s: A\to Z` and a bijection `S: A\to B` such
|
|
429
|
+
that
|
|
430
|
+
|
|
431
|
+
- `s = \tau \circ S`: the statistics `s` and `\tau` are equidistributed and
|
|
432
|
+
`S` is an intertwining bijection.
|
|
433
|
+
|
|
434
|
+
- `\alpha = \beta \circ S`: the statistics `\alpha` and `\beta` are
|
|
435
|
+
equidistributed and `S` is an intertwining bijection.
|
|
436
|
+
|
|
437
|
+
- `s` is constant on the blocks of `P`.
|
|
438
|
+
|
|
439
|
+
- `s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))`.
|
|
440
|
+
|
|
441
|
+
Additionally, we may require that
|
|
442
|
+
|
|
443
|
+
- `s(a)\in Z_a` for specified sets `Z_a\subseteq Z`, and
|
|
444
|
+
|
|
445
|
+
- `s|_{\tilde A}` has a specified distribution for specified sets `\tilde A
|
|
446
|
+
\subset A`.
|
|
447
|
+
|
|
448
|
+
If `\tau` is the identity, the two unknown functions `s` and `S` coincide.
|
|
449
|
+
Although we do not exclude other bijective choices for `\tau`, they
|
|
450
|
+
probably do not make sense.
|
|
451
|
+
|
|
452
|
+
If we want that `S` is graded, i.e. if elements of `A` and `B` have a
|
|
453
|
+
notion of size and `S` should preserve this size, we can add grading
|
|
454
|
+
statistics as `\alpha` and `\beta`. Since `\alpha` and `\beta` will be
|
|
455
|
+
equidistributed with `S` as an intertwining bijection, `S` will then also
|
|
456
|
+
be graded.
|
|
457
|
+
|
|
458
|
+
In summary, we have the following two commutative diagrams, where `s` and
|
|
459
|
+
`S` are unknown functions.
|
|
460
|
+
|
|
461
|
+
.. MATH::
|
|
462
|
+
|
|
463
|
+
\begin{array}{rrl}
|
|
464
|
+
& A \\
|
|
465
|
+
{\scriptstyle\alpha}\swarrow & {\scriptstyle S}\downarrow & \searrow{\scriptstyle s}\\
|
|
466
|
+
W \overset{\beta}{\leftarrow} & B & \overset{\tau}{\rightarrow} Z
|
|
467
|
+
\end{array}
|
|
468
|
+
\qquad
|
|
469
|
+
\begin{array}{lcl}
|
|
470
|
+
A^k &\overset{\pi}{\rightarrow} & A\\
|
|
471
|
+
\downarrow{\scriptstyle s^k} & & \downarrow{\scriptstyle s}\\
|
|
472
|
+
Z^k &\overset{\rho}{\rightarrow} & Z\\
|
|
473
|
+
\end{array}
|
|
474
|
+
|
|
475
|
+
.. NOTE::
|
|
476
|
+
|
|
477
|
+
If `\tau` is the identity map, the partition `P` of `A` necessarily
|
|
478
|
+
consists only of singletons.
|
|
479
|
+
|
|
480
|
+
.. NOTE::
|
|
481
|
+
|
|
482
|
+
The order of invocation of the methods with prefix ``set``, i.e.,
|
|
483
|
+
:meth:`set_statistics`, :meth:`set_intertwining_relations`,
|
|
484
|
+
:meth:`set_constant_blocks`, etc., is irrelevant. Calling any of these
|
|
485
|
+
methods a second time overrides the previous specification.
|
|
486
|
+
"""
|
|
487
|
+
def __init__(self, A, B, tau=None, alpha_beta=tuple(), P=None,
|
|
488
|
+
pi_rho=tuple(), phi_psi=tuple(), Q=None,
|
|
489
|
+
elements_distributions=tuple(),
|
|
490
|
+
value_restrictions=tuple(), solver=None, key=None):
|
|
491
|
+
"""
|
|
492
|
+
Initialize the bijectionist.
|
|
493
|
+
|
|
494
|
+
TESTS:
|
|
495
|
+
|
|
496
|
+
Check that large input sets are handled well::
|
|
497
|
+
|
|
498
|
+
sage: A = B = range(20000)
|
|
499
|
+
sage: bij = Bijectionist(A, B)
|
|
500
|
+
"""
|
|
501
|
+
# glossary of standard letters:
|
|
502
|
+
# A, B, Z, W ... finite sets
|
|
503
|
+
# P ... set partition of A
|
|
504
|
+
# tA, tB, tZ, tP ... subsets
|
|
505
|
+
# a in A, b in B, p in P
|
|
506
|
+
# S: A -> B
|
|
507
|
+
# alpha: A -> W, beta: B -> W
|
|
508
|
+
# s: A -> Z, tau: B -> Z
|
|
509
|
+
# k arity of pi and rho
|
|
510
|
+
# pi: A^k -> A, rho: Z^k -> Z
|
|
511
|
+
# a_tuple in A^k
|
|
512
|
+
self._A = list(A)
|
|
513
|
+
self._B = list(B)
|
|
514
|
+
assert len(self._A) == len(set(self._A)), "A must have distinct items"
|
|
515
|
+
assert len(self._B) == len(set(self._B)), "B must have distinct items"
|
|
516
|
+
self._bmilp = None
|
|
517
|
+
self._sorter = {}
|
|
518
|
+
self._sorter["A"] = lambda x: sorted(x, key=self._A.index)
|
|
519
|
+
self._sorter["B"] = lambda x: sorted(x, key=self._B.index)
|
|
520
|
+
|
|
521
|
+
if tau is None:
|
|
522
|
+
self._tau = {b: b for b in self._B}
|
|
523
|
+
else:
|
|
524
|
+
self._tau = {b: tau(b) for b in self._B}
|
|
525
|
+
# we store Z as a list to keep an order
|
|
526
|
+
self._Z = set(self._tau.values())
|
|
527
|
+
if key is not None and "Z" in key:
|
|
528
|
+
self._sorter["Z"] = lambda x: sorted(x, key=key["Z"])
|
|
529
|
+
self._Z = self._sorter["Z"](self._Z)
|
|
530
|
+
else:
|
|
531
|
+
try:
|
|
532
|
+
self._Z = sorted(self._Z)
|
|
533
|
+
self._sorter["Z"] = lambda x: sorted(x)
|
|
534
|
+
except TypeError:
|
|
535
|
+
self._Z = list(self._Z)
|
|
536
|
+
self._sorter["Z"] = lambda x: list(x)
|
|
537
|
+
if P is None:
|
|
538
|
+
P = []
|
|
539
|
+
|
|
540
|
+
# set optional inputs
|
|
541
|
+
self.set_statistics(*alpha_beta)
|
|
542
|
+
self.set_value_restrictions(*value_restrictions)
|
|
543
|
+
self.set_distributions(*elements_distributions)
|
|
544
|
+
self.set_quadratic_relation(*phi_psi)
|
|
545
|
+
self.set_homomesic(Q)
|
|
546
|
+
self.set_intertwining_relations(*pi_rho)
|
|
547
|
+
self.set_constant_blocks(P)
|
|
548
|
+
|
|
549
|
+
self._solver = solver
|
|
550
|
+
|
|
551
|
+
def set_constant_blocks(self, P):
|
|
552
|
+
r"""
|
|
553
|
+
Declare that `s: A\to Z` is constant on each block of `P`.
|
|
554
|
+
|
|
555
|
+
.. WARNING::
|
|
556
|
+
|
|
557
|
+
Any restriction imposed by a previous invocation of
|
|
558
|
+
:meth:`set_constant_blocks` will be overwritten,
|
|
559
|
+
including restrictions discovered by
|
|
560
|
+
:meth:`set_intertwining_relations` and
|
|
561
|
+
:meth:`solutions_iterator`!
|
|
562
|
+
|
|
563
|
+
A common example is to use the orbits of a bijection acting
|
|
564
|
+
on `A`. This can be achieved using the function
|
|
565
|
+
:meth:`~sage.combinat.cyclic_sieving_phenomenon.orbit_decomposition`.
|
|
566
|
+
|
|
567
|
+
INPUT:
|
|
568
|
+
|
|
569
|
+
- ``P`` -- set partition of `A`, singletons may be omitted
|
|
570
|
+
|
|
571
|
+
EXAMPLES:
|
|
572
|
+
|
|
573
|
+
Initially the partitions are set to singleton blocks. The
|
|
574
|
+
current partition can be reviewed using
|
|
575
|
+
:meth:`constant_blocks`::
|
|
576
|
+
|
|
577
|
+
sage: A = B = 'abcd'
|
|
578
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
|
|
579
|
+
sage: bij.constant_blocks()
|
|
580
|
+
{}
|
|
581
|
+
|
|
582
|
+
sage: bij.set_constant_blocks([['a', 'c']])
|
|
583
|
+
sage: bij.constant_blocks()
|
|
584
|
+
{{'a', 'c'}}
|
|
585
|
+
|
|
586
|
+
We now add a map that combines some blocks::
|
|
587
|
+
|
|
588
|
+
sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
|
|
589
|
+
sage: def rho(s1, s2): return (s1 + s2) % 2
|
|
590
|
+
sage: bij.set_intertwining_relations((2, pi, rho))
|
|
591
|
+
sage: list(bij.solutions_iterator())
|
|
592
|
+
[{'a': 0, 'b': 1, 'c': 0, 'd': 1}]
|
|
593
|
+
sage: bij.constant_blocks()
|
|
594
|
+
{{'a', 'c'}, {'b', 'd'}}
|
|
595
|
+
|
|
596
|
+
Setting constant blocks overrides any previous assignment::
|
|
597
|
+
|
|
598
|
+
sage: bij.set_constant_blocks([['a', 'b']])
|
|
599
|
+
sage: bij.constant_blocks()
|
|
600
|
+
{{'a', 'b'}}
|
|
601
|
+
|
|
602
|
+
If there is no solution, and the coarsest partition is
|
|
603
|
+
requested, an error is raised::
|
|
604
|
+
|
|
605
|
+
sage: bij.constant_blocks(optimal=True)
|
|
606
|
+
Traceback (most recent call last):
|
|
607
|
+
...
|
|
608
|
+
StopIteration
|
|
609
|
+
"""
|
|
610
|
+
self._bmilp = None
|
|
611
|
+
self._P = DisjointSet(self._A)
|
|
612
|
+
P = sorted(self._sorter["A"](p) for p in P)
|
|
613
|
+
for p in P:
|
|
614
|
+
for a in p:
|
|
615
|
+
self._P.union(p[0], a)
|
|
616
|
+
|
|
617
|
+
self._possible_block_values = None
|
|
618
|
+
|
|
619
|
+
def constant_blocks(self, singletons=False, optimal=False):
|
|
620
|
+
r"""
|
|
621
|
+
Return the set partition `P` of `A` such that `s: A\to Z` is
|
|
622
|
+
known to be constant on the blocks of `P`.
|
|
623
|
+
|
|
624
|
+
INPUT:
|
|
625
|
+
|
|
626
|
+
- ``singletons`` -- boolean (default: ``False``); whether or not to
|
|
627
|
+
include singleton blocks in the output
|
|
628
|
+
|
|
629
|
+
- ``optimal`` -- boolean (default: ``False``); whether or not to
|
|
630
|
+
compute the coarsest possible partition
|
|
631
|
+
|
|
632
|
+
.. NOTE::
|
|
633
|
+
|
|
634
|
+
computing the coarsest possible partition may be
|
|
635
|
+
computationally expensive, but may speed up generating
|
|
636
|
+
solutions.
|
|
637
|
+
|
|
638
|
+
EXAMPLES::
|
|
639
|
+
|
|
640
|
+
sage: A = B = ["a", "b", "c"]
|
|
641
|
+
sage: bij = Bijectionist(A, B, lambda x: 0)
|
|
642
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
643
|
+
sage: bij.constant_blocks()
|
|
644
|
+
{{'a', 'b'}}
|
|
645
|
+
|
|
646
|
+
sage: bij.constant_blocks(singletons=True)
|
|
647
|
+
{{'a', 'b'}, {'c'}}
|
|
648
|
+
"""
|
|
649
|
+
if optimal:
|
|
650
|
+
self._forced_constant_blocks()
|
|
651
|
+
if singletons:
|
|
652
|
+
return SetPartition(self._P)
|
|
653
|
+
return SetPartition(p for p in self._P if len(p) > 1)
|
|
654
|
+
|
|
655
|
+
def set_statistics(self, *alpha_beta):
|
|
656
|
+
r"""
|
|
657
|
+
Set constraints of the form `\alpha = \beta\circ S`.
|
|
658
|
+
|
|
659
|
+
.. WARNING::
|
|
660
|
+
|
|
661
|
+
Any restriction imposed by a previous invocation of
|
|
662
|
+
:meth:`set_statistics` will be overwritten!
|
|
663
|
+
|
|
664
|
+
INPUT:
|
|
665
|
+
|
|
666
|
+
- ``alpha_beta`` -- one or more pairs `(\alpha: A\to W,
|
|
667
|
+
\beta: B\to W)`
|
|
668
|
+
|
|
669
|
+
If the statistics `\alpha` and `\beta` are not
|
|
670
|
+
equidistributed, an error is raised.
|
|
671
|
+
|
|
672
|
+
ALGORITHM:
|
|
673
|
+
|
|
674
|
+
We add
|
|
675
|
+
|
|
676
|
+
.. MATH::
|
|
677
|
+
|
|
678
|
+
\sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)}
|
|
679
|
+
= \sum_{b\in B} s^{\tau(b)} t(\beta(b))
|
|
680
|
+
|
|
681
|
+
as a matrix equation to the MILP.
|
|
682
|
+
|
|
683
|
+
EXAMPLES:
|
|
684
|
+
|
|
685
|
+
We look for bijections `S` on permutations such that the
|
|
686
|
+
number of weak exceedences of `S(\pi)` equals the number of
|
|
687
|
+
descents of `\pi`, and statistics `s`, such that the number
|
|
688
|
+
of fixed points of `S(\pi)` equals `s(\pi)`::
|
|
689
|
+
|
|
690
|
+
sage: N = 4; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
|
|
691
|
+
sage: def wex(p): return len(p.weak_excedences())
|
|
692
|
+
sage: def fix(p): return len(p.fixed_points())
|
|
693
|
+
sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
|
|
694
|
+
sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
|
|
695
|
+
sage: bij = Bijectionist(A, B, fix)
|
|
696
|
+
sage: bij.set_statistics((wex, des), (len, len))
|
|
697
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
698
|
+
....: print(solution)
|
|
699
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
700
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
701
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
702
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
703
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
704
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
705
|
+
|
|
706
|
+
sage: bij = Bijectionist(A, B, fix)
|
|
707
|
+
sage: bij.set_statistics((wex, des), (fix, adj), (len, len))
|
|
708
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
709
|
+
....: print(solution)
|
|
710
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
711
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 1}
|
|
712
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 3, [3, 2, 1]: 0}
|
|
713
|
+
|
|
714
|
+
Calling this with non-equidistributed statistics yields an error::
|
|
715
|
+
|
|
716
|
+
sage: bij = Bijectionist(A, B, fix)
|
|
717
|
+
sage: bij.set_statistics((wex, fix))
|
|
718
|
+
Traceback (most recent call last):
|
|
719
|
+
...
|
|
720
|
+
ValueError: statistics alpha and beta are not equidistributed
|
|
721
|
+
|
|
722
|
+
TESTS:
|
|
723
|
+
|
|
724
|
+
Calling ``set_statistics`` without arguments should restore the previous state::
|
|
725
|
+
|
|
726
|
+
sage: N = 3; A = B = [permutation for n in range(N) for permutation in Permutations(n)]
|
|
727
|
+
sage: def wex(p): return len(p.weak_excedences())
|
|
728
|
+
sage: def fix(p): return len(p.fixed_points())
|
|
729
|
+
sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
|
|
730
|
+
sage: bij = Bijectionist(A, B, fix)
|
|
731
|
+
sage: bij.set_statistics((wex, des), (len, len))
|
|
732
|
+
sage: for solution in bij.solutions_iterator():
|
|
733
|
+
....: print(solution)
|
|
734
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2}
|
|
735
|
+
sage: bij.set_statistics()
|
|
736
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
737
|
+
....: print(solution)
|
|
738
|
+
{[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 2}
|
|
739
|
+
{[]: 0, [1]: 0, [1, 2]: 2, [2, 1]: 1}
|
|
740
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2}
|
|
741
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0}
|
|
742
|
+
{[]: 0, [1]: 2, [1, 2]: 0, [2, 1]: 1}
|
|
743
|
+
{[]: 0, [1]: 2, [1, 2]: 1, [2, 1]: 0}
|
|
744
|
+
{[]: 1, [1]: 0, [1, 2]: 0, [2, 1]: 2}
|
|
745
|
+
{[]: 1, [1]: 0, [1, 2]: 2, [2, 1]: 0}
|
|
746
|
+
{[]: 1, [1]: 2, [1, 2]: 0, [2, 1]: 0}
|
|
747
|
+
{[]: 2, [1]: 0, [1, 2]: 0, [2, 1]: 1}
|
|
748
|
+
{[]: 2, [1]: 0, [1, 2]: 1, [2, 1]: 0}
|
|
749
|
+
{[]: 2, [1]: 1, [1, 2]: 0, [2, 1]: 0}
|
|
750
|
+
"""
|
|
751
|
+
self._bmilp = None
|
|
752
|
+
self._n_statistics = len(alpha_beta)
|
|
753
|
+
# TODO: do we really want to recompute statistics every time?
|
|
754
|
+
self._alpha = lambda p: tuple(arg[0](p) for arg in alpha_beta)
|
|
755
|
+
self._beta = lambda p: tuple(arg[1](p) for arg in alpha_beta)
|
|
756
|
+
|
|
757
|
+
# generate fibers
|
|
758
|
+
self._statistics_fibers = {}
|
|
759
|
+
for a in self._A:
|
|
760
|
+
v = self._alpha(a)
|
|
761
|
+
if v not in self._statistics_fibers:
|
|
762
|
+
self._statistics_fibers[v] = ([], [])
|
|
763
|
+
self._statistics_fibers[v][0].append(a)
|
|
764
|
+
|
|
765
|
+
for b in self._B:
|
|
766
|
+
v = self._beta(b)
|
|
767
|
+
if v not in self._statistics_fibers:
|
|
768
|
+
raise ValueError(f"statistics alpha and beta do not have the same image, {v} is not a value of alpha, but of beta")
|
|
769
|
+
self._statistics_fibers[v][1].append(b)
|
|
770
|
+
|
|
771
|
+
# check compatibility
|
|
772
|
+
if not all(len(fiber[0]) == len(fiber[1])
|
|
773
|
+
for fiber in self._statistics_fibers.values()):
|
|
774
|
+
raise ValueError("statistics alpha and beta are not equidistributed")
|
|
775
|
+
|
|
776
|
+
self._W = list(self._statistics_fibers)
|
|
777
|
+
|
|
778
|
+
# the possible values of s(a) are tau(beta^{-1}(alpha(a)))
|
|
779
|
+
tau_beta_inverse = {}
|
|
780
|
+
self._statistics_possible_values = {}
|
|
781
|
+
for a in self._A:
|
|
782
|
+
v = self._alpha(a)
|
|
783
|
+
if v not in tau_beta_inverse:
|
|
784
|
+
tau_beta_inverse[v] = set(self._tau[b]
|
|
785
|
+
for b in self._statistics_fibers[v][1])
|
|
786
|
+
self._statistics_possible_values[a] = tau_beta_inverse[v]
|
|
787
|
+
|
|
788
|
+
def statistics_fibers(self):
|
|
789
|
+
r"""
|
|
790
|
+
Return a dictionary mapping statistic values in `W` to their
|
|
791
|
+
preimages in `A` and `B`.
|
|
792
|
+
|
|
793
|
+
This is a (computationally) fast way to obtain a first
|
|
794
|
+
impression which objects in `A` should be mapped to which
|
|
795
|
+
objects in `B`.
|
|
796
|
+
|
|
797
|
+
EXAMPLES::
|
|
798
|
+
|
|
799
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
800
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
801
|
+
sage: def wex(p): return len(p.weak_excedences())
|
|
802
|
+
sage: def fix(p): return len(p.fixed_points())
|
|
803
|
+
sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
|
|
804
|
+
sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
|
|
805
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
806
|
+
sage: bij.set_statistics((len, len), (wex, des), (fix, adj))
|
|
807
|
+
sage: table([[key, AB[0], AB[1]] for key, AB in bij.statistics_fibers().items()])
|
|
808
|
+
(0, 0, 0) [[]] [[]]
|
|
809
|
+
(1, 1, 1) [[1]] [[1]]
|
|
810
|
+
(2, 2, 2) [[1, 2]] [[2, 1]]
|
|
811
|
+
(2, 1, 0) [[2, 1]] [[1, 2]]
|
|
812
|
+
(3, 3, 3) [[1, 2, 3]] [[3, 2, 1]]
|
|
813
|
+
(3, 2, 1) [[1, 3, 2], [2, 1, 3], [3, 2, 1]] [[1, 3, 2], [2, 1, 3], [2, 3, 1]]
|
|
814
|
+
(3, 2, 0) [[2, 3, 1]] [[3, 1, 2]]
|
|
815
|
+
(3, 1, 0) [[3, 1, 2]] [[1, 2, 3]]
|
|
816
|
+
"""
|
|
817
|
+
return self._statistics_fibers
|
|
818
|
+
|
|
819
|
+
def statistics_table(self, header=True):
|
|
820
|
+
r"""
|
|
821
|
+
Provide information about all elements of `A` with corresponding
|
|
822
|
+
`\alpha` values and all elements of `B` with corresponding
|
|
823
|
+
`\beta` and `\tau` values.
|
|
824
|
+
|
|
825
|
+
INPUT:
|
|
826
|
+
|
|
827
|
+
- ``header`` -- boolean (default: ``True``); whether to include a
|
|
828
|
+
header with the standard Greek letters
|
|
829
|
+
|
|
830
|
+
OUTPUT:
|
|
831
|
+
|
|
832
|
+
A pair of lists suitable for :class:`~sage.misc.table.table`,
|
|
833
|
+
where
|
|
834
|
+
|
|
835
|
+
- the first contains the elements of `A` together with the
|
|
836
|
+
values of `\alpha`
|
|
837
|
+
|
|
838
|
+
- the second contains the elements of `B` together with the
|
|
839
|
+
values of `\tau` and `\beta`
|
|
840
|
+
|
|
841
|
+
EXAMPLES::
|
|
842
|
+
|
|
843
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
844
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
845
|
+
sage: def wex(p): return len(p.weak_excedences())
|
|
846
|
+
sage: def fix(p): return len(p.fixed_points())
|
|
847
|
+
sage: def des(p): return len(p.descents(final_descent=True)) if p else 0
|
|
848
|
+
sage: def adj(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
|
|
849
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
850
|
+
sage: bij.set_statistics((wex, des), (fix, adj))
|
|
851
|
+
sage: a, b = bij.statistics_table()
|
|
852
|
+
sage: table(a, header_row=True, frame=True)
|
|
853
|
+
┌───────────┬────────┬────────┐
|
|
854
|
+
│ a │ α_1(a) │ α_2(a) │
|
|
855
|
+
╞═══════════╪════════╪════════╡
|
|
856
|
+
│ [] │ 0 │ 0 │
|
|
857
|
+
├───────────┼────────┼────────┤
|
|
858
|
+
│ [1] │ 1 │ 1 │
|
|
859
|
+
├───────────┼────────┼────────┤
|
|
860
|
+
│ [1, 2] │ 2 │ 2 │
|
|
861
|
+
├───────────┼────────┼────────┤
|
|
862
|
+
│ [2, 1] │ 1 │ 0 │
|
|
863
|
+
├───────────┼────────┼────────┤
|
|
864
|
+
│ [1, 2, 3] │ 3 │ 3 │
|
|
865
|
+
├───────────┼────────┼────────┤
|
|
866
|
+
│ [1, 3, 2] │ 2 │ 1 │
|
|
867
|
+
├───────────┼────────┼────────┤
|
|
868
|
+
│ [2, 1, 3] │ 2 │ 1 │
|
|
869
|
+
├───────────┼────────┼────────┤
|
|
870
|
+
│ [2, 3, 1] │ 2 │ 0 │
|
|
871
|
+
├───────────┼────────┼────────┤
|
|
872
|
+
│ [3, 1, 2] │ 1 │ 0 │
|
|
873
|
+
├───────────┼────────┼────────┤
|
|
874
|
+
│ [3, 2, 1] │ 2 │ 1 │
|
|
875
|
+
└───────────┴────────┴────────┘
|
|
876
|
+
sage: table(b, header_row=True, frame=True)
|
|
877
|
+
┌───────────┬───┬────────┬────────┐
|
|
878
|
+
│ b │ τ │ β_1(b) │ β_2(b) │
|
|
879
|
+
╞═══════════╪═══╪════════╪════════╡
|
|
880
|
+
│ [] │ 0 │ 0 │ 0 │
|
|
881
|
+
├───────────┼───┼────────┼────────┤
|
|
882
|
+
│ [1] │ 1 │ 1 │ 1 │
|
|
883
|
+
├───────────┼───┼────────┼────────┤
|
|
884
|
+
│ [1, 2] │ 2 │ 1 │ 0 │
|
|
885
|
+
├───────────┼───┼────────┼────────┤
|
|
886
|
+
│ [2, 1] │ 1 │ 2 │ 2 │
|
|
887
|
+
├───────────┼───┼────────┼────────┤
|
|
888
|
+
│ [1, 2, 3] │ 3 │ 1 │ 0 │
|
|
889
|
+
├───────────┼───┼────────┼────────┤
|
|
890
|
+
│ [1, 3, 2] │ 2 │ 2 │ 1 │
|
|
891
|
+
├───────────┼───┼────────┼────────┤
|
|
892
|
+
│ [2, 1, 3] │ 2 │ 2 │ 1 │
|
|
893
|
+
├───────────┼───┼────────┼────────┤
|
|
894
|
+
│ [2, 3, 1] │ 2 │ 2 │ 1 │
|
|
895
|
+
├───────────┼───┼────────┼────────┤
|
|
896
|
+
│ [3, 1, 2] │ 2 │ 2 │ 0 │
|
|
897
|
+
├───────────┼───┼────────┼────────┤
|
|
898
|
+
│ [3, 2, 1] │ 1 │ 3 │ 3 │
|
|
899
|
+
└───────────┴───┴────────┴────────┘
|
|
900
|
+
|
|
901
|
+
TESTS:
|
|
902
|
+
|
|
903
|
+
If no statistics are given, the table should still be able to be generated::
|
|
904
|
+
|
|
905
|
+
sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
|
|
906
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
907
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
908
|
+
sage: a, b = bij.statistics_table()
|
|
909
|
+
sage: table(a, header_row=True, frame=True)
|
|
910
|
+
┌────────┐
|
|
911
|
+
│ a │
|
|
912
|
+
╞════════╡
|
|
913
|
+
│ [] │
|
|
914
|
+
├────────┤
|
|
915
|
+
│ [1] │
|
|
916
|
+
├────────┤
|
|
917
|
+
│ [1, 2] │
|
|
918
|
+
├────────┤
|
|
919
|
+
│ [2, 1] │
|
|
920
|
+
└────────┘
|
|
921
|
+
sage: table(b, header_row=True, frame=True)
|
|
922
|
+
┌────────┬───┐
|
|
923
|
+
│ b │ τ │
|
|
924
|
+
╞════════╪═══╡
|
|
925
|
+
│ [] │ 0 │
|
|
926
|
+
├────────┼───┤
|
|
927
|
+
│ [1] │ 1 │
|
|
928
|
+
├────────┼───┤
|
|
929
|
+
│ [1, 2] │ 2 │
|
|
930
|
+
├────────┼───┤
|
|
931
|
+
│ [2, 1] │ 1 │
|
|
932
|
+
└────────┴───┘
|
|
933
|
+
|
|
934
|
+
We can omit the header::
|
|
935
|
+
|
|
936
|
+
sage: bij.statistics_table(header=True)[1]
|
|
937
|
+
[['b', 'τ'], [[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]]
|
|
938
|
+
sage: bij.statistics_table(header=False)[1]
|
|
939
|
+
[[[], 0], [[1], 1], [[1, 2], 2], [[2, 1], 1]]
|
|
940
|
+
"""
|
|
941
|
+
# table for alpha
|
|
942
|
+
n_statistics = self._n_statistics
|
|
943
|
+
if header:
|
|
944
|
+
output_alphas = [["a"] + ["\u03b1_" + str(i) + "(a)"
|
|
945
|
+
for i in range(1, n_statistics + 1)]]
|
|
946
|
+
else:
|
|
947
|
+
output_alphas = []
|
|
948
|
+
|
|
949
|
+
for a in self._A:
|
|
950
|
+
if n_statistics > 0:
|
|
951
|
+
output_alphas.append([a] + list(self._alpha(a)))
|
|
952
|
+
else:
|
|
953
|
+
output_alphas.append([a])
|
|
954
|
+
|
|
955
|
+
# table for beta and tau
|
|
956
|
+
if header:
|
|
957
|
+
output_tau_betas = [["b", "\u03c4"] + ["\u03b2_" + str(i) + "(b)"
|
|
958
|
+
for i in range(1, n_statistics + 1)]]
|
|
959
|
+
else:
|
|
960
|
+
output_tau_betas = []
|
|
961
|
+
for b in self._B:
|
|
962
|
+
if n_statistics > 0:
|
|
963
|
+
output_tau_betas.append([b, self._tau[b]] + list(self._beta(b)))
|
|
964
|
+
else:
|
|
965
|
+
output_tau_betas.append([b, self._tau[b]])
|
|
966
|
+
|
|
967
|
+
return output_alphas, output_tau_betas
|
|
968
|
+
|
|
969
|
+
def set_value_restrictions(self, *value_restrictions):
|
|
970
|
+
r"""
|
|
971
|
+
Restrict the set of possible values `s(a)` for a given element
|
|
972
|
+
`a`.
|
|
973
|
+
|
|
974
|
+
.. WARNING::
|
|
975
|
+
|
|
976
|
+
Any restriction imposed by a previous invocation of
|
|
977
|
+
:meth:`set_value_restrictions` will be overwritten!
|
|
978
|
+
|
|
979
|
+
INPUT:
|
|
980
|
+
|
|
981
|
+
- ``value_restrictions`` -- one or more pairs `(a\in A, \tilde
|
|
982
|
+
Z\subseteq Z)`
|
|
983
|
+
|
|
984
|
+
EXAMPLES:
|
|
985
|
+
|
|
986
|
+
We may want to restrict the value of a given element to a
|
|
987
|
+
single or multiple values. We do not require that the
|
|
988
|
+
specified values are in the image of `\tau`. In some
|
|
989
|
+
cases, the restriction may not be able to provide a better
|
|
990
|
+
solution, as for size 3 in the following example. ::
|
|
991
|
+
|
|
992
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
993
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
994
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
995
|
+
sage: bij.set_statistics((len, len))
|
|
996
|
+
sage: bij.set_value_restrictions((Permutation([1, 2]), [1]),
|
|
997
|
+
....: (Permutation([3, 2, 1]), [2, 3, 4]))
|
|
998
|
+
sage: for sol in sorted(bij.solutions_iterator(), key=lambda d: sorted(d.items())):
|
|
999
|
+
....: print(sol)
|
|
1000
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3}
|
|
1001
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2}
|
|
1002
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1003
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1004
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1005
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3}
|
|
1006
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2}
|
|
1007
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1008
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 1, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1009
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 3}
|
|
1010
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 3, [3, 2, 1]: 2}
|
|
1011
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 3, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1012
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 3}
|
|
1013
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 3, [3, 2, 1]: 2}
|
|
1014
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 3}
|
|
1015
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 3, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1016
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1017
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 2, [2, 1, 3]: 3, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1018
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1019
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1020
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1021
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1022
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1023
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1024
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1025
|
+
|
|
1026
|
+
However, an error occurs if the set of possible values is
|
|
1027
|
+
empty. In this example, the image of `\tau` under any
|
|
1028
|
+
legal bijection is disjoint to the specified values.
|
|
1029
|
+
|
|
1030
|
+
TESTS::
|
|
1031
|
+
|
|
1032
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1033
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1034
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1035
|
+
sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
|
|
1036
|
+
sage: bij._compute_possible_block_values()
|
|
1037
|
+
Traceback (most recent call last):
|
|
1038
|
+
...
|
|
1039
|
+
ValueError: no possible values found for singleton block [[1, 2]]
|
|
1040
|
+
|
|
1041
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1042
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1043
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1044
|
+
sage: bij.set_constant_blocks([[permutation for permutation in Permutations(n)] for n in range(4)])
|
|
1045
|
+
sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
|
|
1046
|
+
sage: bij._compute_possible_block_values()
|
|
1047
|
+
Traceback (most recent call last):
|
|
1048
|
+
...
|
|
1049
|
+
ValueError: no possible values found for block [[1, 2], [2, 1]]
|
|
1050
|
+
|
|
1051
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1052
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1053
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1054
|
+
sage: bij.set_value_restrictions(((1, 2), [4, 5, 6]))
|
|
1055
|
+
Traceback (most recent call last):
|
|
1056
|
+
...
|
|
1057
|
+
AssertionError: element (1, 2) was not found in A
|
|
1058
|
+
"""
|
|
1059
|
+
# it might be much cheaper to construct the sets as subsets
|
|
1060
|
+
# of _statistics_possible_values - however, we do not want to
|
|
1061
|
+
# insist that set_value_restrictions is called after
|
|
1062
|
+
# set_statistics
|
|
1063
|
+
self._bmilp = None
|
|
1064
|
+
set_Z = set(self._Z)
|
|
1065
|
+
self._restrictions_possible_values = {a: set_Z for a in self._A}
|
|
1066
|
+
for a, values in value_restrictions:
|
|
1067
|
+
assert a in self._A, f"element {a} was not found in A"
|
|
1068
|
+
self._restrictions_possible_values[a] = self._restrictions_possible_values[a].intersection(values)
|
|
1069
|
+
|
|
1070
|
+
def _compute_possible_block_values(self):
|
|
1071
|
+
r"""
|
|
1072
|
+
Update the dictionary of possible values of each block.
|
|
1073
|
+
|
|
1074
|
+
This has to be called whenever ``self._P`` was modified.
|
|
1075
|
+
|
|
1076
|
+
It raises a :exc:`ValueError`, if the restrictions on a
|
|
1077
|
+
block are contradictory.
|
|
1078
|
+
|
|
1079
|
+
TESTS::
|
|
1080
|
+
|
|
1081
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1082
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1083
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1084
|
+
sage: bij.set_value_restrictions((Permutation([1, 2]), [4, 5]))
|
|
1085
|
+
sage: bij._compute_possible_block_values()
|
|
1086
|
+
Traceback (most recent call last):
|
|
1087
|
+
...
|
|
1088
|
+
ValueError: no possible values found for singleton block [[1, 2]]
|
|
1089
|
+
"""
|
|
1090
|
+
self._possible_block_values = {} # P -> Power(Z)
|
|
1091
|
+
for p, block in self._P.root_to_elements_dict().items():
|
|
1092
|
+
sets = ([self._restrictions_possible_values[a] for a in block]
|
|
1093
|
+
+ [self._statistics_possible_values[a] for a in block])
|
|
1094
|
+
self._possible_block_values[p] = _non_copying_intersection(sets)
|
|
1095
|
+
if not self._possible_block_values[p]:
|
|
1096
|
+
if len(block) == 1:
|
|
1097
|
+
raise ValueError(f"no possible values found for singleton block {block}")
|
|
1098
|
+
raise ValueError(f"no possible values found for block {block}")
|
|
1099
|
+
|
|
1100
|
+
def set_distributions(self, *elements_distributions):
|
|
1101
|
+
r"""
|
|
1102
|
+
Specify the distribution of `s` for a subset of elements.
|
|
1103
|
+
|
|
1104
|
+
.. WARNING::
|
|
1105
|
+
|
|
1106
|
+
Any restriction imposed by a previous invocation of
|
|
1107
|
+
:meth:`set_distributions` will be overwritten!
|
|
1108
|
+
|
|
1109
|
+
INPUT:
|
|
1110
|
+
|
|
1111
|
+
- one or more pairs of `(\tilde A, \tilde Z)`, where `\tilde
|
|
1112
|
+
A\subseteq A` and `\tilde Z` is a list of values in `Z` of
|
|
1113
|
+
the same size as `\tilde A`
|
|
1114
|
+
|
|
1115
|
+
This method specifies that `\{s(a) | a\in\tilde A\}` equals
|
|
1116
|
+
`\tilde Z` as a multiset for each of the pairs.
|
|
1117
|
+
|
|
1118
|
+
When specifying several distributions, the subsets of `A` do
|
|
1119
|
+
not have to be disjoint.
|
|
1120
|
+
|
|
1121
|
+
ALGORITHM:
|
|
1122
|
+
|
|
1123
|
+
We add
|
|
1124
|
+
|
|
1125
|
+
.. MATH::
|
|
1126
|
+
|
|
1127
|
+
\sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z,
|
|
1128
|
+
|
|
1129
|
+
where `p(a)` is the block containing `a`, for each given
|
|
1130
|
+
distribution as a vector equation to the MILP.
|
|
1131
|
+
|
|
1132
|
+
EXAMPLES::
|
|
1133
|
+
|
|
1134
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1135
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1136
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1137
|
+
sage: bij.set_statistics((len, len))
|
|
1138
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
|
|
1139
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1140
|
+
....: print(sol)
|
|
1141
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1142
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1143
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 1, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1144
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1145
|
+
|
|
1146
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1147
|
+
{{[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}}
|
|
1148
|
+
sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0]))
|
|
1149
|
+
[([[]], [0]),
|
|
1150
|
+
([[1]], [1]),
|
|
1151
|
+
([[2, 1, 3]], [2]),
|
|
1152
|
+
([[1, 2], [2, 1]], [1, 2]),
|
|
1153
|
+
([[1, 2, 3], [1, 3, 2]], [1, 3])]
|
|
1154
|
+
|
|
1155
|
+
We may also specify multiple, possibly overlapping distributions::
|
|
1156
|
+
|
|
1157
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]),
|
|
1158
|
+
....: ([Permutation([1, 3, 2]), Permutation([3, 2, 1]),
|
|
1159
|
+
....: Permutation([2, 1, 3])], [1, 2, 2]))
|
|
1160
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1161
|
+
....: print(sol)
|
|
1162
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1163
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1164
|
+
|
|
1165
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1166
|
+
{{[1], [1, 3, 2]}, {[2, 1, 3], [2, 3, 1], [3, 1, 2], [3, 2, 1]}}
|
|
1167
|
+
sage: sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0]))
|
|
1168
|
+
[([[]], [0]),
|
|
1169
|
+
([[1]], [1]),
|
|
1170
|
+
([[1, 2, 3]], [3]),
|
|
1171
|
+
([[2, 3, 1]], [2]),
|
|
1172
|
+
([[1, 2], [2, 1]], [1, 2])]
|
|
1173
|
+
|
|
1174
|
+
TESTS:
|
|
1175
|
+
|
|
1176
|
+
Because of the current implementation of the output calculation, we do
|
|
1177
|
+
not improve our solution if we do not gain any unique solutions::
|
|
1178
|
+
|
|
1179
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1180
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1181
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1182
|
+
sage: bij.set_statistics((len, len))
|
|
1183
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [2, 3]))
|
|
1184
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1185
|
+
....: print(sol)
|
|
1186
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1187
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1188
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1189
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1190
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1191
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1192
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1193
|
+
{[]: 0, [1]: 1, [1, 2]: 1, [2, 1]: 2, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1194
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1195
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1196
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1197
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1198
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1199
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 1, [3, 1, 2]: 2, [3, 2, 1]: 2}
|
|
1200
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1201
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1202
|
+
|
|
1203
|
+
Another example with statistics::
|
|
1204
|
+
|
|
1205
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1206
|
+
sage: def alpha(p): return p(1) if len(p) > 0 else 0
|
|
1207
|
+
sage: def beta(p): return p(1) if len(p) > 0 else 0
|
|
1208
|
+
sage: bij.set_statistics((alpha, beta), (len, len))
|
|
1209
|
+
sage: for sol in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1210
|
+
....: print(sol)
|
|
1211
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1212
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 2, [1, 3, 2]: 3, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1213
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 1, [3, 2, 1]: 2}
|
|
1214
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1215
|
+
|
|
1216
|
+
The solution above is not unique. We can add a feasible distribution to force uniqueness::
|
|
1217
|
+
|
|
1218
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1219
|
+
sage: bij.set_statistics((alpha, beta), (len, len))
|
|
1220
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([3, 2, 1])], [1, 3]))
|
|
1221
|
+
sage: for sol in bij.solutions_iterator():
|
|
1222
|
+
....: print(sol)
|
|
1223
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 1, [1, 2, 3]: 3, [1, 3, 2]: 2, [2, 1, 3]: 2, [2, 3, 1]: 2, [3, 1, 2]: 2, [3, 2, 1]: 1}
|
|
1224
|
+
|
|
1225
|
+
Let us try to add a distribution that cannot be satisfied,
|
|
1226
|
+
because there is no solution where a permutation that starts
|
|
1227
|
+
with 1 is mapped onto 1::
|
|
1228
|
+
|
|
1229
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1230
|
+
sage: bij.set_statistics((alpha, beta), (len, len))
|
|
1231
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
|
|
1232
|
+
sage: list(bij.solutions_iterator())
|
|
1233
|
+
[]
|
|
1234
|
+
|
|
1235
|
+
The specified elements have to be in `A` and have to be of the same size::
|
|
1236
|
+
|
|
1237
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1238
|
+
sage: bij.set_statistics((len, len))
|
|
1239
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3, 4])], [1]))
|
|
1240
|
+
Traceback (most recent call last):
|
|
1241
|
+
...
|
|
1242
|
+
ValueError: element [1, 2, 3, 4] was not found in A
|
|
1243
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3])], [-1]))
|
|
1244
|
+
Traceback (most recent call last):
|
|
1245
|
+
...
|
|
1246
|
+
ValueError: value -1 was not found in tau(A)
|
|
1247
|
+
|
|
1248
|
+
Note that the same error occurs when an element that is not the first element of the list is
|
|
1249
|
+
not in `A`.
|
|
1250
|
+
"""
|
|
1251
|
+
self._bmilp = None
|
|
1252
|
+
for tA, tZ in elements_distributions:
|
|
1253
|
+
assert len(tA) == len(tZ), f"{tA} and {tZ} are not of the same size!"
|
|
1254
|
+
for a, z in zip(tA, tZ):
|
|
1255
|
+
if a not in self._A:
|
|
1256
|
+
raise ValueError(f"element {a} was not found in A")
|
|
1257
|
+
if z not in self._Z:
|
|
1258
|
+
raise ValueError(f"value {z} was not found in tau(A)")
|
|
1259
|
+
self._elements_distributions = tuple(elements_distributions)
|
|
1260
|
+
|
|
1261
|
+
def set_intertwining_relations(self, *pi_rho):
|
|
1262
|
+
r"""
|
|
1263
|
+
Add restrictions of the form `s(\pi(a_1,\dots, a_k)) =
|
|
1264
|
+
\rho(s(a_1),\dots, s(a_k))`.
|
|
1265
|
+
|
|
1266
|
+
.. WARNING::
|
|
1267
|
+
|
|
1268
|
+
Any restriction imposed by a previous invocation of
|
|
1269
|
+
:meth:`set_intertwining_relations` will be overwritten!
|
|
1270
|
+
|
|
1271
|
+
INPUT:
|
|
1272
|
+
|
|
1273
|
+
- ``pi_rho`` -- one or more tuples `(k, \pi: A^k\to A, \rho:
|
|
1274
|
+
Z^k\to Z, \tilde A)` where `\tilde A` (optional) is a
|
|
1275
|
+
`k`-ary function that returns true if and only if a
|
|
1276
|
+
`k`-tuple of objects in `A` is in the domain of `\pi`
|
|
1277
|
+
|
|
1278
|
+
ALGORITHM:
|
|
1279
|
+
|
|
1280
|
+
The relation
|
|
1281
|
+
|
|
1282
|
+
.. MATH::
|
|
1283
|
+
|
|
1284
|
+
s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))
|
|
1285
|
+
|
|
1286
|
+
for each pair `(\pi, \rho)` implies immediately that
|
|
1287
|
+
`s(\pi(a_1,\dots, a_k))` only depends on the blocks of
|
|
1288
|
+
`a_1,\dots, a_k`.
|
|
1289
|
+
|
|
1290
|
+
The MILP formulation is as follows. Let `a_1,\dots,a_k \in
|
|
1291
|
+
A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in
|
|
1292
|
+
Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in
|
|
1293
|
+
p_i` for all `i` and that `a\in p`.
|
|
1294
|
+
|
|
1295
|
+
We then want to model the implication
|
|
1296
|
+
|
|
1297
|
+
.. MATH::
|
|
1298
|
+
|
|
1299
|
+
x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1.
|
|
1300
|
+
|
|
1301
|
+
We achieve this by requiring
|
|
1302
|
+
|
|
1303
|
+
.. MATH::
|
|
1304
|
+
|
|
1305
|
+
x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}.
|
|
1306
|
+
|
|
1307
|
+
Note that `z` must be a possible value of `p` and each `z_i`
|
|
1308
|
+
must be a possible value of `p_i`.
|
|
1309
|
+
|
|
1310
|
+
EXAMPLES:
|
|
1311
|
+
|
|
1312
|
+
We can concatenate two permutations by increasing the values
|
|
1313
|
+
of the second permutation by the length of the first
|
|
1314
|
+
permutation::
|
|
1315
|
+
|
|
1316
|
+
sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
|
|
1317
|
+
|
|
1318
|
+
We may be interested in statistics on permutations which are
|
|
1319
|
+
equidistributed with the number of fixed points, such that
|
|
1320
|
+
concatenating permutations corresponds to adding statistic
|
|
1321
|
+
values::
|
|
1322
|
+
|
|
1323
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1324
|
+
sage: bij = Bijectionist(A, B, Permutation.number_of_fixed_points)
|
|
1325
|
+
sage: bij.set_statistics((len, len))
|
|
1326
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1327
|
+
....: print(solution)
|
|
1328
|
+
...
|
|
1329
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3}
|
|
1330
|
+
...
|
|
1331
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 1, [2, 1, 3]: 3, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
1332
|
+
...
|
|
1333
|
+
|
|
1334
|
+
sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y))
|
|
1335
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1336
|
+
....: print(solution)
|
|
1337
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
1338
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 0}
|
|
1339
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 0}
|
|
1340
|
+
|
|
1341
|
+
The domain of the composition may be restricted. E.g., if we
|
|
1342
|
+
concatenate only permutations starting with a 1, we obtain
|
|
1343
|
+
fewer forced elements::
|
|
1344
|
+
|
|
1345
|
+
sage: in_domain = lambda p1, p2: (not p1 or p1(1) == 1) and (not p2 or p2(1) == 1)
|
|
1346
|
+
sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y, in_domain))
|
|
1347
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1348
|
+
....: print(solution)
|
|
1349
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 1}
|
|
1350
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 1}
|
|
1351
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
1352
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 0}
|
|
1353
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 1}
|
|
1354
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
1355
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 0}
|
|
1356
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 0, [3, 2, 1]: 1}
|
|
1357
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 0, [3, 1, 2]: 1, [3, 2, 1]: 0}
|
|
1358
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 0, [1, 2, 3]: 3, [1, 3, 2]: 1, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 0, [3, 2, 1]: 0}
|
|
1359
|
+
|
|
1360
|
+
We can also restrict according to several composition
|
|
1361
|
+
functions. For example, we may additionally concatenate
|
|
1362
|
+
permutations by incrementing the elements of the first::
|
|
1363
|
+
|
|
1364
|
+
sage: skew_concat = lambda p1, p2: Permutation([i + len(p2) for i in p1] + list(p2))
|
|
1365
|
+
sage: bij.set_intertwining_relations((2, skew_concat, lambda x, y: x + y))
|
|
1366
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1367
|
+
....: print(solution)
|
|
1368
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 0, [1, 3, 2]: 0, [2, 1, 3]: 1, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3}
|
|
1369
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3}
|
|
1370
|
+
{[]: 0, [1]: 1, [1, 2]: 0, [2, 1]: 2, [1, 2, 3]: 1, [1, 3, 2]: 0, [2, 1, 3]: 0, [2, 3, 1]: 1, [3, 1, 2]: 1, [3, 2, 1]: 3}
|
|
1371
|
+
|
|
1372
|
+
However, this yields no solution::
|
|
1373
|
+
|
|
1374
|
+
sage: bij.set_intertwining_relations((2, concat, lambda x, y: x + y), (2, skew_concat, lambda x, y: x + y))
|
|
1375
|
+
sage: list(bij.solutions_iterator())
|
|
1376
|
+
[]
|
|
1377
|
+
"""
|
|
1378
|
+
self._bmilp = None
|
|
1379
|
+
Pi_Rho = namedtuple("Pi_Rho", "numargs pi rho domain")
|
|
1380
|
+
self._pi_rho = []
|
|
1381
|
+
|
|
1382
|
+
for pi_rho_tuple in pi_rho:
|
|
1383
|
+
if len(pi_rho_tuple) == 3:
|
|
1384
|
+
k, pi, rho = pi_rho_tuple
|
|
1385
|
+
domain = None
|
|
1386
|
+
else:
|
|
1387
|
+
k, pi, rho, domain = pi_rho_tuple
|
|
1388
|
+
|
|
1389
|
+
self._pi_rho.append(Pi_Rho(numargs=k, pi=pi, rho=rho, domain=domain))
|
|
1390
|
+
|
|
1391
|
+
set_semi_conjugacy = set_intertwining_relations
|
|
1392
|
+
|
|
1393
|
+
def set_quadratic_relation(self, *phi_psi):
|
|
1394
|
+
r"""
|
|
1395
|
+
Add restrictions of the form `s\circ\psi\circ s = \phi`.
|
|
1396
|
+
|
|
1397
|
+
INPUT:
|
|
1398
|
+
|
|
1399
|
+
- ``phi_psi`` -- (optional) a list of pairs `(\phi, \rho)` where `\phi:
|
|
1400
|
+
A\to Z` and `\psi: Z\to A`
|
|
1401
|
+
|
|
1402
|
+
ALGORITHM:
|
|
1403
|
+
|
|
1404
|
+
We add
|
|
1405
|
+
|
|
1406
|
+
.. MATH::
|
|
1407
|
+
|
|
1408
|
+
x_{p(a), z} = x_{p(\psi(z)), \phi(a)}
|
|
1409
|
+
|
|
1410
|
+
for `a\in A` and `z\in Z` to the MILP, where `\phi:A\to Z`
|
|
1411
|
+
and `\psi:Z\to A`. Note that, in particular, `\phi` must be
|
|
1412
|
+
constant on blocks.
|
|
1413
|
+
|
|
1414
|
+
EXAMPLES::
|
|
1415
|
+
|
|
1416
|
+
sage: A = B = DyckWords(3)
|
|
1417
|
+
sage: bij = Bijectionist(A, B)
|
|
1418
|
+
sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises()))
|
|
1419
|
+
sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
|
|
1420
|
+
[ ( [ /\ ] )
|
|
1421
|
+
[ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] )
|
|
1422
|
+
[ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ),
|
|
1423
|
+
<BLANKLINE>
|
|
1424
|
+
( [ /\ ] ) ]
|
|
1425
|
+
( [ /\/\ / \ ] [ /\ ] ) ]
|
|
1426
|
+
( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ]
|
|
1427
|
+
sage: bij.set_quadratic_relation((lambda D: D, lambda D: D))
|
|
1428
|
+
sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
|
|
1429
|
+
[ ( [ /\ ] )
|
|
1430
|
+
[ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] )
|
|
1431
|
+
[ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ),
|
|
1432
|
+
<BLANKLINE>
|
|
1433
|
+
<BLANKLINE>
|
|
1434
|
+
( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] )
|
|
1435
|
+
( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ),
|
|
1436
|
+
<BLANKLINE>
|
|
1437
|
+
( [ /\ ] ) ]
|
|
1438
|
+
( [ / \ ] ) ]
|
|
1439
|
+
( [ / \ ], [ /\/\/\ ] ) ]
|
|
1440
|
+
"""
|
|
1441
|
+
self._bmilp = None
|
|
1442
|
+
self._phi_psi = phi_psi
|
|
1443
|
+
|
|
1444
|
+
def set_homomesic(self, Q):
|
|
1445
|
+
"""
|
|
1446
|
+
Assert that the average of `s` on each block of `Q` is
|
|
1447
|
+
constant.
|
|
1448
|
+
|
|
1449
|
+
INPUT:
|
|
1450
|
+
|
|
1451
|
+
- ``Q`` -- set partition of ``A``
|
|
1452
|
+
|
|
1453
|
+
EXAMPLES::
|
|
1454
|
+
|
|
1455
|
+
sage: A = B = [1,2,3]
|
|
1456
|
+
sage: bij = Bijectionist(A, B, lambda b: b % 3)
|
|
1457
|
+
sage: bij.set_homomesic([[1,2], [3]])
|
|
1458
|
+
sage: list(bij.solutions_iterator())
|
|
1459
|
+
[{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}]
|
|
1460
|
+
"""
|
|
1461
|
+
self._bmilp = None
|
|
1462
|
+
if Q is None:
|
|
1463
|
+
self._Q = None
|
|
1464
|
+
else:
|
|
1465
|
+
self._Q = SetPartition(Q)
|
|
1466
|
+
assert self._Q in SetPartitions(self._A), f"{Q} must be a set partition of A"
|
|
1467
|
+
|
|
1468
|
+
def _forced_constant_blocks(self):
|
|
1469
|
+
r"""
|
|
1470
|
+
Modify current partition into blocks to the coarsest possible
|
|
1471
|
+
one, meaning that after calling this function for every two
|
|
1472
|
+
distinct blocks `p_1`, `p_2` there exists a solution `s` with
|
|
1473
|
+
`s(p_1)\neq s(p_2)`.
|
|
1474
|
+
|
|
1475
|
+
ALGORITHM:
|
|
1476
|
+
|
|
1477
|
+
First we generate an initial solution. For all blocks i, j
|
|
1478
|
+
that have the same value under this initial solution, we add
|
|
1479
|
+
the constraint `x[i, z] + x[j, z] <= 1` for all possible
|
|
1480
|
+
values `z\in Z`. This constraint ensures that the `s` differs
|
|
1481
|
+
on the two blocks. If this modified problem does not have a
|
|
1482
|
+
solution, we know that the two blocks always have the same
|
|
1483
|
+
value and join them. Then we save all values of this new
|
|
1484
|
+
solution and continue looking at pairs of blocks that had the
|
|
1485
|
+
same value under all calculated solutions, until no blocks
|
|
1486
|
+
can be joined anymore.
|
|
1487
|
+
|
|
1488
|
+
EXAMPLES:
|
|
1489
|
+
|
|
1490
|
+
The easiest example is given by a constant `tau`, so everything
|
|
1491
|
+
is forced to be the same value:
|
|
1492
|
+
|
|
1493
|
+
sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
|
|
1494
|
+
sage: bij = Bijectionist(A, B, lambda x: 0)
|
|
1495
|
+
sage: bij.constant_blocks()
|
|
1496
|
+
{}
|
|
1497
|
+
sage: bij.constant_blocks(optimal=True) # indirect doctest
|
|
1498
|
+
{{[], [1], [1, 2], [2, 1]}}
|
|
1499
|
+
|
|
1500
|
+
In this other example we look at permutations with length 2 and 3::
|
|
1501
|
+
|
|
1502
|
+
sage: N = 4
|
|
1503
|
+
sage: A = B = [permutation for n in range(2, N) for permutation in Permutations(n)]
|
|
1504
|
+
sage: def tau(p): return p[0] if len(p) else 0
|
|
1505
|
+
sage: add_n = lambda p1: Permutation(p1 + [1 + len(p1)])
|
|
1506
|
+
sage: add_1 = lambda p1: Permutation([1] + [1 + i for i in p1])
|
|
1507
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1508
|
+
sage: bij.set_intertwining_relations((1, add_n, lambda x: x + 1), (1, add_1, lambda x: x + 1))
|
|
1509
|
+
sage: bij.set_statistics((len, len))
|
|
1510
|
+
|
|
1511
|
+
sage: bij.constant_blocks()
|
|
1512
|
+
{}
|
|
1513
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1514
|
+
{{[1, 3, 2], [2, 1, 3]}}
|
|
1515
|
+
|
|
1516
|
+
Indeed, ``[1,3,2]`` and ``[2,1,3]`` have the same value in
|
|
1517
|
+
all solutions, but different values are possible::
|
|
1518
|
+
|
|
1519
|
+
sage: pi1 = Permutation([1,3,2]); pi2 = Permutation([2,1,3]);
|
|
1520
|
+
sage: set([(solution[pi1], solution[pi2]) for solution in bij.solutions_iterator()])
|
|
1521
|
+
{(2, 2), (3, 3)}
|
|
1522
|
+
|
|
1523
|
+
Another example involving the cycle type of permutations::
|
|
1524
|
+
|
|
1525
|
+
sage: A = B = [permutation for n in range(4) for permutation in Permutations(n)]
|
|
1526
|
+
sage: bij = Bijectionist(A, B, lambda x: x.cycle_type())
|
|
1527
|
+
|
|
1528
|
+
Let us require that each permutation has the same value as its inverse::
|
|
1529
|
+
|
|
1530
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
1531
|
+
sage: P = orbit_decomposition([permutation for n in range(4) for permutation in Permutations(n)], Permutation.inverse)
|
|
1532
|
+
sage: bij.set_constant_blocks(P)
|
|
1533
|
+
sage: bij.constant_blocks()
|
|
1534
|
+
{{[2, 3, 1], [3, 1, 2]}}
|
|
1535
|
+
|
|
1536
|
+
sage: def concat(p1, p2): return Permutation(p1 + [i + len(p1) for i in p2])
|
|
1537
|
+
sage: def union(p1, p2): return Partition(sorted(list(p1) + list(p2), reverse=True))
|
|
1538
|
+
sage: bij.set_intertwining_relations((2, concat, union))
|
|
1539
|
+
|
|
1540
|
+
In this case we do not discover constant blocks by looking at the intertwining_relations only::
|
|
1541
|
+
|
|
1542
|
+
sage: next(bij.solutions_iterator())
|
|
1543
|
+
...
|
|
1544
|
+
sage: bij.constant_blocks()
|
|
1545
|
+
{{[2, 3, 1], [3, 1, 2]}}
|
|
1546
|
+
|
|
1547
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1548
|
+
{{[1, 3, 2], [2, 1, 3], [3, 2, 1]}, {[2, 3, 1], [3, 1, 2]}}
|
|
1549
|
+
|
|
1550
|
+
TESTS::
|
|
1551
|
+
|
|
1552
|
+
sage: N = 4
|
|
1553
|
+
sage: A = B = [permutation for n in range(N + 1) for permutation in Permutations(n)]
|
|
1554
|
+
sage: def alpha1(p): return len(p.weak_excedences())
|
|
1555
|
+
sage: def alpha2(p): return len(p.fixed_points())
|
|
1556
|
+
sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
|
|
1557
|
+
sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:] + [0]) if e == f + 1])
|
|
1558
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
1559
|
+
sage: def rotate_permutation(p):
|
|
1560
|
+
....: cycle = Permutation(tuple(range(1, len(p) + 1)))
|
|
1561
|
+
....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p) + 1)])
|
|
1562
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1563
|
+
sage: bij.set_statistics((alpha1, beta1), (alpha2, beta2))
|
|
1564
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
1565
|
+
sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
|
|
1566
|
+
sage: P = bij.constant_blocks()
|
|
1567
|
+
sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
|
|
1568
|
+
sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
|
|
1569
|
+
sage: for p in P:
|
|
1570
|
+
....: print(p)
|
|
1571
|
+
[[1, 3, 2], [2, 1, 3], [3, 2, 1]]
|
|
1572
|
+
[[1, 4, 3, 2], [3, 2, 1, 4]]
|
|
1573
|
+
[[2, 1, 4, 3], [4, 3, 2, 1]]
|
|
1574
|
+
[[1, 2, 4, 3], [1, 3, 2, 4], [2, 1, 3, 4], [4, 2, 3, 1]]
|
|
1575
|
+
[[1, 3, 4, 2], [2, 3, 1, 4], [2, 4, 3, 1], [3, 2, 4, 1]]
|
|
1576
|
+
[[1, 4, 2, 3], [3, 1, 2, 4], [4, 1, 3, 2], [4, 2, 1, 3]]
|
|
1577
|
+
[[2, 4, 1, 3], [3, 1, 4, 2], [3, 4, 2, 1], [4, 3, 1, 2]]
|
|
1578
|
+
|
|
1579
|
+
sage: P = bij.constant_blocks(optimal=True)
|
|
1580
|
+
sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
|
|
1581
|
+
sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
|
|
1582
|
+
sage: for p in P:
|
|
1583
|
+
....: print(p)
|
|
1584
|
+
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4]]
|
|
1585
|
+
[[1, 3, 2], [2, 1, 3], [3, 2, 1],
|
|
1586
|
+
[1, 2, 4, 3], [1, 3, 2, 4], [1, 3, 4, 2], [1, 4, 3, 2],
|
|
1587
|
+
[2, 1, 3, 4], [2, 1, 4, 3], [2, 3, 1, 4], [2, 3, 4, 1],
|
|
1588
|
+
[2, 4, 3, 1], [3, 2, 1, 4], [3, 2, 4, 1], [4, 2, 3, 1],
|
|
1589
|
+
[4, 3, 2, 1]]
|
|
1590
|
+
[[1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2],
|
|
1591
|
+
[3, 4, 2, 1], [4, 1, 3, 2], [4, 2, 1, 3], [4, 3, 1, 2]]
|
|
1592
|
+
|
|
1593
|
+
The permutation `[2, 1]` is in none of these blocks::
|
|
1594
|
+
|
|
1595
|
+
sage: bij.set_constant_blocks(orbit_decomposition(A, rotate_permutation))
|
|
1596
|
+
sage: all(s[Permutation([2, 1])] == s[Permutation([1])] for s in bij.solutions_iterator())
|
|
1597
|
+
False
|
|
1598
|
+
|
|
1599
|
+
sage: all(s[Permutation([2, 1])] == s[Permutation([1, 3, 2])] for s in bij.solutions_iterator())
|
|
1600
|
+
False
|
|
1601
|
+
|
|
1602
|
+
sage: all(s[Permutation([2, 1])] == s[Permutation([1, 4, 2, 3])] for s in bij.solutions_iterator())
|
|
1603
|
+
False
|
|
1604
|
+
|
|
1605
|
+
sage: A = B = ["a", "b", "c", "d", "e", "f"]
|
|
1606
|
+
sage: tau = {"a": 1, "b": 1, "c": 3, "d": 4, "e": 5, "f": 6}.get
|
|
1607
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1608
|
+
sage: bij.set_distributions((["a", "b"], [1, 1]), (["c", "d", "e"], [3, 4, 5]))
|
|
1609
|
+
sage: bij.constant_blocks()
|
|
1610
|
+
{}
|
|
1611
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1612
|
+
{{'a', 'b'}}
|
|
1613
|
+
|
|
1614
|
+
sage: A = B = ["a", "b", "c", "d", "e", "f"]
|
|
1615
|
+
sage: tau = {"a": 1, "b": 1, "c": 5, "d": 4, "e": 4, "f": 6}.get
|
|
1616
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1617
|
+
sage: bij.set_distributions((["a", "b"], [1, 1]), (["d", "e"], [4, 4]))
|
|
1618
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1619
|
+
{{'a', 'b'}, {'d', 'e'}}
|
|
1620
|
+
|
|
1621
|
+
sage: A = B = ["a", "b", "c", "d"]
|
|
1622
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get
|
|
1623
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1624
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1625
|
+
{}
|
|
1626
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1627
|
+
sage: bij.constant_blocks()
|
|
1628
|
+
{{'a', 'b'}}
|
|
1629
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1630
|
+
{{'a', 'b'}, {'c', 'd'}}
|
|
1631
|
+
"""
|
|
1632
|
+
if self._bmilp is None:
|
|
1633
|
+
self._bmilp = _BijectionistMILP(self)
|
|
1634
|
+
|
|
1635
|
+
solution = next(self._bmilp.solutions_iterator(True, []))
|
|
1636
|
+
# multiple_preimages[tZ] are the blocks p which have the same
|
|
1637
|
+
# value tZ[i] in the i-th known solution
|
|
1638
|
+
multiple_preimages = {(z,): tP
|
|
1639
|
+
for z, tP in _invert_dict(solution).items()
|
|
1640
|
+
if len(tP) > 1}
|
|
1641
|
+
|
|
1642
|
+
# _P has to be copied to not mess with the solution process
|
|
1643
|
+
# since we do not want to regenerate the bmilp in each step,
|
|
1644
|
+
# so blocks have to stay consistent during the whole process
|
|
1645
|
+
tmp_P = copy(self._P)
|
|
1646
|
+
|
|
1647
|
+
# check whether blocks p1 and p2 can have different values,
|
|
1648
|
+
# if so return such a solution
|
|
1649
|
+
def different_values(p1, p2):
|
|
1650
|
+
tmp_constraints = [self._bmilp._x[p1, z] + self._bmilp._x[p2, z] <= 1
|
|
1651
|
+
for z in self._possible_block_values[p1]
|
|
1652
|
+
if z in self._possible_block_values[p2]]
|
|
1653
|
+
return next(self._bmilp.solutions_iterator(True, tmp_constraints))
|
|
1654
|
+
|
|
1655
|
+
# try to find a pair of blocks having the same value on all
|
|
1656
|
+
# known solutions, and a solution such that the values are
|
|
1657
|
+
# different on this solution
|
|
1658
|
+
def merge_until_split():
|
|
1659
|
+
for tZ in list(multiple_preimages):
|
|
1660
|
+
tP = multiple_preimages[tZ]
|
|
1661
|
+
for i2 in range(len(tP) - 1, -1, -1):
|
|
1662
|
+
for i1 in range(i2):
|
|
1663
|
+
try:
|
|
1664
|
+
solution = different_values(tP[i1], tP[i2])
|
|
1665
|
+
except StopIteration:
|
|
1666
|
+
tmp_P.union(tP[i1], tP[i2])
|
|
1667
|
+
if len(multiple_preimages[tZ]) == 2:
|
|
1668
|
+
del multiple_preimages[tZ]
|
|
1669
|
+
else:
|
|
1670
|
+
tP.remove(tP[i2])
|
|
1671
|
+
break # skip all pairs (i, j) containing i2
|
|
1672
|
+
return solution
|
|
1673
|
+
|
|
1674
|
+
while True:
|
|
1675
|
+
solution = merge_until_split()
|
|
1676
|
+
if solution is None:
|
|
1677
|
+
self._P = tmp_P
|
|
1678
|
+
# recreate the MILP
|
|
1679
|
+
self._bmilp = _BijectionistMILP(self,
|
|
1680
|
+
self._bmilp._solution_cache)
|
|
1681
|
+
return
|
|
1682
|
+
|
|
1683
|
+
updated_multiple_preimages = defaultdict(list)
|
|
1684
|
+
for tZ, tP in multiple_preimages.items():
|
|
1685
|
+
for p in tP:
|
|
1686
|
+
updated_multiple_preimages[tZ + (solution[p],)].append(p)
|
|
1687
|
+
multiple_preimages = updated_multiple_preimages
|
|
1688
|
+
|
|
1689
|
+
def possible_values(self, p=None, optimal=False):
|
|
1690
|
+
r"""
|
|
1691
|
+
Return for each block the values of `s` compatible with the
|
|
1692
|
+
imposed restrictions.
|
|
1693
|
+
|
|
1694
|
+
INPUT:
|
|
1695
|
+
|
|
1696
|
+
- ``p`` -- (optional) a block of `P`, or an element of a
|
|
1697
|
+
block of `P`, or a list of these
|
|
1698
|
+
|
|
1699
|
+
- ``optimal`` -- boolean (default: ``False``); whether or not to
|
|
1700
|
+
compute the minimal possible set of statistic values
|
|
1701
|
+
|
|
1702
|
+
.. NOTE::
|
|
1703
|
+
|
|
1704
|
+
Computing the minimal possible set of statistic values
|
|
1705
|
+
may be computationally expensive.
|
|
1706
|
+
|
|
1707
|
+
.. TODO::
|
|
1708
|
+
|
|
1709
|
+
currently, calling this method with ``optimal=True`` does
|
|
1710
|
+
not update the internal dictionary, because this would
|
|
1711
|
+
interfere with the variables of the MILP.
|
|
1712
|
+
|
|
1713
|
+
EXAMPLES::
|
|
1714
|
+
|
|
1715
|
+
sage: A = B = ["a", "b", "c", "d"]
|
|
1716
|
+
sage: tau = {"a": 1, "b": 1, "c": 1, "d": 2}.get
|
|
1717
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1718
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1719
|
+
sage: bij.possible_values(A)
|
|
1720
|
+
{'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}}
|
|
1721
|
+
sage: bij.possible_values(A, optimal=True)
|
|
1722
|
+
{'a': {1}, 'b': {1}, 'c': {1, 2}, 'd': {1, 2}}
|
|
1723
|
+
|
|
1724
|
+
The internal dictionary is not updated::
|
|
1725
|
+
|
|
1726
|
+
sage: bij.possible_values(A)
|
|
1727
|
+
{'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}, 'd': {1, 2}}
|
|
1728
|
+
|
|
1729
|
+
TESTS::
|
|
1730
|
+
|
|
1731
|
+
sage: A = B = ["a", "b", "c", "d"]
|
|
1732
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2}.get
|
|
1733
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1734
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1735
|
+
|
|
1736
|
+
Test if all formats are really possible::
|
|
1737
|
+
|
|
1738
|
+
sage: bij.possible_values(p='a')
|
|
1739
|
+
{'a': {1, 2}, 'b': {1, 2}}
|
|
1740
|
+
sage: bij.possible_values(p=["a", "b"])
|
|
1741
|
+
{'a': {1, 2}, 'b': {1, 2}}
|
|
1742
|
+
sage: bij.possible_values(p=[["a", "b"]])
|
|
1743
|
+
{'a': {1, 2}, 'b': {1, 2}}
|
|
1744
|
+
sage: bij.possible_values(p=[["a", "b"], ["c"]])
|
|
1745
|
+
{'a': {1, 2}, 'b': {1, 2}, 'c': {1, 2}}
|
|
1746
|
+
|
|
1747
|
+
Test an unfeasible problem::
|
|
1748
|
+
|
|
1749
|
+
sage: A = B = 'ab'
|
|
1750
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
|
|
1751
|
+
sage: bij.set_constant_blocks([['a', 'b']])
|
|
1752
|
+
sage: bij.possible_values(p='a')
|
|
1753
|
+
{'a': {0, 1}, 'b': {0, 1}}
|
|
1754
|
+
sage: bij.possible_values(p='a', optimal=True)
|
|
1755
|
+
{'a': set(), 'b': set()}
|
|
1756
|
+
"""
|
|
1757
|
+
# convert input to set of block representatives
|
|
1758
|
+
blocks = set()
|
|
1759
|
+
if p in self._A:
|
|
1760
|
+
blocks.add(self._P.find(p))
|
|
1761
|
+
elif isinstance(p, list): # TODO: this looks very brittle
|
|
1762
|
+
for p1 in p:
|
|
1763
|
+
if p1 in self._A:
|
|
1764
|
+
blocks.add(self._P.find(p1))
|
|
1765
|
+
elif isinstance(p1, list):
|
|
1766
|
+
for p2 in p1:
|
|
1767
|
+
blocks.add(self._P.find(p2))
|
|
1768
|
+
|
|
1769
|
+
if optimal:
|
|
1770
|
+
if self._bmilp is None:
|
|
1771
|
+
self._bmilp = _BijectionistMILP(self)
|
|
1772
|
+
bmilp = self._bmilp
|
|
1773
|
+
solutions = defaultdict(set)
|
|
1774
|
+
try:
|
|
1775
|
+
solution = next(bmilp.solutions_iterator(True, []))
|
|
1776
|
+
except StopIteration:
|
|
1777
|
+
pass
|
|
1778
|
+
else:
|
|
1779
|
+
for p, z in solution.items():
|
|
1780
|
+
solutions[p].add(z)
|
|
1781
|
+
for p in blocks:
|
|
1782
|
+
tmp_constraints = [bmilp._x[p, z] == 0 for z in solutions[p]]
|
|
1783
|
+
while True:
|
|
1784
|
+
try:
|
|
1785
|
+
solution = next(bmilp.solutions_iterator(True, tmp_constraints))
|
|
1786
|
+
except StopIteration:
|
|
1787
|
+
break
|
|
1788
|
+
for p0, z in solution.items():
|
|
1789
|
+
solutions[p0].add(z)
|
|
1790
|
+
# veto new value and try again
|
|
1791
|
+
tmp_constraints.append(bmilp._x[p, solution[p]] == 0)
|
|
1792
|
+
|
|
1793
|
+
# create dictionary to return
|
|
1794
|
+
possible_values = {}
|
|
1795
|
+
for p in blocks:
|
|
1796
|
+
for a in self._P.root_to_elements_dict()[p]:
|
|
1797
|
+
possible_values[a] = solutions[p]
|
|
1798
|
+
else:
|
|
1799
|
+
# create dictionary to return
|
|
1800
|
+
if self._possible_block_values is None:
|
|
1801
|
+
self._compute_possible_block_values()
|
|
1802
|
+
possible_values = {}
|
|
1803
|
+
for p in blocks:
|
|
1804
|
+
for a in self._P.root_to_elements_dict()[p]:
|
|
1805
|
+
possible_values[a] = self._possible_block_values[p]
|
|
1806
|
+
|
|
1807
|
+
return possible_values
|
|
1808
|
+
|
|
1809
|
+
def minimal_subdistributions_iterator(self):
|
|
1810
|
+
r"""
|
|
1811
|
+
Return all minimal subsets `\tilde A` of `A`
|
|
1812
|
+
together with submultisets `\tilde Z` with `s(\tilde A) =
|
|
1813
|
+
\tilde Z` as multisets.
|
|
1814
|
+
|
|
1815
|
+
EXAMPLES::
|
|
1816
|
+
|
|
1817
|
+
sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
|
|
1818
|
+
sage: bij = Bijectionist(A, B, len)
|
|
1819
|
+
sage: bij.set_statistics((len, len))
|
|
1820
|
+
sage: for sol in bij.solutions_iterator():
|
|
1821
|
+
....: print(sol)
|
|
1822
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
|
|
1823
|
+
sage: sorted(bij.minimal_subdistributions_iterator())
|
|
1824
|
+
[([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])]
|
|
1825
|
+
|
|
1826
|
+
Another example::
|
|
1827
|
+
|
|
1828
|
+
sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
|
|
1829
|
+
sage: def tau(D): return D.number_of_touch_points()
|
|
1830
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1831
|
+
sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
|
|
1832
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1833
|
+
....: print(solution)
|
|
1834
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
|
|
1835
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
|
|
1836
|
+
sage: for subdistribution in bij.minimal_subdistributions_iterator():
|
|
1837
|
+
....: print(subdistribution)
|
|
1838
|
+
([[]], [0])
|
|
1839
|
+
([[1, 0]], [1])
|
|
1840
|
+
([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2])
|
|
1841
|
+
|
|
1842
|
+
An example with two elements of the same block in a subdistribution::
|
|
1843
|
+
|
|
1844
|
+
sage: A = B = ["a", "b", "c", "d", "e"]
|
|
1845
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
|
|
1846
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1847
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1848
|
+
sage: bij.set_value_restrictions(("a", [1, 2]))
|
|
1849
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1850
|
+
{{'a', 'b'}}
|
|
1851
|
+
sage: list(bij.minimal_subdistributions_iterator())
|
|
1852
|
+
[(['a', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])]
|
|
1853
|
+
"""
|
|
1854
|
+
# see
|
|
1855
|
+
# https://mathoverflow.net/questions/406751/find-a-subdistribution/406975
|
|
1856
|
+
# and
|
|
1857
|
+
# https://gitlab.com/mantepse/bijection-tools/-/issues/29
|
|
1858
|
+
|
|
1859
|
+
minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver)
|
|
1860
|
+
D = minimal_subdistribution.new_variable(binary=True) # the subset of elements
|
|
1861
|
+
V = minimal_subdistribution.new_variable(integer=True) # the subdistribution
|
|
1862
|
+
minimal_subdistribution.set_objective(sum(D[a] for a in self._A))
|
|
1863
|
+
minimal_subdistribution.add_constraint(sum(D[a] for a in self._A) >= 1)
|
|
1864
|
+
|
|
1865
|
+
if self._bmilp is None:
|
|
1866
|
+
self._bmilp = _BijectionistMILP(self)
|
|
1867
|
+
s = next(self._bmilp.solutions_iterator(False, []))
|
|
1868
|
+
while True:
|
|
1869
|
+
for v in self._Z:
|
|
1870
|
+
minimal_subdistribution.add_constraint(sum(D[a] for a in self._A if s[a] == v) == V[v])
|
|
1871
|
+
try:
|
|
1872
|
+
minimal_subdistribution.solve()
|
|
1873
|
+
except MIPSolverException:
|
|
1874
|
+
return
|
|
1875
|
+
d = minimal_subdistribution.get_values(D, convert=bool, tolerance=0.1) # a dict from A to {0, 1}
|
|
1876
|
+
new_s = self._find_counterexample(self._A, s, d, False)
|
|
1877
|
+
if new_s is None:
|
|
1878
|
+
values = self._sorter["Z"](s[a] for a in self._A if d[a])
|
|
1879
|
+
yield ([a for a in self._A if d[a]], values)
|
|
1880
|
+
|
|
1881
|
+
# get all variables with value 1
|
|
1882
|
+
active_vars = [D[a] for a in self._A
|
|
1883
|
+
if minimal_subdistribution.get_values(D[a], convert=bool, tolerance=0.1)]
|
|
1884
|
+
|
|
1885
|
+
# add constraint that not all of these can be 1, thus vetoing
|
|
1886
|
+
# the current solution
|
|
1887
|
+
minimal_subdistribution.add_constraint(sum(active_vars) <= len(active_vars) - 1,
|
|
1888
|
+
name='veto')
|
|
1889
|
+
else:
|
|
1890
|
+
s = new_s
|
|
1891
|
+
|
|
1892
|
+
def _find_counterexample(self, P, s0, d, on_blocks):
|
|
1893
|
+
r"""
|
|
1894
|
+
Return a solution `s` such that ``d`` is not a subdistribution of
|
|
1895
|
+
`s0`.
|
|
1896
|
+
|
|
1897
|
+
INPUT:
|
|
1898
|
+
|
|
1899
|
+
- ``P`` -- the representatives of the blocks, or `A` if
|
|
1900
|
+
``on_blocks`` is ``False``
|
|
1901
|
+
|
|
1902
|
+
- ``s0`` -- a solution
|
|
1903
|
+
|
|
1904
|
+
- ``d`` -- a subset of `A`, in the form of a dict from `A` to
|
|
1905
|
+
`\{0, 1\}`
|
|
1906
|
+
|
|
1907
|
+
- ``on_blocks`` -- whether to return the counterexample on
|
|
1908
|
+
blocks or on elements
|
|
1909
|
+
|
|
1910
|
+
EXAMPLES::
|
|
1911
|
+
|
|
1912
|
+
sage: A = B = ["a", "b", "c", "d", "e"]
|
|
1913
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
|
|
1914
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1915
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1916
|
+
sage: bij.set_value_restrictions(("a", [1, 2]))
|
|
1917
|
+
sage: next(bij.solutions_iterator())
|
|
1918
|
+
{'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2}
|
|
1919
|
+
|
|
1920
|
+
sage: s0 = {'a': 1, 'b': 1, 'c': 2, 'd': 3, 'e': 2}
|
|
1921
|
+
sage: d = {'a': 1, 'b': 0, 'c': 0, 'd': 0, 'e': 0}
|
|
1922
|
+
sage: bij._find_counterexample(bij._A, s0, d, False)
|
|
1923
|
+
{'a': 2, 'b': 2, 'c': 1, 'd': 3, 'e': 1}
|
|
1924
|
+
"""
|
|
1925
|
+
bmilp = self._bmilp
|
|
1926
|
+
for z in self._Z:
|
|
1927
|
+
z_in_d_count = sum(d[p] for p in P if s0[p] == z)
|
|
1928
|
+
if not z_in_d_count:
|
|
1929
|
+
continue
|
|
1930
|
+
|
|
1931
|
+
# try to find a solution which has a different
|
|
1932
|
+
# subdistribution on d than s0
|
|
1933
|
+
z_in_d = sum(d[p] * bmilp._x[self._P.find(p), z]
|
|
1934
|
+
for p in P
|
|
1935
|
+
if z in self._possible_block_values[self._P.find(p)])
|
|
1936
|
+
|
|
1937
|
+
# it is sufficient to require that z occurs less often as
|
|
1938
|
+
# a value among {a | d[a] == 1} than it does in
|
|
1939
|
+
# z_in_d_count, because, if the distributions are
|
|
1940
|
+
# different, one such z must exist
|
|
1941
|
+
tmp_constraints = [z_in_d <= z_in_d_count - 1]
|
|
1942
|
+
try:
|
|
1943
|
+
solution = next(bmilp.solutions_iterator(on_blocks, tmp_constraints))
|
|
1944
|
+
return solution
|
|
1945
|
+
except StopIteration:
|
|
1946
|
+
pass
|
|
1947
|
+
|
|
1948
|
+
def minimal_subdistributions_blocks_iterator(self):
|
|
1949
|
+
r"""
|
|
1950
|
+
Return all representatives of minimal subsets `\tilde P`
|
|
1951
|
+
of `P` together with submultisets `\tilde Z`
|
|
1952
|
+
with `s(\tilde P) = \tilde Z` as multisets.
|
|
1953
|
+
|
|
1954
|
+
.. WARNING::
|
|
1955
|
+
|
|
1956
|
+
If there are several solutions with the same support
|
|
1957
|
+
(i.e., the sets of block representatives are the same),
|
|
1958
|
+
only one of these will be found, even if the
|
|
1959
|
+
distributions are different, see the doctest below. To
|
|
1960
|
+
find all solutions, use
|
|
1961
|
+
:meth:`minimal_subdistributions_iterator`, which is,
|
|
1962
|
+
however, computationally more expensive.
|
|
1963
|
+
|
|
1964
|
+
EXAMPLES::
|
|
1965
|
+
|
|
1966
|
+
sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
|
|
1967
|
+
sage: bij = Bijectionist(A, B, len)
|
|
1968
|
+
sage: bij.set_statistics((len, len))
|
|
1969
|
+
sage: for sol in bij.solutions_iterator():
|
|
1970
|
+
....: print(sol)
|
|
1971
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
|
|
1972
|
+
sage: sorted(bij.minimal_subdistributions_blocks_iterator())
|
|
1973
|
+
[([[]], [0]), ([[1]], [1]), ([[1, 2]], [2]), ([[2, 1]], [2])]
|
|
1974
|
+
|
|
1975
|
+
Another example::
|
|
1976
|
+
|
|
1977
|
+
sage: N = 2; A = B = [dyck_word for n in range(N+1) for dyck_word in DyckWords(n)]
|
|
1978
|
+
sage: def tau(D): return D.number_of_touch_points()
|
|
1979
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1980
|
+
sage: bij.set_statistics((lambda d: d.semilength(), lambda d: d.semilength()))
|
|
1981
|
+
sage: for solution in sorted(list(bij.solutions_iterator()), key=lambda d: tuple(sorted(d.items()))):
|
|
1982
|
+
....: print(solution)
|
|
1983
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 1, [1, 1, 0, 0]: 2}
|
|
1984
|
+
{[]: 0, [1, 0]: 1, [1, 0, 1, 0]: 2, [1, 1, 0, 0]: 1}
|
|
1985
|
+
sage: for subdistribution in bij.minimal_subdistributions_blocks_iterator():
|
|
1986
|
+
....: print(subdistribution)
|
|
1987
|
+
([[]], [0])
|
|
1988
|
+
([[1, 0]], [1])
|
|
1989
|
+
([[1, 0, 1, 0], [1, 1, 0, 0]], [1, 2])
|
|
1990
|
+
|
|
1991
|
+
An example with two elements of the same block in a subdistribution::
|
|
1992
|
+
|
|
1993
|
+
sage: A = B = ["a", "b", "c", "d", "e"]
|
|
1994
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
|
|
1995
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
1996
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
1997
|
+
sage: bij.set_value_restrictions(("a", [1, 2]))
|
|
1998
|
+
sage: bij.constant_blocks(optimal=True)
|
|
1999
|
+
{{'a', 'b'}}
|
|
2000
|
+
sage: list(bij.minimal_subdistributions_blocks_iterator())
|
|
2001
|
+
[(['b', 'b', 'c', 'd', 'e'], [1, 1, 2, 2, 3])]
|
|
2002
|
+
|
|
2003
|
+
An example with overlapping minimal subdistributions::
|
|
2004
|
+
|
|
2005
|
+
sage: A = B = ["a", "b", "c", "d", "e"]
|
|
2006
|
+
sage: tau = {"a": 1, "b": 1, "c": 2, "d": 2, "e": 3}.get
|
|
2007
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
2008
|
+
sage: bij.set_distributions((["a", "b"], [1, 2]), (["a", "c", "d"], [1, 2, 3]))
|
|
2009
|
+
sage: sorted(bij.solutions_iterator(), key=lambda d: tuple(sorted(d.items())))
|
|
2010
|
+
[{'a': 1, 'b': 2, 'c': 2, 'd': 3, 'e': 1},
|
|
2011
|
+
{'a': 1, 'b': 2, 'c': 3, 'd': 2, 'e': 1},
|
|
2012
|
+
{'a': 2, 'b': 1, 'c': 1, 'd': 3, 'e': 2},
|
|
2013
|
+
{'a': 2, 'b': 1, 'c': 3, 'd': 1, 'e': 2}]
|
|
2014
|
+
sage: bij.constant_blocks(optimal=True)
|
|
2015
|
+
{{'a', 'e'}}
|
|
2016
|
+
sage: list(bij.minimal_subdistributions_blocks_iterator())
|
|
2017
|
+
[(['a', 'b'], [1, 2]), (['a', 'c', 'd'], [1, 2, 3])]
|
|
2018
|
+
|
|
2019
|
+
Fedor Petrov's example from https://mathoverflow.net/q/424187::
|
|
2020
|
+
|
|
2021
|
+
sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"]
|
|
2022
|
+
sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get
|
|
2023
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
2024
|
+
sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A])
|
|
2025
|
+
sage: d = [0]*8+[1]*4
|
|
2026
|
+
sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
|
|
2027
|
+
sage: sorted([s[a] for a in A] for s in bij.solutions_iterator())
|
|
2028
|
+
[[0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 1],
|
|
2029
|
+
[0, 1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
|
|
2030
|
+
[0, 1, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
|
|
2031
|
+
[0, 1, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0],
|
|
2032
|
+
[0, 1, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
|
|
2033
|
+
[1, 0, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0],
|
|
2034
|
+
[1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0, 1, 0, 0],
|
|
2035
|
+
[1, 0, 1, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 1, 0],
|
|
2036
|
+
[1, 0, 1, 0, 0, 0, 1, 0, 1, 0, 0, 0, 1, 0, 0],
|
|
2037
|
+
[1, 1, 0, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 1],
|
|
2038
|
+
[1, 1, 0, 0, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 1]]
|
|
2039
|
+
|
|
2040
|
+
sage: sorted(bij.minimal_subdistributions_blocks_iterator()) # random
|
|
2041
|
+
[(['a1', 'a2', 'a3', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a7', 'a8', 'a8'],
|
|
2042
|
+
[0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1]),
|
|
2043
|
+
(['a3', 'a4', 'd'], [0, 0, 1]),
|
|
2044
|
+
(['a7', 'a8', 'd'], [0, 0, 1])]
|
|
2045
|
+
|
|
2046
|
+
The following solution is not found, because it happens to
|
|
2047
|
+
have the same support as the other::
|
|
2048
|
+
|
|
2049
|
+
sage: D = set(A).difference(['b7', 'b8', 'd'])
|
|
2050
|
+
sage: sorted(a.replace("b", "a") for a in D)
|
|
2051
|
+
['a1', 'a2', 'a3', 'a3', 'a4', 'a4', 'a5', 'a5', 'a6', 'a6', 'a7', 'a8']
|
|
2052
|
+
sage: set(tuple(sorted(s[a] for a in D)) for s in bij.solutions_iterator())
|
|
2053
|
+
{(0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1)}
|
|
2054
|
+
|
|
2055
|
+
But it is, by design, included here::
|
|
2056
|
+
|
|
2057
|
+
sage: sorted(D) in [d for d, _ in bij.minimal_subdistributions_iterator()]
|
|
2058
|
+
True
|
|
2059
|
+
"""
|
|
2060
|
+
# see
|
|
2061
|
+
# https://mathoverflow.net/questions/406751/find-a-subdistribution/406975
|
|
2062
|
+
# and
|
|
2063
|
+
# https://gitlab.com/mantepse/bijection-tools/-/issues/29
|
|
2064
|
+
# see https://mathoverflow.net/q/424187 for Fedor Petrov's example
|
|
2065
|
+
|
|
2066
|
+
minimal_subdistribution = MixedIntegerLinearProgram(maximization=False, solver=self._solver)
|
|
2067
|
+
D = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the submultiset of elements
|
|
2068
|
+
X = minimal_subdistribution.new_variable(binary=True) # the support of D
|
|
2069
|
+
V = minimal_subdistribution.new_variable(integer=True, nonnegative=True) # the subdistribution
|
|
2070
|
+
P = _disjoint_set_roots(self._P)
|
|
2071
|
+
minimal_subdistribution.set_objective(sum(D[p] for p in P))
|
|
2072
|
+
minimal_subdistribution.add_constraint(sum(D[p] for p in P) >= 1)
|
|
2073
|
+
for p in P:
|
|
2074
|
+
minimal_subdistribution.add_constraint(D[p] <= len(self._P.root_to_elements_dict()[p]))
|
|
2075
|
+
minimal_subdistribution.add_constraint(X[p] * len(self._P.root_to_elements_dict()[p]) >= D[p] >= X[p])
|
|
2076
|
+
|
|
2077
|
+
def add_counter_example_constraint(s):
|
|
2078
|
+
for v in self._Z:
|
|
2079
|
+
minimal_subdistribution.add_constraint(sum(D[p] for p in P
|
|
2080
|
+
if s[p] == v) == V[v])
|
|
2081
|
+
|
|
2082
|
+
if self._bmilp is None:
|
|
2083
|
+
self._bmilp = _BijectionistMILP(self)
|
|
2084
|
+
|
|
2085
|
+
s = next(self._bmilp.solutions_iterator(True, []))
|
|
2086
|
+
add_counter_example_constraint(s)
|
|
2087
|
+
while True:
|
|
2088
|
+
try:
|
|
2089
|
+
minimal_subdistribution.solve()
|
|
2090
|
+
except MIPSolverException:
|
|
2091
|
+
return
|
|
2092
|
+
d = minimal_subdistribution.get_values(D, convert=ZZ, tolerance=0.1) # a dict from P to multiplicities
|
|
2093
|
+
new_s = self._find_counterexample(P, s, d, True)
|
|
2094
|
+
if new_s is None:
|
|
2095
|
+
yield ([p for p in P for _ in range(ZZ(d[p]))],
|
|
2096
|
+
self._sorter["Z"](s[p]
|
|
2097
|
+
for p in P
|
|
2098
|
+
for _ in range(ZZ(d[p]))))
|
|
2099
|
+
|
|
2100
|
+
support = [X[p] for p in P if d[p]]
|
|
2101
|
+
# add constraint that the support is different
|
|
2102
|
+
minimal_subdistribution.add_constraint(sum(support) <= len(support) - 1,
|
|
2103
|
+
name='veto')
|
|
2104
|
+
else:
|
|
2105
|
+
s = new_s
|
|
2106
|
+
add_counter_example_constraint(s)
|
|
2107
|
+
|
|
2108
|
+
def _preprocess_intertwining_relations(self):
|
|
2109
|
+
r"""
|
|
2110
|
+
Make ``self._P`` be the finest set partition coarser
|
|
2111
|
+
than ``self._P`` such that composing elements preserves
|
|
2112
|
+
blocks.
|
|
2113
|
+
|
|
2114
|
+
Suppose that `p_1`, `p_2` are blocks of `P`, and `a_1, a'_1
|
|
2115
|
+
\in p_1` and `a_2, a'_2\in p_2`. Then,
|
|
2116
|
+
|
|
2117
|
+
.. MATH:
|
|
2118
|
+
|
|
2119
|
+
s(\pi(a_1, a_2))
|
|
2120
|
+
= \rho(s(a_1), s(a_2))
|
|
2121
|
+
= \rho(s(a'_1), s(a'_2))
|
|
2122
|
+
= s(\pi(a'_1, a'_2)).
|
|
2123
|
+
|
|
2124
|
+
Therefore, `\pi(a_1, a_2)` and `\pi(a'_1, a'_2)` are in the
|
|
2125
|
+
same block.
|
|
2126
|
+
|
|
2127
|
+
In other words, `s(\pi(a_1,\dots,a_k))` only depends on the
|
|
2128
|
+
blocks of `a_1,\dots,a_k`.
|
|
2129
|
+
|
|
2130
|
+
In particular, if `P` consists only if singletons, this
|
|
2131
|
+
method has no effect.
|
|
2132
|
+
|
|
2133
|
+
.. TODO::
|
|
2134
|
+
|
|
2135
|
+
it is not clear whether this method makes sense
|
|
2136
|
+
|
|
2137
|
+
EXAMPLES::
|
|
2138
|
+
|
|
2139
|
+
sage: A = B = 'abcd'
|
|
2140
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
|
|
2141
|
+
sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
|
|
2142
|
+
sage: def rho(s1, s2): return (s1 + s2) % 2
|
|
2143
|
+
sage: bij.set_intertwining_relations((2, pi, rho))
|
|
2144
|
+
sage: bij._preprocess_intertwining_relations()
|
|
2145
|
+
sage: bij._P
|
|
2146
|
+
{{'a'}, {'b'}, {'c'}, {'d'}}
|
|
2147
|
+
|
|
2148
|
+
However, adding that ``'a'`` and ``'c'`` are in the same block,
|
|
2149
|
+
we can infer that also ``'b'`` and ``'d'`` are in the same
|
|
2150
|
+
block::
|
|
2151
|
+
|
|
2152
|
+
sage: bij.set_constant_blocks([['a', 'c']])
|
|
2153
|
+
sage: bij._P
|
|
2154
|
+
{{'a', 'c'}, {'b'}, {'d'}}
|
|
2155
|
+
sage: bij._preprocess_intertwining_relations()
|
|
2156
|
+
sage: bij._P
|
|
2157
|
+
{{'a', 'c'}, {'b', 'd'}}
|
|
2158
|
+
|
|
2159
|
+
Let a group act on permutations::
|
|
2160
|
+
|
|
2161
|
+
sage: A = B = Permutations(3)
|
|
2162
|
+
sage: bij = Bijectionist(A, B, lambda x: x[0])
|
|
2163
|
+
sage: bij.set_intertwining_relations((1, lambda pi: pi.reverse(), lambda z: z))
|
|
2164
|
+
sage: bij._preprocess_intertwining_relations()
|
|
2165
|
+
sage: bij._P
|
|
2166
|
+
{{[1, 2, 3]}, {[1, 3, 2]}, {[2, 1, 3]}, {[2, 3, 1]}, {[3, 1, 2]}, {[3, 2, 1]}}
|
|
2167
|
+
|
|
2168
|
+
Thus, in this case we do not detect the constant blocks::
|
|
2169
|
+
|
|
2170
|
+
sage: bij.constant_blocks(optimal=True)
|
|
2171
|
+
{{[1, 2, 3], [3, 2, 1]}, {[1, 3, 2], [2, 3, 1]}, {[2, 1, 3], [3, 1, 2]}}
|
|
2172
|
+
"""
|
|
2173
|
+
A = self._A
|
|
2174
|
+
P = self._P
|
|
2175
|
+
images = defaultdict(set) # A^k -> A, a_1,...,a_k +-> {pi(a_1,...,a_k) for all pi}
|
|
2176
|
+
for pi_rho in self._pi_rho:
|
|
2177
|
+
for a_tuple in itertools.product(*([A] * pi_rho.numargs)):
|
|
2178
|
+
if pi_rho.domain is not None and not pi_rho.domain(*a_tuple):
|
|
2179
|
+
continue
|
|
2180
|
+
a = pi_rho.pi(*a_tuple)
|
|
2181
|
+
if a in A:
|
|
2182
|
+
images[a_tuple].add(a)
|
|
2183
|
+
|
|
2184
|
+
# merge blocks
|
|
2185
|
+
something_changed = True
|
|
2186
|
+
while something_changed:
|
|
2187
|
+
something_changed = False
|
|
2188
|
+
# collect (preimage, image) pairs by (representatives) of
|
|
2189
|
+
# the blocks of the elements of the preimage
|
|
2190
|
+
updated_images = defaultdict(set) # (p_1,...,p_k) to {a_1,....}
|
|
2191
|
+
for a_tuple, image_set in images.items():
|
|
2192
|
+
representatives = tuple(P.find(a) for a in a_tuple)
|
|
2193
|
+
updated_images[representatives].update(image_set)
|
|
2194
|
+
|
|
2195
|
+
# merge blocks
|
|
2196
|
+
for a_tuple, image_set in updated_images.items():
|
|
2197
|
+
image = image_set.pop()
|
|
2198
|
+
while image_set:
|
|
2199
|
+
P.union(image, image_set.pop())
|
|
2200
|
+
something_changed = True
|
|
2201
|
+
# we keep a representative
|
|
2202
|
+
image_set.add(image)
|
|
2203
|
+
|
|
2204
|
+
images = updated_images
|
|
2205
|
+
|
|
2206
|
+
def solutions_iterator(self):
|
|
2207
|
+
r"""
|
|
2208
|
+
An iterator over all solutions of the problem.
|
|
2209
|
+
|
|
2210
|
+
OUTPUT: an iterator over all possible mappings `s: A\to Z`
|
|
2211
|
+
|
|
2212
|
+
ALGORITHM:
|
|
2213
|
+
|
|
2214
|
+
We solve an integer linear program with a binary variable
|
|
2215
|
+
`x_{p, z}` for each partition block `p\in P` and each
|
|
2216
|
+
statistic value `z\in Z`:
|
|
2217
|
+
|
|
2218
|
+
- `x_{p, z} = 1` if and only if `s(a) = z` for all `a\in p`.
|
|
2219
|
+
|
|
2220
|
+
Then we add the constraint `\sum_{x\in V} x<|V|`, where `V`
|
|
2221
|
+
is the set containing all `x` with `x = 1`, that is, those
|
|
2222
|
+
indicator variables representing the current solution.
|
|
2223
|
+
Therefore, a solution of this new program must be different
|
|
2224
|
+
from all those previously obtained.
|
|
2225
|
+
|
|
2226
|
+
INTEGER LINEAR PROGRAM:
|
|
2227
|
+
|
|
2228
|
+
* Let `m_w(p)`, for a block `p` of `P`, be the multiplicity
|
|
2229
|
+
of the value `w` in `W` under `\alpha`, that is, the number
|
|
2230
|
+
of elements `a \in p` with `\alpha(a)=w`.
|
|
2231
|
+
|
|
2232
|
+
* Let `n_w(z)` be the number of elements `b \in B` with
|
|
2233
|
+
`\beta(b)=w` and `\tau(b)=z` for `w \in W`, `z \in Z`.
|
|
2234
|
+
|
|
2235
|
+
* Let `k` be the arity of a pair `(\pi, \rho)` in an
|
|
2236
|
+
intertwining relation.
|
|
2237
|
+
|
|
2238
|
+
and the following constraints:
|
|
2239
|
+
|
|
2240
|
+
* because every block is assigned precisely one value, for
|
|
2241
|
+
all `p\in P`,
|
|
2242
|
+
|
|
2243
|
+
.. MATH::
|
|
2244
|
+
|
|
2245
|
+
\sum_z x_{p, z} = 1.
|
|
2246
|
+
|
|
2247
|
+
* because the statistics `s` and `\tau` and also `\alpha` and
|
|
2248
|
+
`\beta` are equidistributed, for all `w\in W` and `z\in Z`,
|
|
2249
|
+
|
|
2250
|
+
.. MATH::
|
|
2251
|
+
|
|
2252
|
+
\sum_p m_w(p) x_{p, z} = n_w(z).
|
|
2253
|
+
|
|
2254
|
+
* for each intertwining relation `s(\pi(a_1,\dots, a_k)) =
|
|
2255
|
+
\rho(s(a_1),\dots, s(a_r))`, and for all `k`-combinations
|
|
2256
|
+
of blocks `p_i\in P` such that there exist `(a_1,\dots,
|
|
2257
|
+
a_k)\in p_1\times\dots\times p_k` with `\pi(a_1,\dots,
|
|
2258
|
+
a_k)\in W` and `z = \rho(z_1,\dots, z_k)`,
|
|
2259
|
+
|
|
2260
|
+
.. MATH::
|
|
2261
|
+
|
|
2262
|
+
x_{p, z} \geq 1-k + \sum_{i=1}^k x_{p_i, z_i}.
|
|
2263
|
+
|
|
2264
|
+
* for each distribution restriction, i.e. a set of elements
|
|
2265
|
+
`\tilde A` and a distribution of values given by integers
|
|
2266
|
+
`d_z` representing the multiplicity of each `z \in Z`, and
|
|
2267
|
+
`r_p = |p \cap\tilde A|` indicating the relative size of
|
|
2268
|
+
block `p` in the set of elements of the distribution,
|
|
2269
|
+
|
|
2270
|
+
.. MATH::
|
|
2271
|
+
|
|
2272
|
+
\sum_p r_p x_{p, z} = d_z.
|
|
2273
|
+
|
|
2274
|
+
EXAMPLES::
|
|
2275
|
+
|
|
2276
|
+
sage: A = B = 'abc'
|
|
2277
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver='GLPK')
|
|
2278
|
+
sage: next(bij.solutions_iterator())
|
|
2279
|
+
{'a': 0, 'b': 1, 'c': 0}
|
|
2280
|
+
|
|
2281
|
+
sage: list(bij.solutions_iterator())
|
|
2282
|
+
[{'a': 0, 'b': 1, 'c': 0},
|
|
2283
|
+
{'a': 1, 'b': 0, 'c': 0},
|
|
2284
|
+
{'a': 0, 'b': 0, 'c': 1}]
|
|
2285
|
+
|
|
2286
|
+
sage: N = 4
|
|
2287
|
+
sage: A = B = [permutation for n in range(N) for permutation in Permutations(n)]
|
|
2288
|
+
|
|
2289
|
+
Let `\tau` be the number of non-left-to-right-maxima of a
|
|
2290
|
+
permutation::
|
|
2291
|
+
|
|
2292
|
+
sage: def tau(pi):
|
|
2293
|
+
....: pi = list(pi)
|
|
2294
|
+
....: i = count = 0
|
|
2295
|
+
....: for j in range(len(pi)):
|
|
2296
|
+
....: if pi[j] > i:
|
|
2297
|
+
....: i = pi[j]
|
|
2298
|
+
....: else:
|
|
2299
|
+
....: count += 1
|
|
2300
|
+
....: return count
|
|
2301
|
+
|
|
2302
|
+
We look for a statistic which is constant on conjugacy classes::
|
|
2303
|
+
|
|
2304
|
+
sage: P = [list(a) for n in range(N) for a in Permutations(n).conjugacy_classes()]
|
|
2305
|
+
|
|
2306
|
+
sage: bij = Bijectionist(A, B, tau, solver='GLPK')
|
|
2307
|
+
sage: bij.set_statistics((len, len))
|
|
2308
|
+
sage: bij.set_constant_blocks(P)
|
|
2309
|
+
sage: for solution in bij.solutions_iterator():
|
|
2310
|
+
....: print(solution)
|
|
2311
|
+
{[]: 0, [1]: 0, [1, 2]: 1, [2, 1]: 0, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2}
|
|
2312
|
+
{[]: 0, [1]: 0, [1, 2]: 0, [2, 1]: 1, [1, 2, 3]: 0, [1, 3, 2]: 1, [2, 1, 3]: 1, [3, 2, 1]: 1, [2, 3, 1]: 2, [3, 1, 2]: 2}
|
|
2313
|
+
|
|
2314
|
+
Changing or re-setting problem parameters clears the internal
|
|
2315
|
+
cache. Setting the verbosity prints the MILP which is solved.::
|
|
2316
|
+
|
|
2317
|
+
sage: set_verbose(2)
|
|
2318
|
+
sage: bij.set_constant_blocks(P)
|
|
2319
|
+
sage: _ = list(bij.solutions_iterator())
|
|
2320
|
+
Constraints are:
|
|
2321
|
+
block []: 1 <= x_0 <= 1
|
|
2322
|
+
block [1]: 1 <= x_1 <= 1
|
|
2323
|
+
block [1, 2]: 1 <= x_2 + x_3 <= 1
|
|
2324
|
+
block [2, 1]: 1 <= x_4 + x_5 <= 1
|
|
2325
|
+
block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
|
|
2326
|
+
block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
|
|
2327
|
+
block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
|
|
2328
|
+
statistics: 1 <= x_0 <= 1
|
|
2329
|
+
statistics: 0 <= <= 0
|
|
2330
|
+
statistics: 0 <= <= 0
|
|
2331
|
+
statistics: 1 <= x_1 <= 1
|
|
2332
|
+
statistics: 0 <= <= 0
|
|
2333
|
+
statistics: 0 <= <= 0
|
|
2334
|
+
statistics: 1 <= x_2 + x_4 <= 1
|
|
2335
|
+
statistics: 1 <= x_3 + x_5 <= 1
|
|
2336
|
+
statistics: 0 <= <= 0
|
|
2337
|
+
statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
|
|
2338
|
+
statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
|
|
2339
|
+
statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
|
|
2340
|
+
Variables are:
|
|
2341
|
+
x_0: s([]) = 0
|
|
2342
|
+
x_1: s([1]) = 0
|
|
2343
|
+
x_2: s([1, 2]) = 0
|
|
2344
|
+
x_3: s([1, 2]) = 1
|
|
2345
|
+
x_4: s([2, 1]) = 0
|
|
2346
|
+
x_5: s([2, 1]) = 1
|
|
2347
|
+
x_6: s([1, 2, 3]) = 0
|
|
2348
|
+
x_7: s([1, 2, 3]) = 1
|
|
2349
|
+
x_8: s([1, 2, 3]) = 2
|
|
2350
|
+
x_9: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 0
|
|
2351
|
+
x_10: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 1
|
|
2352
|
+
x_11: s([1, 3, 2]) = s([2, 1, 3]) = s([3, 2, 1]) = 2
|
|
2353
|
+
x_12: s([2, 3, 1]) = s([3, 1, 2]) = 0
|
|
2354
|
+
x_13: s([2, 3, 1]) = s([3, 1, 2]) = 1
|
|
2355
|
+
x_14: s([2, 3, 1]) = s([3, 1, 2]) = 2
|
|
2356
|
+
after vetoing
|
|
2357
|
+
Constraints are:
|
|
2358
|
+
block []: 1 <= x_0 <= 1
|
|
2359
|
+
block [1]: 1 <= x_1 <= 1
|
|
2360
|
+
block [1, 2]: 1 <= x_2 + x_3 <= 1
|
|
2361
|
+
block [2, 1]: 1 <= x_4 + x_5 <= 1
|
|
2362
|
+
block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
|
|
2363
|
+
block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
|
|
2364
|
+
block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
|
|
2365
|
+
statistics: 1 <= x_0 <= 1
|
|
2366
|
+
statistics: 0 <= <= 0
|
|
2367
|
+
statistics: 0 <= <= 0
|
|
2368
|
+
statistics: 1 <= x_1 <= 1
|
|
2369
|
+
statistics: 0 <= <= 0
|
|
2370
|
+
statistics: 0 <= <= 0
|
|
2371
|
+
statistics: 1 <= x_2 + x_4 <= 1
|
|
2372
|
+
statistics: 1 <= x_3 + x_5 <= 1
|
|
2373
|
+
statistics: 0 <= <= 0
|
|
2374
|
+
statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
|
|
2375
|
+
statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
|
|
2376
|
+
statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
|
|
2377
|
+
veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6
|
|
2378
|
+
after vetoing
|
|
2379
|
+
Constraints are:
|
|
2380
|
+
block []: 1 <= x_0 <= 1
|
|
2381
|
+
block [1]: 1 <= x_1 <= 1
|
|
2382
|
+
block [1, 2]: 1 <= x_2 + x_3 <= 1
|
|
2383
|
+
block [2, 1]: 1 <= x_4 + x_5 <= 1
|
|
2384
|
+
block [1, 2, 3]: 1 <= x_6 + x_7 + x_8 <= 1
|
|
2385
|
+
block [1, 3, 2]: 1 <= x_9 + x_10 + x_11 <= 1
|
|
2386
|
+
block [2, 3, 1]: 1 <= x_12 + x_13 + x_14 <= 1
|
|
2387
|
+
statistics: 1 <= x_0 <= 1
|
|
2388
|
+
statistics: 0 <= <= 0
|
|
2389
|
+
statistics: 0 <= <= 0
|
|
2390
|
+
statistics: 1 <= x_1 <= 1
|
|
2391
|
+
statistics: 0 <= <= 0
|
|
2392
|
+
statistics: 0 <= <= 0
|
|
2393
|
+
statistics: 1 <= x_2 + x_4 <= 1
|
|
2394
|
+
statistics: 1 <= x_3 + x_5 <= 1
|
|
2395
|
+
statistics: 0 <= <= 0
|
|
2396
|
+
statistics: 1 <= x_6 + 3 x_9 + 2 x_12 <= 1
|
|
2397
|
+
statistics: 3 <= x_7 + 3 x_10 + 2 x_13 <= 3
|
|
2398
|
+
statistics: 2 <= x_8 + 3 x_11 + 2 x_14 <= 2
|
|
2399
|
+
veto: x_0 + x_1 + x_3 + x_4 + x_6 + x_10 + x_14 <= 6
|
|
2400
|
+
veto: x_0 + x_1 + x_2 + x_5 + x_6 + x_10 + x_14 <= 6
|
|
2401
|
+
|
|
2402
|
+
sage: set_verbose(0)
|
|
2403
|
+
|
|
2404
|
+
TESTS:
|
|
2405
|
+
|
|
2406
|
+
An unfeasible problem::
|
|
2407
|
+
|
|
2408
|
+
sage: A = ["a", "b", "c", "d"]; B = [1, 2, 3, 4]
|
|
2409
|
+
sage: bij = Bijectionist(A, B)
|
|
2410
|
+
sage: bij.set_value_restrictions(("a", [1, 2]), ("b", [1, 2]), ("c", [1, 3]), ("d", [2, 3]))
|
|
2411
|
+
sage: list(bij.solutions_iterator())
|
|
2412
|
+
[]
|
|
2413
|
+
|
|
2414
|
+
Testing interactions between multiple instances using Fedor Petrov's example from https://mathoverflow.net/q/424187::
|
|
2415
|
+
|
|
2416
|
+
sage: A = B = ["a"+str(i) for i in range(1, 9)] + ["b"+str(i) for i in range(3, 9)] + ["d"]
|
|
2417
|
+
sage: tau = {b: 0 if i < 10 else 1 for i, b in enumerate(B)}.get
|
|
2418
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
2419
|
+
sage: bij.set_constant_blocks([["a"+str(i), "b"+str(i)] for i in range(1, 9) if "b"+str(i) in A])
|
|
2420
|
+
sage: d = [0]*8+[1]*4
|
|
2421
|
+
sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
|
|
2422
|
+
sage: iterator1 = bij.solutions_iterator()
|
|
2423
|
+
sage: iterator2 = bij.solutions_iterator()
|
|
2424
|
+
|
|
2425
|
+
Generate a solution in iterator1, iterator2 should generate the same solution and vice versa::
|
|
2426
|
+
|
|
2427
|
+
sage: s1_1 = next(iterator1)
|
|
2428
|
+
sage: s2_1 = next(iterator2)
|
|
2429
|
+
sage: s1_1 == s2_1
|
|
2430
|
+
True
|
|
2431
|
+
sage: s2_2 = next(iterator2)
|
|
2432
|
+
sage: s1_2 = next(iterator1)
|
|
2433
|
+
sage: s1_2 == s2_2
|
|
2434
|
+
True
|
|
2435
|
+
|
|
2436
|
+
Re-setting the distribution resets the cache, so a new
|
|
2437
|
+
iterator will generate the first solutions again, but the old
|
|
2438
|
+
iterator continues::
|
|
2439
|
+
|
|
2440
|
+
sage: bij.set_distributions((A[:8] + A[8+2:-1], d), (A[:8] + A[8:-3], d))
|
|
2441
|
+
sage: iterator3 = bij.solutions_iterator()
|
|
2442
|
+
|
|
2443
|
+
sage: s3_1 = next(iterator3)
|
|
2444
|
+
sage: s1_1 == s3_1
|
|
2445
|
+
True
|
|
2446
|
+
|
|
2447
|
+
sage: s1_3 = next(iterator1)
|
|
2448
|
+
sage: len(set([tuple(sorted(s.items())) for s in [s1_1, s1_2, s1_3]]))
|
|
2449
|
+
3
|
|
2450
|
+
"""
|
|
2451
|
+
if self._bmilp is None:
|
|
2452
|
+
self._bmilp = _BijectionistMILP(self)
|
|
2453
|
+
yield from self._bmilp.solutions_iterator(False, [])
|
|
2454
|
+
|
|
2455
|
+
|
|
2456
|
+
class _BijectionistMILP:
|
|
2457
|
+
r"""
|
|
2458
|
+
Wrapper class for the MixedIntegerLinearProgram (MILP).
|
|
2459
|
+
|
|
2460
|
+
This class is used to manage the MILP, add constraints, solve the
|
|
2461
|
+
problem and check for uniqueness of solution values.
|
|
2462
|
+
"""
|
|
2463
|
+
def __init__(self, bijectionist: Bijectionist, solutions=None):
|
|
2464
|
+
r"""
|
|
2465
|
+
Initialize the mixed integer linear program.
|
|
2466
|
+
|
|
2467
|
+
INPUT:
|
|
2468
|
+
|
|
2469
|
+
- ``bijectionist`` -- an instance of :class:`Bijectionist`
|
|
2470
|
+
|
|
2471
|
+
- ``solutions`` -- (optional) a list of solutions of the
|
|
2472
|
+
problem, each provided as a dictionary mapping `(a, z)` to
|
|
2473
|
+
a boolean, such that at least one element from each block
|
|
2474
|
+
of `P` appears as `a`.
|
|
2475
|
+
|
|
2476
|
+
.. TODO::
|
|
2477
|
+
|
|
2478
|
+
it might be cleaner not to pass the full bijectionist
|
|
2479
|
+
instance, but only those attributes we actually use
|
|
2480
|
+
|
|
2481
|
+
TESTS::
|
|
2482
|
+
|
|
2483
|
+
sage: A = B = ["a", "b", "c", "d"]
|
|
2484
|
+
sage: bij = Bijectionist(A, B)
|
|
2485
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2486
|
+
sage: _BijectionistMILP(bij)
|
|
2487
|
+
<sage.combinat.bijectionist._BijectionistMILP object at ...>
|
|
2488
|
+
"""
|
|
2489
|
+
# the attributes of the bijectionist class we actually use:
|
|
2490
|
+
# _possible_block_values
|
|
2491
|
+
# _elements_distributions
|
|
2492
|
+
# _W, _Z, _A, _B, _P, _alpha, _beta, _tau, _pi_rho, _phi_psi
|
|
2493
|
+
self._bijectionist = bijectionist
|
|
2494
|
+
# the variables of the MILP are indexed by pairs (p, z), for
|
|
2495
|
+
# p in _P and z an element of _posible_block_values[p].
|
|
2496
|
+
# Thus, _P and _posible_block_values have to be fixed before
|
|
2497
|
+
# creating the MILP.
|
|
2498
|
+
bijectionist._preprocess_intertwining_relations()
|
|
2499
|
+
bijectionist._compute_possible_block_values()
|
|
2500
|
+
|
|
2501
|
+
self.milp = MixedIntegerLinearProgram(solver=bijectionist._solver)
|
|
2502
|
+
self.milp.set_objective(None)
|
|
2503
|
+
indices = [(p, z)
|
|
2504
|
+
for p, tZ in bijectionist._possible_block_values.items()
|
|
2505
|
+
for z in tZ]
|
|
2506
|
+
self._x = self.milp.new_variable(binary=True, indices=indices)
|
|
2507
|
+
|
|
2508
|
+
tZ = bijectionist._possible_block_values
|
|
2509
|
+
P = bijectionist._P
|
|
2510
|
+
for p in _disjoint_set_roots(P):
|
|
2511
|
+
self.milp.add_constraint(sum(self._x[p, z] for z in tZ[p]) == 1,
|
|
2512
|
+
name=f"block {p}"[:50])
|
|
2513
|
+
self.add_alpha_beta_constraints()
|
|
2514
|
+
self.add_distribution_constraints()
|
|
2515
|
+
self.add_quadratic_relation_constraints()
|
|
2516
|
+
self.add_homomesic_constraints()
|
|
2517
|
+
self.add_intertwining_relation_constraints()
|
|
2518
|
+
if get_verbose() >= 2:
|
|
2519
|
+
self.show()
|
|
2520
|
+
|
|
2521
|
+
self._solution_cache = []
|
|
2522
|
+
if solutions is not None:
|
|
2523
|
+
for solution in solutions:
|
|
2524
|
+
self._add_solution({(P.find(a), z): value
|
|
2525
|
+
for (a, z), value in solution.items()})
|
|
2526
|
+
|
|
2527
|
+
def show(self, variables=True):
|
|
2528
|
+
r"""
|
|
2529
|
+
Print the constraints and variables of the MILP together
|
|
2530
|
+
with some explanations.
|
|
2531
|
+
|
|
2532
|
+
EXAMPLES::
|
|
2533
|
+
|
|
2534
|
+
sage: A = B = ["a", "b", "c"]
|
|
2535
|
+
sage: bij = Bijectionist(A, B, lambda x: A.index(x) % 2, solver='GLPK')
|
|
2536
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
2537
|
+
sage: next(bij.solutions_iterator())
|
|
2538
|
+
{'a': 0, 'b': 0, 'c': 1}
|
|
2539
|
+
sage: bij._bmilp.show()
|
|
2540
|
+
Constraints are:
|
|
2541
|
+
block a: 1 <= x_0 + x_1 <= 1
|
|
2542
|
+
block c: 1 <= x_2 + x_3 <= 1
|
|
2543
|
+
statistics: 2 <= 2 x_0 + x_2 <= 2
|
|
2544
|
+
statistics: 1 <= 2 x_1 + x_3 <= 1
|
|
2545
|
+
veto: x_0 + x_3 <= 1
|
|
2546
|
+
Variables are:
|
|
2547
|
+
x_0: s(a) = s(b) = 0
|
|
2548
|
+
x_1: s(a) = s(b) = 1
|
|
2549
|
+
x_2: s(c) = 0
|
|
2550
|
+
x_3: s(c) = 1
|
|
2551
|
+
"""
|
|
2552
|
+
print("Constraints are:")
|
|
2553
|
+
b = self.milp.get_backend()
|
|
2554
|
+
varid_name = {}
|
|
2555
|
+
for i in range(b.ncols()):
|
|
2556
|
+
s = b.col_name(i)
|
|
2557
|
+
default_name = str(self.milp.linear_functions_parent()({i: 1}))
|
|
2558
|
+
if s and s != default_name:
|
|
2559
|
+
varid_name[i] = s
|
|
2560
|
+
else:
|
|
2561
|
+
varid_name[i] = default_name
|
|
2562
|
+
for i, (lb, (indices, values), ub) in enumerate(self.milp.constraints()):
|
|
2563
|
+
if b.row_name(i):
|
|
2564
|
+
print(" " + b.row_name(i) + ":", end=" ")
|
|
2565
|
+
if lb is not None:
|
|
2566
|
+
print(str(ZZ(lb)) + " <=", end=" ")
|
|
2567
|
+
first = True
|
|
2568
|
+
for j, c in sorted(zip(indices, values)):
|
|
2569
|
+
c = ZZ(c)
|
|
2570
|
+
if c == 0:
|
|
2571
|
+
continue
|
|
2572
|
+
print((("+ " if (not first and c > 0) else "") +
|
|
2573
|
+
("" if c == 1 else
|
|
2574
|
+
("- " if c == -1 else
|
|
2575
|
+
(str(c) + " " if first and c < 0 else
|
|
2576
|
+
("- " + str(abs(c)) + " " if c < 0 else str(c) + " "))))
|
|
2577
|
+
+ varid_name[j]), end=" ")
|
|
2578
|
+
first = False
|
|
2579
|
+
# Upper bound
|
|
2580
|
+
print("<= " + str(ZZ(ub)) if ub is not None else "")
|
|
2581
|
+
|
|
2582
|
+
if variables:
|
|
2583
|
+
print("Variables are:")
|
|
2584
|
+
P = self._bijectionist._P.root_to_elements_dict()
|
|
2585
|
+
for (p, z), v in self._x.items():
|
|
2586
|
+
print(f" {v}: " + "".join([f"s({a}) = "
|
|
2587
|
+
for a in P[p]]) + f"{z}")
|
|
2588
|
+
|
|
2589
|
+
def _prepare_solution(self, on_blocks, solution):
|
|
2590
|
+
r"""
|
|
2591
|
+
Return the solution as a dictionary from `A` (or `P`) to
|
|
2592
|
+
`Z`.
|
|
2593
|
+
|
|
2594
|
+
INPUT:
|
|
2595
|
+
|
|
2596
|
+
- ``on_blocks`` -- whether to return the solution on blocks
|
|
2597
|
+
or on all elements
|
|
2598
|
+
|
|
2599
|
+
TESTS::
|
|
2600
|
+
|
|
2601
|
+
sage: A = B = ["a", "b", "c"]
|
|
2602
|
+
sage: bij = Bijectionist(A, B, lambda x: 0)
|
|
2603
|
+
sage: bij.set_constant_blocks([["a", "b"]])
|
|
2604
|
+
sage: next(bij.solutions_iterator())
|
|
2605
|
+
{'a': 0, 'b': 0, 'c': 0}
|
|
2606
|
+
sage: bmilp = bij._bmilp
|
|
2607
|
+
sage: bmilp._prepare_solution(True, bmilp._solution_cache[0])
|
|
2608
|
+
{'a': 0, 'c': 0}
|
|
2609
|
+
"""
|
|
2610
|
+
P = self._bijectionist._P
|
|
2611
|
+
tZ = self._bijectionist._possible_block_values
|
|
2612
|
+
mapping = {} # A -> Z or P -> Z, a +-> s(a)
|
|
2613
|
+
for p, block in P.root_to_elements_dict().items():
|
|
2614
|
+
for z in tZ[p]:
|
|
2615
|
+
if solution[p, z] == 1:
|
|
2616
|
+
if on_blocks:
|
|
2617
|
+
mapping[p] = z
|
|
2618
|
+
else:
|
|
2619
|
+
for a in block:
|
|
2620
|
+
mapping[a] = z
|
|
2621
|
+
break
|
|
2622
|
+
return mapping
|
|
2623
|
+
|
|
2624
|
+
def solutions_iterator(self, on_blocks, additional_constraints):
|
|
2625
|
+
r"""
|
|
2626
|
+
Iterate over all solutions satisfying the additional constraints.
|
|
2627
|
+
|
|
2628
|
+
INPUT:
|
|
2629
|
+
|
|
2630
|
+
- ``additional_constraints`` -- list of constraints for the
|
|
2631
|
+
underlying MILP
|
|
2632
|
+
|
|
2633
|
+
- ``on_blocks`` -- whether to return the solution on blocks or
|
|
2634
|
+
on all elements
|
|
2635
|
+
|
|
2636
|
+
TESTS::
|
|
2637
|
+
|
|
2638
|
+
sage: A = B = 'abc'
|
|
2639
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2, solver='GLPK')
|
|
2640
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2641
|
+
sage: bmilp = _BijectionistMILP(bij)
|
|
2642
|
+
sage: it = bmilp.solutions_iterator(False, [])
|
|
2643
|
+
sage: it2 = bmilp.solutions_iterator(False, [bmilp._x[('c', 1)] == 1])
|
|
2644
|
+
sage: next(it)
|
|
2645
|
+
{'a': 0, 'b': 1, 'c': 0}
|
|
2646
|
+
sage: next(it2)
|
|
2647
|
+
{'a': 0, 'b': 0, 'c': 1}
|
|
2648
|
+
sage: next(it)
|
|
2649
|
+
{'a': 0, 'b': 0, 'c': 1}
|
|
2650
|
+
sage: next(it)
|
|
2651
|
+
{'a': 1, 'b': 0, 'c': 0}
|
|
2652
|
+
"""
|
|
2653
|
+
i = 0 # the first unconsidered element of _solution_cache
|
|
2654
|
+
while True:
|
|
2655
|
+
# skip solutions which do not satisfy additional_constraints
|
|
2656
|
+
while i < len(self._solution_cache):
|
|
2657
|
+
solution = self._solution_cache[i]
|
|
2658
|
+
i += 1
|
|
2659
|
+
if all(self._is_solution(constraint, solution)
|
|
2660
|
+
for constraint in additional_constraints):
|
|
2661
|
+
yield self._prepare_solution(on_blocks, solution)
|
|
2662
|
+
break
|
|
2663
|
+
else:
|
|
2664
|
+
new_indices = []
|
|
2665
|
+
for constraint in additional_constraints:
|
|
2666
|
+
new_indices.extend(self.milp.add_constraint(constraint,
|
|
2667
|
+
return_indices=True))
|
|
2668
|
+
try:
|
|
2669
|
+
self.milp.solve()
|
|
2670
|
+
# moving this out of the try...finally block breaks SCIP
|
|
2671
|
+
solution = self.milp.get_values(self._x,
|
|
2672
|
+
convert=bool, tolerance=0.1)
|
|
2673
|
+
except MIPSolverException:
|
|
2674
|
+
return
|
|
2675
|
+
finally:
|
|
2676
|
+
b = self.milp.get_backend()
|
|
2677
|
+
if hasattr(b, "_get_model"):
|
|
2678
|
+
m = b._get_model()
|
|
2679
|
+
if m.getStatus() != 'unknown':
|
|
2680
|
+
m.freeTransform()
|
|
2681
|
+
self.milp.remove_constraints(new_indices)
|
|
2682
|
+
|
|
2683
|
+
self._add_solution(solution)
|
|
2684
|
+
i += 1
|
|
2685
|
+
assert i == len(self._solution_cache)
|
|
2686
|
+
yield self._prepare_solution(on_blocks, solution)
|
|
2687
|
+
if get_verbose() >= 2:
|
|
2688
|
+
print("after vetoing")
|
|
2689
|
+
self.show(variables=False)
|
|
2690
|
+
|
|
2691
|
+
def _add_solution(self, solution):
|
|
2692
|
+
r"""
|
|
2693
|
+
Add the ``solution`` to the cache and an appropriate
|
|
2694
|
+
veto constraint to the MILP.
|
|
2695
|
+
|
|
2696
|
+
INPUT:
|
|
2697
|
+
|
|
2698
|
+
- ``solution`` -- dictionary from the indices of the MILP to
|
|
2699
|
+
a boolean
|
|
2700
|
+
|
|
2701
|
+
EXAMPLES::
|
|
2702
|
+
|
|
2703
|
+
sage: A = B = ["a", "b"]
|
|
2704
|
+
sage: bij = Bijectionist(A, B)
|
|
2705
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2706
|
+
sage: bmilp = _BijectionistMILP(bij)
|
|
2707
|
+
sage: bmilp._add_solution({(a, b): a == b for a in A for b in B})
|
|
2708
|
+
sage: bmilp.show() # random
|
|
2709
|
+
Constraints are:
|
|
2710
|
+
block a: 1 <= x_0 + x_1 <= 1
|
|
2711
|
+
block b: 1 <= x_2 + x_3 <= 1
|
|
2712
|
+
statistics: 1 <= x_1 + x_3 <= 1
|
|
2713
|
+
statistics: 1 <= x_0 + x_2 <= 1
|
|
2714
|
+
veto: x_1 + x_2 <= 1
|
|
2715
|
+
Variables are:
|
|
2716
|
+
x_0: s(a) = b
|
|
2717
|
+
x_1: s(a) = a
|
|
2718
|
+
x_2: s(b) = b
|
|
2719
|
+
x_3: s(b) = a
|
|
2720
|
+
"""
|
|
2721
|
+
active_vars = [self._x[p, z]
|
|
2722
|
+
for p in _disjoint_set_roots(self._bijectionist._P)
|
|
2723
|
+
for z in self._bijectionist._possible_block_values[p]
|
|
2724
|
+
if solution[(p, z)]]
|
|
2725
|
+
self.milp.add_constraint(sum(active_vars) <= len(active_vars) - 1,
|
|
2726
|
+
name='veto')
|
|
2727
|
+
self._solution_cache.append(solution)
|
|
2728
|
+
|
|
2729
|
+
def _is_solution(self, constraint, values):
|
|
2730
|
+
r"""
|
|
2731
|
+
Evaluate the given function at the given values.
|
|
2732
|
+
|
|
2733
|
+
INPUT:
|
|
2734
|
+
|
|
2735
|
+
- ``constraint`` -- a
|
|
2736
|
+
:class:`sage.numerical.linear_functions.LinearConstraint`
|
|
2737
|
+
|
|
2738
|
+
- ``values`` -- a candidate for a solution of the MILP as a
|
|
2739
|
+
dictionary from pairs `(a, z)\in A\times Z` to `0` or `1`,
|
|
2740
|
+
specifying whether `a` is mapped to `z`.
|
|
2741
|
+
|
|
2742
|
+
EXAMPLES::
|
|
2743
|
+
|
|
2744
|
+
sage: A = B = ["a", "b"]
|
|
2745
|
+
sage: bij = Bijectionist(A, B)
|
|
2746
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2747
|
+
sage: bmilp = _BijectionistMILP(bij)
|
|
2748
|
+
sage: f = bmilp._x["a", "a"] + bmilp._x["b", "a"] >= bmilp._x["b", "b"] + 1
|
|
2749
|
+
sage: v = {('a', 'a'): 1, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1}
|
|
2750
|
+
sage: bmilp._is_solution(f, v)
|
|
2751
|
+
True
|
|
2752
|
+
sage: v = {('a', 'a'): 0, ('a', 'b'): 0, ('b', 'a'): 1, ('b', 'b'): 1}
|
|
2753
|
+
sage: bmilp._is_solution(f, v)
|
|
2754
|
+
False
|
|
2755
|
+
"""
|
|
2756
|
+
index_block_value_dict = {}
|
|
2757
|
+
for (p, z), v in self._x.items():
|
|
2758
|
+
variable_index = next(iter(v.dict()))
|
|
2759
|
+
index_block_value_dict[variable_index] = (p, z)
|
|
2760
|
+
|
|
2761
|
+
def evaluate(f):
|
|
2762
|
+
return sum(coeff if index == -1 else
|
|
2763
|
+
coeff * values[index_block_value_dict[index]]
|
|
2764
|
+
for index, coeff in f.dict().items())
|
|
2765
|
+
|
|
2766
|
+
for lhs, rhs in constraint.equations():
|
|
2767
|
+
if evaluate(lhs - rhs):
|
|
2768
|
+
return False
|
|
2769
|
+
for lhs, rhs in constraint.inequalities():
|
|
2770
|
+
if evaluate(lhs - rhs) > 0:
|
|
2771
|
+
return False
|
|
2772
|
+
return True
|
|
2773
|
+
|
|
2774
|
+
def add_alpha_beta_constraints(self):
|
|
2775
|
+
r"""
|
|
2776
|
+
Add constraints enforcing that `(alpha, s)` is equidistributed
|
|
2777
|
+
with `(beta, tau)` and `S` is the intertwining bijection.
|
|
2778
|
+
|
|
2779
|
+
We do this by adding
|
|
2780
|
+
|
|
2781
|
+
.. MATH::
|
|
2782
|
+
|
|
2783
|
+
\sum_{a\in A, z\in Z} x_{p(a), z} s^z t^{\alpha(a)}
|
|
2784
|
+
= \sum_{b\in B} s^{\tau(b)} t(\beta(b))
|
|
2785
|
+
|
|
2786
|
+
as a matrix equation.
|
|
2787
|
+
|
|
2788
|
+
EXAMPLES::
|
|
2789
|
+
|
|
2790
|
+
sage: A = B = [permutation for n in range(3) for permutation in Permutations(n)]
|
|
2791
|
+
sage: bij = Bijectionist(A, B, len)
|
|
2792
|
+
sage: bij.set_statistics((len, len))
|
|
2793
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2794
|
+
sage: bmilp = _BijectionistMILP(bij) # indirect doctest
|
|
2795
|
+
sage: next(bmilp.solutions_iterator(False, []))
|
|
2796
|
+
{[]: 0, [1]: 1, [1, 2]: 2, [2, 1]: 2}
|
|
2797
|
+
"""
|
|
2798
|
+
W = self._bijectionist._W
|
|
2799
|
+
Z = self._bijectionist._Z
|
|
2800
|
+
zero = self.milp.linear_functions_parent().zero()
|
|
2801
|
+
AZ_matrix = [[zero] * len(W) for _ in range(len(Z))]
|
|
2802
|
+
B_matrix = [[zero] * len(W) for _ in range(len(Z))]
|
|
2803
|
+
|
|
2804
|
+
W_dict = {w: i for i, w in enumerate(W)}
|
|
2805
|
+
Z_dict = {z: i for i, z in enumerate(Z)}
|
|
2806
|
+
|
|
2807
|
+
for a in self._bijectionist._A:
|
|
2808
|
+
p = self._bijectionist._P.find(a)
|
|
2809
|
+
for z in self._bijectionist._possible_block_values[p]:
|
|
2810
|
+
w_index = W_dict[self._bijectionist._alpha(a)]
|
|
2811
|
+
z_index = Z_dict[z]
|
|
2812
|
+
AZ_matrix[z_index][w_index] += self._x[p, z]
|
|
2813
|
+
|
|
2814
|
+
for b in self._bijectionist._B:
|
|
2815
|
+
w_index = W_dict[self._bijectionist._beta(b)]
|
|
2816
|
+
z_index = Z_dict[self._bijectionist._tau[b]]
|
|
2817
|
+
B_matrix[z_index][w_index] += 1
|
|
2818
|
+
|
|
2819
|
+
for w in range(len(W)):
|
|
2820
|
+
for z in range(len(Z)):
|
|
2821
|
+
self.milp.add_constraint(AZ_matrix[z][w] == B_matrix[z][w],
|
|
2822
|
+
name='statistics')
|
|
2823
|
+
|
|
2824
|
+
def add_distribution_constraints(self):
|
|
2825
|
+
r"""
|
|
2826
|
+
Add constraints so the distributions given by
|
|
2827
|
+
:meth:`set_distributions` are fulfilled.
|
|
2828
|
+
|
|
2829
|
+
To accomplish this we add
|
|
2830
|
+
|
|
2831
|
+
.. MATH::
|
|
2832
|
+
|
|
2833
|
+
\sum_{a\in\tilde A} x_{p(a), z}t^z = \sum_{z\in\tilde Z} t^z,
|
|
2834
|
+
|
|
2835
|
+
where `p(a)` is the block containing `a`, for each given
|
|
2836
|
+
distribution as a vector equation.
|
|
2837
|
+
|
|
2838
|
+
EXAMPLES::
|
|
2839
|
+
|
|
2840
|
+
sage: A = B = Permutations(3)
|
|
2841
|
+
sage: tau = Permutation.longest_increasing_subsequence_length
|
|
2842
|
+
sage: bij = Bijectionist(A, B, tau)
|
|
2843
|
+
sage: bij.set_distributions(([Permutation([1, 2, 3]), Permutation([1, 3, 2])], [1, 3]))
|
|
2844
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2845
|
+
sage: bmilp = _BijectionistMILP(bij) # indirect doctest
|
|
2846
|
+
sage: next(bmilp.solutions_iterator(False, []))
|
|
2847
|
+
{[1, 2, 3]: 3,
|
|
2848
|
+
[1, 3, 2]: 1,
|
|
2849
|
+
[2, 1, 3]: 2,
|
|
2850
|
+
[2, 3, 1]: 2,
|
|
2851
|
+
[3, 1, 2]: 2,
|
|
2852
|
+
[3, 2, 1]: 2}
|
|
2853
|
+
"""
|
|
2854
|
+
Z = self._bijectionist._Z
|
|
2855
|
+
Z_dict = {z: i for i, z in enumerate(Z)}
|
|
2856
|
+
zero = self.milp.linear_functions_parent().zero()
|
|
2857
|
+
for tA, tZ in self._bijectionist._elements_distributions:
|
|
2858
|
+
tA_sum = [zero] * len(Z_dict)
|
|
2859
|
+
tZ_sum = [zero] * len(Z_dict)
|
|
2860
|
+
for a in tA:
|
|
2861
|
+
p = self._bijectionist._P.find(a)
|
|
2862
|
+
for z in self._bijectionist._possible_block_values[p]:
|
|
2863
|
+
tA_sum[Z_dict[z]] += self._x[p, z]
|
|
2864
|
+
for z in tZ:
|
|
2865
|
+
tZ_sum[Z_dict[z]] += 1
|
|
2866
|
+
|
|
2867
|
+
for a, z in zip(tA_sum, tZ_sum):
|
|
2868
|
+
self.milp.add_constraint(a == z, name=f"d: {a} == {z}")
|
|
2869
|
+
|
|
2870
|
+
def add_intertwining_relation_constraints(self):
|
|
2871
|
+
r"""
|
|
2872
|
+
Add constraints corresponding to the given intertwining
|
|
2873
|
+
relations.
|
|
2874
|
+
|
|
2875
|
+
INPUT:
|
|
2876
|
+
|
|
2877
|
+
- origins, a list of triples `((\pi/\rho, p,
|
|
2878
|
+
(p_1,\dots,p_k))`, where `p` is the block of
|
|
2879
|
+
`\rho(s(a_1),\dots, s(a_k))`, for any `a_i\in p_i`.
|
|
2880
|
+
|
|
2881
|
+
This adds the constraints imposed by
|
|
2882
|
+
:meth:`set_intertwining_relations`,
|
|
2883
|
+
|
|
2884
|
+
.. MATH::
|
|
2885
|
+
|
|
2886
|
+
s(\pi(a_1,\dots, a_k)) = \rho(s(a_1),\dots, s(a_k))
|
|
2887
|
+
|
|
2888
|
+
for each pair `(\pi, \rho)`. The relation implies
|
|
2889
|
+
immediately that `s(\pi(a_1,\dots, a_k))` only depends on the
|
|
2890
|
+
blocks of `a_1,\dots, a_k`.
|
|
2891
|
+
|
|
2892
|
+
The MILP formulation is as follows. Let `a_1,\dots,a_k \in
|
|
2893
|
+
A` and let `a = \pi(a_1,\dots,a_k)`. Let `z_1,\dots,z_k \in
|
|
2894
|
+
Z` and let `z = \rho(z_1,\dots,z_k)`. Suppose that `a_i\in
|
|
2895
|
+
p_i` for all `i` and that `a\in p`.
|
|
2896
|
+
|
|
2897
|
+
We then want to model the implication
|
|
2898
|
+
|
|
2899
|
+
.. MATH::
|
|
2900
|
+
|
|
2901
|
+
x_{p_1, z_1} = 1,\dots, x_{p_k, z_k} = 1 \Rightarrow x_{p, z} = 1.
|
|
2902
|
+
|
|
2903
|
+
We achieve this by requiring
|
|
2904
|
+
|
|
2905
|
+
.. MATH::
|
|
2906
|
+
|
|
2907
|
+
x_{p, z}\geq 1 - k + \sum_{i=1}^k x_{p_i, z_i}.
|
|
2908
|
+
|
|
2909
|
+
Note that `z` must be a possible value of `p` and each `z_i`
|
|
2910
|
+
must be a possible value of `p_i`.
|
|
2911
|
+
|
|
2912
|
+
EXAMPLES::
|
|
2913
|
+
|
|
2914
|
+
sage: A = B = 'abcd'
|
|
2915
|
+
sage: bij = Bijectionist(A, B, lambda x: B.index(x) % 2)
|
|
2916
|
+
sage: def pi(p1, p2): return 'abcdefgh'[A.index(p1) + A.index(p2)]
|
|
2917
|
+
sage: def rho(s1, s2): return (s1 + s2) % 2
|
|
2918
|
+
sage: bij.set_intertwining_relations((2, pi, rho))
|
|
2919
|
+
sage: from sage.combinat.bijectionist import _BijectionistMILP
|
|
2920
|
+
sage: bmilp = _BijectionistMILP(bij) # indirect doctest
|
|
2921
|
+
sage: next(bmilp.solutions_iterator(False, []))
|
|
2922
|
+
{'a': 0, 'b': 1, 'c': 0, 'd': 1}
|
|
2923
|
+
"""
|
|
2924
|
+
A = self._bijectionist._A
|
|
2925
|
+
tZ = self._bijectionist._possible_block_values
|
|
2926
|
+
P = self._bijectionist._P
|
|
2927
|
+
for composition_index, pi_rho in enumerate(self._bijectionist._pi_rho):
|
|
2928
|
+
pi_blocks = set()
|
|
2929
|
+
for a_tuple in itertools.product(A, repeat=pi_rho.numargs):
|
|
2930
|
+
if pi_rho.domain is not None and not pi_rho.domain(*a_tuple):
|
|
2931
|
+
continue
|
|
2932
|
+
a = pi_rho.pi(*a_tuple)
|
|
2933
|
+
if a in A:
|
|
2934
|
+
p_tuple = tuple(P.find(a) for a in a_tuple)
|
|
2935
|
+
p = P.find(a)
|
|
2936
|
+
if (p_tuple, p) not in pi_blocks:
|
|
2937
|
+
pi_blocks.add((p_tuple, p))
|
|
2938
|
+
for z_tuple in itertools.product(*[tZ[p] for p in p_tuple]):
|
|
2939
|
+
rhs = (1 - pi_rho.numargs
|
|
2940
|
+
+ sum(self._x[p_i, z_i]
|
|
2941
|
+
for p_i, z_i in zip(p_tuple, z_tuple)))
|
|
2942
|
+
z = pi_rho.rho(*z_tuple)
|
|
2943
|
+
if z in tZ[p]:
|
|
2944
|
+
c = self._x[p, z] - rhs
|
|
2945
|
+
if c.is_zero():
|
|
2946
|
+
continue
|
|
2947
|
+
self.milp.add_constraint(c >= 0,
|
|
2948
|
+
name=f"pi/rho({composition_index})")
|
|
2949
|
+
else:
|
|
2950
|
+
self.milp.add_constraint(rhs <= 0,
|
|
2951
|
+
name=f"pi/rho({composition_index})")
|
|
2952
|
+
|
|
2953
|
+
def add_quadratic_relation_constraints(self):
|
|
2954
|
+
r"""
|
|
2955
|
+
Add constraints enforcing that `s\circ\phi\circ s =
|
|
2956
|
+
\psi`.
|
|
2957
|
+
|
|
2958
|
+
We do this by adding
|
|
2959
|
+
|
|
2960
|
+
.. MATH::
|
|
2961
|
+
|
|
2962
|
+
x_{p(a), z} = x_{p(\psi(z)), \phi(a)}
|
|
2963
|
+
|
|
2964
|
+
for `a\in A` and `z\in Z`, where `\phi:A\to Z` and `\psi:Z\to
|
|
2965
|
+
A`. Note that, in particular, `\phi` must be constant on
|
|
2966
|
+
blocks.
|
|
2967
|
+
|
|
2968
|
+
EXAMPLES::
|
|
2969
|
+
|
|
2970
|
+
sage: A = B = DyckWords(3)
|
|
2971
|
+
sage: bij = Bijectionist(A, B)
|
|
2972
|
+
sage: bij.set_statistics((lambda D: D.number_of_touch_points(), lambda D: D.number_of_initial_rises()))
|
|
2973
|
+
sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
|
|
2974
|
+
[ ( [ /\ ] )
|
|
2975
|
+
[ ( [ / \ ] ) ( [ /\ /\ ] [ /\ /\/\ ] )
|
|
2976
|
+
[ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \, / \/\ ], [ / \/\, / \ ] ),
|
|
2977
|
+
<BLANKLINE>
|
|
2978
|
+
( [ /\ ] ) ]
|
|
2979
|
+
( [ /\/\ / \ ] [ /\ ] ) ]
|
|
2980
|
+
( [ / \, / \ ], [ /\/\/\, /\/ \ ] ) ]
|
|
2981
|
+
sage: bij.set_quadratic_relation((lambda D: D, lambda D: D)) # indirect doctest
|
|
2982
|
+
sage: ascii_art(sorted(bij.minimal_subdistributions_iterator()))
|
|
2983
|
+
[ ( [ /\ ] )
|
|
2984
|
+
[ ( [ / \ ] ) ( [ /\ ] [ /\/\ ] )
|
|
2985
|
+
[ ( [ /\/\/\ ], [ / \ ] ), ( [ /\/ \ ], [ / \ ] ),
|
|
2986
|
+
<BLANKLINE>
|
|
2987
|
+
<BLANKLINE>
|
|
2988
|
+
( [ /\ ] [ /\ ] ) ( [ /\/\ ] [ /\ ] )
|
|
2989
|
+
( [ / \/\ ], [ / \/\ ] ), ( [ / \ ], [ /\/ \ ] ),
|
|
2990
|
+
<BLANKLINE>
|
|
2991
|
+
( [ /\ ] ) ]
|
|
2992
|
+
( [ / \ ] ) ]
|
|
2993
|
+
( [ / \ ], [ /\/\/\ ] ) ]
|
|
2994
|
+
"""
|
|
2995
|
+
P = self._bijectionist._P
|
|
2996
|
+
for phi, psi in self._bijectionist._phi_psi:
|
|
2997
|
+
for p, block in P.root_to_elements_dict().items():
|
|
2998
|
+
z0 = phi(p)
|
|
2999
|
+
assert all(phi(a) == z0 for a in block), "phi must be constant on the block %s" % block
|
|
3000
|
+
for z in self._bijectionist._possible_block_values[p]:
|
|
3001
|
+
p0 = P.find(psi(z))
|
|
3002
|
+
if z0 in self._bijectionist._possible_block_values[p0]:
|
|
3003
|
+
c = self._x[p, z] - self._x[p0, z0]
|
|
3004
|
+
if c.is_zero():
|
|
3005
|
+
continue
|
|
3006
|
+
self.milp.add_constraint(c == 0, name=f"i: s({p})={z}<->s(psi({z})=phi({p})")
|
|
3007
|
+
else:
|
|
3008
|
+
self.milp.add_constraint(self._x[p, z] == 0, name=f"i: s({p})!={z}")
|
|
3009
|
+
|
|
3010
|
+
def add_homomesic_constraints(self):
|
|
3011
|
+
r"""
|
|
3012
|
+
Add constraints enforcing that `s` has constant average
|
|
3013
|
+
on the blocks of `Q`.
|
|
3014
|
+
|
|
3015
|
+
We do this by adding
|
|
3016
|
+
|
|
3017
|
+
.. MATH::
|
|
3018
|
+
|
|
3019
|
+
\frac{1}{|q|}\sum_{a\in q} \sum_z z x_{p(a), z} =
|
|
3020
|
+
\frac{1}{|q_0|}\sum_{a\in q_0} \sum_z z x_{p(a), z},
|
|
3021
|
+
|
|
3022
|
+
for `q\in Q`, where `q_0` is some fixed block of `Q`.
|
|
3023
|
+
|
|
3024
|
+
EXAMPLES::
|
|
3025
|
+
|
|
3026
|
+
sage: A = B = [1,2,3]
|
|
3027
|
+
sage: bij = Bijectionist(A, B, lambda b: b % 3)
|
|
3028
|
+
sage: bij.set_homomesic([[1,2], [3]]) # indirect doctest
|
|
3029
|
+
sage: list(bij.solutions_iterator())
|
|
3030
|
+
[{1: 2, 2: 0, 3: 1}, {1: 0, 2: 2, 3: 1}]
|
|
3031
|
+
"""
|
|
3032
|
+
Q = self._bijectionist._Q
|
|
3033
|
+
if Q is None:
|
|
3034
|
+
return
|
|
3035
|
+
P = self._bijectionist._P
|
|
3036
|
+
tZ = self._bijectionist._possible_block_values
|
|
3037
|
+
|
|
3038
|
+
def sum_q(q):
|
|
3039
|
+
return sum(sum(z * self._x[P.find(a), z] for z in tZ[P.find(a)])
|
|
3040
|
+
for a in q)
|
|
3041
|
+
q0 = Q[0]
|
|
3042
|
+
v0 = sum_q(q0)
|
|
3043
|
+
for q in Q[1:]:
|
|
3044
|
+
self.milp.add_constraint(len(q0) * sum_q(q) == len(q) * v0,
|
|
3045
|
+
name=f"h: ({q})~({q0})")
|
|
3046
|
+
|
|
3047
|
+
|
|
3048
|
+
def _invert_dict(d):
|
|
3049
|
+
"""
|
|
3050
|
+
Return the dictionary whose keys are the values of the input and
|
|
3051
|
+
whose values are the lists of preimages.
|
|
3052
|
+
|
|
3053
|
+
INPUT:
|
|
3054
|
+
|
|
3055
|
+
- ``d`` -- dictionary
|
|
3056
|
+
|
|
3057
|
+
EXAMPLES::
|
|
3058
|
+
|
|
3059
|
+
sage: from sage.combinat.bijectionist import _invert_dict
|
|
3060
|
+
sage: _invert_dict({1: "a", 2: "a", 3:"b"})
|
|
3061
|
+
{'a': [1, 2], 'b': [3]}
|
|
3062
|
+
|
|
3063
|
+
sage: _invert_dict({})
|
|
3064
|
+
{}
|
|
3065
|
+
"""
|
|
3066
|
+
preimages = {}
|
|
3067
|
+
for k, v in d.items():
|
|
3068
|
+
preimages[v] = preimages.get(v, []) + [k]
|
|
3069
|
+
return preimages
|
|
3070
|
+
|
|
3071
|
+
|
|
3072
|
+
def _disjoint_set_roots(d):
|
|
3073
|
+
"""
|
|
3074
|
+
Return the representatives of the blocks of the disjoint set.
|
|
3075
|
+
|
|
3076
|
+
INPUT:
|
|
3077
|
+
|
|
3078
|
+
- ``d`` -- a :class:`sage.sets.disjoint_set.DisjointSet_of_hashables`
|
|
3079
|
+
|
|
3080
|
+
EXAMPLES::
|
|
3081
|
+
|
|
3082
|
+
sage: from sage.combinat.bijectionist import _disjoint_set_roots
|
|
3083
|
+
sage: d = DisjointSet('abcde')
|
|
3084
|
+
sage: d.union("a", "b")
|
|
3085
|
+
sage: d.union("a", "c")
|
|
3086
|
+
sage: d.union("e", "d")
|
|
3087
|
+
sage: _disjoint_set_roots(d)
|
|
3088
|
+
dict_keys(['a', 'e'])
|
|
3089
|
+
"""
|
|
3090
|
+
return d.root_to_elements_dict().keys()
|
|
3091
|
+
|
|
3092
|
+
|
|
3093
|
+
def _non_copying_intersection(sets):
|
|
3094
|
+
"""
|
|
3095
|
+
Return the intersection of the sets.
|
|
3096
|
+
|
|
3097
|
+
If the intersection is equal to one of the sets, return this
|
|
3098
|
+
set.
|
|
3099
|
+
|
|
3100
|
+
EXAMPLES::
|
|
3101
|
+
|
|
3102
|
+
sage: from sage.combinat.bijectionist import _non_copying_intersection
|
|
3103
|
+
sage: A = set(range(7000)); B = set(range(8000));
|
|
3104
|
+
sage: _non_copying_intersection([A, B]) is A
|
|
3105
|
+
True
|
|
3106
|
+
|
|
3107
|
+
sage: A = set([1,2]); B = set([2,3])
|
|
3108
|
+
sage: _non_copying_intersection([A, B])
|
|
3109
|
+
{2}
|
|
3110
|
+
"""
|
|
3111
|
+
sets = sorted(sets, key=len)
|
|
3112
|
+
result = set.intersection(*sets)
|
|
3113
|
+
n = len(result)
|
|
3114
|
+
for s in sets:
|
|
3115
|
+
N = len(s)
|
|
3116
|
+
if n < N:
|
|
3117
|
+
return result
|
|
3118
|
+
if s == result:
|
|
3119
|
+
return s
|
|
3120
|
+
|
|
3121
|
+
|
|
3122
|
+
"""
|
|
3123
|
+
TESTS::
|
|
3124
|
+
|
|
3125
|
+
sage: As = Bs = [[],
|
|
3126
|
+
....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]],
|
|
3127
|
+
....: [(2,i,j) for i in [-1,0,1] for j in [-1,1]],
|
|
3128
|
+
....: [(3,i,j) for i in [-2,-1,0,1,2] for j in [-1,1]]]
|
|
3129
|
+
|
|
3130
|
+
Note that adding ``[(2,-2,-1), (2,2,-1), (2,-2,1), (2,2,1)]`` makes
|
|
3131
|
+
it take (seemingly) forever::
|
|
3132
|
+
|
|
3133
|
+
sage: def c1(a, b): return (a[0]+b[0], a[1]*b[1], a[2]*b[2])
|
|
3134
|
+
sage: def c2(a): return (a[0], -a[1], a[2])
|
|
3135
|
+
|
|
3136
|
+
sage: bij = Bijectionist(sum(As, []), sum(Bs, []))
|
|
3137
|
+
sage: bij.set_statistics((lambda x: x[0], lambda x: x[0]))
|
|
3138
|
+
sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2))
|
|
3139
|
+
sage: l = list(bij.solutions_iterator()); len(l) # long time -- (2.7 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx)
|
|
3140
|
+
64
|
|
3141
|
+
|
|
3142
|
+
A brute force check would be difficult::
|
|
3143
|
+
|
|
3144
|
+
sage: prod([factorial(len(A)) for A in As])
|
|
3145
|
+
1881169920000
|
|
3146
|
+
|
|
3147
|
+
Let us try a smaller example::
|
|
3148
|
+
|
|
3149
|
+
sage: As = Bs = [[],
|
|
3150
|
+
....: [(1,i,j) for i in [-1,0,1] for j in [-1,1]],
|
|
3151
|
+
....: [(2,i,j) for i in [-1,1] for j in [-1,1]],
|
|
3152
|
+
....: [(3,i,j) for i in [-1,1] for j in [-1,1]]]
|
|
3153
|
+
|
|
3154
|
+
sage: bij = Bijectionist(sum(As, []), sum(Bs, []))
|
|
3155
|
+
sage: bij.set_statistics((lambda x: x[0], lambda x: x[0]))
|
|
3156
|
+
sage: bij.set_intertwining_relations((2, c1, c1), (1, c2, c2))
|
|
3157
|
+
sage: l1 = list(bij.solutions_iterator()); len(l1)
|
|
3158
|
+
16
|
|
3159
|
+
sage: prod([factorial(len(A)) for A in As])
|
|
3160
|
+
414720
|
|
3161
|
+
|
|
3162
|
+
sage: pis = cartesian_product([Permutations(len(A)) for A in As])
|
|
3163
|
+
sage: it = ({a: Bs[n][pi[n][i]-1] for n, A in enumerate(As) for i, a in enumerate(A)} for pi in pis)
|
|
3164
|
+
sage: A = sum(As, [])
|
|
3165
|
+
sage: respects_c1 = lambda s: all(c1(a1, a2) not in A or s[c1(a1, a2)] == c1(s[a1], s[a2]) for a1 in A for a2 in A)
|
|
3166
|
+
sage: respects_c2 = lambda s: all(c2(a1) not in A or s[c2(a1)] == c2(s[a1]) for a1 in A)
|
|
3167
|
+
sage: l2 = [s for s in it if respects_c1(s) and respects_c2(s)] # long time -- (17 seconds on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx)
|
|
3168
|
+
sage: sorted(l1, key=lambda s: tuple(s.items())) == l2 # long time
|
|
3169
|
+
True
|
|
3170
|
+
|
|
3171
|
+
Our benchmark example::
|
|
3172
|
+
|
|
3173
|
+
sage: from sage.combinat.cyclic_sieving_phenomenon import orbit_decomposition
|
|
3174
|
+
sage: def alpha1(p): return len(p.weak_excedences())
|
|
3175
|
+
sage: def alpha2(p): return len(p.fixed_points())
|
|
3176
|
+
sage: def beta1(p): return len(p.descents(final_descent=True)) if p else 0
|
|
3177
|
+
sage: def beta2(p): return len([e for (e, f) in zip(p, p[1:]+[0]) if e == f+1])
|
|
3178
|
+
sage: gamma = Permutation.longest_increasing_subsequence_length
|
|
3179
|
+
sage: def rotate_permutation(p):
|
|
3180
|
+
....: cycle = Permutation(tuple(range(1, len(p)+1)))
|
|
3181
|
+
....: return Permutation([cycle.inverse()(p(cycle(i))) for i in range(1, len(p)+1)])
|
|
3182
|
+
|
|
3183
|
+
sage: N = 5
|
|
3184
|
+
sage: As = [list(Permutations(n)) for n in range(N+1)]
|
|
3185
|
+
sage: A = B = sum(As, [])
|
|
3186
|
+
sage: bij = Bijectionist(A, B, gamma)
|
|
3187
|
+
sage: bij.set_statistics((len, len), (alpha1, beta1), (alpha2, beta2))
|
|
3188
|
+
sage: bij.set_constant_blocks(sum([orbit_decomposition(A, rotate_permutation) for A in As], []))
|
|
3189
|
+
|
|
3190
|
+
sage: P = bij.constant_blocks(optimal=True)
|
|
3191
|
+
sage: P = [sorted(p, key=lambda p: (len(p), p)) for p in P]
|
|
3192
|
+
sage: P = sorted(P, key=lambda p: (len(next(iter(p))), len(p)))
|
|
3193
|
+
sage: for p in P:
|
|
3194
|
+
....: print(p)
|
|
3195
|
+
[[1], [1, 2], [1, 2, 3], [1, 2, 3, 4], [1, 2, 3, 4, 5]]
|
|
3196
|
+
[[2, 1], [1, 3, 2], [2, 1, 3], [2, 3, 1], [3, 2, 1], ...
|
|
3197
|
+
[[3, 1, 2], [1, 4, 2, 3], [2, 4, 1, 3], [3, 1, 2, 4], [3, 1, 4, 2], ...
|
|
3198
|
+
[[4, 1, 2, 3], [1, 5, 2, 3, 4], [4, 1, 2, 3, 5], [4, 5, 1, 2, 3], [5, 1, 2, 4, 3], ...
|
|
3199
|
+
[[1, 3, 2, 5, 4], [2, 1, 3, 5, 4], [2, 1, 4, 3, 5], [5, 2, 4, 3, 1], [5, 3, 2, 4, 1]]
|
|
3200
|
+
[[1, 3, 5, 2, 4], [2, 4, 1, 3, 5], [3, 5, 2, 4, 1], [4, 1, 3, 5, 2], [5, 2, 4, 1, 3]]
|
|
3201
|
+
...
|
|
3202
|
+
|
|
3203
|
+
sage: for d in sorted(bij.minimal_subdistributions_blocks_iterator(), key=lambda d: (len(d[0]), d[0])):
|
|
3204
|
+
....: print(d)
|
|
3205
|
+
([[]], [0])
|
|
3206
|
+
([[1]], [1])
|
|
3207
|
+
([[2, 1]], [2])
|
|
3208
|
+
([[3, 1, 2]], [3])
|
|
3209
|
+
([[4, 1, 2, 3]], [4])
|
|
3210
|
+
([[5, 1, 2, 3, 4]], [5])
|
|
3211
|
+
([[2, 3, 1, 5, 4], [2, 4, 5, 3, 1], [2, 5, 4, 1, 3], [3, 4, 1, 5, 2]], [2, 3, 3, 3])
|
|
3212
|
+
([[3, 1, 2, 5, 4], [4, 1, 2, 5, 3], [3, 5, 2, 1, 4], [4, 1, 5, 2, 3]], [3, 3, 4, 4])
|
|
3213
|
+
([[2, 1, 3, 5, 4], [2, 4, 1, 3, 5], [2, 5, 3, 1, 4], [3, 4, 1, 2, 5], [3, 1, 5, 4, 2], [2, 5, 1, 4, 3], [2, 1, 5, 4, 3]], [2, 2, 3, 3, 3, 3, 3])
|
|
3214
|
+
|
|
3215
|
+
sage: l = list(bij.solutions_iterator()); len(l) # not tested -- (17 seconds with SCIP on AMD Ryzen 5 PRO 3500U w/ Radeon Vega Mobile Gfx)
|
|
3216
|
+
504
|
|
3217
|
+
|
|
3218
|
+
sage: for a, d in bij.minimal_subdistributions_iterator(): # not tested
|
|
3219
|
+
....: print(sorted(a), sorted(d))
|
|
3220
|
+
"""
|