diff --git a/CHANGELOG.md b/CHANGELOG.md index 8f2fafe..295dae3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,8 @@ The version is represented by three digits: a.b.c. FEATURE: - `symmetria.Permutation`: add `lexicographic_rank` method - `symmetria.CycleDecomposition`: add `lexicographic_rank` method +- `symmetria.Permutation`: add `lehmer_code` method +- `symmetria.CycleDecomposition`: add `lehmer_code` method FIX: - `symmetria.Permutation`: fix small typos in class methods diff --git a/docs/source/pages/API_reference/elements/index.rst b/docs/source/pages/API_reference/elements/index.rst index c619f50..762dd7f 100644 --- a/docs/source/pages/API_reference/elements/index.rst +++ b/docs/source/pages/API_reference/elements/index.rst @@ -115,6 +115,11 @@ Here, **P** denotes the class ``Permutation``, **C** the class ``Cycle``, and ** - ✅ - ❌ - ✅ + * - ``lehmer_code`` + - Return the Lehmer code of the permutation + - ✅ + - ❌ + - ✅ * - ``lexicographic_rank`` - Return the lexicographic rank of the permutation - ✅ diff --git a/symmetria/elements/cycle_decomposition.py b/symmetria/elements/cycle_decomposition.py index db6de37..56b23b2 100644 --- a/symmetria/elements/cycle_decomposition.py +++ b/symmetria/elements/cycle_decomposition.py @@ -764,6 +764,27 @@ def is_regular(self) -> bool: """ return all(len(cycle) == len(self[0]) for cycle in self) + def lehmer_code(self) -> List[int]: + """Return the Lehmer code of the cycle decomposition. + + Recall that the Lehmer code of a permutation is a sequence that encodes the permutation as a series of integers. + Each integer represents the number of smaller elements to the right of a given element in the permutation. + + :return: the Lehmer code of the cycle decomposition. + :rtype: List[int] + + :example: + >>> from symmetria import CycleDecomposition, Cycle + ... + >>> CycleDecomposition(Cycle(1)).lehmer_code() + [0] + >>> CycleDecomposition(Cycle(1, 2), Cycle(3)).lehmer_code() + [1, 0, 0] + >>> CycleDecomposition(Cycle(1, 4), Cycle(2, 3)).lehmer_code() + [3, 2, 1, 0] + """ + return symmetria.Permutation.from_cycle_decomposition(self).lehmer_code() + def lexicographic_rank(self) -> int: """Return the lexicographic rank of the cycle decomposition. diff --git a/symmetria/elements/permutation.py b/symmetria/elements/permutation.py index bc6061b..03824be 100644 --- a/symmetria/elements/permutation.py +++ b/symmetria/elements/permutation.py @@ -877,6 +877,41 @@ def is_regular(self) -> bool: cycle_decomposition = self.cycle_decomposition() return all(len(cycle) == len(cycle_decomposition[0]) for cycle in cycle_decomposition) + def lehmer_code(self) -> List[int]: + """Return the Lehmer code of the permutation. + + Recall that the Lehmer code of a permutation is a sequence that encodes the permutation as a series of integers. + Each integer represents the number of smaller elements to the right of a given element in the permutation. + + :return: the Lehmer code of the permutation. + :rtype: List[int] + + :example: + >>> from symmetria import Permutation + ... + >>> Permutation(1).lehmer_code() + [0] + >>> Permutation(2, 1, 3).lehmer_code() + [1, 0, 0] + >>> Permutation(4, 3, 2, 1).lehmer_code() + [3, 2, 1, 0] + >>> Permutation(4, 1, 3, 2, 7, 6, 5, 8).lehmer_code() + [3, 0, 1, 0, 2, 1, 0, 0] + """ + n = len(self) + lehmer_code = [0] * n + stack = [] # (value, count) + + for i in range(n, 0, -1): + count = 0 + while stack and stack[-1][0] < self[i]: + _, old_count = stack.pop() + count += 1 + old_count + lehmer_code[i - 1] = count + stack.append((self[i], count)) + + return lehmer_code + def lexicographic_rank(self) -> int: """Return the lexicographic rank of the permutation. diff --git a/tests/tests_elements/tests_permutation/test_cases.py b/tests/tests_elements/tests_permutation/test_cases.py index b28d1df..3622c11 100644 --- a/tests/tests_elements/tests_permutation/test_cases.py +++ b/tests/tests_elements/tests_permutation/test_cases.py @@ -225,6 +225,17 @@ (Permutation(2, 1), True), (Permutation(2, 1, 3), False), ] +TEST_LEHMER_CODE = [ + (Permutation(1), [0]), + (Permutation(2, 1), [1, 0]), + (Permutation(2, 1, 3), [1, 0, 0]), + (Permutation(1, 2, 3), [0, 0, 0]), + (Permutation(1, 2, 3, 4), [0, 0, 0, 0]), + (Permutation(2, 1, 3, 4), [1, 0, 0, 0]), + (Permutation(4, 3, 2, 1), [3, 2, 1, 0]), + (Permutation(4, 1, 3, 2), [3, 0, 1, 0]), + (Permutation(4, 1, 3, 2, 7, 6, 5, 8), [3, 0, 1, 0, 2, 1, 0, 0]), +] TEST_LEXICOGRAPHIC_RANK = [ (Permutation(1), 1), (Permutation(1, 2), 1), diff --git a/tests/tests_elements/tests_permutation/test_generic_methods.py b/tests/tests_elements/tests_permutation/test_generic_methods.py index 4596d05..e93a35f 100644 --- a/tests/tests_elements/tests_permutation/test_generic_methods.py +++ b/tests/tests_elements/tests_permutation/test_generic_methods.py @@ -22,6 +22,7 @@ TEST_INVERSIONS, TEST_IS_REGULAR, TEST_EXCEEDANCES, + TEST_LEHMER_CODE, TEST_IS_CONJUGATE, TEST_CYCLE_NOTATION, TEST_IS_DERANGEMENT, @@ -257,6 +258,20 @@ def test_is_regular(permutation, expected_value) -> None: ) +@pytest.mark.parametrize( + argnames="permutation, expected_value", + argvalues=TEST_LEHMER_CODE, + ids=[f"{p}.lehmer_code()={m}" for p, m in TEST_LEHMER_CODE], +) +def test_lehmer_core(permutation, expected_value) -> None: + """Tests for the method `lehmer_code()`.""" + _check_values( + expression=f"{permutation.rep()}.lehmer_code()", + evaluation=permutation.lehmer_code(), + expected=expected_value, + ) + + @pytest.mark.parametrize( argnames="permutation, expected_value", argvalues=TEST_LEXICOGRAPHIC_RANK,