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
Hello and welcome to the site. This question is currently rather unclear. I am not really sure how the permutations are ordered. Are they lexicographically ordered? This should be defined in your question. – Post Rock Garf Hunter – 2017-04-02T01:35:10.003
1
We also have a sandbox so you can get this kind of feedback before posting to our main site. It is not mandatory to post there first, but a lot of times it is very helpful.
– Post Rock Garf Hunter – 2017-04-02T01:39:22.890You said 'Uppercase',
zzz
anddcba
is not uppercase. – Matthew Roh – 2017-04-02T02:07:14.290@SIGSEGV corrected – kyrill – 2017-04-02T02:12:18.140
Can the output index be 1-based instead of 0-based? – Luis Mendo – 2017-04-02T02:51:20.940
Well... let's see what you have. – kyrill – 2017-04-02T02:55:23.673
Uhm, Is that a yes? – Luis Mendo – 2017-04-02T02:56:21.147
Yep. It's a yes. – kyrill – 2017-04-02T02:56:43.140
if you decide to account for the case when the input is empty, the output has to be 0 I think this should be removed. Answers can always claim they don't support empty input. Same for You can support other characters than ASCII uppercase letters – Luis Mendo – 2017-04-02T03:03:43.840
I agree in case of empty input, but other characters than uppercase letters would still have well-defined expected output. What I mean is that you don't have to support it, but you don't have to make an effort to treat such input as an error. If they claim they don't support it, then they just don't support it. Rules don't disallow that. – kyrill – 2017-04-02T03:10:26.410
But you say Input: a sequence of uppercase letters. So, considering what would happen for other characters is pointless – Luis Mendo – 2017-04-02T03:23:15.270
Right, I didn't realize that. Other letters than uppercase removed. – kyrill – 2017-04-02T03:28:05.120