3
votes

Comment obtenir directement le ème élément de la ème permutation d'une séquence (sans aucune récursivité)?

Disons que nous avons une chaîne "ABCD" et que nous voulons récupérer la lettre en i-ième position à partir de la n-ième permutation de la chaîne.

Dans cet exemple, je sais qu'il y a factorielle (4) = 24 permutations et la liste peut être récupérée facilement avec itertools.permutations, ce qui donnera ceci:

['ABCD', 'ABDC', 'ACBD', 'ACDB', 'ADBC', 'ADCB', 'BACD', 'BADC', 'BCAD', 'BCDA', 'BDAC', 'BDCA »,« CABD »,« CADB »,« CBAD »,« CBDA »,« CDAB »,« CDBA »,« DABC »,« DACB »,« DBAC »,« DBCA »,« DCAB »,« DCBA »]

La fonction f que je recherche doit donc renvoyer:

f (0, n) == ['A', 'A', 'A', 'A', 'A', 'A', 'B', 'B', 'B', 'B ',' B ',' B ',' C ',' C ',' C ',' C ',' C ',' C ',' D ',' D ',' D ',' D ', 'D', 'D'] [n]

f (1, n) == ['B', 'B', 'C', 'C', 'D', 'D', 'A', 'A', 'C', 'C ',' D ',' D ',' A ',' A ',' B ',' B ',' D ',' D ',' A ',' A ',' B ',' B ', 'C', 'C'] [n]

f (2, n) == ['C', 'D', 'B', 'D', 'B', 'C', 'C', 'D', 'A', 'D ',' A ',' C ',' B ',' D ',' A ',' D ',' A ',' B ',' B ',' C ',' A ',' C ', 'A', 'B'] [n]

f (3, n) == ['D', 'C', 'D', 'B', 'C', 'B', 'D', 'C', 'D', 'A ',' C ',' A ',' D ',' B ',' D ',' A ',' B ',' A ',' C ',' B ',' C ',' A ', 'B', 'A'] [n]

C'est assez facile pour i == 0, nous avons f (0, n) == "ABCD" [n // 6] mais trouver un motif quand i augmente est de plus en plus compliqué. p>

Peu m'importe comment les permutations sont ordonnées, alors peut-être qu'un modèle commun pour toutes les valeurs de i peut être trouvé facilement ...

Je prévois de l'utiliser avec un ensemble de 256 éléments et permutations factorielles (256), donc calculer les permutations n'est pas une option.

Edit: j'ai déjà une fonction pour cela mais c'est trop lent et je me demandais si un résultat équivalent peut être trouvé avec une formule simple utilisant par exemple des opérations au niveau du bit ...

Edit-2: Voici la meilleure solution actuelle grâce à @rici:

f = [factorial(i) for i in range(256)]
    
def _getElt(k, i):
    """
    Returns the <i>th element of the <k>th permutation of 0..255
    """
    table = list(range(256))

    for j in range(255, 254-i, -1):
        r, k = divmod(k, f[j])
        perm[j], perm[r] = perm[r], perm[j] 
    return perm[255 - i]

Edit-3: Voici une autre approche utilisant des approximations polynomiales pour recréer les permutations, donc une formulation différente du problème pourrait être "comment recréer le i-ième coefficient du polynôme pour la n-ième permutation?".

Ceci est une liste de n, permutation, coefficient de polynôme (et finall y la permutation reconstruite à partir des coefficients du polynôme) pour N = 4:

0 [0, 1, 2, 3] [Fraction (0, 1), Fraction (1, 1), Fraction (0, 1), Fraction (0, 1)] [0, 1, 2, 3 ]

1 [0, 1, 3, 2] [Fraction (0, 1), Fraction (-3, 4), Fraction (5, 2), Fraction (-2, 3)] [0, 1, 3 , 2]

2 [0, 2, 1, 3] [Fraction (0, 1), Fraction (11, 2), Fraction (-9, 2), Fraction (1, 1)] [0, 2, 1, 3]

3 [0, 2, 3, 1] [Fraction (0, 1), Fraction (7, 4), Fraction (1, 2), Fraction (-1, 3)] [0, 2, 3, 1]

4 [0, 3, 1, 2] [Fraction (0, 1), Fraction (33, 4), Fraction (-13, 2), Fraction (4, 3)] [0, 3, 1, 2]

5 [0, 3, 2, 1] [Fraction (0, 1), Fraction (19, 3), Fraction (-4, 1), Fraction (2, 3)] [0, 3, 2, 1]

6 [1, 0, 2, 3] [Fraction (1, 1), Fraction (-15, 4), Fraction (7, 2), Fraction (-2, 3)] [1, 0, 2 , 3]

7 [1, 0, 3, 2] [Fraction (1, 1), Fraction (-17, 3), Fraction (6, 1), Fraction (-4, 3)] [1, 0, 3 , 2]

8 [1, 2, 0, 3] [Fraction (1, 1), Fraction (21, 4), Fraction (-11, 2), Fraction (4, 3)] [1, 2, 0, 3]

9 [1, 2, 3, 0] [Fraction (1, 1), Fraction (-1, 3), Fraction (2, 1), Fraction (-2, 3)] [1, 2, 3 , 0]

10 [1, 3, 0, 2] [Fraction (1, 1), Fraction (31, 4), Fraction (-15, 2), Fraction (5, 3)] [1, 3, 0, 2]

11 [1, 3, 2, 0] [Fraction (1, 1), Fraction (17, 4), Fraction (-5, 2), Fraction (1, 3)] [1, 3, 2, 0]

12 [2, 0, 1, 3] [Fraction (2, 1), Fraction (-17, 4), Fraction (5, 2), Fraction (-1, 3)] [2, 0, 1 , 3]

13 [2, 0, 3, 1] [Fraction (2, 1), Fraction (-31, 4), Fraction (15, 2), Fraction (-5, 3)] [2, 0, 3 , 1]

14 [2, 1, 0, 3] [Fraction (2, 1), Fraction (1, 3), Fraction (-2, 1), Fraction (2, 3)] [2, 1, 0, 3]

15 [2, 1, 3, 0] [Fraction (2, 1), Fraction (-21, 4), Fraction (11, 2), Fraction (-4, 3)] [2, 1, 3 , 0]

16 [2, 3, 0, 1] [Fraction (2, 1), Fraction (17, 3), Fraction (-6, 1), Fraction (4, 3)] [2, 3, 0, 1]

17 [2, 3, 1, 0] [Fraction (2, 1), Fraction (15, 4), Fraction (-7, 2), Fraction (2, 3)] [2, 3, 1, 0]

18 [3, 0, 1, 2] [Fraction (3, 1), Fraction (-19, 3), Fraction (4, 1), Fraction (-2, 3)] [3, 0, 1 , 2]

19 [3, 0, 2, 1] [Fraction (3, 1), Fraction (-33, 4), Fraction (13, 2), Fraction (-4, 3)] [3, 0, 2 , 1]

20 [3, 1, 0, 2] [Fraction (3, 1), Fraction (-7, 4), Fraction (-1, 2), Fraction (1, 3)] [3, 1, 0 , 2]

21 [3, 1, 2, 0] [Fraction (3, 1), Fraction (-11, 2), Fraction (9, 2), Fraction (-1, 1)] [3, 1, 2 , 0]

22 [3, 2, 0, 1] [Fraction (3, 1), Fraction (3, 4), Fraction (-5, 2), Fraction (2, 3)] [3, 2, 0, 1]

23 [3, 2, 1, 0] [Fraction (3, 1), Fraction (-1, 1), Fraction (0, 1), Fraction (0, 1)] [3, 2, 1, 0]

On voit bien qu'il y a une symétrie: coefs [i] = [3 - coefs [23-i] [0]] + [-c pour c dans coefs [23-i] [1 :]] donc c'est une façon d'explorer mais je n'ai aucune idée que c'est possible.


4 commentaires

Alors, quelle est exactement votre question?


Essayez de ne pas calculer la factorielle (f) trois fois à chaque étape? Si vous définissez une variable fact = factorial (f) au début, vous pouvez simplement la diviser par f dans la première étape, puis par f-1 , puis f-2 , et ainsi de suite.


@Nelfeal oui ce n'était qu'un exemple, dans mes fonctions d'origine, les factorielles sont précalculées ... Je veux juste me débarrasser de toute la boucle while.


Je crois que tu ne peux pas


3 Réponses :


3
votes

Vous pouvez trouver la n -ème permutation en faisant une division euclidienne répétée (quotient et reste, aka divmod) et en gardant une trace des lettres que vous choisissez. Vous pouvez ensuite choisir la i -ème lettre de cette permutation.

Soit S une copie de la chaîne initiale, L la longueur de cette chaîne et P le nombre de permutations ( L! ). T sera la n -ième permutation de S , construite étape par étape.
Exemple: S = "ABCD" , L = 4 et P = 24 . Prenons n = 15 pour l'exemple. T doit être "CBDA" à la fin.

Définissez P = P / L . Calculez divmod (n, P) , soit q le quotient ( n / P ) et r le reste ( n% P ). Retirez la q -ème lettre de S et ajoutez-la à T . Définissez n = r , décrémentez L et répétez jusqu'à L = 0 .
Exemple:
1) P = 24/4 = 6 , q = 15/6 = 2 , r = 15% 6 = 3 , S = "ABD" , T = "C" , n = r = 3 , L = 3 .
2) P = 6/3 = 2 , q = 3/2 = 1 , r = 3% 2 = 1 , S = "AD" , T = "CB" , n = r = 1 , L = 2 .
3) P = 2/2 = 1 , q = 1/1 = 1 , r = 1% 1 = 0 , S = "A" , T = "CBD" , n = r = 0 , L = 1 .
4) P = 1/1 = 1 , q = 0/1 = 0 , r = 0% 1 = 0 , S = "" , T = "CBDA" , n = r = 0 , L = 0 .

Puisque vous ne voulez que la i -ème lettre, vous pouvez vous arrêter chaque fois que la longueur de T est égale à i + 1 et prendre cette dernière lettre.

Je n'essaierai pas de coder ceci en Python car cela fait trop longtemps que je n'ai pas touché à Python, mais voici une démo en C ++ .


2 commentaires

Merci beaucoup pour votre réponse. J'ai déjà une telle fonction en python, je me demandais si une équivalence simple peut être faite ...


Mais c'est une équivalence simple. Il ne comporte que des étapes L et peut être inversé (également dans les étapes L ). Je suis sûr que vous n'obtiendrez rien de plus simple.



2
votes

Désolé, pour mon Python. L'idée est que la lettre à toutes les positions se répète (number_of_pos - pos)! fois.

Par exemple. ABCD , la lettre à la première position se répéterait 3! fois. Donc, en divisant n par cette factorielle, nous avons maintenant quelle lettre est en première position. Pour trouver la lettre en deuxième position, nous devons supprimer cette lettre de l'ensemble (attribuez plutôt 0 ) et mettre à jour n avec n - n% 3! code> pour maintenant index de la lettre en deuxième position. Lors de l'application de cet index, nous devons faire attention à ne pas compter la lettre qui est en première position.

La complexité est O (kPosCount * kPosCount) .

#include <array>
#include <iostream>

const int kPosCount = 4;
const std::array<int, kPosCount> factorial = { 1,1,2,6 };
const std::array<int, kPosCount> kLetters = { 'A', 'B', 'C', 'D' };

char f(int pos, int n) {
    assert(pos < kPosCount);
    std::array<int, kPosCount> letters = kLetters;

    int res = 0;
    for (int i = 0; i <= pos; ++i) {
        const int letter = n / factorial[kPosCount - i - 1];
        int j = 0;
        for (int stillThere = 0; j < kPosCount; ++j) {
            if (letters[j] != 0) {
                if (stillThere == letter) {
                    break;
                }
                ++stillThere;
            }
        }
        letters[j] = 0;
        res = j;
        n %= factorial[kPosCount - i - 1];
    }
    return kLetters[res];
}

int main() {
    const int kMaxN = factorial.back() * kPosCount;
    for (int i = 0; i < kPosCount; ++i) {
        for (int j = 0; j < kMaxN; ++j) {
            std::cout << f(i, j) << " ";
        }
        std::cout << std::endl;
    }
}


0 commentaires

1
votes

Voici une version de votre fonction avec un ordre de permutation différent. Je n'ai pas forcé la taille de la permutation à 256 ici, vous devez donc la fournir comme premier argument.

>>> print('\n'.join(' '.join(str(elt_opt(4, k, i)) for i in range(4))
                    for k in range(factorial(4))))
0 3 2 1
0 3 1 2
0 1 3 2
0 1 2 3
0 2 3 1
0 2 1 3
1 0 2 3
1 0 3 2
1 3 0 2
1 3 2 0
1 2 0 3
1 2 3 0
2 0 3 1
2 0 1 3
2 1 0 3
2 1 3 0
2 3 0 1
2 3 1 0
3 0 2 1
3 0 1 2
3 1 0 2
3 1 2 0
3 2 0 1
3 2 1 0

Cela a deux différences avec votre code.

Tout d'abord, les chiffres sont découpés de l'index de droite à gauche, ce qui signifie que les divmods bignum sont tous des divmods par un petit dividende. Je pensais que ce serait plus rapide que d'utiliser un dividende bignum, mais cela s'avère beaucoup plus lent. Cependant, cela évite d'avoir à précalculer les factorielles.

Deuxièmement, plutôt que de faire une série de pop s à partir du milieu d'une table, ce qui va être de complexité quadratique car l'opération pop est O (n), je échangez simplement chaque élément avec un élément avant. C'est beaucoup plus rapide, bien que l'arithmétique bignum domine le calcul.

En effet, elt (n, k, i) effectue les premières i étapes de un mélange Fisher-Yates de range (n) en utilisant la décomposition à base mixte de k comme quelles seraient les valeurs aléatoires dans un mélange aléatoire. Étant donné que le shuffle FY produit un résultat différent pour chaque séquence possible de valeurs aléatoires, et que la décomposition en base mixte de k produit une séquence différente pour chaque valeur différente de k , tous des permutations seront générées.

Voici à quoi ressemble l'ordre pour n = 4:

def elt_opt(n, k, i, fact=[1]):
    """Returns element i of permutation k from permutations of length n.
       Let the last argument default.
    """
    while len(fact) < n: fact.append(fact[-1]*len(fact))
    perm = list(range(n))
    for j in range(n-1, n-2-i, -1):
        r, k = divmod(k, fact[j])
        perm[j], perm[r] = perm[r], perm[j]
    return perm[n-i-1]

Pour accélérer réellement la fonction, j'ai réintroduit les factorielles précalculées (en tant que variable locale qui est prolongé si nécessaire). J'ai également micro-optimisé la boucle interne en supprimant autant d'arithmétique que possible, ce qui signifiait faire le mélange F-Y de l'arrière vers l'avant. Il en résulte un autre ordre de permutation; voir ci-dessous pour un exemple.

La fonction optimisée finale est environ 15% plus rapide que la version dans la question (c'est-à-dire qu'il faut environ 85% du temps pour faire un simple benchmark).

>>> print('\n'.join(' '.join(str(elt(4, k, i)) for i in range(4))
                    for k in range(factorial(4))))
0 1 2 3
1 0 2 3
2 1 0 3
3 1 2 0
0 2 1 3
1 2 0 3
2 0 1 3
3 2 1 0
0 3 2 1
1 3 2 0
2 3 0 1
3 0 2 1
0 1 3 2
1 0 3 2
2 1 3 0
3 1 0 2
0 2 3 1
1 2 3 0
2 0 3 1
3 2 0 1
0 3 1 2
1 3 0 2
2 3 1 0
3 0 1 2

Ordre de permutation:

def elt(n,k,i):
  """Returns element i of permutation k from permutations of length n"""
  perm = [*range(n)]
  for j in range(i+1):
    k, r = divmod(k, n-j)
    perm[j], perm[j+r] = perm[j+r], perm[j]
  return perm[i]


8 commentaires

Merci beaucoup, les trucs sur Fisher-Yates sont très intéressants et génèrent également des permutations sans calculer les factorielles! Mais après l'analyse comparative, j'ai trouvé que cette solution était un peu plus lente que la mienne. Je ne sais pas pourquoi, peut-être que python 3.7 est préférable de gérer list.pop ()


@fbparis: il s'avère que je me suis trompé sur la division; python est en fait environ 60% plus rapide en divisant deux bignums avec un petit quotient qu'en divisant un bignum par un petit dividende. Cela accable l'accélération de l'échange.


@fbparis: J'ai ajouté la version la plus optimisée que je pourrais proposer. Mais ce n'est pas beaucoup plus rapide que le vôtre.


bien fait (je suis plus proche de 10% d'amélioration de la vitesse) mais ce n'est toujours pas adapté à l'utilisation prévue, je suppose que j'ai besoin de quelque chose


@fbparis: je ne crois pas que le sous-linéaire existe, mais vous pourrez peut-être reformuler votre problème d'une manière ou d'une autre. Par exemple, si vous avez besoin de plus d'un élément de la même permutation, tous sauf un sont gratuits. Donc ça pourrait beaucoup aider


il existe un autre moyen de l'améliorer en utilisant la symétrie des permutations pour ne jamais boucler plus de N // 2


continuons cette discussion dans le chat .


@fbparis. Bien sûr, mais les bignums sont plus gros au début de la boucle, donc la première itération est la plus lente. Pourtant, j'ai du mal à imaginer un cas d'utilisation où vous n'avez besoin que d'un élément d'une permutation désignée.