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.
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 numberscout << rand() << endl;cout << rand() << endl;cout << rand() << endl;cout << rand() << endl;cout << rand() << endl;cout << "RAND_MAX: " << RAND_MAX << endl;return 0;}
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.
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:
refers to the set of integers modulo , where is a prime number. This set consists of integers ranging from 0 to . For example, would consist of the integers . In this set, addition and multiplication are performed modulo . For instance, if we take the example of , then would be equal to in as , and would be equal to in , as .
Another nice observation related to modular arithmetic is that if we are working in any , then the following expression is equivalent:
For example, if we are working in modulus 13, then the following expression will be equivalent:
In some implementations of rand()
, a polynomial is used of a small degree with 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:
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:
In this equation, are the coefficients of the polynomial, is a non-negative integer that represents the degree of the polynomial (i.e., the highest power of in the expression), and is the independent variable.
Here’s an example of evaluating a polynomial at a particular value of the independent variable :
Consider the polynomial:
To evaluate at , we substitute into the polynomial expression:
Therefore, .
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 + 13return ans;}// A global variable seed, we call it seed variable with 0 as initial valueint 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 ).
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 functionreturn ans;}// A global variable seed, we call it seed variable with 0 as initial valueint 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.
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 herecout << "Time(in seconds) since 01, January 1970: " << time(0) << endl;return 0;}
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:
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....}
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.
Usually, the random number generation works not by having one prime number as a modulus but by two big prime numbers. Why?
In modulo , the generated sequence is expected to not repeat for a small sequence. However, it is known that the sequence will start repeating after 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 .
So to solve this problem, rand()
functions maintains two primes, and , where (reads as is very very smaller than ). The value of the sequence is generated in the following fashion:
Notice now that the shrunk value is repeatable in although it will not be repeated , as there are multiple numbers in which will map to one numbering .
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 proportionreturn result;}// A global variable seed, we call it seed variable with 0 as initial valueint 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 functionreturn ans;}void srand(int v){seed = v;}int rand(){seed = evals(seed)%pbig; // Update the seed variable with return valuereturn 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:
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.
Pseudorandom number generations (PRNGs) through the srand(time(0))
and rand()
functions have the following limitations:
Limited periodicity: Pseudorandom sequences repeat after a certain number of iterations (this is the exact reason why we work in two prime numbers).
Seed dependency: Same seed generates the same sequence of random numbers.
Lack of true randomness: Pseudorandom numbers are deterministic (once the first seed is decided), not truly random.
Statistical properties: The quality and statistical properties of generated random numbers may be inadequate.
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.
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
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.
Free Resources