Writing

tgen::miniblog(1): uniform generation implies distinct generation

2026-06-18 · Originally published on Codeforces

This is a blog 1 of a series of blogs about algorithmic challenges I came across when creating tgen.

In this blog we will tackle:

  1. Generate kk uniformly random distinct integers in the range [left,right][\text{left}, \text{right}];
  2. Generate kk uniformly random distinct strings with nn characters in [a,z][\texttt{a}, \texttt{z}].

Let’s assume we have a function next(left, right), which I will call the inner generation, that returns a uniformly random integer in the range [left,right][\text{left}, \text{right}]. A trivial algorithm is:

std::vector<int> seq;
std::set<int> s;
while (s.size() < k) {
    int x = next(left, right);
    if (s.insert(x).second)
        seq.push_back(x);
}
return seq;

Algorithm 1: distinct generation.

Surprisingly, this simple algorithm is both uniform and fast.

It is easy to see that this algorithm returns a uniform sequence of kk distinct integers in the range, that is, every sequence of kk distinct integers in the list is equally likely to be generated (the proof is left as an exercise).

However, bounding the time complexity is a little more involved. If kk is relatively small compared to the total number of elements, we can expect this to be fast. But there is actually a worst-case expected bound we can prove without that assumption.

Theorem 1: Algorithm 1 runs in O(Tklogk+klog2k)\mathcal O(T \cdot k \log k + k \log^2 k) expected time, if the inner generation is uniform and takes O(T)\mathcal O(T) time.

Proof: Let NN be the total number of elements that can be generated (in our example, N=rightleft+1N = \text{right} - \text{left} + 1). Let’s try to bound the number of iterations LiL_i required for the while\texttt{while} loop when the set has ii elements (0i<k0 \leq i \lt k). On that moment, the probability of generating a new element is

pi=NiN,p_i = \frac{N - i}{N},

assuming our generation is uniform (out of NN possible values, NiN - i yields a new one). Since draws are independent, we can calculate the expected value E[Li]\mathbb E[L_i] in the following way. We either succeed in the first try (1 iteration), with probability pip_i, or we fail and need to repeat (1 extra iteration), with probability 1pi1 - p_i. So the expected value must satisfy:

E[Li]=1pi+(1+E[Li])(1pi).\mathbb E[L_i] = 1 \cdot p_i + (1 + \mathbb E[L_i]) \cdot (1 - p_i).

Solving for E[Li]\mathbb E[L_i], we get

E[Li]=1pi=NNi.\mathbb E[L_i] = \frac{1}{p_i} = \frac{N}{N - i}.

Adding the expected cost for every 0i<k0 \leq i \lt k, we get

i=0k1E[Li]=i=0k1NNii=0k1kki.\sum\limits_{i=0}^{k-1} \mathbb E[L_i] = \sum\limits_{i=0}^{k-1} \frac{N}{N - i} \leq \sum\limits_{i=0}^{k-1} \frac{k}{k - i}.

This last inequality is implied from kNk \leq N. Finally,

i=0k1E[Li]i=0k1kki=ki=1k1iO(klogk).\sum\limits_{i=0}^{k-1} \mathbb E[L_i] \leq \sum\limits_{i=0}^{k-1} \frac{k}{k - i} = k \sum\limits_{i=1}^{k} \frac{1}{i} \in \mathcal O(k \log k).

This last identity is well known from the harmonic series. To finish off, each iteration of the while\texttt{while} loop has cost O(T+logk)\mathcal O(T + \log k), from generating plus the binary search tree. Multiplying by that, we get the final time complexity.

\square

What this means for us is: if we have any universe set UU, as long as we have an algorithm to generate a uniform element from UU in O(T)\mathcal O(T) time, we can easily create an algorithm that generates distinct elements from UU, and each generated element will have amortized expected cost O(Tlogk+log2k)\mathcal O(T \cdot \log k + \log^2 k), if kk distinct elements will be generated in total.

In other words, uniform generation implies distinct generation, with only a logarithmic factor overhead. Pretty cool, right?

Finally, we address problem (2). We can use the same strategy, and the inner generation will just be a for loop that chooses each character from [a,z][\texttt{a}, \texttt{z}] independently. The amortized expected time complexity for generating each string will then be O(nlogk+log2k)\mathcal O(n \log k + \log^2 k).

References

Coupon collector’s problem: https://en.wikipedia.org/wiki/Coupon_collector%27s_problem