This is a blog 2 of a series of blogs about algorithmic challenges I came across when creating tgen.
In this blog we will tackle:
- Generate a uniformly random permutation of length ;
- Generate uniformly random distinct integers in the range in time.
In my previous blog tgen::miniblog(1) we saw a way to solve (2) in time, so we will improve this bound.
The Fisher-Yates’ algorithm is very simple. We start with the identity permutation and at the end we will have a permutation of uniformly at random, that is, each permutation will be generated with probability . We will do the following.
std::vector<int> p(n);
for (int i = 0; i < n; i++) p[i] = i;
for (int i = 0; i < n; i++) {
std::swap(p[i], p[next(i, n-1)]);
}
return p;
Algorithm 1: Fisher-Yates’ algorithm.
Here, next(left, right) is a function that returns a uniformly random integer in the range . Let’s prove that the resulting permutation is uniform.
Theorem 1: Algorithm 1 creates a uniformly random permutation of length .
Proof: Fix an arbitrary permutation of . We will compute the probability that Algorithm 1 outputs exactly .
Observe that after iteration , the value stored at position will never move again, since all future swaps only involve positions greater than or equal to .
Therefore, for the final permutation to be equal to at iteration , the algorithm must place at position .
Suppose that iterations have already placed the correct values. At the beginning of iteration , the positions contain exactly the values in some order. Hence, among the indices , there is exactly one index whose value is .
The algorithm chooses an index uniformly at random from the range . Since there are possible choices for , exactly one of which places at position , conditioned on all previous iterations placing the correct values, the probability that iteration places the correct value is .
Therefore, by multiplying these conditional probabilities, the probability that every iteration places the correct value is
Thus, the probability that Algorithm 1 outputs the permutation is . Since this holds for every permutation , the output distribution is uniform.
Note that this can shuffle any list, not just the identity permutation.
To solve problem (2) using this idea, we would like to create an identity array of length , shuffle it uniformly at random and then return the first elements of that permutation. Since after shuffling every permutation is equally likely, then every distinct prefix of length is also equally likely, with probability .
We can also check that each set of numbers is represented exactly times, so each subset of size of is chosen with probability
so we can just sort the chosen list to get a uniformly random subset of numbers in .
However, the problem is that might be large, so we cannot explicitly create the permutation. But we only need to run the algorithm for iterations, since other iterations won’t affect the first elements of the shuffled permutation. So we can just run iterations and use a map as the permutation (and if some element is not present, we initialize it with ; some sort of lazy initialization).
std::map<int, int> p;
std::vector<int> ret(k);
for (int i = 0; i < k; i++) {
int j = next(i, n-1);
int p_i = p.count(i) ? p[i] : i;
int p_j = p.count(j) ? p[j] : j;
p[i] = p_j;
p[j] = p_i;
ret[i] = p[i];
}
return ret;
Algorithm 2: random values in by Fisher-Yates.
Algorithm 2 clearly runs in time, and its output is uniformly random.