I give you Nth permutation, you give me N
Python 2, 69 bytes
Try it online
from itertools import*
lambda S:sorted(set(permutations(S))).index(S)
Python, 302 287 bytes
Dead Possum has already posted a short Pythonic solution, so I decided to go for the extra kudos. This solution does not generate all of the permutations. It can rapidly calculate the permutation index of a rather large string; it also handles an empty string correctly.
from math import factorial as f
from itertools import groupby as g
def p(t,b=''):
if len(t)<2:return 0
z,b=0,b or sorted(t)
for i,c in enumerate(b):
w=b[:i]+b[i+1:]
if c==t[0]:return z+p(t[1:],w)
if i<1 or c!=b[i-1]:
n=f(len(w))
for _,v in g(w):n//=f(len(list(v)))
z+=n
Testing code:
def lexico_permute_string(s):
''' Generate all permutations of `s` in lexicographic order '''
a = sorted(s)
n = len(a) - 1
while True:
yield ''.join(a)
for j in range(n-1, -1, -1):
if a[j] < a[j + 1]:
break
else:
return
v = a[j]
for k in range(n, j, -1):
if v < a[k]:
break
a[j], a[k] = a[k], a[j]
a[j+1:] = a[j+1:][::-1]
def test_all(base):
for i, s in enumerate(lexico_permute_string(base)):
rank = p(s)
assert rank == i, (i, s, rank)
print('{:2} {} {:2}'.format(i, s, rank))
print(repr(base), 'ok\n')
for base in ('AAB', 'abbbbc'):
test_all(base)
def test(s):
print('{!r}\n{}\n'.format(s, p(s)))
for s in ('ZZZ', 'DCBA', 'a quick brown fox jumps over the lazy dog'):
test(s)
output
0 AAB 0
1 ABA 1
2 BAA 2
'AAB' ok
0 abbbbc 0
1 abbbcb 1
2 abbcbb 2
3 abcbbb 3
4 acbbbb 4
5 babbbc 5
6 babbcb 6
7 babcbb 7
8 bacbbb 8
9 bbabbc 9
10 bbabcb 10
11 bbacbb 11
12 bbbabc 12
13 bbbacb 13
14 bbbbac 14
15 bbbbca 15
16 bbbcab 16
17 bbbcba 17
18 bbcabb 18
19 bbcbab 19
20 bbcbba 20
21 bcabbb 21
22 bcbabb 22
23 bcbbab 23
24 bcbbba 24
25 cabbbb 25
26 cbabbb 26
27 cbbabb 27
28 cbbbab 28
29 cbbbba 29
'abbbbc' ok
'ZZZ'
0
'DCBA'
23
'a quick brown fox jumps over the lazy dog'
436629906477779191275460617121351796379337
Non-golfed version:
''' Determine the rank (lexicographic index) of a permutation
The permutation may contain repeated items
Written by PM 2Ring 2017.04.03
'''
from math import factorial as fac
from itertools import groupby
def lexico_permute_string(s):
''' Generate all permutations of `s` in lexicographic order '''
a = sorted(s)
n = len(a) - 1
while True:
yield ''.join(a)
for j in range(n-1, -1, -1):
if a[j] < a[j + 1]:
break
else:
return
v = a[j]
for k in range(n, j, -1):
if v < a[k]:
break
a[j], a[k] = a[k], a[j]
a[j+1:] = a[j+1:][::-1]
def perm_count(s):
''' Count the total number of permutations of sorted sequence `s` '''
n = fac(len(s))
for _, g in groupby(s):
n //= fac(sum(1 for u in g))
return n
def perm_rank(target, base):
''' Determine the permutation rank of string `target`
given the rank zero permutation string `base`,
i.e., the chars in `base` are in lexicographic order.
'''
if len(target) < 2:
return 0
total = 0
head, newtarget = target[0], target[1:]
for i, c in enumerate(base):
newbase = base[:i] + base[i+1:]
if c == head:
return total + perm_rank(newtarget, newbase)
elif i and c == base[i-1]:
continue
total += perm_count(newbase)
base = 'abcccdde'
print('total number', perm_count(base))
for i, s in enumerate(lexico_permute_string(base)):
rank = perm_rank(s, base)
assert rank == i, (i, s, rank)
#print('{:2} {} {:2}'.format(i, s, rank))
print('ok')
About lexico_permute_string
This algorithm, due to Narayana Pandita, is from https://en.wikipedia.org/wiki/Permutation#Generation_in_lexicographic_order
To produce the next permutation in lexicographic order of sequence a
- Find the largest index j such that a[j] < a[j + 1]. If no such index exists, the permutation is the last permutation.
- Find the largest index k greater than j such that a[j] < a[k].
- Swap the value of a[j] with that of a[k].
- Reverse the sequence from a[j + 1] up to and including the final element a[n].
FWIW, you can see an annotated version of that function here.
FWIW, here's the inverse function.
def perm_unrank(rank, base, head=''):
''' Determine the permutation with given rank of the
rank zero permutation string `base`.
'''
if len(base) < 2:
return head + ''.join(base)
total = 0
for i, c in enumerate(base):
if i < 1 or c != base[i-1]:
newbase = base[:i] + base[i+1:]
newtotal = total + perm_count(newbase)
if newtotal > rank:
return perm_unrank(rank - total, newbase, head + c)
total = newtotal
# Test
target = 'a quick brown fox jumps over the lazy dog'
base = ''.join(sorted(target))
rank = perm_rank(target, base)
print(target)
print(base)
print(rank)
print(perm_unrank(rank, base))
output
a quick brown fox jumps over the lazy dog
aabcdeefghijklmnoooopqrrstuuvwxyz
436629906477779191275460617121351796379337
a quick brown fox jumps over the lazy dog
And here's a function that I wrote while developing perm_unrank
which shows the breakdown of the subcounts.
def counts(base):
for i, c in enumerate(base):
newbase = base[:i] + base[i+1:]
if newbase and (i < 1 or c != base[i-1]):
yield c, perm_count(newbase)
for h, k in counts(newbase):
yield c + h, k
def show_counts(base):
TAB = ' ' * 4
for s, t in counts(base):
d = len(s) - 1
print('{}{} {}'.format(TAB * d, s, t))
# Test
base = 'abccc'
print('total number', perm_count(base))
show_counts(base)
output
a 4
ab 1
abc 1
abcc 1
ac 3
acb 1
acbc 1
acc 2
accb 1
accc 1
b 4
ba 1
bac 1
bacc 1
bc 3
bca 1
bcac 1
bcc 2
bcca 1
bccc 1
c 12
ca 3
cab 1
cabc 1
cac 2
cacb 1
cacc 1
cb 3
cba 1
cbac 1
cbc 2
cbca 1
cbcc 1
cc 6
cca 2
ccab 1
ccac 1
ccb 2
ccba 1
ccbc 1
ccc 2
ccca 1
cccb 1
MATL, 13 12 11 bytes
1 byte saved thanks to G B!
tY@Xu=!Af1)
Output is 1-based.
Try it online! Or verify all test cases.
Explanation
t % Input string implicitly. Duplicate
Y@ % All permutations, sorted; each in a different row
Xu % Unique rows
= % Compare for equality, with broadcast
! % Transpose
A % All: true for columns that contain all entries true
f % Find: indices of nonzero elements
1) % Get first of those indices. Implicitly display