Home/Blog/Learn to Code/Generating pseudorandom numbers with C++ rand() and srand()
Home/Blog/Learn to Code/Generating pseudorandom numbers with C++ rand() and srand()

Generating pseudorandom numbers with C++ rand() and srand()

Sarfraz Raza
Aug 09, 2023
15 min read

Become a Software Engineer in Months, Not Years

From your first line of code, to your first day on the job — Educative has you covered. Join 2M+ developers learning in-demand programming skills.

Picture this: You’re working on an exciting project that requires some randomness—maybe for a video game (where you need to spawn random enemies at random spots of the labyrinth of the map of the game), statistical analysis, or even cryptography (where you need to generate a random key). The ability to generate random numbers can be a crucial component to the success of your project. In this blog, we’ll explore the powerful rand() function - a powerful tool that can help you achieve pseudorandomness in various applications.

A pseudorandom integral number refers to a number that appears to be random but is actually generated through a deterministic algorithm. In computer programming, true randomness is often difficult to achieve, so pseudorandom number generators (PRNGs) are used to generate sequences of numbers that exhibit characteristics of randomness.

Using rand() function and its problem#

In C++, the rand() function is a powerful tool that generates a pseudorandom integer within the range of 0 to RAND_MAX, equivalent to the maximum value the int data type can hold (RAND_MAX is a constant defined in cstdlib which has the value 2147483647).

Let’s see how rand() works in practice. In the following three examples, we will call the rand() function five times to generate five random numbers:

#include<iostream>
#include<cstdlib>
#include<ctime>
using namespace std;
int main()
{
// Generating 5 random numbers
cout << rand() << endl;
cout << rand() << endl;
cout << rand() << endl;
cout << rand() << endl;
cout << rand() << endl;
cout << "RAND_MAX: " << RAND_MAX << endl;
return 0;
}
Random number generation

Instruction: Run each of the three tabs above at least three times and observe the following:

1. In the rand_without_srand.cpp tab

Executing this code multiple times will result in the same sequence of numbers each time, which is not desirable for random number generation.

2. In the rand_with_fixed_srand.cpp tab

In this code, we set the seed (the argument) for the random number generator to 1 using srand(). This produces the same sequence of numbers every time the program is run unless the seed value is changed. To test this, change the seed value (to srand(2), srand(3), and so on) to see how the pattern of generated numbers changes.

3. In the code-tab rand_with_srand_time_0.cpp tab

We utilize srand() to set the seed value for the random number generator in this code. The seed value is set to the current time using time(0), producing a distinct sequence of numbers every time the program is run.

time(0) returns the current time in seconds since the epoch (January 1, 1970, 00:00:00 UTC), which is a large integer that increases every second. As a result, it is frequently utilized as a seed for random number generators to generate varied outcomes each time a program is executed.

Let’s now understand how these two functions, rand() and srand(), actually work and how time(0) helps in generating these pseudorandom numbers.

How the rand() functions works#

The rand() function in C++ generates a pseudorandom number by performing some arithmetic operations on the previously generated number or the seed value (if it’s the first call). The arithmetic operations used by rand() are typically based on a linear congruential generator (LCG), which is based on a simple mathematical formula that involves multiplying the previous number by a constant and adding another constant to it and then taking the result modulo some value. The formula can be written as:

Xn+1=(aXn+c) mod m\begin{equation} X_{n+1} = (a X_n + c) \space mod \space m \end{equation}

ZpZ_p refers to the set of integers modulo pp, where pp is a prime number. This set consists of integers ranging from 0 to p1p-1. For example, Z7Z_7 would consist of the integers {0,1,2,3,4,5,6}\{0, 1, 2, 3, 4, 5, 6\}. In this set, addition and multiplication are performed modulo pp. For instance, if we take the example of Z7Z_7, then 4+54+5 would be equal to 22 in Z7Z_7 as (4+5) mod 7=2(4+5) \space mod \space 7 = 2, and 4545 would be equal to 66 in Z7Z_7, as (45) mod 7=6(45) \space mod \space 7 = 6.

Another nice observation related to modular arithmetic is that if we are working in any ZpZ_p, then the following expression is equivalent:

(a×b) mod p((a mod p)×(b mod p)) mod p(a \times b) \space mod \space p \equiv ((a \space mod \space p) \times (b \space mod \space p)) \space mod \space p

For example, if we are working in modulus 13, then the following expression will be equivalent:

(20×14) mod 13((20 mod 13)×(14))(7×1) mod 137(20 \times 14)\space mod\space 13 \equiv ((20\space mod \space 13) \times (14)) \equiv (7 \times 1) \space mod \space 13 \equiv 7

In some implementations of rand(), a polynomial is used of a small degree dd with d+1d+1 prime coefficients (preferably). This is because prime numbers have good properties for generating seemingly random sequences of numbers within a prime modular field, such as being relatively far apart from each other and having a low correlation with other numbers. The polynomial will look like the following:

Xn+1=(a1Xnd+a2Xnd1+...+ad+1) mod m\begin{equation} X_{n+1} = (a_1 X_n^d + a_2 X_n^{d-1} + ... + a_{d+1}) \space mod \space m \end{equation}

Using prime numbers in the LCG can help to produce a more uniform distribution of random numbers.

In this blog, we will stick to a polynomial-based implementation of random number generation. Let’s revisit polynomials:


Polynomial

A polynomial is a mathematical function of the following form:

f(x)=anxn+an1xn1++a1x+a0\begin{equation} f(x) = a_n x^n + a_{n-1} x^{n-1} + \ldots + a_1 x + a_0 \end{equation}

In this equation, a0,a1,,ana_0, a_1, \ldots, a_n are the coefficients of the polynomial, nn is a non-negative integer that represents the degree of the polynomial (i.e., the highest power of xx in the expression), and xx is the independent variable.

Here’s an example of evaluating a polynomial at a particular value of the independent variable xx:

Consider the polynomial:

f(x)=2x3+3x2+5x+1\begin{equation} f(x) = 2x^3 + 3x^2 + 5x + 1 \end{equation}

To evaluate f(x)f(x) at x=2x = 2, we substitute x=2x = 2 into the polynomial expression:

f(2)=2(2)3+3(2)2+5(2)+1=16+12+10+1=39\begin{equation} f(2) = 2(2)^3 + 3(2)^2 + 5(2) + 1 = 16 + 12 + 10 + 1 = 39 \end{equation}

Therefore, f(2)=39f(2) = 39.


The rand() function is implemented using a polynomial function evaluator called evals(), which evaluates the polynomial at a given parameter. The rand() function also uses a global variable called seed. When rand() is called, it passes the current value of seed to evals(), takes the result modulo a large number, preferably a prime number (in our example, we will take 593 so that values do not blow out of the range), and then stores the result back in seed before returning it. Since seed is a global variable, subsequent calls to rand() will use this changed seed as an argument to evals() and generate the next number.

Look at the example implementation:

#include<iostream>
#include<math.h>
using namespace std;
long long evals(int x)
{
long long ans = (113*pow(x,3) + 119*pow(x,2)
+ 53*pow(x,1) + 13); // Polynomial function
// We have used the polynomial: 113 x^3 + 119 x^2 + 53 x + 13
return ans;
}
// A global variable seed, we call it seed variable with 0 as initial value
int seed = 0;
int rand()
{
return seed = evals(seed)%593; // Update the seed variable with return value
}
int main()
{
for(int i=1; i<=10; i++)
cout<<rand()<<endl;
return 0;
}

Instruction: Execute the program multiple times and observe the sequence.

Here is a visual representation of how a polynomial function may appear to generate a sequence modulo 11 (sequence will be numbers between {0,1,2,...,10}\{0,1,2,...,10\}).

canvasAnimation-image
1 of 6

Did you observe that the number sequence remains the same every time the program is executed?

Let us see why that is the case.

Every time the program is executed, the seed is initialized to 0, and the first call forces the pattern to begin the evaluation of the polynomial at 0, and afterward, the pattern follows.

Ideally, we do not want the sequence to be repeated when we execute the program again. Ideally, it should always be random, or at least it should appear random.

One way to get around this issue is to change the seed value every time the program starts. That can be done by the srand() function. Here is its possible implementation:

#include<iostream>
#include<math.h>
using namespace std;
long long evals(int x)
{
long long ans = (113*pow(x,3) + 119*pow(x,2)
+ 53*pow(x,1) + 13); // Polynomial function
return ans;
}
// A global variable seed, we call it seed variable with 0 as initial value
int seed = 0;
void srand(int v)
{
seed = v;
}
int rand()
{
return seed = evals(seed)%593; // Update the seed variable with return value
}
int main()
{
int s;
s = 10;
srand(s);
for(int i=1; i<=10; i++)
cout<<rand()<<endl;
}

Instruction: Change the value of the s inside the main() function several times and see whether the pattern changes along with that change in the seed value s.

Every time we change the seed value in the main and pass it to the srand() function, the pattern changes.


What is the importance of generating a unique seed with every time the execution of the program?

Generating a unique seed with every execution of the program is important to ensure the production of different random number sequences. It allows for diverse and unpredictable outcomes, enabling applications such as simulations, cryptography, and games to have varied and non-repetitive behavior.


Does this approach solve the problem?

The problem remains unresolved since we need to change the seed value manually every time. The user (the programmer) has complete responsibility and control for assigning the seed value that determines the next sequence entirely. If someone knows the given polynomial as coded in the program above, knowing the seed means knowing the sequence hence no randomness.

Therefore, we need a solution where changing the seed value should not be under the user's control. So we need to answer the following question:

How can we create a unique seed value at every new program launch, and it should not be in the control of the user?

We aim to automatically assign a new seed to the program each time it starts without requiring the user to determine it. However, if we assign a completely random value to the seed each time we start the program, we encounter the same problem we were trying to solve originally:  We again need to develop a new random number generator to initialize the seed at the beginning of each program run.

Are we really back at the same problem?

Before we answer this question, let’s introduce ourselves to the time(0) function.

The time(0)function#

time(0) is a function in programming that returns the current time in seconds since the Unix epoch (January 1, 1970, 00:00:00 UTC).

The Unix epoch is a reference point that is used as a standard in computer systems to measure and compare time. It represents the starting point of the Unix timekeeping system, which measures time as the number of seconds that have elapsed since January 1st, 1970, 00:00:00 UTC. This system provides a universal and consistent representation of time across different platforms and programming languages. The Unix epoch is particularly useful for computer systems as it provides a standard for measuring time unaffected by different time zones or daylight saving time adjustments. This allows for more accurate and reliable time comparisons across different systems and applications.

A useful application of the time(0) function is that whenever our program runs and this function is called, it returns a different long integer. Test the following program several times and see how the time value is changing after every second.

#include <iostream>
#include <ctime>
using namespace std;
int main()
{
// your code goes here
cout << "Time(in seconds) since 01, January 1970: " << time(0) << endl;
return 0;
}

How does time(0)solve our problem?#

The problem was that we did not want the seed to be in control of the end user. We wanted it to be randomly generated too. In light of the time(0) function, the question then naturally becomes the following:

Can time(0)be used as the seed to start the sequence generation process?#

Yes! All we need to do is that the first line of our program must call srand(time(0)). This will feed in the time(0) value as a first new seed:

.
.
.
int main()
{
srand(time(0)); // On every new execution,
// the seed will be allocated with the new time value.
.
.
.
}

A common programming error#

One thing to particularly look out for when generating random numbers is that if we would like to generate only a few random numbers (for example, just 10), then we must not write the srand(time(0)) line inside the loop. If that happens, our program will generate the same random numbers again and again. This is because the loop usually runs very fast. During one second, the loop will execute several times and yield the same time(0) value again and again. This will result in the seed being reassigned to the same second’s value and cause the same number to be generated repeatedly.

Look at the following erroneous code and also its correct implementation:

#include <iostream>
#include <ctime>
using namespace std;
int main()
{
for(int i=1; i<=10; i++)
{
srand(time(0));
cout << i << ". "<<rand()<<endl;
}
return 0;
}

The above error is very annoying in the sense that if you try to use the debugger, you will never be able to catch that error. During debugging, we execute one iteration of the program step by step (using the Step Over command), which means by the time we will reach back to the next iteration, a few seconds will have passed. Therefore, it will generate a different random number, giving the impression that it works fine in debug mode only.

Remember#

In applications such as machine learning, random numbers are generated and used for experimentation. To reproduce the same sequence of random numbers, we save the seed value instead of the entire sequence. This is because the seed value uniquely determines the entire sequence for a single polynomial.


Food for thought (for the more interested reader)#

Usually, the random number generation works not by having one prime number as a modulus but by two big prime numbers. Why?

In modulo pp, the generated sequence is expected to not repeat for a small sequence. However, it is known that the sequence will start repeating after pp numbers, and in some cases, it can even repeat earlier. Also, we actually want the number to be repeatable (given the effect that a previously generated number is equally likely to appear again as any other number in the Zp={0,1,2,3,...,p1}Z_p = \{0,1,2,3,...,p-1\}.

So to solve this problem, rand() functions maintains two primes, psmallp_{small} and pbigp_{big}, where psmallpbigp_{small} \ll p_{big} (reads as psmallp_{small} is very very smaller than pbigp_{big}). The value of the sequence is generated in the following fashion:

  • The number xx is generated first in the set Zpbig={0,1,2,3,...,pbig1}Z_{p_{big}} = \{0,1,2,3,...,p_{big}-1\}.
  • xx is saved as the seed and shrunk in modulo of psmallp_{small} yielding one of the numbers in Zpsmall={0,1,2,3,...,psmall1}Z_{p_{small}} = \{0,1,2,3,...,p_{small}-1\} and that is returned as a pseudorandom number.

Notice now that the shrunk value xx is repeatable in ZpsmallZ_{p_{small}} although it will not be repeated ZpbigZ_{p_{big}}, as there are multiple numbers in ZpbigZ_{p_{big}} which will map to one numbering ZpsmallZ_{p_{small}}.

We leave this implementation as an exercise for you to understand:

#include<iostream>
#include<math.h>
using namespace std;
long long pow(int x, int y, int mod)
{
long long result = 1;
for(int i=1; i<=y; i++)
{
result = (result*x)%mod; // Keep taking modulus along, to shrink the answer as
} // soon as it tries to grow out of proportion
return result;
}
// A global variable seed, we call it seed variable with 0 as initial value
int seed = 0, psmall=593, pbig=1000000007;
long long evals(int x)
{
long long ans = (113*pow(x,3, pbig) + 119*pow(x,2,pbig)
+ 53*pow(x,1,pbig) + 13); // Polynomial function
return ans;
}
void srand(int v)
{
seed = v;
}
int rand()
{
seed = evals(seed)%pbig; // Update the seed variable with return value
return seed%psmall; // return the random number in the smaller prime field
}
int main()
{
int s;
s = 10;
srand(s);
for(int i=1; i<=1000; i++)
cout<<rand()<<endl;
}
  • In lines 8–11, in the pow() function, we have passed a mod variable to make sure that during the power computation, the answer never grows out of the range of int capacity, and should be shrunk back in the range.

  • On line 16, we have used the two variables psmall=593, pbig=1000000007.

  • In lines 30–31, we first calculated the new seed in the modulo of pbig, and then the seed value is shrunk to the range within the smaller prime by taking the modulus from psmall.

Visually, it can be seen in the following animation:

canvasAnimation-image
1 of 9

As an example, the outer ring is working in modulo 29 and the inner ring is working in modulo 5. First the number is generated in modulo 29 and then it is mapped to modulo 5. This way will generate the same number multiple times.


Limitations of PRGs#

Pseudorandom number generations (PRNGs) through the srand(time(0)) and rand() functions have the following limitations:

  1. Limited periodicity: Pseudorandom sequences repeat after a certain number of iterations (this is the exact reason why we work in two prime numbers).

  2. Seed dependency: Same seed generates the same sequence of random numbers.

  3. Lack of true randomness: Pseudorandom numbers are deterministic (once the first seed is decided), not truly random.

  4. Statistical properties: The quality and statistical properties of generated random numbers may be inadequate.

Conclusion#

In conclusion, we discussed pseudorandom number generators and their use in generating random numbers that appear to be random but are deterministic. We looked at the rand() function in C++ and the use of the srand() function to set a specific seed for generating pseudorandom numbers. We also explored using time-based seeds (using time(0)) to create pseudorandom sequences. Furthermore, while diving into how polynomials are used in generating pseudorandom sequences, we discussed the problem of generating duplicate sequences and how to avoid it by working with two primes — a smaller prime number and a much larger prime number.

Further Reading#

There are several ways of using these functions to generate random numbers, like generating random even numbers, generating random multiples of any value, or generating a pseudoprime number, etc. You may refer to the following course linked below (in the chapter "The Introduction to Arrays," we discuss generating random data of different kinds). To enhance your programming skills and learn C++, this course offers practical insights and examples.

Beginner to Advanced Computing and Logic Building

Cover
Beginner to Advanced Computing and Logic Building

This course focuses on building logic to solve simple to advanced coding problems. You will learn advanced computational techniques to solve real-world problems. The solutions for most problems are curated in a way that each solution is followed by a more efficient one, enabling you to devise the most efficient algorithm from the start. The course starts with the fundamentals of variables, loops, and arrays, and progresses to more advanced concepts such as creating games like Gomoku, Word Search, and Game of Life using the divide and conquer approach. The course also includes a built-in debugger to help you understand the code by setting breakpoints and analyzing variable values. Concepts will be demonstrated via animations and illustrations to engage your interest and give you hands-on experience in building interactive games. By the end of the course, you will be well-prepared to tackle coding challenges commonly found in FAANG interviews.

46hrs
Beginner
31 Challenges
44 Quizzes

  

Free Resources