diff --git a/src/sage/combinat/SJT.py b/src/sage/combinat/SJT.py new file mode 100644 index 00000000000..03883eeb401 --- /dev/null +++ b/src/sage/combinat/SJT.py @@ -0,0 +1,246 @@ +r""" +The Steinhaus-Johnson-Trotter algorithm generates all permutations of a list in +an order such that each permutation is obtained by transposing two adjacent +elements from the previous permutation. + +Each element of the list has a direction (initialized at -1) that changes at +each permutation and that is used to determine which elements to transpose. Thus +in addition to the permutation itself, the direction of each element is also +stored. + +Note that the permutations are not generated in lexicographic order. + +AUTHORS: + +- Martin Grenouilloux (2024-05-22): initial version +""" + +# **************************************************************************** +# Copyright (C) 2024 Martin Grenouilloux +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation, either version 2 of the License, or +# (at your option) any later version. +# https://www.gnu.org/licenses/ +# **************************************************************************** +from sage.combinat.combinat import CombinatorialElement + +class SJT(CombinatorialElement): + r""" + A representation of a list permuted using the Steinhaus-Johnson-Trotter + algorithm. + + Each element of the list has a direction (initialized at -1) that changes at + each permutation and that is used to determine which elements to transpose. + The directions have three possible values: + + - ``-1``: element tranposes to the left + + - ``1``: element transposes to the right + + - ``0``: element does not move + + Thus in addition to the permutation itself, the direction of each element is + also stored. + + Note that the permutations are not generated in lexicographic order. + + .. WARNING:: + + An ``SJT`` object should always be created with identity permutation for + the algorithm to behave properly. If the identity permutation is not + provided, it expects a coherent list of directions according to the + provided input. This list is not checked. + + .. TODO:: + + Implement the previous permutation for the Steinhaus-Johnson-Trotter + algorithm. + + EXAMPLES:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 2, 3, 4]); s + [1, 2, 3, 4] + sage: s = s.next(); s + [1, 2, 4, 3] + sage: p = Permutation(s._list, algorithm='sjt', sjt=s) + sage: p + [1, 2, 4, 3] + sage: p.next() + [1, 4, 2, 3] + + TESTS:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 2, 3, 4]); s + [1, 2, 3, 4] + sage: s = SJT([1]); s + [1] + sage: s = s.next(); s + False + sage: s = SJT([]); s + [] + sage: s = s.next(); s + False + """ + def __init__(self, l, directions=None) -> None: + r""" + Transpose two elements at positions ``a`` and ``b`` in ``perm`` and + their corresponding directions as well following the + Steinhaus-Johnson-Trotter algorithm. + + Each permutation is obtained by transposing two adjacent elements from + the previous permutation. + + INPUT: + + - ``l`` -- list; a list of ordered ``int``. + + - ``directions`` -- list (default: ``None``); a list of directions for + each element in the permuted list. Used when constructing permutations + from a pre-defined internal state. + + EXAMPLES:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 2, 3, 4]); s + [1, 2, 3, 4] + sage: s = s.next(); s + [1, 2, 4, 3] + sage: p = Permutation(s._list, algorithm='sjt', sjt=s) + sage: p + [1, 2, 4, 3] + sage: p.next() + [1, 4, 2, 3] + + TESTS:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 3, 2, 4]) + Traceback (most recent call last): + ... + ValueError: no internal state directions were given for non-identity + starting permutation for Steinhaus-Johnson-Trotter algorithm + sage: s = SJT([]); s + [] + sage: s = s.next(); s + False + """ + # The permuted list. + self._list = l + + # The length of the permuted list. Return early on empty list. + self._n = len(l) + if self._n == 0: + return + + if directions is None: + if not all(l[i] <= l[i+1] for i in range(self._n - 1)): + raise ValueError("no internal state directions were given for " + "non-identity starting permutation for " + "Steinhaus-Johnson-Trotter algorithm") + self._directions = [-1] * self._n + + # The first element has null direction. + self._directions[0] = 0 + else: + self._directions = directions + + def __idx_largest_element_non_zero_direction(self, perm, directions): + r""" + Find the largest element in ``perm`` with a non null direction. + """ + largest = 0 + index = None + for i in range(self._n): + if directions[i] != 0: + e = perm[i] + if e > largest: + index = i + largest = e + + return index + + def next(self): + r""" + Produce the next permutation of ``self`` following the + Steinhaus-Johnson-Trotter algorithm. + + OUTPUT: the list of the next permutation + + EXAMPLES:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 2, 3, 4]) + sage: s = s.next(); s + [1, 2, 4, 3] + sage: s = s.next(); s + [1, 4, 2, 3] + + TESTS:: + + sage: from sage.combinat.SJT import SJT + sage: s = SJT([1, 2, 3]) + sage: s.next() + [1, 3, 2] + + sage: s = SJT([1]) + sage: s.next() + False + """ + # Return on empty list. + if self._n == 0: + return False + + # Copying lists of permutation and directions to avoid changing internal + # state of the algorithm if ``next()`` is called without reassigning. + perm = self._list[:] + directions = self._directions[:] + + # Assume that the element to move is n (which will be in most cases). + selected_elt = self._n + xi = perm.index(selected_elt) + direction = directions[xi] + + # If this element has null direction, find the largest whose is + # non-null. + if direction == 0: + xi = self.__idx_largest_element_non_zero_direction(perm, directions) + if xi is None: + # We have created every permutation. Detected when all elements + # have null direction. + return False + direction = directions[xi] + selected_elt = perm[xi] + + new_pos = xi + direction + + # Proceed to transpose elements and corresponding directions. + perm[xi], perm[new_pos] = perm[new_pos], perm[xi] + directions[xi], directions[new_pos] = \ + directions[new_pos], directions[xi] + + # If the transposition results in the largest element being on one edge + # or if the following element in its direction is greater than it, then + # then set its direction to 0 + if new_pos == 0 or new_pos == self._n - 1 or \ + perm[new_pos + direction] > selected_elt: + directions[new_pos] = 0 + + # After each permutation, update each element's direction. If one + # element is greater than selected element, change its direction towards + # the selected element. This loops has no reason to be if selected + # element is n and this will be the case most of the time. + if selected_elt != self._n: + for i in range(self._n): + if perm[i] > selected_elt: + if i < new_pos: + directions[i] = 1 + if i > new_pos: + directions[i] = -1 + + return SJT(perm, directions) + + __next__ = next diff --git a/src/sage/combinat/permutation.py b/src/sage/combinat/permutation.py index 4f0cd8e97df..1ca90eff943 100644 --- a/src/sage/combinat/permutation.py +++ b/src/sage/combinat/permutation.py @@ -250,6 +250,7 @@ from sage.categories.infinite_enumerated_sets import InfiniteEnumeratedSets from sage.categories.sets_with_grading import SetsWithGrading from sage.combinat.backtrack import GenericBacktracker +from sage.combinat.SJT import SJT from sage.combinat.combinat import CombinatorialElement, catalan_number from sage.combinat.combinatorial_map import combinatorial_map from sage.combinat.composition import Composition @@ -308,9 +309,22 @@ class Permutation(CombinatorialElement): the permutation obtained from the pair using the inverse of the Robinson-Schensted algorithm. - - ``check`` (boolean) -- whether to check that input is correct. Slows - the function down, but ensures that nothing bad happens. This is set to - ``True`` by default. + - ``check`` -- boolean (default: ``True``); whether to check that input is + correct. Slows the function down, but ensures that nothing bad happens. + This is set to ``True`` by default. + + - ``algorithm`` -- string (default: ``lex``); the algorithm used to generate + the permutations. Supported algorithms are: + + - ``lex``: lexicographic order generation, this is the default algorithm. + + - ``sjt``: Steinhaus-Johnson-Trotter algorithm to generate permutations + using only transposition of two elements in the list. It is highly + recommended to set ``check=True`` (default value). + + - ``sjt`` -- SJT (default: ``None``); the ``SJT`` object holding the + permutation internal state. This should only be specified when + initializing with non-identity permutation. .. WARNING:: @@ -378,6 +392,27 @@ class Permutation(CombinatorialElement): sage: type(p) + Generate permutations using the Steinhaus-Johnson Trotter algorithm. The + output is not in lexicographic order:: + + sage: p = Permutation([1, 2, 3, 4], algorithm='sjt'); p + [1, 2, 3, 4] + sage: p = p.next(); p + [1, 2, 4, 3] + sage: p = p.next(); p + [1, 4, 2, 3] + sage: p = Permutation([1, 2, 3], algorithm='sjt') + sage: for _ in range(6): + ....: p = p.next() + sage: p + False + + sage: Permutation([1, 3, 2, 4], algorithm='sjt') + Traceback (most recent call last): + ... + ValueError: no internal state directions were given for non-identity + starting permutation for Steinhaus-Johnson-Trotter algorithm + Construction from a string in cycle notation:: sage: p = Permutation( '(4,5)' ); p @@ -427,6 +462,11 @@ class Permutation(CombinatorialElement): sage: Permutation( [1] ) [1] + sage: Permutation([1, 2, 3, 4], algorithm='blah') + Traceback (most recent call last): + ... + ValueError: unsupported algorithm blah; expected 'lex' or 'sjt' + From a pair of empty tableaux :: sage: Permutation( ([], []) ) # needs sage.combinat @@ -436,7 +476,7 @@ class Permutation(CombinatorialElement): """ @staticmethod @rename_keyword(deprecation=35233, check_input='check') - def __classcall_private__(cls, l, check=True): + def __classcall_private__(cls, l, algorithm='lex', sjt=None, check=True): """ Return a permutation in the general permutations parent. @@ -487,10 +527,10 @@ def __classcall_private__(cls, l, check=True): raise ValueError("cannot convert l (= %s) to a Permutation" % l) # otherwise, it gets processed by CombinatorialElement's __init__. - return Permutations()(l, check=check) + return Permutations()(l, algorithm, sjt, check) @rename_keyword(deprecation=35233, check_input='check') - def __init__(self, parent, l, check=True): + def __init__(self, parent, l, algorithm='lex', sjt=None, check=True): """ Constructor. Checks that INPUT is not a mess, and calls :class:`CombinatorialElement`. It should not, because @@ -500,11 +540,23 @@ def __init__(self, parent, l, check=True): - ``l`` -- a list of ``int`` variables - - ``check`` (boolean) -- whether to check that input is - correct. Slows the function down, but ensures that nothing bad + - ``check`` -- boolean (default: ``True``); whether to check that input + is correct. Slows the function down, but ensures that nothing bad happens. - This is set to ``True`` by default. + - ``algorithm`` -- string (default: ``lex``); the algorithm used to + generate the permutations. Supported algorithms are: + + - ``lex``: lexicographic order generation, this is the default + algorithm. + + - ``sjt``: Steinhaus-Johnson-Trotter algorithm to generate + permutations using only transposition of two elements in the list. + It is highly recommended to set ``check=True`` (default value). + + - ``sjt`` -- SJT (default: ``None``); the ``SJT`` object holding the + permutation internal state. This should only be specified when + initializing with non-identity permutation. TESTS:: @@ -521,12 +573,30 @@ def __init__(self, parent, l, check=True): sage: Permutation([1,2,4,5]) Traceback (most recent call last): ... - ValueError: The permutation has length 4 but its maximal element is + ValueError: the permutation has length 4 but its maximal element is 5. Some element may be repeated, or an element is missing, but there is something wrong with its length. + + sage: Permutation([1, 3, 2], algorithm='sjt') + Traceback (most recent call last): + ... + ValueError: no internal state directions were given for non-identity + starting permutation for Steinhaus-Johnson-Trotter algorithm + + sage: Permutation([1, 3, 2], algorithm='sjt', check=False) + Traceback (most recent call last): + ... + ValueError: no internal state directions were given for non-identity + starting permutation for Steinhaus-Johnson-Trotter algorithm """ l = list(l) + self._algorithm = algorithm.lower() + + if self._algorithm != "lex" and self._algorithm != "sjt": + raise ValueError("unsupported algorithm %s; expected 'lex' or 'sjt'" + % self._algorithm) + if check and len(l) > 0: # Make a copy to sort later lst = list(l) @@ -538,18 +608,19 @@ def __init__(self, parent, l, check=True): except TypeError: raise ValueError("the elements must be integer variables") if i < 1: - raise ValueError("the elements must be strictly positive integers") + raise ValueError("the elements must be strictly positive " + "integers") lst.sort() # Is the maximum element of the permutation the length of input, # or is some integer missing ? if int(lst[-1]) != len(lst): - raise ValueError("The permutation has length "+str(len(lst)) + + raise ValueError("the permutation has length "+str(len(lst)) + " but its maximal element is " + - str(int(lst[-1]))+". Some element " + - "may be repeated, or an element is missing" + - ", but there is something wrong with its length.") + str(int(lst[-1])) + ". Some element may be " + + "repeated, or an element is missing, but " + + "there is something wrong with its length.") # Do the elements appear only once ? previous = lst[0]-1 @@ -559,6 +630,9 @@ def __init__(self, parent, l, check=True): raise ValueError("an element appears twice in the input") previous = i + if self._algorithm == "sjt": + self._sjt = SJT(l) if sjt is None else sjt + CombinatorialElement.__init__(self, parent, l) def __setstate__(self, state): @@ -761,9 +835,18 @@ def cycle_string(self, singletons=False) -> str: def __next__(self): r""" - Return the permutation that follows ``self`` in lexicographic order on - the symmetric group containing ``self``. If ``self`` is the last - permutation, then ``next`` returns ``False``. + Return the permutation that follows ``self`` on the symmetric group + containing ``self``. If ``self`` is the last permutation, then ``next`` + returns ``False``. If the ``algorithm`` parameter is specified, the + permutations will be generated according to it. Supported algorithms + are: + + - ``lex``: lexicographic order generation, this is the default + algorithm. + + - ``sjt``: Steinhaus-Johnson-Trotter algorithm to generate + permutations using only transposition of two elements in the list. + It is highly recommended to set ``check=True`` (default value). EXAMPLES:: @@ -773,13 +856,41 @@ def __next__(self): sage: p = Permutation([4,3,2,1]) sage: next(p) False + sage: p = Permutation([1, 2, 3], algorithm='sjt') + sage: p = next(p); p + [1, 3, 2] + sage: p = next(p); p + [3, 1, 2] TESTS:: sage: p = Permutation([]) sage: next(p) False + sage: p = Permutation([], algorithm='sjt') + sage: next(p) + False + sage: p = Permutation([1], algorithm='sjt') + sage: next(p) + False + sage: l = [1, 2, 3, 4] + sage: s = set() + sage: p = Permutation(l, algorithm='sjt') + sage: for _ in range(factorial(len(l))): + ....: s.add(p) + ....: p = p.next() + sage: p + False + sage: assert(len(s)) == factorial(len(l)) """ + if self._algorithm == "sjt": + # Ensure the same permutation is yielded when called multiple times + # without reassigning + sjt = self._sjt.next() + if sjt is False: + return False + return Permutations()(sjt._list, algorithm='sjt', sjt=sjt) + p = self[:] n = len(self) first = -1 @@ -818,6 +929,7 @@ def prev(self): Return the permutation that comes directly before ``self`` in lexicographic order on the symmetric group containing ``self``. If ``self`` is the first permutation, then it returns ``False``. + Does not support the Steinhaus-Johnson-Trotter algorithm for the moment. EXAMPLES:: @@ -834,11 +946,26 @@ def prev(self): sage: p.prev() False + sage: p = Permutation([1,2,3], algorithm='sjt') + sage: p.prev() + Traceback (most recent call last): + ... + NotImplementedError: previous permutation for SJT algorithm is not + yet implemented + Check that :issue:`16913` is fixed:: sage: Permutation([1,4,3,2]).prev() [1, 4, 2, 3] + + .. TODO:: + + Implement the previous permutation for the Steinhaus-Johnson-Trotter + algorithm. """ + if self._algorithm == "sjt": + raise NotImplementedError("previous permutation for SJT algorithm " + "is not yet implemented") p = self[:] n = len(self)