Writing

tgen::miniblog(2): Fisher-Yates' algorithm

2026-06-21 · Originally published on Codeforces

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:

  1. Generate a uniformly random permutation of length nn;
  2. Generate kk uniformly random distinct integers in the range [0,n)[0, n) in O(klogk)\mathcal O(k \log k) time.

In my previous blog tgen::miniblog(1) we saw a way to solve (2) in O(klog2k)\mathcal O(k \log^2 k) time, so we will improve this bound.

The Fisher-Yates’ algorithm is very simple. We start with the identity permutation p=[0,1,,n1]p = [0, 1, \dots, n-1] and at the end we will have a permutation of pp uniformly at random, that is, each permutation will be generated with probability 1n!\frac{1}{n!}. 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 [left,right][\text{left}, \text{right}]. Let’s prove that the resulting permutation is uniform.

Theorem 1: Algorithm 1 creates a uniformly random permutation of length nn.

Proof: Fix an arbitrary permutation π\pi of 0,1,,n1{0, 1, \dots, n-1}. We will compute the probability that Algorithm 1 outputs exactly π\pi.

Observe that after iteration ii, the value stored at position ii will never move again, since all future swaps only involve positions greater than or equal to i+1i+1.

Therefore, for the final permutation to be equal to π\pi at iteration ii, the algorithm must place πi\pi_i at position ii.

Suppose that iterations 0,1,,i10, 1, \dots, i-1 have already placed the correct values. At the beginning of iteration ii, the positions i,i+1,,n1i, i+1, \dots, n-1 contain exactly the values πi,πi+1,,πn1\pi_i, \pi_{i+1}, \dots, \pi_{n-1} in some order. Hence, among the indices i,i+1,,n1i, i+1, \dots, n-1, there is exactly one index whose value is πi\pi_i.

The algorithm chooses an index jj uniformly at random from the range [i,n1][i, n-1]. Since there are nin-i possible choices for jj, exactly one of which places πi\pi_i at position ii, conditioned on all previous iterations placing the correct values, the probability that iteration ii places the correct value is 1ni\frac{1}{n-i}.

Therefore, by multiplying these conditional probabilities, the probability that every iteration places the correct value is

i=0n11ni=1n!.\prod\limits_{i=0}^{n-1} \frac{1}{n-i} = \frac{1}{n!}.

Thus, the probability that Algorithm 1 outputs the permutation π\pi is 1n!\frac{1}{n!}. Since this holds for every permutation π\pi, the output distribution is uniform.

\square

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 nn, shuffle it uniformly at random and then return the first kk elements of that permutation. Since after shuffling every permutation is equally likely, then every distinct prefix of length kk is also equally likely, with probability (nk)!1n!=(nk)!n!(n-k)! \cdot \frac{1}{n!} = \frac{(n-k)!}{n!}.

We can also check that each set of kk numbers is represented exactly k!k! times, so each subset of size kk of [0,n)[0, n) is chosen with probability

k!(nk)!n!=1(nk),k! \cdot \frac{(n-k)!}{n!} = \frac{1}{\binom{n}{k}},

so we can just sort the chosen list to get a uniformly random subset of kk numbers in [0,n)[0, n).

However, the problem is that nn might be large, so we cannot explicitly create the permutation. But we only need to run the algorithm for kk iterations, since other iterations won’t affect the first kk elements of the shuffled permutation. So we can just run kk iterations and use a map as the permutation (and if some element is not present, we initialize it with p[i]=ip[i] = i; 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 kk values in [0,n)[0, n) by Fisher-Yates.

Algorithm 2 clearly runs in O(klogk)\mathcal O(k \log k) time, and its output is uniformly random.