Comment générer efficacement une liste de K entiers non répétitifs entre 0 et une limite supérieure N [dupliquer]

cette question a déjà une réponse ici:

la question donne toutes les données nécessaires: qu'est-ce qu'un algorithme efficace pour générer une séquence de K entiers non répétitifs dans un donné intervalle [0,N-1] . L'algorithme trivial (générer des nombres aléatoires et, avant de les ajouter à la séquence, les rechercher pour voir s'ils étaient déjà là) est très coûteux si K est assez grand et assez proche de N .

l'algorithme fourni dans sélectionner efficacement un ensemble d'éléments aléatoires à partir d'une liste liée semble plus compliqué que nécessaire, et nécessite certaines de mise en œuvre. Je viens de trouver un autre algorithme qui semble faire l'affaire, tant que vous connaissez tous les paramètres pertinents, en un seul passage.

29
demandé sur Community 2008-10-01 21:21:30

13 réponses

le module aléatoire de la bibliothèque Python le rend extrêmement facile et efficace:

from random import sample
print sample(xrange(N), K)

sample renvoie une liste de K éléments uniques choisis dans la séquence donnée.

xrange est un "émulateur de liste", c'est-à-dire qu'il se comporte comme une liste de nombres consécutifs sans la créer en mémoire, ce qui le rend super-rapide pour des tâches comme celle-ci.

12
répondu DzinX 2016-10-03 11:41:08

Dans The Art of Computer Programming, Volume 2: Seminumerical Algorithmes, Troisième Édition , Knuth décrit la sélection suivante algorithme d'échantillonnage:

algorithms (Selection sampling technique). Pour sélectionner n enregistrements au hasard à partir d'un ensemble de N, où 0 < N ≤ N.

S1. [Initialiser.] Set t ← 0, m ← 0. (Au cours de cet algorithme, m représente le nombre d'enregistrements sélectionnés jusqu'à présent, et t est le nombre total d'enregistrements d'entrée que nous avons abordés.)

S2. [Générer U.] générer un nombre aléatoire U, uniformément réparti entre zéro et un.

S3. [Test.] Si (N – t)U ≥ n – m, passer à l'étape S5.

S4. [Sélectionner.] Sélectionnez l'enregistrement suivant pour l'échantillon et augmentez m et t de 1. Si m < n, passez à l'étape S2; sinon, l'échantillon est complet et l'algorithme se termine.

S5. [Sauter.] Sauter le prochain enregistrement (ne pas l'inclure dans l'échantillon), augmenter t de 1, et revenir à l'étape S2.

une implémentation peut être plus facile à suivre que la description. Voici une implémentation Lisp commune qui sélectionne n membres au hasard dans une liste:

(defun sample-list (n list &optional (length (length list)) result)
  (cond ((= length 0) result)
        ((< (* length (random 1.0)) n)
         (sample-list (1- n) (cdr list) (1- length)
                      (cons (car list) result)))
        (t (sample-list n (cdr list) (1- length) result))))

et voici une implémentation qui n'utilise pas la récursion, et qui fonctionne avec toutes sortes de séquences:

(defun sample (n sequence)
  (let ((length (length sequence))
        (result (subseq sequence 0 n)))
    (loop
       with m = 0
       for i from 0 and u = (random 1.0)
       do (when (< (* (- length i) u) 
                   (- n m))
            (setf (elt result m) (elt sequence i))
            (incf m))
       until (= m n))
    result))
12
répondu Vebjorn Ljosa 2008-10-09 16:02:12

Il est effectivement possible de le faire dans l'espace proportionnel au nombre d'éléments sélectionnés, plutôt que la taille de l'ensemble que vous achetez à partir, quelle que soit la proportion de l'ensemble total que vous sélectionnez. Vous faites cela en générant une permutation aléatoire, puis en sélectionnant comme ceci:

choisir un chiffre de bloc, tel que thé ou XTEA. Utilisez XOR folding pour réduire la taille du bloc à la plus petite puissance de deux plus grands que le set que vous choisissez. Utilisez la graine aléatoire comme la clé du cipher. Pour générer un élément n dans la permutation, chiffrez n avec le chiffre. Si le numéro de sortie n'est pas dans votre jeu, cryptez cela. Répétez jusqu'à ce que le nombre soit à l'intérieur du jeu. En moyenne, vous aurez à faire à moins de deux chiffrements par nombre généré. Cela a l'avantage que si votre graine est cryptographiquement sûr, est à votre entière permutation.

j'ai écrit à ce sujet dans beaucoup plus de détails ici .

5
répondu Nick Johnson 2008-10-02 09:50:34

le code suivant (en C, origine inconnue) semble résoudre le problème extrêmement bien:

 /* generate N sorted, non-duplicate integers in [0, max[ */
 int *generate(int n, int max) {
    int i, m, a;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    m = 0;
    for (i=0; i<max; i++) {
        a = random_in_between(0, max - i);
        if (a < n - m) {
            g[m] = i;
            m ++;
        }
    }
    return g;
 }

est-ce que quelqu'un sait où je peux trouver d'autres gemmes comme celle-ci?

3
répondu tucuxi 2008-10-01 17:27:50

génère un tableau 0...N-1 remplit a[i] = i .

puis mélangez les premiers K articles.

Traînante:

  • Démarrer J = N-1
  • choisir un nombre aléatoire 0...J (dire, R )
  • swap a[R] avec a[J]
    • depuis R peut être égal à J , l'élément peut être échangé avec lui-même
  • soustrayez 1 de J et répétez.

enfin, prenez K derniers éléments.

il s'agit essentiellement de choisir un élément aléatoire dans la liste, de le déplacer, puis de choisir un élément aléatoire dans la liste restante, et ainsi de suite.

Œuvres O(K) et O(N) temps, exige O(N) entreposage.

la partie de mélange est appelée Fisher-Yates shuffle ou knut's shuffle , décrit dans le deuxième volume de L'Art de la programmation informatique.

2
répondu James Curran 2016-09-07 17:40:50

accélère l'algorithme trivial en stockant les nombres K dans une mémoire de hachage. Connaître K avant de commencer enlève toute l'inefficacité de l'insertion dans une carte hash, et vous obtenez toujours l'avantage de la recherche rapide.

1
répondu Bill the Lizard 2008-10-01 17:25:36

ma solution est orientée C++, mais je suis sûr qu'elle pourrait être traduite dans d'autres langues car c'est assez simple.

  • tout d'abord, générer une liste liée avec des éléments K, allant de 0 à K
  • Alors tant que la liste n'est pas vide, générer un nombre aléatoire entre 0 et la taille du vecteur
  • prendre cet élément, le pousser dans un autre vecteur, et le supprimer de la liste originale

cette solution implique seulement deux itérations de boucle, et pas de recherche de table de hachage ou quoi que ce soit de la sorte. Ainsi en code réel:

// Assume K is the highest number in the list
std::vector<int> sorted_list;
std::vector<int> random_list;

for(int i = 0; i < K; ++i) {
    sorted_list.push_back(i);
}

// Loop to K - 1 elements, as this will cause problems when trying to erase
// the first element
while(!sorted_list.size() > 1) {
    int rand_index = rand() % sorted_list.size();
    random_list.push_back(sorted_list.at(rand_index));
    sorted_list.erase(sorted_list.begin() + rand_index);
}                 

// Finally push back the last remaining element to the random list
// The if() statement here is just a sanity check, in case K == 0
if(!sorted_list.empty()) {
    random_list.push_back(sorted_list.at(0));
}
1
répondu Nik Reiman 2008-10-01 18:17:38

Étape 1: générez votre liste d'entiers.

Étape 2: Effectuer Knuth Shuffle .

notez que vous n'avez pas besoin de mélanger la liste entière, puisque L'algorithme Knuth Shuffle vous permet d'appliquer seulement n shuffles, où n est le nombre d'éléments à retourner. La création de la liste prendra tout de même un temps proportionnel à la taille de la liste, mais vous pouvez réutiliser votre liste existante pour les besoins futurs de mélange (en supposant que la taille reste la même) sans qu'il soit nécessaire de présélectionner la liste partiellement mélangée avant de redémarrer l'algorithme de mélange.

l'algorithme de base pour Knuth Shuffle est que vous commencez avec une liste d'entiers. Ensuite, vous changez le premier entier avec n'importe quel nombre dans la liste et retournez le (nouveau) Premier entier courant. Ensuite, vous changez le deuxième entier avec n'importe quel nombre dans la liste (sauf le premier) et retournez le (nouveau) deuxième entier courant. Puis...etc...

c'est un algorithme ridiculement simple, mais attention que vous incluiez l'élément courant dans la liste lors de l'exécution du swap ou vous briseriez l'algorithme.

1
répondu Brian 2010-03-22 19:06:47

la version D'échantillonnage du réservoir est assez simple:

my $N = 20;
my $k;
my @r;

while(<>) {
  if(++$k <= $N) {
    push @r, $_;
  } elsif(rand(1) <= ($N/$k)) {
    $r[rand(@r)] = $_;
  }
}

print @r;

C'est $N lignes choisies au hasard de STDIN. Remplacez le truc <>/$_ par quelque chose d'autre si vous n'utilisez pas les lignes d'un fichier, mais c'est un algorithme assez simple.

0
répondu Michael Cramer 2008-10-01 18:01:56

si la liste est triée, par exemple, si vous voulez extraire des éléments K de N, Mais que vous ne vous souciez pas de leur ordre relatif, un algorithme efficace est proposé dans l'article un algorithme efficace pour L'échantillonnage aléatoire séquentiel (Jeffrey Scott Vitter, ACM Transactions on Mathematical Software , Vol. 13, No 1, Mars 1987, Pages 56-67.).

révisé à ajoutez le code en c++ en utilisant boost. Je viens de le taper et il pourrait y avoir beaucoup d'erreurs. Les nombres aléatoires viennent de la bibliothèque boost, avec une graine stupide, donc ne faites rien de sérieux avec ça.

/* Sampling according to [Vitter87].
 * 
 * Bibliography
 * [Vitter 87]
 *   Jeffrey Scott Vitter, 
 *   An Efficient Algorithm for Sequential Random Sampling
 *   ACM Transactions on MAthematical Software, 13 (1), 58 (1987).
 */

#include <stdlib.h>
#include <string.h>
#include <math.h>
#include <string>
#include <iostream>

#include <iomanip>

#include <boost/random/linear_congruential.hpp>
#include <boost/random/variate_generator.hpp>
#include <boost/random/uniform_real.hpp>

using namespace std;

// This is a typedef for a random number generator.
// Try boost::mt19937 or boost::ecuyer1988 instead of boost::minstd_rand
typedef boost::minstd_rand base_generator_type;

    // Define a random number generator and initialize it with a reproducible
    // seed.
    // (The seed is unsigned, otherwise the wrong overload may be selected
    // when using mt19937 as the base_generator_type.)
    base_generator_type generator(0xBB84u);
    //TODO : change the seed above !
    // Defines the suitable uniform ditribution.
    boost::uniform_real<> uni_dist(0,1);
    boost::variate_generator<base_generator_type&, boost::uniform_real<> > uni(generator, uni_dist);



void SequentialSamplesMethodA(int K, int N) 
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method A.
    {
    int top=N-K, S, curr=0, currsample=-1;
    double Nreal=N, quot=1., V;

    while (K>=2)
        {
        V=uni();
        S=0;
        quot=top/Nreal;
        while (quot > V)
            {
            S++; top--; Nreal--;
            quot *= top/Nreal;
            }
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        Nreal--; K--;curr++;
        }
    // special case K=1 to avoid overflow
    S=floor(round(Nreal)*uni());
    currsample+=1+S;
    cout << curr << " : " << currsample << "\n";
    }

void SequentialSamplesMethodD(int K, int N)
// Outputs K sorted random integers out of 0..N, taken according to 
// [Vitter87], method D. 
    {
    const int negalphainv=-13; //between -20 and -7 according to [Vitter87]
    //optimized for an implementation in 1987 !!!
    int curr=0, currsample=0;
    int threshold=-negalphainv*K;
    double Kreal=K, Kinv=1./Kreal, Nreal=N;
    double Vprime=exp(log(uni())*Kinv);
    int qu1=N+1-K; double qu1real=qu1;
    double Kmin1inv, X, U, negSreal, y1, y2, top, bottom;
    int S, limit;
    while ((K>1)&&(threshold<N))
        {
        Kmin1inv=1./(Kreal-1.);
        while(1)
            {//Step D2: generate X and U
            while(1)
                {
                X=Nreal*(1-Vprime);
                S=floor(X);
                if (S<qu1) {break;}
                Vprime=exp(log(uni())*Kinv);
                }
            U=uni();
            negSreal=-S;
            //step D3: Accept ?
            y1=exp(log(U*Nreal/qu1real)*Kmin1inv);
            Vprime=y1*(1. - X/Nreal)*(qu1real/(negSreal+qu1real));
            if (Vprime <=1.) {break;} //Accept ! Test [Vitter87](2.8) is true
            //step D4 Accept ?
            y2=0; top=Nreal-1.;
            if (K-1 > S)
                {bottom=Nreal-Kreal; limit=N-S;}
            else {bottom=Nreal+negSreal-1.; limit=qu1;}
            for(int t=N-1;t>=limit;t--)
                {y2*=top/bottom;top--; bottom--;}
            if (Nreal/(Nreal-X)>=y1*exp(log(y2)*Kmin1inv))
                {//Accept !
                Vprime=exp(log(uni())*Kmin1inv);
                break;
                }
            Vprime=exp(log(uni())*Kmin1inv);
            }
        // Step D5: Select the (S+1)th record
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        curr++;
        N-=S+1; Nreal+=negSreal-1.;
        K-=1; Kreal-=1; Kinv=Kmin1inv;
        qu1-=S; qu1real+=negSreal;
        threshold+=negalphainv;
        }
    if (K>1) {SequentialSamplesMethodA(K, N);}
    else {
        S=floor(N*Vprime);
        currsample+=1+S;
        cout << curr << " : " << currsample << "\n";
        }
    }


int main(void)
    {
    int Ntest=10000000, Ktest=Ntest/100;
    SequentialSamplesMethodD(Ktest,Ntest);
    return 0;
    }

$ time ./sampling|tail

donne l'ouptut suivant sur mon ordinateur portable

99990 : 9998882
99991 : 9998885
99992 : 9999021
99993 : 9999058
99994 : 9999339
99995 : 9999359
99996 : 9999411
99997 : 9999427
99998 : 9999584
99999 : 9999745

real    0m0.075s
user    0m0.060s
sys 0m0.000s
0
répondu Frédéric Grosshans 2010-03-22 18:53:57

ce code Ruby présente la méthode D'échantillonnage du réservoir , algorithme R . Dans chaque cycle, je sélectionne n=5 entiers aléatoires uniques de [0,N=10) gamme:

t=0
m=0
N=10
n=5
s=0
distrib=Array.new(N,0)
for i in 1..500000 do
 t=0
 m=0
 s=0
 while m<n do

  u=rand()
  if (N-t)*u>=n-m then
   t=t+1
  else 
   distrib[s]+=1
   m=m+1
   t=t+1
  end #if
  s=s+1
 end #while
 if (i % 100000)==0 then puts i.to_s + ". cycle..." end
end #for
puts "--------------"
puts distrib

sortie:

100000. cycle...
200000. cycle...
300000. cycle...
400000. cycle...
500000. cycle...
--------------
250272
249924
249628
249894
250193
250202
249647
249606
250600
250034

tous les entiers entre 0-9 ont été choisis avec presque la même probabilité.

c'est essentiellement algorithme de Knuth appliqué à des séquences arbitraires (en effet, cette réponse a une version LISP de cela). L'algorithme est O(N) dans le temps et peut être O (1) dans la mémoire si la séquence y est striée comme indiqué dans @Michaelcramer'S réponse .

0
répondu Konstantin 2017-05-23 12:34:41

Voici un moyen de le faire en O(N) sans stockage supplémentaire. Je suis presque sûr que ce n'est pas une distribution purement aléatoire, mais c'est probablement assez proche pour de nombreux usages.

/* generate N sorted, non-duplicate integers in [0, max[  in O(N))*/
 int *generate(int n, int max) {
    float step,a,v=0;
    int i;    
    int *g = (int *)calloc(n, sizeof(int));
    if ( ! g) return 0;

    for (i=0; i<n; i++) {
        step = (max-v)/(float)(n-i);
        v+ = floating_pt_random_in_between(0.0, step*2.0);
        if ((int)v == g[i-1]){
          v=(int)v+1;             //avoid collisions
        }
        g[i]=v;
    }
    while (g[i]>max) {
      g[i]=max;                   //fix up overflow
      max=g[i--]-1;
    }
    return g;
 }
-1
répondu AShelly 2008-10-01 18:16:33

C'est le code Perl. Grep est un filtre, et comme toujours, je n'ai pas testé ce code.

@list = grep ($_ % I) == 0, (0..N);
  • I = intervalle
  • N = Limite Supérieure

n'obtenez que des nombres qui correspondent à votre intervalle via l'opérateur de module.

@list = grep ($_ % 3) == 0, (0..30);

renvoie 0, 3, 6,... 30

c'est un pseudo code Perl. Vous devrez peut-être ajuster pour la compiler.

-2
répondu J.J. 2008-10-01 17:31:23