36 votes

Algorithme permettant de sélectionner une combinaison unique et aléatoire de valeurs ?

Disons que j'ai y des valeurs distinctes et je veux sélectionner x d'entre eux au hasard. Quel est l'algorithme efficace pour faire cela ? Je pourrais simplement appeler rand() x temps, mais les performances seraient médiocres si x , y étaient grandes.

Notez que combinaisons sont nécessaires ici : chaque valeur doit avoir la même probabilité d'être sélectionnée mais leur ordre dans le résultat n'est pas important. Bien sûr, tout algorithme générant permutations serait admissible, mais je me demande s'il est possible de le faire plus efficacement sans l'exigence d'un ordre aléatoire.

Comment générer efficacement une liste de K entiers non répétitifs compris entre 0 et une borne supérieure N couvre ce cas pour les permutations.

59voto

Jerry Coffin Points 237758

Robert Floyd a inventé un algorithme d'échantillonnage pour de telles situations. Il est généralement supérieur à celui qui consiste à mélanger puis à prendre le premier x puisqu'il ne nécessite pas O(y) de stockage. Tel qu'il est écrit à l'origine, il prend des valeurs de 1..N, mais il est trivial de produire 0..N et/ou d'utiliser des valeurs non contiguës en traitant simplement les valeurs qu'il produit comme des indices dans un vecteur/réseau/quelque chose.

En pseuocode, l'algorithme s'exécute comme suit (en s'inspirant de l'ouvrage de Jon Bentley intitulé Perles de la programmation colonne "Un échantillon de brillance").

initialize set S to empty
for J := N-M + 1 to N do
    T := RandInt(1, J)
    if T is not in S then
        insert T in S
    else
        insert J in S

La dernière partie (insérer J si T est déjà dans S) est la partie délicate. L'essentiel est que il assure la probabilité mathématique correcte d'insérer J afin qu'il produise des résultats impartiaux.

C'est O(x) 1 y O(1) en ce qui concerne y , O(x) stockage.

Il convient de noter que, conformément à la combinaisons dans la question, l'algorithme ne garantit qu'une probabilité égale pour chaque élément d'apparaître dans le résultat, et non leur ordre relatif dans celui-ci.


1 <em>O(x <sup>2 </sup>) </em>dans le pire des cas pour la carte de hachage concernée qui peut être négligée puisqu'il s'agit d'un cas pathologique pratiquement inexistant où toutes les valeurs ont le même hachage

11voto

Steve Jessop Points 166970

En supposant que vous souhaitez que l'ordre soit également aléatoire (ou que cela ne vous dérange pas qu'il le soit), j'utiliserais simplement un shuffle Fisher-Yates tronqué. Lancez l'algorithme de brassage, mais arrêtez-vous une fois que vous avez sélectionné le premier élément de la liste. x au lieu de "sélectionner aléatoirement" toutes les y d'entre eux.

Fisher-Yates fonctionne comme suit :

  • sélectionner un élément au hasard, et l'échanger avec l'élément à la fin du tableau.
  • Récupérer (ou plus probablement itérer) sur le reste du tableau, en excluant le dernier élément.

Les étapes qui suivent la première ne modifient pas le dernier élément du tableau. Les pas après les deux premiers n'affectent pas les deux derniers éléments. Les pas après les x premiers n'affectent pas les x derniers éléments. Donc, à ce stade, vous pouvez vous arrêter - le haut du tableau contient des données sélectionnées de manière uniformément aléatoire. Le bas du tableau contient des éléments quelque peu aléatoires, mais la permutation que vous obtenez d'eux n'est pas uniformément distribuée.

Bien sûr, cela signifie que vous avez détruit le tableau d'entrée - si cela signifie que vous devez en prendre une copie avant de commencer, et que x est petit par rapport à y, alors copier le tableau entier n'est pas très efficace. Notez cependant que si tout ce que vous comptez utiliser à l'avenir est de faire d'autres sélections, alors le fait qu'il soit dans un ordre quelque peu aléatoire n'a pas d'importance, vous pouvez simplement le réutiliser. Par conséquent, si vous effectuez la sélection plusieurs fois, vous pouvez ne faire qu'une seule copie au départ et amortir le coût.

2voto

Andras Vass Points 8021

Si vous avez seulement besoin de générer combinaisons - où l'ordre des éléments n'a pas d'importance - vous pouvez utiliser combinadics comme ils le sont mis en œuvre, par exemple, ici par James McCaffrey .

Comparez cela avec k-permutations où l'ordre des éléments est important.

Dans le premier cas (1,2,3) , (1,3,2) , (2,1,3) , (2,3,1) , (3,1,2) , (3,2,1) sont considérés comme identiques - dans ce dernier cas, ils sont considérés comme distincts, bien qu'ils contiennent les mêmes éléments.

Si vous avez besoin de combinaisons, il se peut que vous n'ayez besoin de générer qu'un seul nombre aléatoire (bien qu'il puisse être un peu grand) - qui peut être utilisé directement pour trouver le m e combinaison. Puisque ce nombre aléatoire représente l'indice d'une combinaison particulière, il s'ensuit que votre nombre aléatoire doit être compris entre 0 et C(n,k) . Le calcul de la combinadique peut également prendre un certain temps.

Ça ne vaut peut-être pas la peine - d'ailleurs Réponse de Jerry et Federico est certainement plus simple que l'implémentation de la combinadique. Cependant, si vous n'avez vraiment besoin que d'une combinaison et que vous avez le souci de générer le nombre exact de bits aléatoires nécessaires et rien de plus... ;-)

Bien qu'il ne soit pas clair si vous voulez des combinaisons ou des k-permutations, voici un code C# pour ces dernières (oui, nous pourrions générer uniquement un complément si x > y/2, mais nous nous serions alors retrouvés avec une combinaison qui doit être mélangée pour obtenir une véritable k-permutation) :

static class TakeHelper
{
    public static IEnumerable<T> TakeRandom<T>(
        this IEnumerable<T> source, Random rng, int count)
    {
        T[] items = source.ToArray();

        count = count < items.Length ? count : items.Length;

        for (int i = items.Length - 1 ; count-- > 0; i--)
        {
            int p = rng.Next(i + 1);
            yield return items[p];
            items[p] = items[i];
        }
    }
}

class Program
{
    static void Main(string[] args)
    {
        Random rnd = new Random(Environment.TickCount);
        int[] numbers = new int[] { 1, 2, 3, 4, 5, 6, 7 };
        foreach (int number in numbers.TakeRandom(rnd, 3))
        {
            Console.WriteLine(number);
        }
    }
}

Une autre mise en œuvre, plus élaborée, qui génère k-permutations Je pense qu'il s'agit d'une amélioration par rapport aux algorithmes existants si vous n'avez besoin que d'itérer sur les résultats. Bien qu'il doive aussi générer x des nombres aléatoires, il utilise uniquement O(min(y/2, x)) la mémoire dans le processus :

    /// <summary>
    /// Generates unique random numbers
    /// <remarks>
    /// Worst case memory usage is O(min((emax-imin)/2, num))
    /// </remarks>
    /// </summary>
    /// <param name="random">Random source</param>
    /// <param name="imin">Inclusive lower bound</param>
    /// <param name="emax">Exclusive upper bound</param>
    /// <param name="num">Number of integers to generate</param>
    /// <returns>Sequence of unique random numbers</returns>
    public static IEnumerable<int> UniqueRandoms(
        Random random, int imin, int emax, int num)
    {
        int dictsize = num;
        long half = (emax - (long)imin + 1) / 2;
        if (half < dictsize)
            dictsize = (int)half;
        Dictionary<int, int> trans = new Dictionary<int, int>(dictsize);
        for (int i = 0; i < num; i++)
        {
            int current = imin + i;
            int r = random.Next(current, emax);
            int right;
            if (!trans.TryGetValue(r, out right))
            {
                right = r;
            }
            int left;
            if (trans.TryGetValue(current, out left))
            {
                trans.Remove(current);
            }
            else
            {
                left = current;
            }
            if (r > current)
            {
                trans[r] = left;
            }
            yield return right;
        }
    }

L'idée générale est de faire un Le remaniement Fisher-Yates y mémoriser les transpositions dans la permutation . Il n'a été publié nulle part et n'a fait l'objet d'aucun examen par les pairs. Je pense qu'il s'agit d'une curiosité plutôt que d'une valeur pratique. Néanmoins, je suis très ouvert à la critique et j'aimerais savoir si vous trouvez quelque chose à redire à cet article - merci de le prendre en considération (et d'ajouter un commentaire avant de rétrograder).

1voto

Federico A. Ramponi Points 23106

Une petite suggestion : si x >> y/2, il est probablement préférable de sélectionner au hasard y - x éléments, puis de choisir l'ensemble complémentaire.

0voto

Peter Alexander Points 31990

Pourquoi les performances seraient-elles mauvaises si x ou y étaient grands ? Quelles performances espérez-vous ? Par exemple, comment proposez-vous de sélectionner x éléments au hasard en moins de O(x) temps ?

En C++, vous pouvez utiliser std::random_shuffle puis sélectionnez les x premiers éléments. std::random_shuffle utilise le remaniement Fisher-Yates mentionné par polygenelubricants.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X