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:
- Generate uniformly random distinct integers in the range ;
- Generate uniformly random distinct strings with characters in .
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 . 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 distinct integers in the range, that is, every sequence of 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 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 expected time, if the inner generation is uniform and takes time.
Proof: Let be the total number of elements that can be generated (in our example, ). Let’s try to bound the number of iterations required for the loop when the set has elements (). On that moment, the probability of generating a new element is
assuming our generation is uniform (out of possible values, yields a new one). Since draws are independent, we can calculate the expected value in the following way. We either succeed in the first try (1 iteration), with probability , or we fail and need to repeat (1 extra iteration), with probability . So the expected value must satisfy:
Solving for , we get
Adding the expected cost for every , we get
This last inequality is implied from . Finally,
This last identity is well known from the harmonic series. To finish off, each iteration of the loop has cost , from generating plus the binary search tree. Multiplying by that, we get the final time complexity.
What this means for us is: if we have any universe set , as long as we have an algorithm to generate a uniform element from in time, we can easily create an algorithm that generates distinct elements from , and each generated element will have amortized expected cost , if 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 independently. The amortized expected time complexity for generating each string will then be .
References
Coupon collector’s problem: https://en.wikipedia.org/wiki/Coupon_collector%27s_problem