Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,22 @@ and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.

## Unreleased

### Added
- `permuta.permutils.robinson_schensted`, whose functions `permuta.permutils`
exports:
- `rsk` and `rsk_inverse`, the Robinson-Schensted correspondence on words of
distinct letters;
- `evacuation`, Schützenberger's evacuation of standard Young tableaux, and
`evacuate_recording_tableau`, the word with the same insertion tableau and
the evacuated recording tableau;
- `is_knuth_closed` and `is_shape_closed`, whether the permutations avoiding
some patterns are closed under Knuth equivalence, or determined by their RSK
shape.

### Changed
- The BiSC properties `yt_perm_avoids_22` and `yt_perm_avoids_32` take their
insertion tableau from `rsk`. Their answers are unchanged, and they are faster.

### Fixed
- Fixed bug in `Perm.rtlmax_ltrmin_decomposition` and fixed associated tests.

Expand Down
30 changes: 5 additions & 25 deletions permuta/bisc/perm_properties.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
from itertools import islice
from typing import List

from permuta.patterns.meshpatt import MeshPatt
from permuta.patterns.perm import Perm
from permuta.permutils.groups import dihedral_group
from permuta.permutils.robinson_schensted import rsk

_SMOOTH_PATT = (Perm((0, 2, 1, 3)), Perm((1, 0, 3, 2)))

Expand Down Expand Up @@ -57,41 +57,21 @@ def in_alternating_group(perm: Perm) -> bool:
return perm.count_inversions() % 2 == 0


def _perm_to_yt(perm: Perm) -> List[List[int]]:
# transform perm to standard young table

def insert_in_row(i, k):
cur_row = res[i]
found = next(((ind, cur) for ind, cur in enumerate(cur_row) if cur > k), None)
if not found:
cur_row.append(k)
else:
ind, cur = found
cur_row[ind] = k
if len(res) <= i + 1:
res.append([cur])
else:
insert_in_row(i + 1, cur)

res: List[List[int]] = [[perm[0]]] if perm else []
for val in islice(perm, 1, None):
insert_in_row(0, val)
return res


def _tableau_contains_shape(tab: List[List[int]], shape: List[int]) -> bool:
# Return True if the tableaux tab contains the shape
return len(tab) >= len(shape) and all(s <= t for s, t in zip(shape, map(len, tab)))


def yt_perm_avoids_22(perm: Perm) -> bool:
"""Returns true if perm's standard young table avoids shape [2,2]."""
return not _tableau_contains_shape(_perm_to_yt(perm), [2, 2])
insertion_tableau, _ = rsk(perm)
return not _tableau_contains_shape(insertion_tableau, [2, 2])


def yt_perm_avoids_32(perm: Perm) -> bool:
"""Returns true if perm's standard young table avoids shape [3,2]."""
return not _tableau_contains_shape(_perm_to_yt(perm), [3, 2])
insertion_tableau, _ = rsk(perm)
return not _tableau_contains_shape(insertion_tableau, [3, 2])


_AV_231_AND_MESH_PATT = (
Expand Down
14 changes: 14 additions & 0 deletions permuta/permutils/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,14 @@
from .groups import dihedral_group
from .insertion_encodable import InsertionEncodablePerms
from .polynomial import PolyPerms
from .robinson_schensted import (
evacuate_recording_tableau,
evacuation,
is_knuth_closed,
is_shape_closed,
rsk,
rsk_inverse,
)
from .symmetry import (
all_symmetry_sets,
antidiagonal_set,
Expand Down Expand Up @@ -41,4 +49,10 @@
"rotate_90_clockwise_set",
"rotate_180_clockwise_set",
"rotate_270_clockwise_set",
"evacuate_recording_tableau",
"evacuation",
"is_knuth_closed",
"is_shape_closed",
"rsk",
"rsk_inverse",
]
208 changes: 208 additions & 0 deletions permuta/permutils/robinson_schensted.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
"""
The Robinson-Schensted correspondence (RSK) on words of distinct letters,
Schützenberger's evacuation of standard Young tableaux, and tests of whether the
permutations avoiding some patterns are closed under Knuth equivalence, or are
determined by their RSK shape.

A word here is a sequence of distinct integers, of which only the order matters. A
word with a repeated letter, or a tableau that is not standard where a standard one
is asked for, is not checked for, and gives no meaningful answer.
"""

from bisect import bisect_left
from functools import lru_cache
from typing import FrozenSet, Iterable, Iterator, List, Sequence, Tuple

from permuta.patterns.perm import Perm

Tableau = List[List[int]]


def rsk(word: Sequence[int]) -> Tuple[Tableau, Tableau]:
"""Return the insertion tableau P and the recording tableau Q of a word of
distinct letters, by row insertion. Q holds the step, 1, 2, ..., at which each
cell was added, and both have the same shape, the word's RSK shape.

Examples:
>>> rsk((1, 3, 0, 2))
([[0, 2], [1, 3]], [[1, 2], [3, 4]])
>>> rsk((7, 2, 9, 4))
([[2, 4], [7, 9]], [[1, 3], [2, 4]])
"""
insertion_tableau: Tableau = []
recording_tableau: Tableau = []
for step, letter in enumerate(word, 1):
# insert the letter into the first row, and each letter it bumps into the next
for insertion_row, recording_row in zip(insertion_tableau, recording_tableau):
position = bisect_left(insertion_row, letter)
if position == len(insertion_row):
insertion_row.append(letter)
recording_row.append(step)
break
letter, insertion_row[position] = insertion_row[position], letter
else:
# bumped out of the last row: a new row below it
insertion_tableau.append([letter])
recording_tableau.append([step])
return insertion_tableau, recording_tableau


def rsk_inverse(
insertion_tableau: Sequence[Sequence[int]],
recording_tableau: Sequence[Sequence[int]],
) -> List[int]:
"""Return the word whose insertion and recording tableaux are the given ones, as
rsk gives them: tableaux of the same shape, the recording tableau standard, of
the steps 1 to n. Raise ValueError if their shapes differ; whether the recording
tableau is standard is not checked. The tableaux given are not changed.

Examples:
>>> rsk_inverse([[0, 2], [1, 3]], [[1, 2], [3, 4]])
[1, 3, 0, 2]
"""
insertion_rows = [list(row) for row in insertion_tableau]
recording_rows = [list(row) for row in recording_tableau]
if [len(row) for row in insertion_rows] != [len(row) for row in recording_rows]:
raise ValueError("The insertion and recording tableaux have different shapes.")
row_of = {
step: row_index for row_index, row in enumerate(recording_rows) for step in row
}
word = [0] * len(row_of)
for step in range(len(word), 0, -1):
# undo the step: take out the cell it added, and bump its letter back up
row_index = row_of[step]
recording_rows[row_index].pop()
letter = insertion_rows[row_index].pop()
for upper in range(row_index - 1, -1, -1):
# the letter bumped out of this row was the largest one smaller than it
insertion_row = insertion_rows[upper]
position = bisect_left(insertion_row, letter) - 1
letter, insertion_row[position] = insertion_row[position], letter
word[step - 1] = letter
return word


def evacuation(tableau: Sequence[Sequence[int]]) -> Tableau:
"""Return Schützenberger's evacuation of a standard Young tableau of the entries
1 to n: delete the smallest entry and slide the hole out by jeu de taquin, n
times, writing n, n - 1, ... in the cells vacated. It is an involution that
keeps the shape, and the recording tableau of a permutation's reverse-complement
is the evacuation of the permutation's. Whether the tableau is standard is not
checked, and the tableau given is not changed.

Examples:
>>> evacuation([[1, 2], [3]])
[[1, 3], [2]]
"""
remaining = [list(row) for row in tableau]
evacuated: Tableau = [[0] * len(row) for row in remaining]
size = sum(len(row) for row in remaining)
for label in range(size, 0, -1):
# the smallest entry is in the corner: slide its hole out of the tableau,
# each time into the smaller of the entries to its right and below it
row, column = 0, 0
while True:
right_entry = (
remaining[row][column + 1] if column + 1 < len(remaining[row]) else None
)
entry_below = (
remaining[row + 1][column]
if row + 1 < len(remaining) and column < len(remaining[row + 1])
else None
)
if right_entry is not None and (
entry_below is None or right_entry < entry_below
):
remaining[row][column] = right_entry
column += 1
elif entry_below is not None:
remaining[row][column] = entry_below
row += 1
else:
break
remaining[row].pop()
if not remaining[row]:
remaining.pop()
evacuated[row][column] = label
return evacuated


def evacuate_recording_tableau(word: Sequence[int]) -> List[int]:
"""Return the word whose insertion tableau is the word's, and whose recording
tableau is the evacuation of the word's: RSK^-1(P(w), evac(Q(w))) for the word
w, with P and Q the tableaux rsk gives. The word returned is Knuth equivalent to
w, as its insertion tableau is w's. For w of m letters, its letters in positions
m + 1 - j to m + 1 - i have the RSK shape of w's letters in positions i to j.
The map is an involution.

Examples:
>>> evacuate_recording_tableau((1, 3, 0, 2))
[1, 3, 0, 2]
>>> evacuate_recording_tableau((0, 2, 1))
[2, 0, 1]
"""
insertion_tableau, recording_tableau = rsk(word)
return rsk_inverse(insertion_tableau, evacuation(recording_tableau))


def _knuth_neighbors(perm: Perm) -> Iterator[Perm]:
"""Yield the permutations one elementary Knuth move from the permutation:
y z x <-> y x z and x z y <-> z x y, for x < y < z."""
word = list(perm)
for index in range(len(word) - 2):
first, second, third = word[index], word[index + 1], word[index + 2]
if min(second, third) < first < max(second, third):
yield Perm(word[: index + 1] + [third, second] + word[index + 3 :])
if min(first, second) < third < max(first, second):
yield Perm(word[:index] + [second, first] + word[index + 2 :])


def is_knuth_closed(basis: Iterable[Perm]) -> bool:
"""Return True if the permutations avoiding all the patterns are closed under
Knuth equivalence. It is decided locally: an occurrence that a Knuth move
destroys uses both letters it exchanges, and with the letter that allows the
move it lies in a permutation at most one entry longer than the pattern. The
basis is an iterable of patterns, such as a list, not a single Perm. The answers
are cached by set of patterns.

Examples:
>>> is_knuth_closed([Perm((0, 2, 1))])
False
>>> is_knuth_closed([Perm((0, 2, 1)), Perm((2, 0, 1))])
True
"""
return _is_knuth_closed(frozenset(basis))


# Tilings' Knuth slide asks about the patterns of many tilings in a search, so the
# cache keeps up to 65,536 sets of patterns.
@lru_cache(maxsize=1 << 16)
def _is_knuth_closed(patterns: FrozenSet[Perm]) -> bool:
# a permutation one Knuth move from a pattern, or from one of its one-point
# extensions, that avoids every pattern shows that the avoiders are not closed
return not any(
neighbor.avoids(*patterns)
for pattern in patterns
for container in (pattern, *pattern.coveredby())
for neighbor in _knuth_neighbors(container)
)


def is_shape_closed(basis: Iterable[Perm]) -> bool:
"""Return True if whether a permutation avoids all the patterns depends only on
its RSK shape. It does exactly when the permutations avoiding the patterns, and
those avoiding their inverses, are both closed under Knuth equivalence
(is_knuth_closed of the patterns and of their inverses), as for monotone
patterns. The basis is an iterable of patterns, such as a list, not a single
Perm.

Examples:
>>> is_shape_closed([Perm((2, 1, 0))])
True
>>> is_shape_closed([Perm((0, 2, 1)), Perm((2, 0, 1))])
False
"""
patterns = frozenset(basis)
return _is_knuth_closed(patterns) and _is_knuth_closed(
frozenset(pattern.inverse() for pattern in patterns)
)
53 changes: 32 additions & 21 deletions tests/bisc/test_perm_properties.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import pytest

from permuta import Perm
from permuta.bisc.perm_properties import (
_perm_to_yt,
av_231_and_mesh,
baxter,
dihedral,
Expand All @@ -12,6 +13,7 @@
yt_perm_avoids_22,
yt_perm_avoids_32,
)
from permuta.permutils.robinson_schensted import rsk


def test_smooth():
Expand Down Expand Up @@ -153,26 +155,6 @@ def test_in_alternating_group():
assert 2520 == sum(1 for p in Perm.of_length(7) if in_alternating_group(p))


def test__perm_to_yt():
z = {i: set() for i in range(8)}
for i in range(8):
for p in Perm.of_length(i):
z[i].add(tuple(map(tuple, _perm_to_yt(p))))
expected = [1, 1, 2, 4, 10, 26, 76, 232]
for i in range(8):
assert len(z[i]) == expected[i]
assert _perm_to_yt(Perm((1, 0))) == [[0], [1]]
assert _perm_to_yt(Perm((1, 0, 3, 5, 2, 4))) == [[0, 2, 4], [1, 3, 5]]
assert _perm_to_yt(Perm((1, 0, 2, 3))) == [[0, 2, 3], [1]]
assert _perm_to_yt(Perm((3, 1, 4, 0, 2))) == [[0, 2], [1, 4], [3]]
assert _perm_to_yt(Perm((5, 4, 1, 0, 3, 2))) == [[0, 2], [1, 3], [4], [5]]
assert _perm_to_yt(Perm((0, 1))) == [[0, 1]]
assert _perm_to_yt(Perm((0,))) == [[0]]
assert _perm_to_yt(Perm((2, 3, 0, 1))) == [[0, 1], [2, 3]]
assert _perm_to_yt(Perm((1, 2, 3, 0))) == [[0, 2, 3], [1]]
assert _perm_to_yt(Perm((4, 0, 1, 2, 5, 3))) == [[0, 1, 2, 3], [4, 5]]


def test_yt_perm_avoids_22():
assert yt_perm_avoids_22(Perm())
assert yt_perm_avoids_22(Perm((0,)))
Expand Down Expand Up @@ -297,3 +279,32 @@ def test_hard_mesh():
assert not hard_mesh(Perm((7, 0, 5, 4, 6, 3, 2, 9, 1, 8)))
assert not hard_mesh(Perm((2, 6, 9, 1, 8, 0, 10, 5, 7, 3, 4)))
assert 692 == sum(1 for p in Perm.of_length(7) if hard_mesh(p))


def first_two_rows_by_greene(word):
"""The first two rows of the RSK shape of a word of distinct letters, by Greene's
theorem and not by rsk: the first row is the length of the longest increasing
subsequence, and the first two together are the most letters in two increasing
subsequences."""
# the most letters of two increasing subsequences that end in the two letters
best = {(-1, -1): 0}
for letter in word:
for (first, second), count in list(best.items()):
if letter > first:
best[letter, second] = max(best.get((letter, second), 0), count + 1)
if letter > second:
best[first, letter] = max(best.get((first, letter), 0), count + 1)
longest_increasing = max(count for (_, second), count in best.items() if second < 0)
union_of_two = max(best.values())
return longest_increasing, union_of_two - longest_increasing


@pytest.mark.parametrize("size", range(8))
def test_the_bisc_properties_of_the_shape_against_greene(size):
for perm in Perm.of_length(size):
first_row, second_row = first_two_rows_by_greene(perm)
insertion_tableau, _ = rsk(perm)
rows = [len(row) for row in insertion_tableau] + [0, 0]
assert (first_row, second_row) == (rows[0], rows[1])
assert yt_perm_avoids_22(perm) == (second_row < 2)
assert yt_perm_avoids_32(perm) == (first_row < 3 or second_row < 2)
Loading
Loading