Search code examples
crandom

how do I make my C program randomly select


I want to make a C program which randomly selects 6 numbers from the number range 1 - 37 without repeating any previous permutations. For example, suppose the program randomly selects 1,2,3,4,5,6. If the next permutation is randomly selected as 2,1,3,4,5,6, then that is OK. However, if 1,2,3,4,5,6 is selected again, that is not OK. I want this to continue until there are no more possible selections available.

How would I go about writing such a program?


Solution

  • Now the KNUTH shuffle answer posted below is quite elegant. But it doesn't meet a particular requirement set forth by the OP in his question.

    The OP said he wanted to be able to select all sets in random order until his program has consumed them all. So here goes. For purposes of demonstrate, we'll understand "select" to mean "print".

    The total number of possible sets of "6 unique digits in the range of 1-37" can be expressed as:

    TOTAL_NUMBER_OF_SETS = 37*36*35*34*33*32 = 1673844480
    

    1673844480 (1.6 billion) fits nicely within a signed 32-bit number. And every unique set could potentially be assigned a unique integer id.

    So... if you can generate a random number between [0,1673844479], we can map that to a very specific set of 6 unique integers.

    To construct the set, we'll need a helper function that will allow us to keep track of which values between 1-37 have already been used during the iteration process of constructing the set. Then a little modulo arithmetic math to help us map an ID number to it's set of 6-digits:

    #include <stdio.h>
    #include <stdlib.h
    #include <stdint.h>
    
    const uint32_t TOTAL_NUMBER_OF_SETS = 37*36*35*34*33*32; // = 1673844480
    
    // returns the Nth value value from the ordered set {1,range},
    //  skipping over elements previous selected
    int GetAvailableElementFromSet(int n, int range, int inuse[])
    {
        int i = 0, x;
        for (x = 0; x < range; x++)
        {
            if (inuse[x] == 0)
            {
               if (i == n)
               {
                   inuse[x] = 1;
                   return x + 1; // +1 since the loop variable has a zero-based index
               }
               i++;
            }
        }
        return -1; // error
    }
    
    
    void GetSpecificSet(uint32_t setindex, int output[])
    {
        int index;
        int inuse[37] = {}; // boolean array of elements already picked for the output set. zero-init to all false
        int j,k;
    
        if (setindex >= TOTAL_NUMBER_OF_SETS)
            return; // error!
    
        for (j = 0; j < 6; j++)
        {
            index = setindex % (37-j);
            output[j] = GetAvailableElementFromSet(index, 37, inuse);
            setindex = setindex / (37-j) ;
        }
    }
    

    And just to prove that this works, we can have another function iterate over all sets:

    void PrintSet(uint32_t setindex)
    {
        int output[6];
        GetSpecificSet(setindex, output);
        printf("%d, %d, %d, %d, %d, %d\n", output[0], output[1], output[2], output[3], output[4], output[5]);
    }
    
    void PrintAllSetsInOrder()
    {
        uint32_t index;
        for (index = 0; index < TOTAL_NUMBER_OF_SETS; index++)
        {
            PrintSet(index);
        }
    }
    

    Now the program above will print out all the sets starting from:

    {1,2,3,4,5,6}  // first set
    {2,1,3,4,5,6}  // second set
    {3,1,2,4,5,6}  // third set
    

    And ending with

    {36, 37, 35, 34, 33, 32} // next to last set
    {37, 36, 35, 34, 33, 32} // last set
    

    And then obviously to print a random set:

    void PrintRandomSet()
    {
        PrintSet(rand() % TOTAL_NUMBER_OF_SETS);
    }
    

    But the OP wanted all sets printed in random order without repeats. This gets tricky, because we have to keep track of random number values previously generated. I can think of several ways to do this. The most obivous candidate solution is to keep a bitmask comprised of TOTAL_NUMBER_OF_SETS bits. That is:

    #define IS_BIT_SET(bmask, bitindex) (bmask[bitindex/8] &  (0x01<<(bitindex%8)))
    #define    SET_BIT(bmask, bitindex) {bmask[bitindex/8] |= (0x01<<(bitindex%8));}
    uint8_t* bitmask = calloc(TOTAL_NUMBER_OF_SETS/8 + 1);
    

    That's about 200MB of memory allocated. Large, but workable. Then we keep picking random numbers from the range [0-TOTAL_NUMBER_OF_SETS), checking the bitmask if it's already been used, then call PrintSet with the random number after setting its bitmask position. Repeat until all TOTAL_NUMBER_OF_SETS have been printed.

    Pseudo code for a working, yet problematic solution

    for (x = 0; x < TOTAL_NUMBER_OF_SETS; x++)
    {
        index = rand()%TOTAL_NUMBER_OF_SETS;
        while (IS_BIT_SET(bitmask, index))
        {
            index = (index + 1) % TOTAL_NUMBER_OF_SETS;
        }
        SET_BIT(bitmask, index);
        PrintSet(index);
    }
    

    Now this should work just fine. But it's going to get dog-slow as the bitmask array starts to get filled up. The later iterations will spend most of its time just scanning the array of bits looking for an unset index value. There have been other discussions on StackOverflow on how to do efficient and uniform permutations of large sets. Perhaps a database is warranted. Go search for those solutions and apply it here for the win.