Je voudrais générer toutes les permutations d'un ensemble (une collection), comme ceci :
Collection: 1, 2, 3
Permutations: {1, 2, 3}
{1, 3, 2}
{2, 1, 3}
{2, 3, 1}
{3, 1, 2}
{3, 2, 1}
Il ne s'agit pas d'une question de "comment", en général, mais plutôt de la manière la plus efficace. En outre, je ne voudrais pas générer TOUTES les permutations et les renvoyer, mais seulement générer une seule permutation à la fois, et ne continuer que si nécessaire (un peu comme les itérateurs - que j'ai également essayés, mais qui se sont avérés moins efficaces).
J'ai testé de nombreux algorithmes et approches et j'ai abouti à ce code, qui est le plus efficace de ceux que j'ai essayés :
public static bool NextPermutation<T>(T[] elements) where T : IComparable<T>
{
// More efficient to have a variable instead of accessing a property
var count = elements.Length;
// Indicates whether this is the last lexicographic permutation
var done = true;
// Go through the array from last to first
for (var i = count - 1; i > 0; i--)
{
var curr = elements[i];
// Check if the current element is less than the one before it
if (curr.CompareTo(elements[i - 1]) < 0)
{
continue;
}
// An element bigger than the one before it has been found,
// so this isn't the last lexicographic permutation.
done = false;
// Save the previous (bigger) element in a variable for more efficiency.
var prev = elements[i - 1];
// Have a variable to hold the index of the element to swap
// with the previous element (the to-swap element would be
// the smallest element that comes after the previous element
// and is bigger than the previous element), initializing it
// as the current index of the current item (curr).
var currIndex = i;
// Go through the array from the element after the current one to last
for (var j = i + 1; j < count; j++)
{
// Save into variable for more efficiency
var tmp = elements[j];
// Check if tmp suits the "next swap" conditions:
// Smallest, but bigger than the "prev" element
if (tmp.CompareTo(curr) < 0 && tmp.CompareTo(prev) > 0)
{
curr = tmp;
currIndex = j;
}
}
// Swap the "prev" with the new "curr" (the swap-with element)
elements[currIndex] = prev;
elements[i - 1] = curr;
// Reverse the order of the tail, in order to reset it's lexicographic order
for (var j = count - 1; j > i; j--, i++)
{
var tmp = elements[j];
elements[j] = elements[i];
elements[i] = tmp;
}
// Break since we have got the next permutation
// The reason to have all the logic inside the loop is
// to prevent the need of an extra variable indicating "i" when
// the next needed swap is found (moving "i" outside the loop is a
// bad practice, and isn't very readable, so I preferred not doing
// that as well).
break;
}
// Return whether this has been the last lexicographic permutation.
return done;
}
Son utilisation consisterait à envoyer un tableau d'éléments, et à recevoir en retour un booléen indiquant s'il s'agit de la dernière permutation lexicographique ou non, ainsi qu'à faire passer le tableau à la permutation suivante.
Exemple d'utilisation :
var arr = new[] {1, 2, 3};
PrintArray(arr);
while (!NextPermutation(arr))
{
PrintArray(arr);
}
Le problème est que je ne suis pas satisfait de la vitesse du code.
L'itération sur toutes les permutations d'un tableau de taille 11 prend environ 4 secondes. Bien que cela puisse être considéré comme impressionnant, puisque la quantité de permutations possibles d'un ensemble de taille 11 est 11!
qui est de près de 40 millions.
Logiquement, avec un tableau de taille 12, cela prendra environ 12 fois plus de temps, puisque 12!
est 11! * 12
Avec un tableau de taille 13, il faudra environ 13 fois plus de temps qu'avec un tableau de taille 12, et ainsi de suite.
Vous pouvez donc facilement comprendre comment, avec un tableau de taille 12 et plus, il faut vraiment beaucoup de temps pour passer en revue toutes les permutations.
Et j'ai la ferme conviction que je peux réduire considérablement ce temps (sans passer à un autre langage que C# - car l'optimisation du compilateur est vraiment très efficace, et je doute de pouvoir optimiser aussi bien, manuellement, en Assembly).
Quelqu'un connaît-il un autre moyen de le faire plus rapidement ? Avez-vous une idée sur la façon de rendre l'algorithme actuel plus rapide ?
Notez que je ne veux pas utiliser une bibliothèque ou un service externe pour faire cela - je veux avoir le code lui-même et je veux qu'il soit aussi efficace qu'il est humainement possible.
12 votes
Génération de todo Les permutations ne peuvent pas être effectuées plus rapidement que le nombre de permutations.
0 votes
Je suis confus par cette ligne : "mais en ne générant qu'une seule permutation, à la fois, et en ne continuant que si nécessaire". Quel est votre objectif ?
0 votes
Vous pouvez aussi essayer d'écrire quelque chose qui "visite" chaque permutation, en appelant une fonction spécifiée par l'utilisateur pour chacune d'entre elles. De cette façon, vous pourriez écrire une fonction récursive pour générer les permutations. Cela peut être plus rapide ou non : la "position actuelle" est maintenue sur la pile plutôt que d'être trouvée à chaque fois par le check-and-continue au début de votre boucle. Mais cette vérification ne prend pas beaucoup d'étapes en moyenne pour trouver le point à partir duquel elle doit travailler, donc vous ne trouverez pas d'ordre de grandeur d'amélioration de cette façon.
1 votes
L'ensemble ne doit-il contenir que des éléments uniques ?
0 votes
Si vous voulez faire de votre mise en œuvre plus rapidement, alors je pourrais être en mesure de vous aider. Je suppose qu'une grande partie du temps est consacrée à l'impression du résultat.
6 votes
En fait, puisque la chose que vous faites est intrinsèquement
O(n!)
-il y aura toujours un petit nombre pour lequel vous direz "il faut quelques secondes pour faire M, mais M+1 prendra M+1 fois plus de temps". Même si vous pouviez accélérer votre code un million de fois, vous ne passeriez que de 12 à 17. Cela vous rendrait-il un million de fois plus heureux ?0 votes
@nhahtdh C'est évident, mais je pense toujours que l'algorithme actuel peut être amélioré.
0 votes
@EmilVikström Cela signifie que si le code extérieur veut itérer sur les permutations, et décide de s'arrêter à un certain point, je n'aurais pas créé de permutations inutiles.
0 votes
@SteveJessop J'ai également essayé quelques approches récursives, mais aucune n'a été jugée aussi bonne que celle que j'utilise actuellement.
0 votes
@Lieven Vous pouvez supposer que c'est le cas.
0 votes
@nhahtdh Les tests de vitesse ont été réalisés en itérant uniquement sur les permutations - pas d'impression ou autre.
0 votes
@SteveJessop Très bon point - mais oui, cela me rendrait un million de fois plus heureux :)
0 votes
Je ne connais pas le C# ni son mode d'optimisation, mais vous devriez peut-être déterminer si le JIT a réussi à supprimer tous les contrôles de limites de ces accès aux tableaux. Logiquement, aucun des index ne peut dépasser les limites du tableau. Si le compilateur/runtime n'a pas compris cela, vous pourriez être en mesure d'en réduire un peu le temps si vous pouvez l'aider. Cela pourrait être en massant ce code, ou en utilisant des pointeurs.
0 votes
Avez-vous profilé votre code ? Etes-vous sûr que vous n'êtes pas lié aux E/S et que vous ne passez pas la plupart du temps en
PrintArray()
?0 votes
@SteveJessop Bonne suggestion, mais je ne peux pas m'attendre à ce que l'amélioration soit supérieure à 1%, ce qui n'est pas vraiment ce que je recherche.
0 votes
"Compresser" les données en octet ou demi-octet (nibble) - si possible en C#. C'est la seule optimisation à laquelle je peux penser. Votre algorithme est probablement le même que celui de Wikipedia. Avez-vous comparé votre code avec C++ next_permutation, juste pour voir la qualité de votre code ?
0 votes
@Blastfurnace Le PrintArray est juste un exemple de comment l'utiliser. Lorsque je teste des vitesses, je n'imprime pas le tableau et je ne fais rien avec - je me contente d'itérer sur les permutations.
0 votes
@nhahtdh Que voulez-vous dire par "compresser les données" ? Veuillez préciser.
0 votes
@YoryeNathan : D'accord, cela pourrait ne pas valoir le temps passé à trouver comment vérifier le code natif réel généré. Surtout s'il s'avère que le JIT est assez intelligent, et donc vous n'obtenez aucune accélération du tout !
0 votes
@YoryeNathan : Au lieu d'opérer sur 4 octets, opérez sur 1 octet, ou un demi-octet. Je ne suis pas sûr que cela soit plus rapide, cependant. Esp. sur de si petites données. Cela fonctionne très bien sur un grand tableau en C, cependant.
0 votes
@nhahtdh Non, ce ne serait pas plus rapide, et je ne voudrais pas limiter le code externe à l'utilisation d'octets - c'est pourquoi toute la méthode est générique. Je n'utilise que des entiers pour les tests de vitesse, mais l'implémentation fonctionne pour chaque type qui implémente IComparable - et le temps de comparaison ne dépend pas vraiment de moi.
0 votes
@YoryeNathan : Je ne pense pas qu'il soit possible d'améliorer significativement votre implémentation alors.
0 votes
@nhahtdh Je suis d'accord, mais peut-être améliorer l'algorithme lui-même ?
0 votes
stackoverflow.com/questions/4319049/
1 votes
@DaveBish En quoi cela m'aide-t-il ? Cela génère des combinaisons, pas des permutations.
0 votes
J'aimerais souligner que cet algo ne gère pas la permutation(n,k) lorsque k < n.
0 votes
@SimpleVar, Vous semblez vouloir le résultat dans l'ordre lexicographique alors que ce n'est pas mentionné clairement dans votre question. Est-ce une obligation ou non ? Si c'est le cas, alors vous devriez l'indiquer clairement dans votre question car cela ajoute une contrainte sur l'algorithme qui pourrait clairement affecter les performances.
0 votes
@SimpleVar, Aussi, si vous cherchiez l'algorithme le plus rapide que vous pouvez arrêter, je suis presque sûr que vous ne trouverez jamais quelque chose qui batte ma réponse. Mais ce n'est pas dans l'ordre lexicographique.
0 votes
@EricOuellet Cela semble en effet prometteur. Au départ, je pense que je voulais faire de l'ordonnancement lexicographique, mais il s'est rapidement avéré qu'il y avait des choses plus intéressantes à faire pour les perms non ordonnés. Donc, le fil de discussion concerne à la fois lex et non-lex, la course séparément, je suppose. Mais je vous assure que j'ai arrêté de chercher ;)
0 votes
@SimpleVar, je ne suis pas sûr mais je suppose qu'il ne peut pas y avoir de solution qui soit la plus rapide et qui soit aussi lexicograhique. Cela est dû au fait que la solution la plus rapide n'implique qu'un seul échange de seulement 2 éléments alors que la solution lexicographique implique plus d'un échange (parfois) s'il y a plus de 2 éléments. Je ne dis pas que c'est impossible mais je ne vois pas comment :-) !
0 votes
@EricOuellet Beats me, them maths are full of surprises. Peut-être qu'un jour un gars trouvera un moyen de le faire (lexicographique) de manière itérative en
n! + n log (n!)
ou quelque chose comme ça. Ma logique est d'accord avec toi, mais mon amour-propre ne l'est pas :)