Can I exclude a number or subrange of numbers inside a range of random numbers in modern C++?

Viewed 375

I have:

std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<int> probability(0, 100);

I want to exclude some numbers in this range of probabilities.
Example 1: Let's say, I want to generate a random number between 0 and 100, but this number can never be 4.
Example 2: Let's say, I want to generate a random number between 0 and 100, but this number can never be any number between 4 and 7.

I wonder if it is possible to achieve in modern C++ without using std::rand?

5 Answers

If you want to stay with a uniform_int_distribution you can do it manually like this:

Example1: Let's say, I want to generate a random number in between 0 and 100, but this number can never be 4.

std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<int> distribution(0,99);
auto temp = distribution(mt);
auto random_number = (temp < 4) ? temp : temp + 1;

Example2: Let's say, I want to generate a random number in between 0 and 100, but this number can never be any number between 4 and 7.

std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<int> distribution(0,96);
auto temp = distribution(mt);
auto random_number = (temp < 4) ? temp : temp + 4;

This could be generalize to write a function random_int_between_excluding(int first, int last, std::vector<int> exclude), though at some point it will be simpler to follow NathanOlivers suggestion and use a std::discrete_distribution instead.

Example2: Let's say, I want to generate a random number in between 0 and 100, but this number can never be any number between 4 and 7.

This is what std::piecewise_constant_distribution is for.

    std::vector<int> i{0,  4, 8, 101};
    std::vector<int> w{ 4,  0,  93};
    std::piecewise_constant_distribution<> d(i.begin(), i.end(), w.begin());

Live demo

If you want to miss out 4 say, then a very good way (which doesn't compromise any statistical properties of the generator), is to draw in the half-open interval [0, 99) then add 1 if the number is 4 or greater.

You do something similar to omit numbers in a range.

This method is a surprisingly good way of modelling the quantile function associated with the desired probability distribution.

You can use a filter of arbitrary complexity on uniform distribution:

template<typename D, typename G, typename F>
auto sample(D &distribution, G &generator, F const &filter)
{
    while(true)
    {
        auto const value = distribution(generator);
        if(filter(value))
            return value;
    }
}

Your example case transforms into the following

std::random_device rd;
std::mt19937 mt(rd());
std::uniform_int_distribution<int> probability(0, 100);

auto const filter = +[](int n) {return n < 4 || n > 7;}
int const i = sample(probability, mt, filter);

You have to keep in mind that this kind of filtering comes at a cost.

Let N be the number of distinct values the distribution returns, F - the number of these values filtered out; then, if you need to sample S values, you have to sample and filter S * N / (N - F) values at average. It's okay if F is small compared to N, but horribly inefficient when F approaches N. In your case, N = 100, F = 4, and N / (N - F) = 1.04166...

If you care prefer readability and simplicity, that's your choice. Otherwise, if you need performance, you'd better try out piecewise distributions or mess with the value range manually.

There is an option to do it manually within a reasonable range of numbers..., create a look up table and exclude the numbers that are invalid:

 static int rand_pool[]{1,2,3,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23}; //no number 4
    srand((int)time(0));
        int random_number = rand_pool[rand() % 22];
Related