14

I've been working on this for hours but couldn't figure it out.

Define a permutation's degree to be the minimum number of transpositions that need to be composed to create it. So a the degree of (0, 1, 2, 3) is 0, the degree of (0, 1, 3, 2) is 1, the degree of (1, 0, 3, 2) is 2, etc.

Look at the space Snd as the space of all permutations of a sequence of length n that have degree d.

I want two algorithms. One that takes a permutation in that space and assigns it an index number, and another that takes an index number of an item in Snd and retrieves its permutation. The index numbers should obviously be successive (i.e. in the range 0 to len(Snd)-1, with each permutation having a distinct index number.)

I'd like this implemented in O(sane); which means that if you're asking for permutation number 17, the algorithm shouldn't go over all the permutations between 0 and 16 to retrieve your permutation.

Any idea how to solve this?

(If you're going to include code, I prefer Python, thank you.)

Update:

I want a solution in which

  1. The permutations are ordered according to their lexicographic order (and not by manually ordering them, but by an efficient algorithm that gives them with lexicographic order to begin with) and
  2. I want the algorithm to accept a sequence of different degrees as well, so I could say "I want permutation number 78 out of all permutations of degrees 1, 3 or 4 out of the permutation space of range(5)". (Basically the function would take a tuple of degrees.) This'll also affect the reverse function that calculates index from permutation; based on the set of degrees, the index would be different.

I've tried solving this for the last two days and I was not successful. If you could provide Python code, that'd be best.

Ram Rachum
  • 84,019
  • 84
  • 236
  • 374
  • "I want two algorithms. One that takes a permutation in that space..." So are you always working with permutations of a specific degree when mapping from permutation to index? Or is this supposed to work for all permutations (and in that case why are you concerned with degrees at all)? – The Archetypal Paul May 16 '14 at 15:44
  • Divide and conquer; a binary search will find all the left swaps, all the right ones, then you need to adjust for the left/right swaps. – Reblochon Masque May 16 '14 at 15:45
  • @Paul Yes, it's constrained to permutations of a specific degree. – Ram Rachum May 16 '14 at 15:45
  • But in any case, if you can generate all permutations of degree 'd', you can lexicographically sort them, then use a binary search to locate the correct index (the other algoritm is just array-indexing) – The Archetypal Paul May 16 '14 at 15:46
  • 2
    What's the degree of `3 1 2 0`? – David Eisenstat May 16 '14 at 15:46
  • I don't want to generate them (I'll add this to the question.) I want the algorithm to work without generating all the permutations. – Ram Rachum May 16 '14 at 15:47
  • 1
    @DavidEisenstat The degree of `3 1 2 0` is 1 because it's a transposition itself. – Ram Rachum May 16 '14 at 15:48
  • So if you do have a way of lexiograhically generating them (and there are algorithms for that), then the algorithms turn into "the step this permutation would have been produced by such an algorithm" and "directly produce the nth permutation produced by such an algorithm". These sound feasible – The Archetypal Paul May 16 '14 at 15:53
  • Coincidentally, I asked a question about lexiographic generation of permutations: http://stackoverflow.com/questions/4284971/generalising-a-next-permutation-function – The Archetypal Paul May 16 '14 at 15:55
  • Foo. You need to lexicographically generate ones of degree 'd', of course. Not sure how to do that. – The Archetypal Paul May 16 '14 at 15:57
  • I might be wrong, but one single index can actually be mapped to multiple permutations. how do you pick which permutation you want? or do you want to generate all of them? – Samy Arous May 16 '14 at 17:47
  • @SamyArous Yes, you're wrong. We're basically looking for an indexing system; if your indexing system has multiple permutations mapped to the same index, then it's not an indexing system. – Ram Rachum May 17 '14 at 23:37

7 Answers7

4

The permutations of length n and degree d are exactly those that can be written as a composition of k = n - d cycles that partition the n elements. The number of such permutations is given by the Stirling numbers of the first kind, written n atop k in square brackets.

Stirling numbers of the first kind satisfy a recurrence relation

[n]           [n - 1]   [n - 1]
[ ] = (n - 1) [     ] + [     ]
[k]           [  k  ]   [k - 1],

which means, intuitively, the number of ways to partition n elements into k cycles is to partition n - 1 non-maximum elements into k cycles and splice in the maximum element in one of n - 1 ways, or put the maximum element in its own cycle and partition the n - 1 non-maximum elements into k - 1 cycles. Working from a table of recurrence values, it's possible to trace the decisions down the line.

memostirling1 = {(0, 0): 1}
def stirling1(n, k):
    if (n, k) not in memostirling1:
        if not (1 <= k <= n): return 0
        memostirling1[(n, k)] = (n - 1) * stirling1(n - 1, k) + stirling1(n - 1, k - 1)
    return memostirling1[(n, k)]

def unrank(n, d, i):
    k = n - d
    assert 0 <= i <= stirling1(n, k)
    if d == 0:
        return list(range(n))
    threshold = stirling1(n - 1, k - 1)
    if i < threshold:
        perm = unrank(n - 1, d, i)
        perm.append(n - 1)
    else:
        (q, r) = divmod(i - threshold, stirling1(n - 1, k))
        perm = unrank(n - 1, d - 1, r)
        perm.append(perm[q])
        perm[q] = n - 1
    return perm
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • Very nice! Is it n-1 instead of n in "and partition the n non-maximum elements into k - 1 cycles"? – Ante May 16 '14 at 17:33
  • @Ante Yeah, I was transcribing the formula from Wikipedia, which specifies n + 1 instead of n, and I forgot to change one. – David Eisenstat May 16 '14 at 17:36
  • @DavidEisenstat This algorithm seems to work. However, I really want the permutations to be in lexicographic order, and to be able to get multiple degrees. (See my update above.) I've spent the last two days working on this, trying to modify the algorithm to give the permutations in lexicographic order, but no success. – Ram Rachum May 18 '14 at 23:28
1

This answer is less elegant/efficient than my other one, but it describes a polynomial-time algorithm that copes with the additional constraints on the ordering of permutations. I'm going to describe a subroutine that, given a prefix of an n-element permutation and a set of degrees, counts how many permutations have that prefix and a degree belonging to the set. Given this subroutine, we can do an n-ary search for the permutation of a specified rank in the specified subset, extending the known prefix one element at a time.

We can visualize an n-element permutation p as an n-vertex, n-arc directed graph where, for each vertex v, there is an arc from v to p(v). This digraph consists of a collection of vertex-disjoint cycles. For example, the permutation 31024 looks like

 _______
/       \
\->2->0->3
 __     __
/  |   /  |
1<-/   4<-/ .

Given a prefix of a permutation, we can visualize the subgraph corresponding to that prefix, which will be a collection of vertex-disjoint paths and cycles. For example, the prefix 310 looks like

2->0->3
 __
/  |
1<-/ .

I'm going to describe a bijection between (1) extensions of this prefix that are permutations and (2) complete permutations on a related set of elements. This bijection preserves up to a constant term the number of cycles (which is the number of elements minus the degree). The constant term is the number of cycles in the prefix.

The permutations mentioned in (2) are on the following set of elements. Start with the original set, delete all elements involved in cycles that are complete in the prefix, and introduce a new element for each path. For example, if the prefix is 310, then we delete the complete cycle 1 and introduce a new element A for the path 2->0->3, resulting in the set {4, A}. Now, given a permutation in set (1), we obtain a permutation in set (2) by deleting the known cycles and replacing each path by its new element. For example, the permutation 31024 corresponds to the permutation 4->4, A->A, and the permutation 31042 corresponds to the permutation 4->A, A->4. I claim (1) that this map is a bijection and (2) that it preserves degrees as described before.

The definition, more or less, of the (n,k)-th Stirling number of the first kind, written

[n]
[ ]
[k]

(ASCII art square brackets), is the number of n-element permutations of degree n - k. To compute the number of extensions of an r-element prefix of an n-element permutation, count c, the number of complete cycles in the prefix. Sum, for each degree d in the specified set, the Stirling number

[  n - r  ]
[         ]
[n - d - c]

of the first kind, taking the terms with "impossible" indices to be zero (some analytically motivated definitions of the Stirling numbers are nonzero in unexpected places).

To get a rank from a permutation, we do n-ary search again, except this time, we use the permutation rather than the rank to guide the search.

Here's some Python code for both (including a test function).

import itertools

memostirling1 = {(0, 0): 1}
def stirling1(n, k):
    ans = memostirling1.get((n, k))
    if ans is None:
        if not 1 <= k <= n: return 0
        ans = (n - 1) * stirling1(n - 1, k) + stirling1(n - 1, k - 1)
        memostirling1[(n, k)] = ans
    return ans

def cyclecount(prefix):
    c = 0
    visited = [False] * len(prefix)
    for (i, j) in enumerate(prefix):
        while j < len(prefix) and not visited[j]:
            visited[j] = True
            if j == i:
                c += 1
                break
            j = prefix[j]
    return c

def extcount(n, dset, prefix):
    c = cyclecount(prefix)
    return sum(stirling1(n - len(prefix), n - d - c) for d in dset)

def unrank(n, dset, rnk):
    assert rnk >= 0
    choices = set(range(n))
    prefix = []
    while choices:
        for i in sorted(choices):
            prefix.append(i)
            count = extcount(n, dset, prefix)
            if rnk < count:
                choices.remove(i)
                break
            del prefix[-1]
            rnk -= count
        else:
            assert False
    return tuple(prefix)

def rank(n, dset, perm):
    assert n == len(perm)
    rnk = 0
    prefix = []
    choices = set(range(n))
    for j in perm:
        choices.remove(j)
        for i in sorted(choices):
            if i < j:
                prefix.append(i)
                rnk += extcount(n, dset, prefix)
                del prefix[-1]
        prefix.append(j)
    return rnk

def degree(perm):
    return len(perm) - cyclecount(perm)

def test(n, dset):
    for (rnk, perm) in enumerate(perm for perm in itertools.permutations(range(n)) if degree(perm) in dset):
        assert unrank(n, dset, rnk) == perm
        assert rank(n, dset, perm) == rnk

test(7, {2, 3, 5})
David Eisenstat
  • 64,237
  • 7
  • 60
  • 120
  • This looks like the most promising answer and I'm working on integrating this into my package. But, I asked for algorithms for both ways. (You're missing that one that takes the permutation and gives out its number.) – Ram Rachum May 20 '14 at 20:06
  • @RamRachum It's there now, as `rank`. – David Eisenstat May 21 '14 at 13:32
0

I think you're looking for a variant of the Levenshtein distance which is used to measure the number of edits between two strings. The efficient way to compute this is by employing a technique called dynamic programming - a pseudo-algorithm for the "normal" Levenshtein distance is provided in the linked article. You would need to adapt this to account for the fact that instead of adding, deleting, or substituting a character, the only allowed operation is exchanging elements at two positions.

Concerning your second algorithm: It's not a 1:1 relationship between degrees of permutation and "a" resulting permutation, instead the number of possible results grows exponentially with the number of swaps: For a sequence of k elements, there's k*(k-1)/2 possible pairs of indices between which to swap. If we call that number l, after d swaps you have l^d possible results (even though some of them might be identical, as in first swapping 0<>1 then 2<>3, or first 2<>3 then 0<>1).

Nicolas78
  • 5,124
  • 1
  • 23
  • 41
  • If you can't go from index number to permutation, then that index number is useless to me. Does your solution provide both ways or not? – Ram Rachum May 16 '14 at 16:16
  • what I'm saying is the mathematics of your problem does not provide both ways ;) – Nicolas78 May 16 '14 at 16:28
  • ie there is no such thing as "your permutation" (singular) at degree x. To refer to @David Eisenstat's much more sophisticated post, the Stirling number is always > 1 – Nicolas78 May 16 '14 at 17:18
0

I wrote this stackoverflow answer to a similar problem: https://stackoverflow.com/a/13056801/10562 . Could it help?

The difference might be in the swapping bit for generating the perms, but an index-to-perm and perm-to-index function is given in Python.

I later went on to create this Rosetta Code task that is fleshed out with references and more code: http://rosettacode.org/wiki/Permutations/Rank_of_a_permutation.

Hope it helps :-)

Community
  • 1
  • 1
Paddy3118
  • 4,704
  • 27
  • 38
0

The first part is straight forward if you work wholly in the lexiographic side of things. Given my answer on the other thread, you can go from a permutation to the factorial representation instantly. Basically, you imagine a list {0,1,2,3} and the number that I need to go along is the factorial representation, so for 1,2,3,4, i keep taking the zeroth element and get 000 (0*3+0*!2+0*!1!).

0,1,2,3, => 000

1032 = 3!+1! = 8th permuation (as 000 is the first permutation) => 101

And you can work out the degree trivially, as each transposition which swaps a pair of numbers (a,b) a

So 0123 -> 1023 is 000 -> 100.

if a>b you swap the numbers and then subtract one from the right hand number.

Given two permuations/lexiographic numbers, I just permute the digits from right to left like a bubble sort, counting the degree that I need, and building the new lexiographic number as I go. So to go from 0123 to the 1032 i first move the 1 to the left, then the zero is in the right position, and then I move the 2 into position, and both of those had pairs with the rh number greater than the left hand number, so both add a 1, so 101.

This deals with your first problem. The second is much more difficult, as the numbers of degree two are not evenly distributed. I don't see anything better than getting the global lexiographic number (global meaning here the number without any exclusions) of the permutation you want, e.g. 78 in your example, and then go through all the lexiographic numbers and each time that you get to one which is degree 2, then add one to your global lexiographic number, e.g. 78 -> 79 when you find the first number of degree 2. Obvioulsy, this will not be fast. Alternatively you could try generating all the numbers of degree to. Given a set of n elements, there are (n-1)(n-2) numbers of degree 2, but its not clear that this holds going forward, at least to me, which might easily be a lot less work than computing all the numbers up to your target. and you could just see which ones have lexiographic number less than your target number, and again add one to its global lexiographic number.

Ill see if i can come up with something better.

phil_20686
  • 4,000
  • 21
  • 38
0

This seemed like fun so I thought about it some more.

Let's take David's example of 31042 and find its index. First we determine the degree, which equals the sum of the cardinalities of the permutation cycles, each subtracted by 1.

01234
31042

permutation cycles (0342)(1)
degree = (4-1) + (1-1) = 3

def cycles(prefix):
  _cycles = []
  i = j = 0
  visited = set()

  while j < len(prefix):
    if prefix[i] == i:
      _cycles.append({"is":[i],"incomplete": False})
      j = j + 1
      i = i + 1
    elif not i in visited:
      cycle = {"is":[],"incomplete": False}
      cycleStart = -1
      while True:
        if i >= len(prefix):
          for k in range(len(_cycles) - 1,-1,-1):
            if any(i in cycle["is"] for i in _cycles[k]["is"]):
              cycle["is"] = list(set(cycle["is"] + _cycles[k]["is"]))
              del _cycles[k]
          cycle["incomplete"] = True
          _cycles.append(cycle)
          break
        elif cycleStart == i:
          _cycles.append(cycle)
          break
        else:
          if prefix[i] == j + 1:
            j = j + 1
          visited.add(i)
          if cycleStart == -1:
            cycleStart = i
          cycle["is"].append(i)
          i = prefix[i]
    while j in visited:
      j = j + 1
    i = j
  return _cycles

def degree(cycles):
  d = 0
  for i in cycles:
    if i["incomplete"]:
      d = d + len(i["is"])
    else:
      d = d + len(i["is"]) - 1
  return d

Next we determine how many permutations of degree 3 start with either zero, one, or two; using David's formula:

number of permutations of n=5,d=3 that start with "0" = S(4,4-3) = 6
number of permutations of n=5,d=3 that start with "1" = S(4,4-2) = 11

[just in case you're wondering, I believe the ones starting with "1" are:
 (01)(234)
 (01)(243)
 (201)(34)
 (301)(24)
 (401)(23)
 (2301)(4)
 (2401)(3)
 (3401)(2)
 (3201)(4)
 (4201)(3)
 (4301)(2) notice what's common to all of them?]

number of permutations of n=5,d=3 that start with "2" = S(4,4-2) = 11

We wonder whether there might be a lexicographically-lower permutation of degree 3 that also starts with "310". The only possibility seems to be 31024:

01234
31024 ?
permutaiton cycles (032)(4)(1)
degree = (3-1) + (1-1) + (1-1) = 2
since its degree is different, we will not apply 31024 to our calculation

The permutations of degree 3 that start with "3" and are lexicographically lower than 31042 must start with the prefix "30". Their count is equal to the number of ways we can maintain "three" before "zero" and "zero" before "one" in our permutation cycles while keeping the sum of the cardinalities of the cycles, each subtracted by 1 (i.e., the degree), at 3.

(031)(24)
(0321)(4)
(0341)(2)
count = 3

It seems that there are 6 + 11 + 11 + 3 = 31 permutations of n=5, d=3 before 31042.

def next(prefix,target):
  i = len(prefix) - 1
  if prefix[i] < target[i]:
    prefix[i] = prefix[i] + 1
  elif prefix[i] == target[i]:
    prefix.append(0)
    i = i + 1
  while prefix[i] in prefix[0:i]:
    prefix[i] = prefix[i] + 1
  return prefix

def index(perm,prefix,ix):
  if prefix == perm:
    print ix
  else:
    permD = degree(cycles(perm))
    prefixD = degree(cycles(prefix))
    n = len(perm) - len(prefix)
    k = n - (permD - prefixD)
    if prefix != perm[0:len(prefix)] and permD >= prefixD:
      ix = ix + S[n][k]
    index(perm,next(prefix,perm),ix)

S = [[1]
    ,[0,1]
    ,[0,1,1]
    ,[0,2,3,1]
    ,[0,6,11,6,1]
    ,[0,24,50,35,10,1]]

(Let's try to confirm with David' program (I'm using a PC with windows):

C:\pypy>pypy test.py REM print(index([3,1,0,4,2],[0],0))
31

C:\pypy>pypy davids_rank.py REM print(rank(5,{3},[3,1,0,2,4]))
31
גלעד ברקן
  • 23,602
  • 3
  • 25
  • 61
0

A bit late and not in Python but in C#...

I think the following code should work for you. It works for permutation possibilities where for x items, the number of permutations are x!

The algo calculate the index of a permutation and the reverse of it.

using System;
using System.Collections.Generic;

namespace WpfPermutations
{
    public class PermutationOuelletLexico3<T>
    {
        // ************************************************************************
        private T[] _sortedValues;

        private bool[] _valueUsed;

        public readonly long MaxIndex; // long to support 20! or less 

        // ************************************************************************
        public PermutationOuelletLexico3(T[] sortedValues)
        {
            if (sortedValues.Length <= 0)
            {
                throw new ArgumentException("sortedValues.Lenght should be greater than 0");
            }

            _sortedValues = sortedValues;
            Result = new T[_sortedValues.Length];
            _valueUsed = new bool[_sortedValues.Length];

            MaxIndex = Factorial.GetFactorial(_sortedValues.Length);
        }

        // ************************************************************************
        public T[] Result { get; private set; }

        // ************************************************************************
        /// <summary>
        /// Return the permutation relative to the index received, according to 
        /// _sortedValues.
        /// Sort Index is 0 based and should be less than MaxIndex. Otherwise you get an exception.
        /// </summary>
        /// <param name="sortIndex"></param>
        /// <param name="result">Value is not used as inpu, only as output. Re-use buffer in order to save memory</param>
        /// <returns></returns>
        public void GetValuesForIndex(long sortIndex)
        {
            int size = _sortedValues.Length;

            if (sortIndex < 0)
            {
                throw new ArgumentException("sortIndex should be greater or equal to 0.");
            }

            if (sortIndex >= MaxIndex)
            {
                throw new ArgumentException("sortIndex should be less than factorial(the lenght of items)");
            }

            for (int n = 0; n < _valueUsed.Length; n++)
            {
                _valueUsed[n] = false;
            }

            long factorielLower = MaxIndex;

            for (int index = 0; index < size; index++)
            {
                long factorielBigger = factorielLower;
                factorielLower = Factorial.GetFactorial(size - index - 1);  //  factorielBigger / inverseIndex;

                int resultItemIndex = (int)(sortIndex % factorielBigger / factorielLower);

                int correctedResultItemIndex = 0;
                for(;;)
                {
                    if (! _valueUsed[correctedResultItemIndex])
                    {
                        resultItemIndex--;
                        if (resultItemIndex < 0)
                        {
                            break;
                        }
                    }
                    correctedResultItemIndex++;
                }

                Result[index] = _sortedValues[correctedResultItemIndex];
                _valueUsed[correctedResultItemIndex] = true;
            }
        }

        // ************************************************************************
        /// <summary>
        /// Calc the index, relative to _sortedValues, of the permutation received
        /// as argument. Returned index is 0 based.
        /// </summary>
        /// <param name="values"></param>
        /// <returns></returns>
        public long GetIndexOfValues(T[] values)
        {
            int size = _sortedValues.Length;
            long valuesIndex = 0;

            List<T> valuesLeft = new List<T>(_sortedValues);

            for (int index = 0; index < size; index++)
            {
                long indexFactorial = Factorial.GetFactorial(size - 1 - index);

                T value = values[index];
                int indexCorrected = valuesLeft.IndexOf(value);
                valuesIndex = valuesIndex + (indexCorrected * indexFactorial);
                valuesLeft.Remove(value);
            }
            return valuesIndex;
        }

        // ************************************************************************
    }
}
Eric Ouellet
  • 10,996
  • 11
  • 84
  • 119