brunomont's blog

By brunomont, history, 3 months ago, In English

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 $$$k$$$ uniformly random distinct integers in the range $$$[\text{left}, \text{right}]$$$;
  2. Generate $$$k$$$ uniformly random distinct strings with $$$n$$$ characters in $$$[\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 $$$[\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 $$$k$$$ distinct integers in the range, that is, every sequence of $$$k$$$ 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 $$$k$$$ 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 $$$\mathcal O(T \cdot k \log k + k \log^2 k)$$$ expected time, if the inner generation is uniform and takes $$$\mathcal O(T)$$$ time.

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

$$$ p_i = \frac{N - i}{N}, $$$

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

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

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

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

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

$$$ \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 $$$k \leq N$$$. Finally,

$$$ \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 $$$\texttt{while}$$$ loop has cost $$$\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 $$$U$$$, as long as we have an algorithm to generate a uniform element from $$$U$$$ in $$$\mathcal O(T)$$$ time, we can easily create an algorithm that generates distinct elements from $$$U$$$, and each generated element will have amortized expected cost $$$\mathcal O(T \cdot \log k + \log^2 k)$$$, if $$$k$$$ 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 $$$[\texttt{a}, \texttt{z}]$$$ independently. The amortized expected time complexity for generating each string will then be $$$\mathcal O(n \log k + \log^2 k)$$$.

References

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

  • Vote: I like it
  • +56
  • Vote: I do not like it

»
3 months ago, hide # |
 
Vote: I like it 0 Vote: I do not like it

Nice!

»
3 months ago, hide # |
Rev. 2  
Vote: I like it +28 Vote: I do not like it

If you have the ability to iterate through your universe set U in O(|U|) than you can avoid the log factor from the harmonic series by instead generating the excluded elements with this process when K > |U|/2.

  • »
    »
    3 months ago, hide # ^ |
     
    Vote: I like it 0 Vote: I do not like it

    In this regime, where $$$K = \Omega(|U|)$$$, there is another thing we can do.

    vector<T> U = ...;  // list whole universe
    for (int i = 0; i < k; ++i) {
      int j = next(i, U.size() - 1);
      swap(U[i], U[j]);
    }
    return vector(U.begin(), U.begin() + k);
    

    This is $$$\mathcal{O}(|U| + T\cdot k) = \mathcal{O}(T\cdot k)$$$ time and $$$\mathcal{O}(|U|) = \mathcal{O}(k)$$$ space.

    • »
      »
      »
      3 months ago, hide # ^ |
       
      Vote: I like it +18 Vote: I do not like it

      And in any regime there is the following magical way to choose $$$k$$$ uniformly random distinct (unsorted) elements from $$$\{ 1, 2, \ldots, n \}$$$ in $$$O(k)$$$ time:

      S = {}
      for j in [n-k+1, n]:
          r = random from [1, j]
          if r in S:
              S.add(j)
          else:
              S.add(r)
      

      It is pretty easy to prove by induction that this approach works, but I do not know any easy and straightforward way to come up with it: either some trickery or some willingness to delve into details is needed, I believe.

    • »
      »
      »
      3 months ago, hide # ^ |
       
      Vote: I like it 0 Vote: I do not like it

      This will be the topic of a future miniblog :)

»
3 months ago, hide # |
 
Vote: I like it +18 Vote: I do not like it

This method of generating numbers until you find one that matches your criteria is called rejection sampling, which is used in std::uniform_int_distribution.

It's actually provable that, assuming you only have an RNG which gives a uniform distribution over all possible n-bit numbers, you cannot get a uniform distribution over any number of elements other than powers of two without rejection sampling. Or any probability distribution with probabilities that aren't of the form $$$\frac{a}{2^n}$$$, for that matter.