Skip to content

Commit 723630c

Browse files
Merge pull request #20433 from MarkCBell/rotate
Change minlex to use the linear time least_rotation.
2 parents e036b1c + 9ad0ac5 commit 723630c

File tree

4 files changed

+35
-63
lines changed

4 files changed

+35
-63
lines changed

sympy/combinatorics/permutations.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1083,7 +1083,7 @@ def cyclic_form(self):
10831083
unchecked[j] = False
10841084
if len(cycle) > 1:
10851085
cyclic_form.append(cycle)
1086-
assert cycle == list(minlex(cycle, is_set=True))
1086+
assert cycle == list(minlex(cycle))
10871087
cyclic_form.sort()
10881088
self._cyclic_form = cyclic_form[:]
10891089
return cyclic_form

sympy/combinatorics/polyhedron.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from sympy.core import Basic, Tuple
44
from sympy.core.compatibility import as_int
55
from sympy.sets import FiniteSet
6-
from sympy.utilities.iterables import (minlex, unflatten, flatten)
6+
from sympy.utilities.iterables import (minlex, unflatten, flatten, default_sort_key)
77

88
rmul = Perm.rmul
99

@@ -386,7 +386,7 @@ def __new__(cls, corners, faces=[], pgroup=[]):
386386
.. [1] www.ocf.berkeley.edu/~wwu/articles/platonicsolids.pdf
387387
388388
"""
389-
faces = [minlex(f, directed=False, is_set=True) for f in faces]
389+
faces = [minlex(f, directed=False, key=default_sort_key) for f in faces]
390390
corners, faces, pgroup = args = \
391391
[Tuple(*a) for a in (corners, faces, pgroup)]
392392
obj = Basic.__new__(cls, *args)
@@ -697,7 +697,7 @@ def _pgroup_of_double(polyh, ordered_faces, pgroup):
697697
reorder = unflatten([c[j] for j in flat_faces], n)
698698
# make them canonical
699699
reorder = [tuple(map(as_int,
700-
minlex(f, directed=False, is_set=True)))
700+
minlex(f, directed=False)))
701701
for f in reorder]
702702
# map face to vertex: the resulting list of vertices are the
703703
# permutation that we seek for the double

sympy/utilities/iterables.py

Lines changed: 30 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,10 @@
99
from sympy.core import Basic
1010

1111
# this is the logical location of these functions
12-
from sympy.core.compatibility import (
13-
as_int, default_sort_key, is_sequence, iterable, ordered
14-
)
12+
from sympy.core.compatibility import (as_int, is_sequence, iterable, ordered)
13+
from sympy.core.compatibility import default_sort_key # noqa: F401
14+
15+
import sympy
1516

1617
from sympy.utilities.enumerative import (
1718
multiset_partitions_taocp, list_visitor, MultisetPartitionTraverser)
@@ -1274,7 +1275,7 @@ def rotate_right(x, y):
12741275
return x[y:] + x[:y]
12751276

12761277

1277-
def least_rotation(x):
1278+
def least_rotation(x, key=None):
12781279
'''
12791280
Returns the number of steps of left rotation required to
12801281
obtain lexicographically minimal string/list/tuple, etc.
@@ -1295,18 +1296,19 @@ def least_rotation(x):
12951296
.. [1] https://en.wikipedia.org/wiki/Lexicographically_minimal_string_rotation
12961297
12971298
'''
1299+
if key is None: key = sympy.Id
12981300
S = x + x # Concatenate string to it self to avoid modular arithmetic
12991301
f = [-1] * len(S) # Failure function
13001302
k = 0 # Least rotation of string found so far
13011303
for j in range(1,len(S)):
13021304
sj = S[j]
13031305
i = f[j-k-1]
13041306
while i != -1 and sj != S[k+i+1]:
1305-
if sj < S[k+i+1]:
1307+
if key(sj) < key(S[k+i+1]):
13061308
k = j-i-1
13071309
i = f[i]
13081310
if sj != S[k+i+1]:
1309-
if sj < S[k]:
1311+
if key(sj) < key(S[k]):
13101312
k = j
13111313
f[j-k] = -1
13121314
else:
@@ -2398,20 +2400,18 @@ def generate_oriented_forest(n):
23982400
break
23992401

24002402

2401-
def minlex(seq, directed=True, is_set=False, small=None):
2403+
def minlex(seq, directed=True, key=None):
24022404
"""
2403-
Return a tuple representing the rotation of the sequence in which
2404-
the lexically smallest elements appear first, e.g. `cba ->acb`.
2405+
Return the rotation of the sequence in which the lexically smallest
2406+
elements appear first, e.g. `cba ->acb`.
2407+
2408+
The sequence returned is a tuple, unless the input sequence is a string
2409+
in which case a string is returned.
24052410
24062411
If ``directed`` is False then the smaller of the sequence and the
24072412
reversed sequence is returned, e.g. `cba -> abc`.
24082413
2409-
For more efficient processing, ``is_set`` can be set to True if there
2410-
are no duplicates in the sequence.
2411-
2412-
If the smallest element is known at the time of calling, it can be
2413-
passed as ``small`` and the calculation of the smallest element will
2414-
be omitted.
2414+
If ``key`` is not None then it is used to extract a comparison key from each element in iterable.
24152415
24162416
Examples
24172417
========
@@ -2429,51 +2429,22 @@ def minlex(seq, directed=True, is_set=False, small=None):
24292429
>>> minlex('11010011000', directed=False)
24302430
'00011001011'
24312431
2432+
>>> minlex(('bb', 'aaa', 'c', 'a'))
2433+
('a', 'bb', 'aaa', 'c')
2434+
>>> minlex(('bb', 'aaa', 'c', 'a'), key=len)
2435+
('c', 'a', 'bb', 'aaa')
2436+
24322437
"""
2433-
is_str = isinstance(seq, str)
2434-
seq = list(seq)
2435-
if small is None:
2436-
small = min(seq, key=default_sort_key)
2437-
if is_set:
2438-
i = seq.index(small)
2439-
if not directed:
2440-
n = len(seq)
2441-
p = (i + 1) % n
2442-
m = (i - 1) % n
2443-
if default_sort_key(seq[p]) > default_sort_key(seq[m]):
2444-
seq = list(reversed(seq))
2445-
i = n - i - 1
2446-
if i:
2447-
seq = rotate_left(seq, i)
2448-
best = seq
2449-
else:
2450-
count = seq.count(small)
2451-
if count == 1 and directed:
2452-
best = rotate_left(seq, seq.index(small))
2453-
else:
2454-
# if not directed, and not a set, we can't just
2455-
# pass this off to minlex with is_set True since
2456-
# peeking at the neighbor may not be sufficient to
2457-
# make the decision so we continue...
2458-
best = seq
2459-
for i in range(count):
2460-
seq = rotate_left(seq, seq.index(small, count != 1))
2461-
if seq < best:
2462-
best = seq
2463-
# it's cheaper to rotate now rather than search
2464-
# again for these in reversed order so we test
2465-
# the reverse now
2466-
if not directed:
2467-
seq = rotate_left(seq, 1)
2468-
seq = list(reversed(seq))
2469-
if seq < best:
2470-
best = seq
2471-
seq = list(reversed(seq))
2472-
seq = rotate_right(seq, 1)
2473-
# common return
2474-
if is_str:
2475-
return ''.join(best)
2476-
return tuple(best)
2438+
2439+
if key is None: key = sympy.Id
2440+
best = rotate_left(seq, least_rotation(seq, key=key))
2441+
if not directed:
2442+
rseq = seq[::-1]
2443+
rbest = rotate_left(rseq, least_rotation(rseq, key=key))
2444+
best = min(best, rbest, key=key)
2445+
2446+
# Convert to tuple, unless we started with a string.
2447+
return tuple(best) if not isinstance(seq, str) else best
24772448

24782449

24792450
def runs(seq, op=gt):

sympy/utilities/tests/test_iterables.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -645,6 +645,7 @@ def test_minlex():
645645
assert minlex((1, 0, 2)) == (0, 2, 1)
646646
assert minlex((1, 0, 2), directed=False) == (0, 1, 2)
647647
assert minlex('aba') == 'aab'
648+
assert minlex(('bb', 'aaa', 'c', 'a'), key=len) == ('c', 'a', 'bb', 'aaa')
648649

649650

650651
def test_ordered():

0 commit comments

Comments
 (0)