web-dev-qa-db-fra.com

Trouver la n-ième permutation sans en calculer d'autres

Étant donné un tableau de N éléments représentant les atomes de permutation, existe-t-il un algorithme comme celui-ci:

function getNthPermutation( $atoms, $permutation_index, $size )

$atoms est le tableau d'éléments, $permutation_index est l'index de la permutation et $size est la taille de la permutation.

Par exemple:

$atoms = array( 'A', 'B', 'C' );
// getting third permutation of 2 elements
$perm = getNthPermutation( $atoms, 3, 2 );

echo implode( ', ', $perm )."\n";

Souhaitez imprimer:

B, A

Sans calculer toutes les permutations jusqu'à $ permutation_index?

J'ai entendu parler de permutations factoradiques, mais chaque implémentation que j'ai trouvée donne comme résultat une permutation de même taille que V, ce qui n'est pas mon cas.

Merci.

37

Comme l'a déclaré RickyBobby, lorsque vous considérez l'ordre lexicographique des permutations, vous devez utiliser la décomposition factorielle à votre avantage.

D'un point de vue pratique, voici comment je vois les choses:

  • Effectuez une sorte de division euclidienne, sauf que vous le faites avec des nombres factoriels, en commençant par (n-1)!, (n-2)!, etc.
  • Gardez les quotients dans un tableau. La i- ème quotient doit être un nombre compris entre 0 et n-i-1 inclus, où i passe de 0 à n-1.
  • Ce tableau est votre permutation. Le problème est que chaque quotient ne tient pas compte des valeurs précédentes, vous devez donc les ajuster. Plus explicitement, vous devez incrémenter chaque valeur autant de fois qu'il y a de valeurs précédentes inférieures ou égales.

Le code C suivant devrait vous donner une idée de la façon dont cela fonctionne (n est le nombre d'entrées et i est l'index de la permutation):

/**
 * @param n The number of entries
 * @param i The index of the permutation
 */
void ithPermutation(const int n, int i)
{
   int j, k = 0;
   int *fact = (int *)calloc(n, sizeof(int));
   int *perm = (int *)calloc(n, sizeof(int));

   // compute factorial numbers
   fact[k] = 1;
   while (++k < n)
      fact[k] = fact[k - 1] * k;

   // compute factorial code
   for (k = 0; k < n; ++k)
   {
      perm[k] = i / fact[n - 1 - k];
      i = i % fact[n - 1 - k];
   }

   // readjust values to obtain the permutation
   // start from the end and check if preceding values are lower
   for (k = n - 1; k > 0; --k)
      for (j = k - 1; j >= 0; --j)
         if (perm[j] <= perm[k])
            perm[k]++;

   // print permutation
   for (k = 0; k < n; ++k)
      printf("%d ", perm[k]);
   printf("\n");

   free(fact);
   free(perm);
}

Par exemple, ithPermutation(10, 3628799) imprime, comme prévu, la dernière permutation de dix éléments:

9 8 7 6 5 4 3 2 1 0
42
FelixCQ

Voici une solution qui permet de choisir la taille de la permutation. Par exemple, en plus de pouvoir générer toutes les permutations de 10 éléments, il peut générer des permutations de paires entre 10 éléments. En outre, il permute des listes d'objets arbitraires, pas seulement des entiers.

C'est PHP, mais il y a aussi JavaScript et Haskell impementation.

function nth_permutation($atoms, $index, $size) {
    for ($i = 0; $i < $size; $i++) {
        $item = $index % count($atoms);
        $index = floor($index / count($atoms));
        $result[] = $atoms[$item];
        array_splice($atoms, $item, 1);
    }
    return $result;
}

Exemple d'utilisation:

for ($i = 0; $i < 6; $i++) {
    print_r(nth_permutation(['A', 'B', 'C'], $i, 2));
}
// => AB, BA, CA, AC, BC, CB

Comment ça marche?

Il y a une idée très intéressante derrière cela. Prenons la liste A, B, C, D. Nous pouvons construire une permutation en en tirant des éléments comme dans un jeu de cartes. Au départ, nous pouvons dessiner l'un des quatre éléments. Puis l'un des trois éléments restants, et ainsi de suite, jusqu'à ce qu'il ne nous reste finalement plus rien.

Decision tree for permutations of 4 elements

Voici une séquence de choix possible. En partant du haut, nous prenons le troisième chemin, puis le premier, le deuxième et enfin le premier. Et c'est notre permutation # 13.

Pensez à la façon dont, compte tenu de cette séquence de choix, vous obtiendriez le nombre treize par algorithme. Inversez ensuite votre algorithme et c’est ainsi que vous pourrez reconstruire la séquence à partir d’un entier.

Essayons de trouver un schéma général pour intégrer une séquence de choix dans un entier sans redondance et la décompresser.

Un système intéressant est appelé système de nombres décimaux. "27" peut être considéré comme choisissant le chemin n ° 2 sur 10, puis le chemin n ° 7 sur 10.

Decision three for number 27 in decimal

Mais chaque chiffre ne peut encoder que des choix parmi 10 alternatives. D'autres systèmes à base fixe, tels que binaire et hexadécimal, ne peuvent également coder que des séquences de choix à partir d'un nombre fixe d'alternatives. Nous voulons un système à base variable, un peu comme les unités de temps, "14:05:29" correspond aux heures 14 à 24, la minute 5 à 60, la seconde 29 à 60.

Que se passe-t-il si nous prenons des fonctions génériques nombre à chaîne et chaîne à nombre et les trompons en utilisant des bases mélangées? Au lieu de prendre une seule base, comme parseInt ('beef', 16) et (48879) .toString (16) , ils prendront une base par chiffre.

function pack(digits, radixes) {
    var n = 0;
    for (var i = 0; i < digits.length; i++) {
        n = n * radixes[i] + digits[i];
    }
    return n;
}

function unpack(n, radixes) {
    var digits = [];
    for (var i = radixes.length - 1; i >= 0; i--) {
        digits.unshift(n % radixes[i]);
        n = Math.floor(n / radixes[i]);
    }
    return digits;
}

Est-ce que ça marche?

// Decimal system
pack([4, 2], [10, 10]); // => 42

// Binary system
pack([1, 0, 1, 0, 1, 0], [2, 2, 2, 2, 2, 2]); // => 42

// Factorial system
pack([1, 3, 0, 0, 0], [5, 4, 3, 2, 1]); // => 42

Et maintenant à l'envers:

unpack(42, [10, 10]); // => [4, 2]

unpack(42, [5, 4, 3, 2, 1]); // => [1, 3, 0, 0, 0]

C'est tellement beau. Appliquons maintenant ce système de nombres paramétriques au problème des permutations. Nous allons considérer la longueur 2 permutations de A, B, C, D. Quel est le nombre total d'entre eux? Voyons voir: nous dessinons d'abord l'un des 4 éléments, puis l'un des 3 autres, c'est-à-dire 4 * 3 = 12 façons de dessiner 2 éléments. Ces 12 manières peuvent être regroupées dans des entiers [0..11]. Alors, supposons que nous les avons déjà emballés et essayons de les décompresser:

for (var i = 0; i < 12; i++) {
    console.log(unpack(i, [4, 3]));
}

// [0, 0], [0, 1], [0, 2],
// [1, 0], [1, 1], [1, 2],
// [2, 0], [2, 1], [2, 2],
// [3, 0], [3, 1], [3, 2]

Ces nombres représentent des choix et non des index dans le tableau d'origine. [0, 0] ne signifie pas que vous prenez A, A, cela signifie que vous prenez le n ° 0 de A, B, C, D (c'est A), puis le n ° 0 de la liste restante B, C, D (c'est B). Et la permutation qui en résulte est A, B.

Un autre exemple: [3, 2] signifie que l’élément n ° 3 de A, B, C, D (D) est pris, puis que l’élément n ° 2 de la liste restante A, B, C (C). Et la permutation qui en résulte est D, C.

Ce mappage s'appelle code Lehmer . Mappons tous ces codes de Lehmer en permutations:

AB, AC, AD, BA, BC, BD, CA, CB, CD, DA, DB, DC

C'est exactement ce dont nous avons besoin. Mais si vous regardez la fonction unpack, vous remarquerez qu'elle produit des chiffres de droite à gauche (pour inverser les actions de pack). Le choix de 3 est décompressé avant le choix de 4. C'est dommage, car nous voulons choisir parmi 4 éléments avant de choisir parmi 3. Sans cela, nous devons d'abord calculer le code de Lehmer, l'accumuler dans un tableau temporaire, puis appliquez-le au tableau d'éléments pour calculer la permutation réelle.

Mais si nous ne nous soucions pas de l'ordre lexicographique, nous pouvons prétendre que nous voulons choisir parmi 3 éléments avant de choisir parmi 4. Ensuite, le choix parmi 4 sortira de unpack en premier. En d'autres termes, nous utiliserons unpack(n, [3, 4]) au lieu de unpack(n, [4, 3]). Cette astuce permet de calculer le prochain chiffre de code Lehmer et de l’appliquer immédiatement à la liste. Et c'est exactement comme cela que fonctionne nth_permutation().

Une dernière chose que je veux mentionner est que unpack(i, [4, 3]) est étroitement lié au système de nombres factoriels. Regardez à nouveau ce premier arbre, si nous voulons des permutations de longueur 2 sans doublons, nous pouvons simplement ignorer chaque indice de permutation sur deux. Cela nous donnera 12 permutations de longueur 4, qui peuvent être coupées à la longueur 2.

for (var i = 0; i < 12; i++) {
    var lehmer = unpack(i * 2, [4, 3, 2, 1]); // Factorial number system
    console.log(lehmer.slice(0, 2));
}
27
Alexey Lebedev

Cela dépend de la façon dont vous "triez" vos permutations (ordre lexicographique par exemple).

Une façon de le faire est le système de nombres factoriels , il vous donne une bijection entre [0, n!] Et toutes les permutations.

Ensuite, pour tout nombre i dans [0, n!], Vous pouvez calculer la ième permutation sans calculer les autres.

Cette écriture factorielle est basée sur le fait que tout nombre compris entre [0 et n!] Peut être écrit ainsi: 

SUM( ai.(i!) for i in range [0,n-1]) where ai <i 

(c'est assez similaire à la décomposition de base)

pour plus d'informations sur cette décomposition, consultez le fil de discussion suivant: https://math.stackexchange.com/questions/53262/factorial-decomposition-of-integers }

j'espère que ça aide


Comme indiqué sur cet article de wikipedia , cette approche équivaut à calculer le code lehmer :

Un moyen évident de générer des permutations de n consiste à générer des valeurs pour Le code de Lehmer (éventuellement en utilisant la représentation factorielle De nombres entiers jusqu’à n!), Puis de les convertir en code . .] permutations correspondantes. Cependant, cette dernière étape, bien que simple, est difficile à mettre en œuvre efficacement, car elle nécessite n opérations chacune parmi la sélection d’une séquence et son retrait, À une position arbitraire; des représentations évidentes de la séquence sous forme de tableau ou de liste chaînée, les deux requièrent (pour différentes raisons ]) environ n2/4 opérations pour effectuer la conversion. Avec n Susceptible d'être plutôt petit (surtout s'il faut générer toutes les Permutations), cela ne pose pas trop de problème, mais il s'avère que pour la génération systématique, il existe des solutions simples qui font beaucoup mieux. Pour cette raison, Ne semble pas utile, bien que certainement possible, d’employer une structure de données Spéciale qui permettrait de convertir le code de Lehmer En permutation dans O (n log n) heure.

Donc, le mieux que vous puissiez faire pour un ensemble de n éléments est O (n ln (n)) avec une structure de données adaptée.

15
Ricky Bobby

Voici un algorithme pour convertir les permutations et les rangs en temps linéaire. Cependant, le classement qu’il utilise n’est pas lexicographique. C'est bizarre, mais cohérent. Je vais donner deux fonctions, une qui convertit un rang en une permutation et une qui fait l'inverse.

Tout d'abord, pour ne pas être rangé (passer du rang à la permutation)

Initialize:
n = length(permutation)
r = desired rank
p = identity permutation of n elements [0, 1, ..., n]

unrank(n, r, p)
  if n > 0 then
    swap(p[n-1], p[r mod n])
    unrank(n-1, floor(r/n), p)
  fi
end

Ensuite, pour classer:

Initialize:
p = input permutation
q = inverse input permutation (in linear time, q[p[i]] = i for 0 <= i < n)
n = length(p)

rank(n, p, q)
  if n=1 then return 0 fi
  s = p[n-1]
  swap(p[n-1], p[q[n-1]])
  swap(q[s], q[n-1])
  return s + n * rank(n-1, p, q)
end

La durée d'exécution de ces deux éléments est O (n).

Il existe un beau document lisible expliquant pourquoi cela fonctionne: Classement et permutations inédites en temps linéaire, par Myrvold & Ruskey, Lettres relatives au traitement de l'information Volume 79, numéro 6, 30 septembre 2001, pages 281 à 284.

http://webhome.cs.uvic.ca/~ruskey/Publications/RankPerm/MyrvoldRuskey.pdf

7
Dave

Voici une solution courte et très rapide (linéaire dans le nombre d'éléments) en python, fonctionnant pour toute liste d'éléments (les 13 premières lettres de l'exemple ci-dessous):

from math import factorial

def nthPerm(n,elems):#with n from 0
    if(len(elems) == 1):
        return elems[0]
    sizeGroup = factorial(len(elems)-1)
    q,r = divmod(n,sizeGroup)
    v = elems[q]
    elems.remove(v)
    return v + ", " + ithPerm(r,elems)

Exemples :

letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m']

ithPerm(0,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, k, l, m
ithPerm(4,letters[:])          #--> a, b, c, d, e, f, g, h, i, j, m, k, l
ithPerm(3587542868,letters[:]) #--> h, f, l, i, c, k, a, e, g, m, d, b, j

Remarque: Je donne letters[:] (une copie de letters) et non des lettres car la fonction modifie son paramètre elems (supprime l'élément choisi).

5
ismax

Le code suivant calcule la kth permutation pour n donné.

c'est-à-dire n = 3. Les différentes permutations sont 123 132 213 231 312 321

Si k = 5, retourne 312. En d'autres termes, la k-ième permutation lexicographique est donnée.

    public static String getPermutation(int n, int k) {
        char temp[] = IntStream.range(1, n + 1).mapToObj(i -> "" + i).collect(Collectors.joining()).toCharArray();
        return getPermutationUTIL(temp, k, 0);
    }

    private static String getPermutationUTIL(char temp[], int k, int start) {
        if (k == 1)
            return new String(temp);
        int p = factorial(temp.length - start - 1);
        int q = (int) Math.floor(k / p);
        if (k % p == 0)
            q = q - 1;
        if (p <= k) {
            char a = temp[start + q];
            for (int j = start + q; j > start; j--)
                temp[j] = temp[j - 1];
            temp[start] = a;
        }
        return k - p >= 0 ? getPermutationUTIL(temp, k - (q * p), start + 1) : getPermutationUTIL(temp, k, start + 1);
    }

    private static void swap(char[] arr, int j, int i) {
        char temp = arr[i];
        arr[i] = arr[j];
        arr[j] = temp;
    }

    private static int factorial(int n) {
        return n == 0 ? 1 : (n * factorial(n - 1));
    }
1
Koustav Chatterjee

C'est calculable. Ceci est un code C # qui le fait pour vous.

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;
        }

        // ************************************************************************
    }
}
0
Eric Ouellet