Skip to content

Commit

Permalink
sagemathgh-38064: Steinhaus-Johnson-Trotter algorithm for permutations
Browse files Browse the repository at this point in the history
    
<!-- ^ Please provide a concise and informative title. -->
<!-- ^ Don't put issue numbers in the title, do this in the PR
description below. -->
<!-- ^ For example, instead of "Fixes sagemath#12345" use "Introduce new method
to calculate 1 + 2". -->
<!-- v Describe your changes below in detail. -->
<!-- v Why is this change required? What problem does it solve? -->
<!-- v If this PR resolves an open issue, please link to it here. For
example, "Fixes sagemath#12345". -->
I suggest this diff against develop to implement the Steinhaus-Johnson-
Trotter algorithm that generates the permutations of a list using only
transpositions of two elements of the list. The algorithm can be
selected upon initialization with the parameter of the same name,
defaults to "lex" which is the current algorithm.

Since the `Permutation` class is a bit weird and creates a new list and
a new object on every iteration, I had to tweak a bit the parameters in
input to the class `__init__` method adding
- `algorithm`: this one is fine, it allows to choose which algorithm to
use to generate the permutations
- `directions`: this one is meant to keep track of the internal state
specific to the SJT algorithm. To find the two elements to transpose for
the next permutation, we need to find elements according to some
conditions regarding each of their direction. Since the `Permutation`
class creates a new object at each iteration, I tried to find the best
way to pass on the internal state to the new object. Maybe the internal
state can be directly computed from the list, I haven't checked. Because
of this dependency on the internal state, only the identity permutation
can be used when initializing a `Permutation` object with
`algorithm='sjt'` (see examples from doc). I am aware that adding an
extra optional parameter to the object construction might not be the
best option thus I will gladly take your feedback and opinions.

I haven't implemented the `prev()` method. I think it should be done if
this is accepted.

### 📝 Checklist

<!-- Put an `x` in all the boxes that apply. -->

- [x] The title is concise and informative.
- [x] The description explains in detail what this PR is about.
- [ ] I have linked a relevant issue or discussion.
- [x] I have created tests covering the changes.
- [x] I have updated the documentation and checked the documentation
preview.

### ⌛ Dependencies

<!-- List all open PRs that this PR logically depends on. For example,
-->
<!-- - sagemath#12345: short description why this is a dependency -->
<!-- - sagemath#34567: ... -->
    
URL: sagemath#38064
Reported by: grnx
Reviewer(s): grhkm21, grnx
  • Loading branch information
Release Manager committed Jun 9, 2024
2 parents be266c7 + 3139536 commit 21ab24f
Show file tree
Hide file tree
Showing 2 changed files with 391 additions and 18 deletions.
246 changes: 246 additions & 0 deletions src/sage/combinat/SJT.py
Original file line number Diff line number Diff line change
@@ -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 <[email protected]>
#
# 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
Loading

0 comments on commit 21ab24f

Please sign in to comment.