7

I am trying to find an efficient algorithm to find permutation of a multiset, given an index.

Ex: given {1, 3, 3}. All permutations in an ascending lexicographic order are {133, 313, 331}. These elements are indexed as {0, 1, 2}. Given index=2, the result is 331.

I found an algorithm to find permutation of a set given a lexicographic index. His algorithm is efficient: O(n^2).

However, the algorithm is tested on a proper set (e.g. {1, 2, 3}), and not correct on my test. I describe his python code here so that you can easily follow.

from math import factorial, floor #// python library
from math import factorial, floor #// python library
i=5 #// i is the lexicographic index (counting starts from 0)
n=3 #// n is the length of the permutation
p = range(1,n+1) #// p is a list from 1 to n
for k in range(1,n+1): #// k goes from 1 to n
    d = i//factorial(n-k) #// use integer division (like division+floor)
    print(p[d]),
    p.remove(p[d])   #//delete p[d] from p
    i = i % factorial(n-k) #// reduce i to its remainder
Community
  • 1
  • 1
santa
  • 193
  • 10
  • Do you mean `{{1,3,3}, {3,1,3}, {3,3,1}}` as a result when you say `{133, 313, 331}` – UmNyobe Jul 01 '14 at 09:21
  • @UmNyobe: Yes, this is what I mean. The final result I expect is only {3, 3, 1}. The set of all possible combinations is not need to print out as the result. In the example, I just describe the set to explain. Thank you very much. – santa Jul 01 '14 at 09:29
  • I believe `itertools` can help you to implement it in python. Pay attention to combinatoric section here https://docs.python.org/2/library/itertools.html – Ivan Klass Jul 01 '14 at 09:39
  • @KlassIvan I don't think it will be efficient. – user189 Jul 01 '14 at 09:44
  • Related: http://stackoverflow.com/questions/19676109/how-to-generate-all-the-permutations-of-a-multiset – ecatmur Jul 01 '14 at 09:47
  • 2
    If you can efficiently count the number of permutations of a given multiset, then you have everything you need for an efficient algorithm: just loop through each possible distinct element that could appear at position 1 in lex order, and for each calculate the number of multiset permutations that remain when that element is removed. Add these counts to a total. As soon as you find the point where the total crosses your target index, you have decided that position, and can recurse to process the smaller subproblem. – j_random_hacker Jul 01 '14 at 10:35
  • Do you realize that you need Omega(n) bits to specify the index? – Niklas B. Jul 01 '14 at 10:46
  • @santa Hmm, I believe that what Bartosz Marcinkowski proposed is fast enough, I cannot think of any better approach, one idea is you can use binary search to find the correct number for each index, but you still need to calculate the permutations for each number in every position, which can hardly reduce to less than O(n^2) (remember that n should be very small , with n < 10 for integer and n < 18 for long, so O(n^2) is very very fast) – Pham Trung Jul 15 '14 at 07:10
  • The following geeksforgeeks.org article supposedly runs in O(n) time and space: - [Find n-th lexicographically permutation of a string](https://www.geeksforgeeks.org/find-n-th-lexicographically-permutation-string-set-2/) – Vepir Aug 25 '20 at 20:44
  • @Vepir that runs in O(n) where n is the nth permutation. that is very slow. there could be a factorial number of permutations... – Quantum Guy 123 Jun 21 '22 at 16:49

1 Answers1

5
# Python 2
from collections import Counter
from math import factorial


def count_permutations(counter):
    values = counter.values()
    return (
        factorial(sum(values))/reduce(lambda a, v: a * factorial(v), values, 1)
    )


def permutation(l, index):
    l = sorted(l)

    if not index:
        return l

    counter = Counter(l)
    total_count = count_permutations(counter)
    acc = 0
    for i, v in enumerate(l):

        if i > 0 and v == l[i-1]:
            continue

        count = total_count * counter[v] / len(l)

        if acc + count > index:
            return [v] + permutation(l[:i] + l[i + 1:], index - acc)

        acc += count

    raise ValueError("Not enough permutations")

Seems to work as expected

In [17]: for x in range(50): print x, permutation([1, 1, 2, 2, 2], x)
0 [1, 1, 2, 2, 2]
1 [1, 2, 1, 2, 2]
2 [1, 2, 2, 1, 2]
3 [1, 2, 2, 2, 1]
4 [2, 1, 1, 2, 2]
5 [2, 1, 2, 1, 2]
6 [2, 1, 2, 2, 1]
7 [2, 2, 1, 1, 2]
8 [2, 2, 1, 2, 1]
9 [2, 2, 2, 1, 1]
10---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
[...]
ValueError: Not enough permutations

Time complexity: O(n^2).

Bartosz Marcinkowski
  • 6,651
  • 4
  • 39
  • 69
  • @Thank you. But I have tried your code on the test: permutation([0, 0, 1, 1], 2), it prints out: [1, 0, 0, 1]. However, I think: because the set of permutation is {0011, 0101, 0110, 1001, 1010, 1100} in which the elements are indexed as {0, .., 5}; so, given index=2, it should print [0, 1, 1, 0]? – santa Jul 01 '14 at 12:38
  • Yeah, there was a bug. I fixed it. – Bartosz Marcinkowski Jul 01 '14 at 12:53
  • @Furthermore, when I test your code with index =4,5 on [0,0, 1, 1], it cannot print out the results although there are enough permutations. – santa Jul 01 '14 at 12:56
  • I have tried all my test cases and they are correct. Thank you very much – santa Jul 01 '14 at 13:47
  • Hi Bartosz, I have another trouble with this code. When I run on the test when len(l) = 1024, it printed out: "RuntimeError: maximum recursion depth exceeded in cmp". Could you please suggest me to solve the error. Thanks in advance – santa Jul 10 '14 at 03:43
  • I just figured out that just use: sys.setrecursionlimit(10000) to solve it – santa Jul 10 '14 at 04:36
  • Changing recursion limit is one way to solve it; another way is to refactor this code so that is uses iteration instead of recursion. I used recursion for sake of readability. – Bartosz Marcinkowski Jul 10 '14 at 06:48
  • Hi Bartosz, I am trying to speed up the code. I have tried gmpy2.fac instead of factorial, but the time does not reduce much. Could you kindly please suggest me – santa Jul 14 '14 at 12:19
  • 1
    Well, n^2 complexity must by slow for big input. You can speed it up a bit by replacing recursion with iteration, sorting input only once, using previous result of `count_permutations` to calculate another instead of calling it again, and finally push as much as possible of work to C code instead of Python code: use libraries implemented in C or write all your code in C, and then include it in your own cPython library so that you can use it in Python. – Bartosz Marcinkowski Jul 14 '14 at 14:08
  • @BartoszMarcinkowski Could you also provide the reverse process (From permutation -> index)? Is it also O(n^2)? – Anastasios Andronidis Mar 21 '16 at 12:14
  • 1
    @AnastasiosAndronidis For forward conversion (from string to index/rank), see [Permutation with Repetition Index Conversion](https://math.stackexchange.com/a/3803239/318073). – Vepir Aug 25 '20 at 20:41
  • @BartoszMarcinkowski the runtime for this is actually much higher than O(n^2) (even with the speedups you suggested in the comment section). in the for loop you have a multiplication with 'total_count'. 'total_count' could be at most n factorial, which needs nlogn bits to be represented. This means that the multiplication operation would cost O(n(logn)^2) (see here for more info https://en.wikipedia.org/wiki/Computational_complexity_of_mathematical_operations). This algorithm is O(n^3(logn)^2). – Quantum Guy 123 Jun 21 '22 at 18:26