-1

I generate a random integer number 1-6 with a random number generator. I want to change the generation to avoid situations like this:

  • number 3 being generated 4th time in a row
  • number 2 wasn't generated in last 30 generations

So in general I want more level distribution of numbers over shorter period of time.

I'm aware that such numbers are not truly random anymore, but as long as they are unpredictable, this is fine.

It looks like a common problem. Is there any typical solution so I don't reinvent the wheel?

Code in any language is fine, but C# preferred.

UPDATE:

I'm not sure why question get down votes, maybe I explained something wrong..

In comments JohnColeman suggest what I need is random number generation as a human would do this - I think this is a very good point.

Fisher Yates Shuffle is also a good suggestion. Not perfect, but an improvement in my case.

The other algorithm I can think about is having a weight assigned to each number and make the probability of selecting this number proportional to this weight. Each time you select a number, you could decrease its weight and increase other numbers weights. But this would need to be tested and performance could be poor (but this is not critical in my case). In general I hoped the problem is known and there are some solutions already.

Arek
  • 1,276
  • 1
  • 10
  • 19
  • See following which generates normal distribution : https://stackoverflow.com/questions/27078101/random-number-geneation-in-c-sharp-using-normal-distr – jdweng Dec 19 '18 at 14:09
  • 1
    How are you going to use it? C# `Random` provides pretty even distribution, but random is random - the change of getting "1, 1, 1, 1" is the same as the chance of getting "82, 37, 41, 55". If you want something more uniform, then you may want to generate a range from 1 to X, then shuffle this array. – Yeldar Kurmangaliyev Dec 19 '18 at 14:09
  • So how predictable do you want them? – 500 - Internal Server Error Dec 19 '18 at 14:10
  • "avoid situations like this … " the use of the vague "like" underspecifies the problem. – John Coleman Dec 19 '18 at 14:11
  • 2
    3-3-3-3 isn't any less random than any other specific sequence. – 500 - Internal Server Error Dec 19 '18 at 14:14
  • The question is manifestly not a duplicate of one about generating normal random numbers. Closing it as such borders on the absurd. Voting to reopen. – John Coleman Dec 19 '18 at 14:22
  • 4
    Here is my take on your question: it is well known that if an untrained human tries to simulate a sequence of dice rolls then the sequence that they come up with will fail to be random precisely because it will tend to avoid the scenarios that you are asking about. This *doesn't* mean that their sequence is deterministic. Far from it. In effect, you are asking how to simulate a human who is attempting to simulate dice rolls. If so, it is an interesting question which has no obvious answer. – John Coleman Dec 19 '18 at 14:38
  • @JohnColeman Yes, very good point. A human would not generate these problematic situations quoted above (or they would be very unlikely). And in reality, human generations would be much better in many situations, this includes my application. So I'm looking for something like this. – Arek Dec 19 '18 at 15:15
  • 1
    Fisher-Yates shuffle? https://stackoverflow.com/questions/25943286/fisher-yates-shuffle-on-a-cards-list. So in each block of 6 there would be no repetitions, only in the next block of six. But it makes sampling somewhat predictable - if you know you're at position 5 of the block, and know previous samples, you could predict next one with absolute certainty. – Severin Pappadeux Dec 19 '18 at 15:20
  • @500-InternalServerError "So how predictable do you want them?" I don't know. I would expect the algorithm to have some parameter there, which on one extreme makes it generate just random numbers, and on the other end makes it guarantee the in 6 generations each number appears once (but then we loose unpredictability of course). Something in the middle would probably work fine after tuning to the right value. – Arek Dec 19 '18 at 15:20
  • @SeverinPappadeux That's a good point. I would prefer something more unpredictable, but this shuffle is already an improvement over random generation in my case. I can also imagine having each number twice in the array and shuffling this - this adds more unpredictability and allows a number to be repeated twice, yet each number is guaranteed to appear in a reasonable time. – Arek Dec 19 '18 at 15:32

2 Answers2

3

Well, I think I could apply inverse weighting I once implemented (see How do I randomly equalize unequal values?) to your case.

Basically, sample probabilities inverse to their population number. Initial population will be your guidance parameter - if it is high, inverse would be low, and accumulated counter will have little effect, so it would be pretty close to uniform. If initial population is low (say, 1) then accumulated counter will affect sampling more.

Second parameter to consider when you want to drop accumulated probabilities and go back to original ones, otherwise effect of low initial counter will dissipate with time.

Code, use Math .NET for categorical sampling in [0...6) range, .NET Core 2.2, x64.

using System;
using System.Linq;
using MathNet.Numerics.Random;
using MathNet.Numerics.Distributions;

namespace EqualizedSampling
{
    class Program
    {
        static void Main(string[] args)
        {
            int increment         = 10; // how much inverse probabilities are updated per sample
            int guidanceParameter = 1000000; // Small one - consequtive sampling is more affected by outcome. Large one - closer to uniform sampling

            int[]    invprob = new int [6];
            double[] probabilities = new double [6];

            int[] counter = new int [] {0, 0, 0, 0, 0, 0};
            int[] repeat  = new int [] {0, 0, 0, 0, 0, 0};
            int prev = -1;
            for(int k = 0; k != 100000; ++k ) {
                if (k % 60 == 0 ) { // drop accumulation, important for low guidance
                    for(int i = 0; i != 6; ++i) {
                        invprob[i] = guidanceParameter;
                    }
                }
                for(int i = 0; i != 6; ++i) {
                    probabilities[i] = 1.0/(double)invprob[i];
                }
                var cat = new Categorical(probabilities);
                var q = cat.Sample();
                counter[q] += 1;
                invprob[q] += increment;
                if (q == prev)
                    repeat[q] += 1;
                prev = q;
            }
            counter.ToList().ForEach(Console.WriteLine);
            repeat.ToList().ForEach(Console.WriteLine);
        }
    }
}

I counted repeated pairs as well as total appearance of numbers. With low guidance parameters it is more uniform with lower appearance of the consecutive pairs:

16670
16794
16713
16642
16599
16582
2431
2514
2489
2428
2367
2436

With guidance parameter of 1000000, there is higher probability to have consecutive pairs selected

16675
16712
16651
16677
16663
16622
2745
2707
2694
2792
2682
2847

UPDATE

We could add another parameter, increment per one sample. Large increment will make consecutive sampling even more unlikely. Code updated, output

16659
16711
16618
16609
16750
16653
2184
2241
2285
2259
2425
2247
Severin Pappadeux
  • 18,636
  • 3
  • 38
  • 64
2

I ended up modifying a solution from Severin to better suit my needs, so I thought I share it here in case anyone has same problem. What I did:

  • Replaced Categorical with own code based on Random class, because Categorical was giving strange results for me.
  • Changed the way probabilities are calculated.
  • Added more statistics.

The key parameter to change is ratio:

  • minimum value is 1.0, which makes it behave just like a random number generator
  • the higher the value, the more it becomes similar to shuffling algorithms, so numbers are guaranteed to appear in near future and are not repeating. Still the order is not predictable.

Results for ratio 1.0:

This is just like pseudo-random number generation.

3, 5, 3, 3, 3, 3, 0, 3, 3, 5, 5, 5, 2, 1, 3, 5, 3, 3, 2, 3, 1, 0, 4, 1, 5, 1, 3, 5, 1, 5, -

Number of occurences:
2
5
2
12
1
8

Max occurences in a row:
1
1
1
4
1
3

Max length where this number did not occur:
14
13
12
6
22
8

Results for ratio 5.0

My favorite. Nice distribution, occasional repeats, not so long gaps where some number does not happen.

4, 1, 5, 3, 2, 5, 0, 0, 1, 3, 2, 4, 2, 1, 5, 0, 4, 3, 1, 4, 0, 2, 4, 3, 5, 5, 2, 4, 0, 1, -

Number of occurences:
5
5
5
4
6
5

Max occurences in a row:
2
1
1
1
1
2

Max length where this number did not occur:
7
10
8
7
10
9

Results for ratio 1000.0

Very uniform distribution, yet still with some randomness.

4, 5, 2, 0, 3, 1, 4, 0, 1, 5, 2, 3, 4, 3, 0, 2, 5, 1, 4, 2, 5, 1, 3, 0, 2, 4, 5, 0, 3, 1, -

Number of occurences:
5
5
5
5
5
5

Max occurences in a row:
1
1
1
1
1
1

Max length where this number did not occur:
8
8
7
8
6
7

Code:

using System;
using System.Linq;

namespace EqualizedSampling
{
    class Program
    {
        static Random rnd = new Random(DateTime.Now.Millisecond);

        /// <summary>
        /// Returns a random int number from [0 .. numNumbers-1] range using probabilities.
        /// Probabilities have to add up to 1.
        /// </summary>
        static int Sample(int numNumbers, double[] probabilities)
        {
            // probabilities have to add up to 1
            double r = rnd.NextDouble();
            double sum = 0.0;

            for (int i = 0; i < numNumbers; i++)
            {
                sum = sum + probabilities[i];
                if (sum > r)
                    return i;
            }

            return numNumbers - 1;
        }

        static void Main(string[] args)
        {
            const int numNumbers = 6;
            const int numSamples = 30;

            // low ratio makes everything behave more random
            // min is 1.0 which makes things behave like a random number generator.
            // higher ratio makes number selection more "natural"
            double ratio = 5.0;

            double[] probabilities = new double[numNumbers];

            int[] counter = new int[numNumbers];        // how many times number occured
            int[] maxRepeat = new int[numNumbers];      // how many times in a row this number (max)
            int[] maxDistance = new int[numNumbers];    // how many samples happened without this number (max)
            int[] lastOccurence = new int[numNumbers];  // last time this number happened

            // init
            for (int i = 0; i < numNumbers; i++)
            {
                counter[i] = 0;
                maxRepeat[i] = 0;
                probabilities[i] = 1.0 / numNumbers;
                lastOccurence[i] = -1;
            }

            int prev = -1;
            int numRepeats = 1;

            for (int k = 0; k < numSamples; k++)
            {
                // sample next number
                //var cat = new Categorical(probabilities);
                //var q = cat.Sample();
                var q = Sample(numNumbers, probabilities);
                Console.Write($"{q}, ");

                // affect probability of the selected number
                probabilities[q] /= ratio;

                // rescale all probabilities so they add up to 1
                double sumProbabilities = 0;
                probabilities.ToList().ForEach(d => sumProbabilities += d);
                for (int i = 0; i < numNumbers; i++)
                    probabilities[i] /= sumProbabilities;

                // gather statistics
                counter[q] += 1;
                numRepeats = q == prev ? numRepeats + 1 : 1;
                maxRepeat[q] = Math.Max(maxRepeat[q], numRepeats);
                lastOccurence[q] = k;
                for (int i = 0; i < numNumbers; i++)
                    maxDistance[i] = Math.Max(maxDistance[i], k - lastOccurence[i]);
                prev = q;
            }

            Console.WriteLine("-\n");
            Console.WriteLine("Number of occurences:");
            counter.ToList().ForEach(Console.WriteLine);

            Console.WriteLine();
            Console.WriteLine("Max occurences in a row:");
            maxRepeat.ToList().ForEach(Console.WriteLine);

            Console.WriteLine();
            Console.WriteLine("Max length where this number did not occur:");
            maxDistance.ToList().ForEach(Console.WriteLine);

            Console.ReadLine();
        }
    }
}
Arek
  • 1,276
  • 1
  • 10
  • 19